From ad3443f60da7404e05d2e650d7c5478e4aaccad4 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sat, 24 Oct 2026 20:18:56 -0400 Subject: [PATCH] fix(benchmark): carry each nest's clamp rail into benchmark jobs The benchmark built stock from spacing and quadrant only, so engines were scored on clamp-free sheets and baselines were never checked against the clamps they were made with. Stock now comes from NestStockBuilder.FromTemplate, as in desktop Auto Nest: the source plate's rail is resolved for every candidate size (explicit zones are offered unchanged), and baseline plates keep their rail and removed clamps. Co-Authored-By: Claude Opus 5.5 --- OpenNest.Benchmark/BenchmarkJob.cs | 42 +++++++++++++++---- OpenNest.Benchmark/BenchmarkRunner.cs | 9 +--- OpenNest.Benchmark/JobLoader.cs | 34 ++++++++++++--- .../Benchmark/BenchmarkRunnerTests.cs | 39 ++++++++++++++++- docs/performance/engine-benchmarking.md | 2 +- 5 files changed, 102 insertions(+), 24 deletions(-) diff --git a/OpenNest.Benchmark/BenchmarkJob.cs b/OpenNest.Benchmark/BenchmarkJob.cs index 158a20fa..e9b006d6 100644 --- a/OpenNest.Benchmark/BenchmarkJob.cs +++ b/OpenNest.Benchmark/BenchmarkJob.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -37,6 +38,12 @@ namespace OpenNest.Benchmark public double PartSpacing { get; init; } public int Quadrant { get; init; } + /// Source plate's machine clamp rail, resolved for each candidate size as Auto Nest does; null when none. + public ClampRail ClampRail { get; init; } + + /// Source plate's explicit clamp zones when it has no rail, offered unchanged on every size. + public IReadOnlyList ClampZones { get; init; } = Array.Empty(); + /// Saved source-nest setting; manifests have no saved salvage rate and use zero. public double SalvageRate { get; init; } @@ -60,7 +67,8 @@ namespace OpenNest.Benchmark /// (unlimited quantity - the engine under test decides how many of each /// size it actually uses, and how demand splits across plates). The /// engine owns its own multi-plate/size strategy; this harness no - /// longer picks plate sizes on the engine's behalf. + /// longer picks plate sizes on the engine's behalf. Stock comes from + /// the same template builder as desktop Auto Nest, so clamps match it. /// public NestJob BuildNestJob( int maxPlates, @@ -71,14 +79,13 @@ namespace OpenNest.Benchmark 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 - )); + // No options would make the builder offer the template itself; an empty pool stays empty. + var stock = CandidateSizes.Count == 0 + ? Array.Empty() + : NestStockBuilder.FromTemplate( + StockTemplate(), + CandidateSizes.Select(size => new PlateOption { Width = size.Width, Length = size.Length }).ToList() + ); return new NestJob( parts, stock, @@ -90,5 +97,22 @@ namespace OpenNest.Benchmark ) ); } + + /// A plate carrying this job's spacing, quadrant and clamps, sized to the largest candidate. + private Plate StockTemplate() + { + var template = new Plate(CandidateSizes.MaxBy(size => size.Width * size.Length)) + { + PartSpacing = PartSpacing, + EdgeSpacing = EdgeSpacing, + Quadrant = Quadrant, + }; + if (ClampRail != null) + template.SetClampRail(ClampRail); + else + foreach (var zone in ClampZones) + template.ClampZones.Add(zone); + return template; + } } } diff --git a/OpenNest.Benchmark/BenchmarkRunner.cs b/OpenNest.Benchmark/BenchmarkRunner.cs index a1981e5e..06a6e1c3 100644 --- a/OpenNest.Benchmark/BenchmarkRunner.cs +++ b/OpenNest.Benchmark/BenchmarkRunner.cs @@ -126,14 +126,7 @@ namespace OpenNest.Benchmark .BaselinePlateRuns.Select( (run, index) => { - var stock = new NestPlateStock( - $"baseline-{index}", - run.Plate.Size, - 1, - run.Plate.PartSpacing, - run.Plate.EdgeSpacing, - run.Plate.Quadrant - ); + var stock = DrawingJobMapper.FromPlate($"baseline-{index}", run.Plate, 1); var placements = run .Parts.Select(part => { diff --git a/OpenNest.Benchmark/JobLoader.cs b/OpenNest.Benchmark/JobLoader.cs index 875bde57..40d435b9 100644 --- a/OpenNest.Benchmark/JobLoader.cs +++ b/OpenNest.Benchmark/JobLoader.cs @@ -74,6 +74,8 @@ namespace OpenNest.Benchmark EdgeSpacing = template.EdgeSpacing, PartSpacing = partSpacingOverride ?? template.PartSpacing, Quadrant = template.Quadrant, + ClampRail = template.ClampRail, + ClampZones = template.ClampZones, SalvageRate = nest.SalvageRate, BaselinePlateRuns = BuildBaselinePlateRuns(nest, partSpacingOverride), Requests = requests, @@ -182,23 +184,45 @@ namespace OpenNest.Benchmark plate.EdgeSpacing.Top ), }; + // Clamps are part of the sheet the layout was made on. + if (plate.ClampRail != null) + validationPlate.SetClampRail(plate.ClampRail, plate.RemovedClamps); + else + foreach (var zone in plate.ClampZones) + validationPlate.ClampZones.Add(zone); 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 - ) + private static ( + Spacing EdgeSpacing, + double PartSpacing, + int Quadrant, + ClampRail ClampRail, + IReadOnlyList ClampZones + ) ResolvePlateTemplate(Nest nest) { var source = nest.Plates?.FirstOrDefault(); if (source != null) - return (source.EdgeSpacing, source.PartSpacing, source.Quadrant); + return ( + source.EdgeSpacing, + source.PartSpacing, + source.Quadrant, + source.ClampRail, + source.ClampRail == null ? source.ClampZones.ToList() : Array.Empty() + ); var defaults = nest.PlateDefaults; - return (defaults.EdgeSpacing, defaults.PartSpacing, defaults.Quadrant); + return ( + defaults.EdgeSpacing, + defaults.PartSpacing, + defaults.Quadrant, + defaults.ClampRail, + Array.Empty() + ); } private static List ResolveSheetSizes(Nest nest) diff --git a/OpenNest.Tests/Benchmark/BenchmarkRunnerTests.cs b/OpenNest.Tests/Benchmark/BenchmarkRunnerTests.cs index 5c5602bd..d75f722e 100644 --- a/OpenNest.Tests/Benchmark/BenchmarkRunnerTests.cs +++ b/OpenNest.Tests/Benchmark/BenchmarkRunnerTests.cs @@ -51,7 +51,8 @@ public sealed class BenchmarkRunnerTests : IDisposable int requiredQuantity = 1, int partCount = 1, double partSpacing = 0, - double partRotation = 0 + double partRotation = 0, + ClampRail clampRail = null ) { var path = Path.Combine(_dir, name + ".nest"); @@ -72,6 +73,8 @@ public sealed class BenchmarkRunnerTests : IDisposable var nest = new Nest(name) { SalvageRate = salvageRate }; nest.Drawings.Add(drawing); var plate = new Plate(10, 10) { Quantity = plateQuantity, PartSpacing = partSpacing }; + if (clampRail != null) + plate.SetClampRail(clampRail); for (var index = 0; index < partCount; index++) { var part = Part.CreateAtOrigin(drawing); @@ -114,6 +117,40 @@ public sealed class BenchmarkRunnerTests : IDisposable Assert.Equal(0, nestJob.Options.MinimumSalvageDimension); } + [Fact] + public void NestJobResolvesSourceClampRailForEveryCandidateSize() + { + var rail = new ClampRail(1, 0.5, new[] { 5.0, 15.0 }); + var job = Assert.Single( + JobLoader.Load( + WriteBaselineNest("clamps", clampRail: rail), + new[] { new OpenNest.Geometry.Size(10, 10), new OpenNest.Geometry.Size(10, 20) } + ) + ); + + var stock = job.BuildNestJob(maxPlates: 1).Plates; + + // Clamps belong to the machine: the longer sheet reaches the second clamp. + Assert.Equal(new[] { 1, 2 }, stock.Select(sheet => sheet.ClampZones.Count)); + Assert.Equal(4.5, stock[0].ClampZones[0].Bounds.X); + } + + [Fact] + public void Run_RejectsBaselineLayoutOnAClamp() + { + var results = BenchmarkRunner.Run( + JobLoader.Load( + WriteBaselineNest("clamped-baseline", clampRail: new ClampRail(1, 0.5, new[] { 1.0 })) + ), + FakeEngines(new ConcurrencyProbe(), 0), + maxParallelism: 1 + ); + + var baseline = Assert.Single(results.Where(result => result.EngineName == "Baseline")); + Assert.False(baseline.Valid); + Assert.Contains(baseline.Violations, violation => violation.Contains("clamp")); + } + [Fact] public void Run_ScoresTheOriginalBaselineLayoutAlongsideEngines() { diff --git a/docs/performance/engine-benchmarking.md b/docs/performance/engine-benchmarking.md index a0bcee77..cc382141 100644 --- a/docs/performance/engine-benchmarking.md +++ b/docs/performance/engine-benchmarking.md @@ -7,6 +7,6 @@ dotnet run --project OpenNest.Benchmark -- ./benchmark-jobs \ --sheet-sizes 48x96,60x120,72x120 --engines Irregular,Rectangles --csv results.csv ``` -Supply `--sheet-sizes` for an independent candidate pool; otherwise a saved nest offers only its original sheet sizes, which can bias comparisons. The default `--parallel 3` runs solves concurrently and confounds per-engine timing; use `--parallel 1` for timing comparisons. `--progress` logs each solve's start, engine progress, and finish. Retain input hashes, exact commands, validity, fulfillment, costs, and raw results outside source control when comparing revisions; see [fill performance verification](fill-verification.md). +Supply `--sheet-sizes` for an independent candidate pool; otherwise a saved nest offers only its original sheet sizes, which can bias comparisons. A saved nest's first plate also supplies spacing, quadrant and clamps: its machine clamp rail is resolved for every candidate size, as desktop Auto Nest does, and its baseline layout is checked against the clamps it was made with. Manifests have no clamps. A saved nest records its salvage rate, but salvage credit applies only with a positive `--min-salvage-dimension`. Without it, every complete one-sheet layout costs the same, as in desktop Auto Nest on a single plate size. The default `--parallel 3` runs solves concurrently and confounds per-engine timing; use `--parallel 1` for timing comparisons. `--progress` logs each solve's start, engine progress, and finish. Retain input hashes, exact commands, validity, fulfillment, costs, and raw results outside source control when comparing revisions; see [fill performance verification](fill-verification.md). Custom engines are DLLs implementing `INestingEngine` with a public parameterless constructor, loaded from `Engines/` beside the benchmark executable. See [nesting engines](../nesting-engines.md) for built-ins and engine-change rules. Run `dotnet run --project OpenNest.Benchmark -- --help` for all options.