Add tested caller-stock StockLadder baseline with strict geometry validation

This commit is contained in:
aj
2026-09-19 11:24:36 -04:00
parent 9b69c67572
commit ea4bd836cd
11 changed files with 759 additions and 11 deletions
+2 -2
View File
@@ -48,13 +48,13 @@ namespace OpenNest.Benchmark
/// engine owns its own multi-plate/size strategy; this harness no
/// longer picks plate sizes on the engine's behalf.
/// </summary>
public NestJob BuildNestJob(int maxPlates)
public NestJob BuildNestJob(int maxPlates, double salvageRate = 0, double minimumSalvageDimension = 0)
{
var parts = Requests.Select(r =>
DrawingJobMapper.FromDrawing(r.Drawing.Id.ToString(), r.Drawing, r.Quantity));
var stock = CandidateSizes.Select(size =>
new NestPlateStock(size.ToString(1), size, null, PartSpacing, EdgeSpacing, Quadrant));
return new NestJob(parts, stock, new NestJobOptions("Default", maxPlates));
return new NestJob(parts, stock, new NestJobOptions("Default", maxPlates, salvageRate, minimumSalvageDimension));
}
}
}
+35 -4
View File
@@ -23,7 +23,8 @@ namespace OpenNest.Benchmark
/// <summary>Wall-clock budget for one engine solving one job.</summary>
private static readonly TimeSpan SolveTimeout = TimeSpan.FromMinutes(5);
public static List<JobResult> Run(List<BenchmarkJob> jobs, IReadOnlyList<NestingEngineInfo> engines)
public static List<JobResult> Run(List<BenchmarkJob> jobs, IReadOnlyList<NestingEngineInfo> engines,
double salvageRate = 0, double minimumSalvageDimension = 0, string outputDirectory = null)
{
var results = new List<JobResult>(jobs.Count * engines.Count);
@@ -31,21 +32,22 @@ namespace OpenNest.Benchmark
{
foreach (var engineInfo in engines)
{
results.Add(RunOne(job, engineInfo));
results.Add(RunOne(job, engineInfo, salvageRate, minimumSalvageDimension, outputDirectory));
}
}
return results;
}
private static JobResult RunOne(BenchmarkJob job, NestingEngineInfo engineInfo)
private static JobResult RunOne(BenchmarkJob job, NestingEngineInfo engineInfo,
double salvageRate, double minimumSalvageDimension, string outputDirectory)
{
var requested = job.TotalRequestedQuantity;
var sw = Stopwatch.StartNew();
try
{
var nestJob = job.BuildNestJob(MaxPlates);
var nestJob = job.BuildNestJob(MaxPlates, salvageRate, minimumSalvageDimension);
var engine = engineInfo.Factory();
using var cts = new CancellationTokenSource(SolveTimeout);
var jobResult = engine.Solve(nestJob, null, cts.Token);
@@ -70,6 +72,35 @@ namespace OpenNest.Benchmark
.OrderByDescending(g => g.Count())
.ToDictionary(g => g.Key, g => g.Count());
if (validation.Valid && outputDirectory != null)
{
System.IO.Directory.CreateDirectory(outputDirectory);
// Keep names and job metadata for a useful inspectable output; never modify source.
var source = new OpenNest.IO.NestReader(job.SourceFile).Read();
materialized.Nest.Name = source.Name;
materialized.Nest.Units = source.Units;
materialized.Nest.Material = source.Material;
materialized.Nest.Thickness = source.Thickness;
materialized.Nest.SalvageRate = salvageRate;
foreach (var request in job.Requests)
materialized.DrawingsByPartId[request.Drawing.Id.ToString()].Name = request.Drawing.Name;
var path = System.IO.Path.Combine(outputDirectory, $"{job.Name}-{engineInfo.Name}.nest");
if (System.IO.Path.GetFullPath(path) == System.IO.Path.GetFullPath(job.SourceFile))
throw new InvalidOperationException("Output must not overwrite the source nest.");
new OpenNest.IO.NestWriter(materialized.Nest).Write(path);
var report = new
{
Source = job.SourceFile, Engine = engineInfo.Name, jobResult.Status, jobResult.StopReason,
Requested = requested, Placed = totalPlaced, SheetArea = plateArea, PlacedArea = placedArea,
SalvageRate = salvageRate, MinimumSalvageDimension = minimumSalvageDimension,
EstimatedNetArea = jobResult.Plates.Sum(p => StockLadderNestingEngine.EstimateNetArea(nestJob, p)),
Fulfillment = jobResult.Fulfillment, StockUsage = jobResult.StockUsage,
Plates = jobResult.Plates, validation.Violations
};
System.IO.File.WriteAllText(System.IO.Path.ChangeExtension(path, ".json"),
System.Text.Json.JsonSerializer.Serialize(report,
new System.Text.Json.JsonSerializerOptions { WriteIndented = true }));
}
sw.Stop();
return new JobResult
+17 -1
View File
@@ -70,7 +70,7 @@ static class BenchmarkConsole
Console.WriteLine($"Engines: {string.Join(", ", engines.Select(e => e.Name))}");
var results = BenchmarkRunner.Run(jobs, engines);
var results = BenchmarkRunner.Run(jobs, engines, options.SalvageRate, options.MinimumSalvageDimension, options.OutputDirectory);
Report.PrintDetailed(results);
Report.PrintSummary(results);
@@ -111,6 +111,16 @@ static class BenchmarkConsole
o.CsvPath = args[++i];
break;
case "--salvage-rate" when i + 1 < args.Length:
o.SalvageRate = double.Parse(args[++i], System.Globalization.CultureInfo.InvariantCulture);
break;
case "--min-salvage-dimension" when i + 1 < args.Length:
o.MinimumSalvageDimension = double.Parse(args[++i], System.Globalization.CultureInfo.InvariantCulture);
break;
case "--output" when i + 1 < args.Length:
o.OutputDirectory = args[++i];
break;
case "--help":
PrintUsage();
return null;
@@ -162,6 +172,9 @@ static class BenchmarkConsole
Console.Error.WriteLine(" --spacing <value> Override part spacing for every job");
Console.Error.WriteLine(" --engines Name1,Name2,... Only benchmark these registered engines (default: all)");
Console.Error.WriteLine(" --csv <path> Write a flat CSV of all results");
Console.Error.WriteLine(" --salvage-rate <0..1> Fraction of eligible offcut area credited (default 0)");
Console.Error.WriteLine(" --min-salvage-dimension <value> Both offcut dimensions must qualify; 0 disables credit");
Console.Error.WriteLine(" --output <directory> Save valid layouts as .nest plus detailed JSON reports");
Console.Error.WriteLine(" --help Show this message");
}
@@ -172,5 +185,8 @@ static class BenchmarkConsole
public double? PartSpacing;
public List<string> EngineNames = new();
public string CsvPath;
public string OutputDirectory;
public double SalvageRate;
public double MinimumSalvageDimension;
}
}