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}",
+ };
+ }
}
}
}