diff --git a/OpenNest.Benchmark/BenchmarkRunner.cs b/OpenNest.Benchmark/BenchmarkRunner.cs index 5f0c2cf..0273782 100644 --- a/OpenNest.Benchmark/BenchmarkRunner.cs +++ b/OpenNest.Benchmark/BenchmarkRunner.cs @@ -21,7 +21,7 @@ namespace OpenNest.Benchmark private const int MaxPlates = 40; /// Wall-clock budget for one engine solving one job. - private static readonly TimeSpan Timeout = TimeSpan.FromMinutes(5); + private static readonly TimeSpan SolveTimeout = TimeSpan.FromMinutes(5); public static List Run(List jobs, IReadOnlyList engines) { @@ -40,14 +40,14 @@ namespace OpenNest.Benchmark private static JobResult RunOne(BenchmarkJob job, NestingEngineInfo engineInfo) { - var nestJob = job.BuildNestJob(MaxPlates); var requested = job.TotalRequestedQuantity; var sw = Stopwatch.StartNew(); try { + var nestJob = job.BuildNestJob(MaxPlates); var engine = engineInfo.Factory(); - using var cts = new CancellationTokenSource(Timeout); + using var cts = new CancellationTokenSource(SolveTimeout); var jobResult = engine.Solve(nestJob, null, cts.Token); var materialized = NestResultMaterializer.Materialize(nestJob, jobResult); @@ -55,7 +55,12 @@ namespace OpenNest.Benchmark .Select(plate => (Plate: plate, Parts: plate.Parts.ToList())) .ToList(); - var validation = NestValidator.Validate(plateRuns, job); + var requirements = job.Requests.ToDictionary( + r => materialized.DrawingsByPartId[r.Drawing.Id.ToString()], + r => (r.Drawing.Name, r.Quantity), + ReferenceEqualityComparer.Instance); + + var validation = NestValidator.Validate(plateRuns, requirements); var totalPlaced = plateRuns.Sum(pr => pr.Parts.Count); var placedArea = validation.Valid ? plateRuns.Sum(pr => pr.Parts.Sum(p => p.BaseDrawing.Area)) : 0; var plateArea = plateRuns.Sum(pr => pr.Plate.Area()); @@ -92,7 +97,7 @@ namespace OpenNest.Benchmark Valid = false, PartsRequested = requested, ElapsedMs = sw.ElapsedMilliseconds, - Error = $"Timed out after {Timeout.TotalMinutes:F0} minute(s)", + Error = $"Timed out after {SolveTimeout.TotalMinutes:F0} minute(s)", }; } catch (Exception ex) diff --git a/OpenNest.Benchmark/NestValidator.cs b/OpenNest.Benchmark/NestValidator.cs index bf0717c..add185f 100644 --- a/OpenNest.Benchmark/NestValidator.cs +++ b/OpenNest.Benchmark/NestValidator.cs @@ -25,7 +25,14 @@ namespace OpenNest.Benchmark /// public static class NestValidator { - public static ValidationResult Validate(List<(Plate Plate, List Parts)> plateRuns, BenchmarkJob job) + /// + /// requirements maps each materialized part's BaseDrawing (by reference - materialized + /// Drawing instances are freshly reconstructed per NestResultMaterializer.Materialize, so + /// identity must never be inferred from Name, which is only incidentally seeded from the + /// originating NestJobPart id) to its original quantity limit and display name. + /// + public static ValidationResult Validate(List<(Plate Plate, List Parts)> plateRuns, + IReadOnlyDictionary requirements) { var result = new ValidationResult(); var allParts = plateRuns.SelectMany(pr => pr.Parts).ToList(); @@ -33,38 +40,33 @@ namespace OpenNest.Benchmark if (allParts.Count == 0) return result; - ValidateQuantities(allParts, job, result); + ValidateQuantities(allParts, requirements, result); foreach (var (plate, parts) in plateRuns) { if (parts.Count == 0) continue; - ValidateBounds(parts, plate, result); + ValidateBounds(parts, plate, requirements, result); ValidateAreaBudget(parts, plate, result); - ValidateSpacing(parts, plate.PartSpacing, result); + ValidateSpacing(parts, plate.PartSpacing, requirements, result); } return result; } - private static void ValidateQuantities(List parts, BenchmarkJob job, ValidationResult result) + private static void ValidateQuantities(List parts, + IReadOnlyDictionary requirements, ValidationResult result) { - // Materialized parts reference freshly reconstructed Drawing objects (NestResultMaterializer - // rebuilds them via DrawingJobMapper.CreateDrawing, which sets the materialized Drawing's Name - // to the originating NestJobPart id - a fresh, unrelated Drawing.Id gets auto-generated instead). - // BuildNestJob sets each NestJobPart's id to the original Drawing.Id.ToString(), so that string - - // materialized as BaseDrawing.Name - is the stable identity across the materialization boundary. - var allowed = job.Requests.ToDictionary(r => r.Drawing.Id.ToString(), r => (r.Quantity, r.Drawing.Name)); var placedCounts = parts - .GroupBy(p => p.BaseDrawing.Name) + .GroupBy(p => p.BaseDrawing, ReferenceEqualityComparer.Instance) .ToDictionary(g => g.Key, g => g.Count()); - foreach (var (partId, placed) in placedCounts) + foreach (var (drawing, placed) in placedCounts) { - if (!allowed.TryGetValue(partId, out var requirement)) + if (!requirements.TryGetValue(drawing, out var requirement)) { - result.Violations.Add($"Placed drawing id={partId} which was not requested for this job"); + result.Violations.Add($"Placed drawing '{drawing.Name}' which was not requested for this job"); continue; } @@ -76,7 +78,8 @@ namespace OpenNest.Benchmark } } - private static void ValidateBounds(List parts, Plate plate, ValidationResult result) + private static void ValidateBounds(List parts, Plate plate, + IReadOnlyDictionary requirements, ValidationResult result) { var workArea = plate.WorkArea(); @@ -92,7 +95,7 @@ namespace OpenNest.Benchmark if (outLeft || outBottom || outRight || outTop) { result.Violations.Add( - $"'{part.BaseDrawing.Name}' at ({part.Location.X:F2},{part.Location.Y:F2}) falls outside the work area " + + $"'{DisplayName(part, requirements)}' at ({part.Location.X:F2},{part.Location.Y:F2}) falls outside the work area " + $"of a {plate.Size} plate"); } } @@ -121,7 +124,8 @@ namespace OpenNest.Benchmark } } - private static void ValidateSpacing(List parts, double spacing, ValidationResult result) + private static void ValidateSpacing(List parts, double spacing, + IReadOnlyDictionary requirements, ValidationResult result) { var worldPolygons = new Polygon[parts.Count]; var inflatedPolygons = new Polygon[parts.Count]; @@ -145,12 +149,18 @@ namespace OpenNest.Benchmark if (Collision.HasOverlap(inflatedPolygons[i], worldPolygons[j])) { result.Violations.Add( - $"'{parts[i].BaseDrawing.Name}' and '{parts[j].BaseDrawing.Name}' are closer than the required spacing ({spacing:F3})"); + $"'{DisplayName(parts[i], requirements)}' and '{DisplayName(parts[j], requirements)}' are closer than the required spacing ({spacing:F3})"); } } } } + /// Friendly name for a violation message, falling back to the materialized + /// Drawing's own Name (the raw partId string) if this part wasn't in requirements at all - + /// that mismatch is already reported by ValidateQuantities, so this is display-only. + private static string DisplayName(Part part, IReadOnlyDictionary requirements) => + requirements.TryGetValue(part.BaseDrawing, out var requirement) ? requirement.Name : part.BaseDrawing.Name; + /// /// Extracts a part's perimeter as a world-space polygon, optionally inflated /// outward by the given spacing, mirroring Part.Intersects' own geometry diff --git a/OpenNest.Benchmark/Program.cs b/OpenNest.Benchmark/Program.cs index 0e16ab7..f8e5664 100644 --- a/OpenNest.Benchmark/Program.cs +++ b/OpenNest.Benchmark/Program.cs @@ -137,7 +137,7 @@ static class BenchmarkConsole Console.Error.WriteLine($"Warning: could not parse sheet size '{token}', skipping"); } - return sizes; + return sizes.Distinct().ToList(); } private static void PrintUsage() diff --git a/OpenNest.Engine.Tests/Jobs/FixedStrategyNestingEngineTests.cs b/OpenNest.Engine.Tests/Jobs/FixedStrategyNestingEngineTests.cs index b753099..8297c01 100644 --- a/OpenNest.Engine.Tests/Jobs/FixedStrategyNestingEngineTests.cs +++ b/OpenNest.Engine.Tests/Jobs/FixedStrategyNestingEngineTests.cs @@ -1,4 +1,3 @@ -using OpenNest.CNC; using OpenNest.Geometry; using Xunit; diff --git a/README.md b/README.md index 2b27ab3..3923e0c 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,8 @@ dotnet run --project OpenNest.Benchmark/OpenNest.Benchmark.csproj -- job.nest \ An engine's layout is rejected (scoring zero for that job) if any part falls outside the work area, any two parts are closer than the required spacing, or a drawing gets more parts placed than requested. A run that doesn't finish within its time budget also scores zero, as a timeout. +Custom competitor engines can be added by dropping a DLL implementing `INestingEngine` with a public parameterless constructor into the `Engines/` directory next to the benchmark executable; each one is registered under its own CLR type name. This is a separate plugin contract from the desktop app's `NestEngineRegistry`/`NestEngineBase` (which requires a `(Plate)` constructor) — a `NestEngineBase` plugin dropped into the benchmark's `Engines/` folder is silently skipped, since the benchmark only ever solves whole jobs. + ## Project Structure ```