diff --git a/OpenNest.Benchmark/BenchmarkJob.cs b/OpenNest.Benchmark/BenchmarkJob.cs index 4572242..c3a4140 100644 --- a/OpenNest.Benchmark/BenchmarkJob.cs +++ b/OpenNest.Benchmark/BenchmarkJob.cs @@ -41,50 +41,20 @@ namespace OpenNest.Benchmark public int TotalRequestedQuantity => Requests.Sum(r => r.Quantity); /// - /// A blank plate carrying only the job's spacing/quadrant template. - /// MultiPlateNester.CreatePlate copies these settings onto whichever - /// size it ultimately picks; its Size is only the fallback used when - /// nothing in the candidate pool fits, so it's set to the largest - /// candidate rather than an arbitrary one. + /// Builds the whole-job request this job represents: one NestJobPart per + /// requested drawing, and one NestPlateStock per candidate sheet size + /// (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. /// - public Plate CreateTemplatePlate() + public NestJob BuildNestJob(int maxPlates) { - var fallbackSize = CandidateSizes - .OrderByDescending(s => s.Width * s.Length) - .FirstOrDefault(); - - return new Plate(fallbackSize) - { - EdgeSpacing = EdgeSpacing, - PartSpacing = PartSpacing, - Quadrant = Quadrant, - }; - } - - /// - /// The candidate sizes as PlateOptions for MultiPlateNester.CreatePlate. - /// Cost is area-proportional since no real per-size material pricing is - /// available here - this only affects which size is preferred when more - /// than one candidate fits, favoring the smaller/cheaper sheet. - /// - public List BuildPlateOptions() - { - return CandidateSizes - .Select(s => new PlateOption { Width = s.Width, Length = s.Length, Cost = s.Width * s.Length }) - .ToList(); - } - - public List CreateItems() - { - return Requests.Select(r => new NestItem - { - Drawing = r.Drawing, - Quantity = r.Quantity, - Priority = r.Priority, - StepAngle = r.StepAngle, - RotationStart = r.RotationStart, - RotationEnd = r.RotationEnd, - }).ToList(); + 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)); } } } diff --git a/OpenNest.Benchmark/BenchmarkRunner.cs b/OpenNest.Benchmark/BenchmarkRunner.cs index 90afb12..5f0c2cf 100644 --- a/OpenNest.Benchmark/BenchmarkRunner.cs +++ b/OpenNest.Benchmark/BenchmarkRunner.cs @@ -7,23 +7,23 @@ using System.Threading; namespace OpenNest.Benchmark { /// - /// Runs every candidate engine against every job. A job may need several - /// plates to place everything it asks for; this drives that loop itself, - /// since NestEngineBase.Nest() fills exactly one already-sized plate and - /// has no say in picking its own size. For each plate the loop needs, the - /// smallest candidate size that fits the largest still-unplaced drawing is - /// chosen via the codebase's own MultiPlateNester.CreatePlate, then the - /// engine's Nest() fills that plate with whatever of the remaining items - /// fit. This is applied identically to every engine, so no engine gets to - /// (or has to) implement sheet-size selection itself. + /// Runs every candidate engine against every job. Each engine is a full + /// INestingEngine: it owns its own plate/size selection and multi-plate + /// strategy for the whole job, rather than being handed one already-sized + /// plate at a time by this harness. A per-run timeout guards against a + /// runaway or hanging engine — cooperative cancellation, so it reliably + /// stops engines built on NestJobRunner (all four built-ins) but can't + /// forcibly interrupt an engine that never checks its token. /// public static class BenchmarkRunner { - /// Safety cap so a degenerate engine (placing almost nothing - /// per plate) can't loop indefinitely. + /// Physical-sheet cap passed to every job's NestJobOptions.MaxPlates. private const int MaxPlates = 40; - public static List Run(List jobs, IReadOnlyList engines) + /// Wall-clock budget for one engine solving one job. + private static readonly TimeSpan Timeout = TimeSpan.FromMinutes(5); + + public static List Run(List jobs, IReadOnlyList engines) { var results = new List(jobs.Count * engines.Count); @@ -38,60 +38,53 @@ namespace OpenNest.Benchmark return results; } - private static JobResult RunOne(BenchmarkJob job, NestEngineInfo engineInfo) + private static JobResult RunOne(BenchmarkJob job, NestingEngineInfo engineInfo) { - var template = job.CreateTemplatePlate(); - var options = job.BuildPlateOptions(); - var remaining = job.CreateItems(); + var nestJob = job.BuildNestJob(MaxPlates); var requested = job.TotalRequestedQuantity; - - var plateRuns = new List<(Plate Plate, List Parts)>(); - string engineError = null; var sw = Stopwatch.StartNew(); try { - while (remaining.Any(i => i.Quantity > 0) && plateRuns.Count < MaxPlates) + var engine = engineInfo.Factory(); + using var cts = new CancellationTokenSource(Timeout); + var jobResult = engine.Solve(nestJob, null, cts.Token); + + var materialized = NestResultMaterializer.Materialize(nestJob, jobResult); + var plateRuns = materialized.Nest.Plates + .Select(plate => (Plate: plate, Parts: plate.Parts.ToList())) + .ToList(); + + var validation = NestValidator.Validate(plateRuns, job); + 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()); + + var sizeBreakdown = plateRuns + .GroupBy(pr => pr.Plate.Size.ToString(1)) + .OrderByDescending(g => g.Count()) + .ToDictionary(g => g.Key, g => g.Count()); + + sw.Stop(); + + return new JobResult { - var largest = remaining - .Where(i => i.Quantity > 0) - .OrderByDescending(i => BoundsArea(i)) - .First(); - - var plate = MultiPlateNester.CreatePlate(template, options, largest.Drawing.Program.BoundingBox()); - var engine = engineInfo.Factory(plate); - var itemsClone = CloneItems(remaining); - - var parts = engine.Nest(itemsClone, null, CancellationToken.None) ?? new List(); - - if (parts.Count == 0) - { - // Not even the largest available candidate size could fit - // the current largest remaining part - stop here rather - // than loop forever; whatever's left is reported unplaced. - break; - } - - plateRuns.Add((plate, parts)); - - foreach (var item in remaining) - { - var placed = parts.Count(p => p.BaseDrawing.Id == item.Drawing.Id); - - if (placed > 0) - item.Quantity = System.Math.Max(0, item.Quantity - placed); - } - } + EngineName = engineInfo.Name, + JobName = job.Name, + Valid = validation.Valid, + Violations = validation.Violations, + PartsPlaced = totalPlaced, + PartsRequested = requested, + PlacedArea = placedArea, + PlateArea = plateArea, + PlatesUsed = plateRuns.Count, + SizeBreakdown = sizeBreakdown, + ElapsedMs = sw.ElapsedMilliseconds, + }; } - catch (Exception ex) - { - engineError = $"{ex.GetType().Name}: {ex.Message}"; - } - - sw.Stop(); - - if (engineError != null) + catch (OperationCanceledException) { + sw.Stop(); return new JobResult { EngineName = engineInfo.Name, @@ -99,53 +92,22 @@ namespace OpenNest.Benchmark Valid = false, PartsRequested = requested, ElapsedMs = sw.ElapsedMilliseconds, - Error = engineError, + Error = $"Timed out after {Timeout.TotalMinutes:F0} minute(s)", }; } - - var validation = NestValidator.Validate(plateRuns, job); - 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()); - - var sizeBreakdown = plateRuns - .GroupBy(pr => pr.Plate.Size.ToString(1)) - .OrderByDescending(g => g.Count()) - .ToDictionary(g => g.Key, g => g.Count()); - - return new JobResult + catch (Exception ex) { - EngineName = engineInfo.Name, - JobName = job.Name, - Valid = validation.Valid, - Violations = validation.Violations, - PartsPlaced = totalPlaced, - PartsRequested = requested, - PlacedArea = placedArea, - PlateArea = plateArea, - PlatesUsed = plateRuns.Count, - SizeBreakdown = sizeBreakdown, - ElapsedMs = sw.ElapsedMilliseconds, - }; - } - - private static double BoundsArea(NestItem item) - { - var bb = item.Drawing.Program.BoundingBox(); - return bb.Width * bb.Length; - } - - private static List CloneItems(List items) - { - return items.Select(i => new NestItem - { - Drawing = i.Drawing, - Quantity = i.Quantity, - Priority = i.Priority, - StepAngle = i.StepAngle, - RotationStart = i.RotationStart, - RotationEnd = i.RotationEnd, - }).ToList(); + sw.Stop(); + return new JobResult + { + EngineName = engineInfo.Name, + JobName = job.Name, + Valid = false, + PartsRequested = requested, + ElapsedMs = sw.ElapsedMilliseconds, + Error = $"{ex.GetType().Name}: {ex.Message}", + }; + } } } }