refactor(benchmark): drive engines through INestingEngine.Solve instead of a hand-rolled multi-plate loop
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -41,50 +41,20 @@ namespace OpenNest.Benchmark
|
|||||||
public int TotalRequestedQuantity => Requests.Sum(r => r.Quantity);
|
public int TotalRequestedQuantity => Requests.Sum(r => r.Quantity);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A blank plate carrying only the job's spacing/quadrant template.
|
/// Builds the whole-job request this job represents: one NestJobPart per
|
||||||
/// MultiPlateNester.CreatePlate copies these settings onto whichever
|
/// requested drawing, and one NestPlateStock per candidate sheet size
|
||||||
/// size it ultimately picks; its Size is only the fallback used when
|
/// (unlimited quantity - the engine under test decides how many of each
|
||||||
/// nothing in the candidate pool fits, so it's set to the largest
|
/// size it actually uses, and how demand splits across plates). The
|
||||||
/// candidate rather than an arbitrary one.
|
/// engine owns its own multi-plate/size strategy; this harness no
|
||||||
|
/// longer picks plate sizes on the engine's behalf.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Plate CreateTemplatePlate()
|
public NestJob BuildNestJob(int maxPlates)
|
||||||
{
|
{
|
||||||
var fallbackSize = CandidateSizes
|
var parts = Requests.Select(r =>
|
||||||
.OrderByDescending(s => s.Width * s.Length)
|
DrawingJobMapper.FromDrawing(r.Drawing.Id.ToString(), r.Drawing, r.Quantity));
|
||||||
.FirstOrDefault();
|
var stock = CandidateSizes.Select(size =>
|
||||||
|
new NestPlateStock(size.ToString(1), size, null, PartSpacing, EdgeSpacing, Quadrant));
|
||||||
return new Plate(fallbackSize)
|
return new NestJob(parts, stock, new NestJobOptions("Default", maxPlates));
|
||||||
{
|
|
||||||
EdgeSpacing = EdgeSpacing,
|
|
||||||
PartSpacing = PartSpacing,
|
|
||||||
Quadrant = Quadrant,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 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.
|
|
||||||
/// </summary>
|
|
||||||
public List<PlateOption> BuildPlateOptions()
|
|
||||||
{
|
|
||||||
return CandidateSizes
|
|
||||||
.Select(s => new PlateOption { Width = s.Width, Length = s.Length, Cost = s.Width * s.Length })
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<NestItem> 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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,23 +7,23 @@ using System.Threading;
|
|||||||
namespace OpenNest.Benchmark
|
namespace OpenNest.Benchmark
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Runs every candidate engine against every job. A job may need several
|
/// Runs every candidate engine against every job. Each engine is a full
|
||||||
/// plates to place everything it asks for; this drives that loop itself,
|
/// INestingEngine: it owns its own plate/size selection and multi-plate
|
||||||
/// since NestEngineBase.Nest() fills exactly one already-sized plate and
|
/// strategy for the whole job, rather than being handed one already-sized
|
||||||
/// has no say in picking its own size. For each plate the loop needs, the
|
/// plate at a time by this harness. A per-run timeout guards against a
|
||||||
/// smallest candidate size that fits the largest still-unplaced drawing is
|
/// runaway or hanging engine — cooperative cancellation, so it reliably
|
||||||
/// chosen via the codebase's own MultiPlateNester.CreatePlate, then the
|
/// stops engines built on NestJobRunner (all four built-ins) but can't
|
||||||
/// engine's Nest() fills that plate with whatever of the remaining items
|
/// forcibly interrupt an engine that never checks its token.
|
||||||
/// fit. This is applied identically to every engine, so no engine gets to
|
|
||||||
/// (or has to) implement sheet-size selection itself.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class BenchmarkRunner
|
public static class BenchmarkRunner
|
||||||
{
|
{
|
||||||
/// <summary>Safety cap so a degenerate engine (placing almost nothing
|
/// <summary>Physical-sheet cap passed to every job's NestJobOptions.MaxPlates.</summary>
|
||||||
/// per plate) can't loop indefinitely.</summary>
|
|
||||||
private const int MaxPlates = 40;
|
private const int MaxPlates = 40;
|
||||||
|
|
||||||
public static List<JobResult> Run(List<BenchmarkJob> jobs, IReadOnlyList<NestEngineInfo> engines)
|
/// <summary>Wall-clock budget for one engine solving one job.</summary>
|
||||||
|
private static readonly TimeSpan Timeout = TimeSpan.FromMinutes(5);
|
||||||
|
|
||||||
|
public static List<JobResult> Run(List<BenchmarkJob> jobs, IReadOnlyList<NestingEngineInfo> engines)
|
||||||
{
|
{
|
||||||
var results = new List<JobResult>(jobs.Count * engines.Count);
|
var results = new List<JobResult>(jobs.Count * engines.Count);
|
||||||
|
|
||||||
@@ -38,60 +38,53 @@ namespace OpenNest.Benchmark
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JobResult RunOne(BenchmarkJob job, NestEngineInfo engineInfo)
|
private static JobResult RunOne(BenchmarkJob job, NestingEngineInfo engineInfo)
|
||||||
{
|
{
|
||||||
var template = job.CreateTemplatePlate();
|
var nestJob = job.BuildNestJob(MaxPlates);
|
||||||
var options = job.BuildPlateOptions();
|
|
||||||
var remaining = job.CreateItems();
|
|
||||||
var requested = job.TotalRequestedQuantity;
|
var requested = job.TotalRequestedQuantity;
|
||||||
|
|
||||||
var plateRuns = new List<(Plate Plate, List<Part> Parts)>();
|
|
||||||
string engineError = null;
|
|
||||||
var sw = Stopwatch.StartNew();
|
var sw = Stopwatch.StartNew();
|
||||||
|
|
||||||
try
|
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
|
EngineName = engineInfo.Name,
|
||||||
.Where(i => i.Quantity > 0)
|
JobName = job.Name,
|
||||||
.OrderByDescending(i => BoundsArea(i))
|
Valid = validation.Valid,
|
||||||
.First();
|
Violations = validation.Violations,
|
||||||
|
PartsPlaced = totalPlaced,
|
||||||
var plate = MultiPlateNester.CreatePlate(template, options, largest.Drawing.Program.BoundingBox());
|
PartsRequested = requested,
|
||||||
var engine = engineInfo.Factory(plate);
|
PlacedArea = placedArea,
|
||||||
var itemsClone = CloneItems(remaining);
|
PlateArea = plateArea,
|
||||||
|
PlatesUsed = plateRuns.Count,
|
||||||
var parts = engine.Nest(itemsClone, null, CancellationToken.None) ?? new List<Part>();
|
SizeBreakdown = sizeBreakdown,
|
||||||
|
ElapsedMs = sw.ElapsedMilliseconds,
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (OperationCanceledException)
|
||||||
{
|
|
||||||
engineError = $"{ex.GetType().Name}: {ex.Message}";
|
|
||||||
}
|
|
||||||
|
|
||||||
sw.Stop();
|
|
||||||
|
|
||||||
if (engineError != null)
|
|
||||||
{
|
{
|
||||||
|
sw.Stop();
|
||||||
return new JobResult
|
return new JobResult
|
||||||
{
|
{
|
||||||
EngineName = engineInfo.Name,
|
EngineName = engineInfo.Name,
|
||||||
@@ -99,53 +92,22 @@ namespace OpenNest.Benchmark
|
|||||||
Valid = false,
|
Valid = false,
|
||||||
PartsRequested = requested,
|
PartsRequested = requested,
|
||||||
ElapsedMs = sw.ElapsedMilliseconds,
|
ElapsedMs = sw.ElapsedMilliseconds,
|
||||||
Error = engineError,
|
Error = $"Timed out after {Timeout.TotalMinutes:F0} minute(s)",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
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
|
|
||||||
{
|
{
|
||||||
EngineName = engineInfo.Name,
|
sw.Stop();
|
||||||
JobName = job.Name,
|
return new JobResult
|
||||||
Valid = validation.Valid,
|
{
|
||||||
Violations = validation.Violations,
|
EngineName = engineInfo.Name,
|
||||||
PartsPlaced = totalPlaced,
|
JobName = job.Name,
|
||||||
PartsRequested = requested,
|
Valid = false,
|
||||||
PlacedArea = placedArea,
|
PartsRequested = requested,
|
||||||
PlateArea = plateArea,
|
ElapsedMs = sw.ElapsedMilliseconds,
|
||||||
PlatesUsed = plateRuns.Count,
|
Error = $"{ex.GetType().Name}: {ex.Message}",
|
||||||
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<NestItem> CloneItems(List<NestItem> 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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user