[verified] add benchmark baseline and rotation fixes

This commit is contained in:
aj
2026-09-22 16:35:25 -04:00
parent 5bbb7b7461
commit 8e4735d25d
11 changed files with 599 additions and 43 deletions
+15 -4
View File
@@ -1,9 +1,9 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using OpenNest.Geometry;
using OpenNest.Engine.Jobs;
using OpenNest.Engine.Jobs.Adapters;
using OpenNest.Geometry;
namespace OpenNest.Benchmark
{
@@ -36,6 +36,12 @@ namespace OpenNest.Benchmark
public Spacing EdgeSpacing { get; init; }
public double PartSpacing { get; init; }
public int Quadrant { get; init; }
/// <summary>Saved source-nest setting; manifests have no saved salvage rate and use zero.</summary>
public double SalvageRate { get; init; }
/// <summary>Original hand-authored placements, if the source was a .nest with any real parts.</summary>
public List<(Plate Plate, List<Part> Parts)> BaselinePlateRuns { get; init; }
public List<DrawingRequest> Requests { get; init; }
public string Name => Path.GetFileNameWithoutExtension(SourceFile);
@@ -58,8 +64,8 @@ namespace OpenNest.Benchmark
/// </summary>
public NestJob BuildNestJob(
int maxPlates,
double salvageRate = 0,
double minimumSalvageDimension = 0
double? salvageRate = null,
double? minimumSalvageDimension = null
)
{
var parts = Requests.Select(r =>
@@ -76,7 +82,12 @@ namespace OpenNest.Benchmark
return new NestJob(
parts,
stock,
new NestJobOptions("Default", maxPlates, salvageRate, minimumSalvageDimension)
new NestJobOptions(
"Default",
maxPlates,
salvageRate ?? SalvageRate,
minimumSalvageDimension ?? 0
)
);
}
}
+163 -9
View File
@@ -30,8 +30,8 @@ namespace OpenNest.Benchmark
public static List<JobResult> Run(
List<BenchmarkJob> jobs,
IReadOnlyList<NestingEngineInfo> engines,
double salvageRate = 0,
double minimumSalvageDimension = 0,
double? salvageRate = null,
double? minimumSalvageDimension = null,
string outputDirectory = null,
int maxParallelism = 1
)
@@ -44,6 +44,16 @@ namespace OpenNest.Benchmark
MaxDegreeOfParallelism = System.Math.Max(1, maxParallelism),
};
var baselineResults = new JobResult[jobs.Count];
Parallel.ForEach(
Partitioner.Create(
Enumerable.Range(0, jobs.Count),
EnumerablePartitionerOptions.NoBuffering
),
options,
i => baselineResults[i] = RunBaseline(jobs[i], salvageRate, minimumSalvageDimension)
);
// NoBuffering hands out one pair at a time: solves run for seconds to minutes,
// so chunked partitioning would leave workers idle behind a slow engine.
Parallel.ForEach(
@@ -63,14 +73,156 @@ namespace OpenNest.Benchmark
);
// Indexed writes keep the report in job-then-engine order whatever finishes first.
return results.ToList();
var ordered = new List<JobResult>(
results.Length + baselineResults.Count(result => result != null)
);
for (var jobIndex = 0; jobIndex < jobs.Count; jobIndex++)
{
if (baselineResults[jobIndex] != null)
ordered.Add(baselineResults[jobIndex]);
var firstResult = jobIndex * engines.Count;
for (var engineIndex = 0; engineIndex < engines.Count; engineIndex++)
ordered.Add(results[firstResult + engineIndex]);
}
return ordered;
}
private static JobResult RunBaseline(
BenchmarkJob job,
double? salvageRate,
double? minimumSalvageDimension
)
{
if (job.BaselinePlateRuns == null)
return null;
var requested = job.TotalRequestedQuantity;
try
{
var requirements = job.Requests.ToDictionary<
DrawingRequest,
Drawing,
(string Name, int Quantity)
>(
request => request.Drawing,
request => (request.Drawing.Name, request.Quantity),
ReferenceEqualityComparer.Instance
);
var partIds = job.Requests.ToDictionary<DrawingRequest, Drawing, string>(
request => request.Drawing,
request => request.Drawing.Id.ToString(),
ReferenceEqualityComparer.Instance
);
var validation = NestValidator.Validate(job.BaselinePlateRuns, requirements);
var benchmarkJob = job.BuildNestJob(
MaxPlates,
salvageRate,
minimumSalvageDimension
);
var instanceIndices = new Dictionary<string, int>(StringComparer.Ordinal);
var plateResults = job
.BaselinePlateRuns.Select(
(run, index) =>
{
var stock = new NestPlateStock(
$"baseline-{index}",
run.Plate.Size,
1,
run.Plate.PartSpacing,
run.Plate.EdgeSpacing,
run.Plate.Quadrant
);
var placements = run
.Parts.Select(part =>
{
var partId = partIds[part.BaseDrawing];
instanceIndices.TryGetValue(partId, out var instanceIndex);
instanceIndices[partId] = instanceIndex + 1;
return new NestJobPlacement(
partId,
instanceIndex,
part.Location.X,
part.Location.Y,
part.Rotation
);
})
.ToList();
return new NestJobPlateResult(index, stock, placements);
}
)
.ToList();
var baselineJob = new NestJob(
benchmarkJob.Parts,
plateResults.Select(result => result.Stock),
benchmarkJob.Options
);
var baselineJobResult = new NestJobResult(
NestJobStatus.Complete,
NestJobStopReason.Completed,
plateResults,
Array.Empty<PartFulfillment>(),
Array.Empty<StockUsage>()
);
NestValidator.ValidateAgainstJob(
baselineJob,
baselineJobResult,
job.Requests.ToDictionary(
request => request.Drawing.Id.ToString(),
request => request.Drawing.Name
),
validation
);
var plateRuns = job.BaselinePlateRuns;
var placedArea = validation.Valid
? plateRuns.Sum(run => run.Parts.Sum(part => part.BaseDrawing.Area))
: 0;
var plateArea = plateRuns.Sum(run => run.Plate.Area());
var netSheetArea = validation.Valid
? plateResults.Sum(result =>
StockLadderNestingEngine.EstimateNetArea(baselineJob, result)
)
: 0;
var sizeBreakdown = plateRuns
.GroupBy(run => run.Plate.Size.ToString(1))
.OrderByDescending(group => group.Count())
.ToDictionary(group => group.Key, group => group.Count());
return new JobResult
{
EngineName = "Baseline",
JobName = job.Name,
Valid = validation.Valid,
Violations = validation.Violations,
PartsPlaced = plateRuns.Sum(run => run.Parts.Count),
PartsRequested = requested,
PlacedArea = placedArea,
PlateArea = plateArea,
NetSheetArea = netSheetArea,
UnplacedPartPenalty = job.UnplacedPartPenalty,
PlatesUsed = plateRuns.Count,
SizeBreakdown = sizeBreakdown,
ElapsedMs = 0,
};
}
catch (Exception ex)
{
return new JobResult
{
EngineName = "Baseline",
JobName = job.Name,
Valid = false,
PartsRequested = requested,
UnplacedPartPenalty = job.UnplacedPartPenalty,
Error = $"{ex.GetType().Name}: {ex.Message}",
};
}
}
private static JobResult RunOne(
BenchmarkJob job,
NestingEngineInfo engineInfo,
double salvageRate,
double minimumSalvageDimension,
double? salvageRate,
double? minimumSalvageDimension,
string outputDirectory
)
{
@@ -113,7 +265,9 @@ namespace OpenNest.Benchmark
var plateArea = plateRuns.Sum(pr => pr.Plate.Area());
// Salvage credit is recomputed from the job's own geometry, never taken from the engine.
var netSheetArea = validation.Valid
? jobResult.Plates.Sum(p => StockLadderNestingEngine.EstimateNetArea(nestJob, p))
? jobResult.Plates.Sum(p =>
StockLadderNestingEngine.EstimateNetArea(nestJob, p)
)
: 0;
var sizeBreakdown = plateRuns
@@ -138,7 +292,7 @@ namespace OpenNest.Benchmark
{
materialized.Nest.Name = job.Name;
}
materialized.Nest.SalvageRate = salvageRate;
materialized.Nest.SalvageRate = nestJob.Options.SalvageRate;
foreach (var request in job.Requests)
materialized.DrawingsByPartId[request.Drawing.Id.ToString()].Name = request
.Drawing
@@ -165,8 +319,8 @@ namespace OpenNest.Benchmark
Placed = totalPlaced,
SheetArea = plateArea,
PlacedArea = placedArea,
SalvageRate = salvageRate,
MinimumSalvageDimension = minimumSalvageDimension,
SalvageRate = nestJob.Options.SalvageRate,
MinimumSalvageDimension = nestJob.Options.MinimumSalvageDimension,
EstimatedNetArea = netSheetArea,
Fulfillment = jobResult.Fulfillment,
StockUsage = jobResult.StockUsage,
+31
View File
@@ -73,6 +73,8 @@ namespace OpenNest.Benchmark
EdgeSpacing = template.EdgeSpacing,
PartSpacing = partSpacingOverride ?? template.PartSpacing,
Quadrant = template.Quadrant,
SalvageRate = nest.SalvageRate,
BaselinePlateRuns = BuildBaselinePlateRuns(nest, partSpacingOverride),
Requests = requests,
}
);
@@ -156,6 +158,35 @@ namespace OpenNest.Benchmark
return requests;
}
private static List<(Plate Plate, List<Part> Parts)> BuildBaselinePlateRuns(
Nest nest,
double? partSpacingOverride
)
{
var runs = new List<(Plate Plate, List<Part> Parts)>();
foreach (var plate in nest.Plates ?? Enumerable.Empty<Plate>())
{
var parts = plate.Parts.Where(part => !part.BaseDrawing.IsCutOff).ToList();
if (parts.Count == 0)
continue;
var validationPlate = new Plate(new Size(plate.Size.Width, plate.Size.Length))
{
Quantity = 1,
Quadrant = plate.Quadrant,
PartSpacing = partSpacingOverride ?? plate.PartSpacing,
EdgeSpacing = new Spacing(
plate.EdgeSpacing.Left,
plate.EdgeSpacing.Bottom,
plate.EdgeSpacing.Right,
plate.EdgeSpacing.Top
),
};
for (var copy = 0; copy < plate.Quantity; copy++)
runs.Add((validationPlate, parts));
}
return runs.Count > 0 ? runs : null;
}
private static (Spacing EdgeSpacing, double PartSpacing, int Quadrant) ResolvePlateTemplate(
Nest nest
)
+17 -6
View File
@@ -4,8 +4,8 @@ using System.IO;
using System.Linq;
using OpenNest;
using OpenNest.Benchmark;
using OpenNest.Geometry;
using OpenNest.Engine.Jobs;
using OpenNest.Geometry;
return BenchmarkConsole.Run(args);
@@ -96,6 +96,17 @@ static class BenchmarkConsole
Console.WriteLine($"Engines: {string.Join(", ", engines.Select(e => e.Name))}");
var effectiveSalvageRates = jobs.Select(job => options.SalvageRate ?? job.SalvageRate);
if (
effectiveSalvageRates.Any(rate => rate > 0)
&& (options.MinimumSalvageDimension ?? 0) <= 0
)
{
Console.Error.WriteLine(
"Warning: salvage credit is disabled because --min-salvage-dimension was not set to a positive value."
);
}
var solves = jobs.Count * engines.Count;
if (options.Parallel > 1 && solves > 1)
@@ -294,13 +305,13 @@ static class BenchmarkConsole
" --csv <path> Write a flat CSV of all results"
);
Console.Error.WriteLine(
" --salvage-rate <0..1> Fraction of the usable offcut credited back in the"
" --salvage-rate <0..1> Fraction of eligible offcut area credited (default: saved .nest rate;"
);
Console.Error.WriteLine(
" score (default 0; needs --min-salvage-dimension)"
" manifests 0; needs positive --min-salvage-dimension)"
);
Console.Error.WriteLine(
" --min-salvage-dimension <value> Both offcut dimensions must qualify; 0 disables credit"
" --min-salvage-dimension <value> Both offcut dimensions must qualify; positive value enables credit (default 0)"
);
Console.Error.WriteLine(
" --output <directory> Save valid layouts as .nest plus detailed JSON reports"
@@ -322,8 +333,8 @@ static class BenchmarkConsole
public List<string> EngineNames = new();
public string CsvPath;
public string OutputDirectory;
public double SalvageRate;
public double MinimumSalvageDimension;
public double? SalvageRate;
public double? MinimumSalvageDimension;
public int Parallel = 3;
}
}