diff --git a/OpenNest.Benchmark/BenchmarkJob.cs b/OpenNest.Benchmark/BenchmarkJob.cs
index f83761d..4572242 100644
--- a/OpenNest.Benchmark/BenchmarkJob.cs
+++ b/OpenNest.Benchmark/BenchmarkJob.cs
@@ -20,28 +20,40 @@ namespace OpenNest.Benchmark
}
///
- /// An immutable specification for one benchmark job: a set of drawings/quantities
- /// to be nested onto a plate of a given size. Every engine under test gets a fresh
- /// Plate and NestItem list built from this spec via CreatePlate()/CreateItems(),
- /// so one engine's run can never leak mutated state into another's.
+ /// An immutable specification for one benchmark job: the full set of
+ /// drawings/quantities that must be nested, and the pool of sheet sizes the
+ /// engine may draw from while doing it. A single run may use several
+ /// plates - possibly of different sizes - to place everything, the same
+ /// way a real production job spreads across whatever plates it needs
+ /// rather than being handed one fixed-size sheet.
///
public class BenchmarkJob
{
public string SourceFile { get; init; }
- public string SheetSizeLabel { get; init; }
- public Size PlateSize { get; init; }
+ public List CandidateSizes { get; init; }
public Spacing EdgeSpacing { get; init; }
public double PartSpacing { get; init; }
public int Quadrant { get; init; }
public List Requests { get; init; }
- public string Name => $"{Path.GetFileNameWithoutExtension(SourceFile)} [{SheetSizeLabel}]";
+ public string Name => Path.GetFileNameWithoutExtension(SourceFile);
public int TotalRequestedQuantity => Requests.Sum(r => r.Quantity);
- public Plate CreatePlate()
+ ///
+ /// 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.
+ ///
+ public Plate CreateTemplatePlate()
{
- return new Plate(PlateSize)
+ var fallbackSize = CandidateSizes
+ .OrderByDescending(s => s.Width * s.Length)
+ .FirstOrDefault();
+
+ return new Plate(fallbackSize)
{
EdgeSpacing = EdgeSpacing,
PartSpacing = PartSpacing,
@@ -49,6 +61,19 @@ namespace OpenNest.Benchmark
};
}
+ ///
+ /// 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
diff --git a/OpenNest.Benchmark/BenchmarkRunner.cs b/OpenNest.Benchmark/BenchmarkRunner.cs
index 38c7dc7..90afb12 100644
--- a/OpenNest.Benchmark/BenchmarkRunner.cs
+++ b/OpenNest.Benchmark/BenchmarkRunner.cs
@@ -1,4 +1,3 @@
-using OpenNest.Geometry;
using System;
using System.Collections.Generic;
using System.Diagnostics;
@@ -8,13 +7,22 @@ using System.Threading;
namespace OpenNest.Benchmark
{
///
- /// Runs every candidate engine against every job. Each (job, engine) pair gets
- /// its own freshly-built Plate and NestItem list (via BenchmarkJob.CreatePlate/
- /// CreateItems), so no engine can see another's mutated state and no job can
- /// leak partial state into the next run of the same engine.
+ /// 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.
///
public static class BenchmarkRunner
{
+ /// Safety cap so a degenerate engine (placing almost nothing
+ /// per plate) can't loop indefinitely.
+ private const int MaxPlates = 40;
+
public static List Run(List jobs, IReadOnlyList engines)
{
var results = new List(jobs.Count * engines.Count);
@@ -32,22 +40,58 @@ namespace OpenNest.Benchmark
private static JobResult RunOne(BenchmarkJob job, NestEngineInfo engineInfo)
{
- var plate = job.CreatePlate();
- var items = job.CreateItems();
+ var template = job.CreateTemplatePlate();
+ var options = job.BuildPlateOptions();
+ var remaining = job.CreateItems();
var requested = job.TotalRequestedQuantity;
+ var plateRuns = new List<(Plate Plate, List Parts)>();
+ string engineError = null;
var sw = Stopwatch.StartNew();
- List parts;
try
{
- var engine = engineInfo.Factory(plate);
- parts = engine.Nest(items, null, CancellationToken.None) ?? new List();
+ while (remaining.Any(i => i.Quantity > 0) && plateRuns.Count < MaxPlates)
+ {
+ 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);
+ }
+ }
}
catch (Exception ex)
{
- sw.Stop();
+ engineError = $"{ex.GetType().Name}: {ex.Message}";
+ }
+ sw.Stop();
+
+ if (engineError != null)
+ {
return new JobResult
{
EngineName = engineInfo.Name,
@@ -55,19 +99,19 @@ namespace OpenNest.Benchmark
Valid = false,
PartsRequested = requested,
ElapsedMs = sw.ElapsedMilliseconds,
- Error = $"{ex.GetType().Name}: {ex.Message}",
+ Error = engineError,
};
}
- sw.Stop();
+ 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 validation = NestValidator.Validate(parts, plate, job);
-
- // Matches Plate.Utilization(): full sheet area, not just the cuttable
- // work area, since that's what the material actually costs.
- var plateArea = plate.Area();
- var placedArea = validation.Valid ? parts.Sum(p => p.BaseDrawing.Area) : 0;
- var usedBox = parts.Count > 0 ? parts.GetBoundingBox() : Box.Empty;
+ var sizeBreakdown = plateRuns
+ .GroupBy(pr => pr.Plate.Size.ToString(1))
+ .OrderByDescending(g => g.Count())
+ .ToDictionary(g => g.Key, g => g.Count());
return new JobResult
{
@@ -75,13 +119,33 @@ namespace OpenNest.Benchmark
JobName = job.Name,
Valid = validation.Valid,
Violations = validation.Violations,
- PartsPlaced = parts.Count,
+ PartsPlaced = totalPlaced,
PartsRequested = requested,
PlacedArea = placedArea,
PlateArea = plateArea,
- UsedBoundingBoxArea = usedBox.Width * usedBox.Length,
+ 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();
+ }
}
}
diff --git a/OpenNest.Benchmark/JobLoader.cs b/OpenNest.Benchmark/JobLoader.cs
index 0cff2e5..7911c77 100644
--- a/OpenNest.Benchmark/JobLoader.cs
+++ b/OpenNest.Benchmark/JobLoader.cs
@@ -8,11 +8,12 @@ using System.Linq;
namespace OpenNest.Benchmark
{
///
- /// Builds BenchmarkJobs from .nest files on disk. Fully generic: works on any
- /// valid .nest file, using whatever drawings/quantities/plate settings it contains.
- /// Optionally sweeps a fixed list of sheet sizes instead of the sizes embedded
- /// in the file, so the same drawing set can be benchmarked across a standard
- /// sheet-size lineup.
+ /// Builds BenchmarkJobs from .nest files on disk. Fully generic: works on
+ /// any valid .nest file, using whatever drawings/quantities/plate settings
+ /// it contains. One job per file, carrying the full pool of candidate
+ /// sheet sizes the engine may use across the whole nest - by default the
+ /// distinct sizes already present in that file, or a fixed override list
+ /// (e.g. a standard sheet-size lineup) applied to every file.
///
public static class JobLoader
{
@@ -46,22 +47,18 @@ namespace OpenNest.Benchmark
var template = ResolvePlateTemplate(nest);
var sizes = sheetSizeOverrides != null && sheetSizeOverrides.Count > 0
- ? sheetSizeOverrides
+ ? sheetSizeOverrides.ToList()
: ResolveSheetSizes(nest);
- foreach (var size in sizes)
+ jobs.Add(new BenchmarkJob
{
- jobs.Add(new BenchmarkJob
- {
- SourceFile = file,
- SheetSizeLabel = size.ToString(1),
- PlateSize = size,
- EdgeSpacing = template.EdgeSpacing,
- PartSpacing = partSpacingOverride ?? template.PartSpacing,
- Quadrant = template.Quadrant,
- Requests = requests,
- });
- }
+ SourceFile = file,
+ CandidateSizes = sizes,
+ EdgeSpacing = template.EdgeSpacing,
+ PartSpacing = partSpacingOverride ?? template.PartSpacing,
+ Quadrant = template.Quadrant,
+ Requests = requests,
+ });
}
return jobs;
diff --git a/OpenNest.Benchmark/JobResult.cs b/OpenNest.Benchmark/JobResult.cs
index afa4298..9b09599 100644
--- a/OpenNest.Benchmark/JobResult.cs
+++ b/OpenNest.Benchmark/JobResult.cs
@@ -3,8 +3,11 @@ using System.Collections.Generic;
namespace OpenNest.Benchmark
{
///
- /// Outcome of running one engine against one job. An invalid or crashed run
- /// always scores zero utilization for that job, per the benchmark rules.
+ /// Outcome of running one engine against one job. A job may span several
+ /// plates (PlatesUsed, SizeBreakdown), since the engine may need more than
+ /// one sheet - possibly of different sizes - to place everything asked of
+ /// it. An invalid or crashed run always scores zero utilization, per the
+ /// benchmark rules.
///
public class JobResult
{
@@ -17,11 +20,16 @@ namespace OpenNest.Benchmark
public int PartsRequested { get; init; }
public double PlacedArea { get; init; }
public double PlateArea { get; init; }
- public double UsedBoundingBoxArea { get; init; }
+ public int PlatesUsed { get; init; }
+ public Dictionary SizeBreakdown { get; init; } = new();
public long ElapsedMs { get; init; }
public bool Crashed => Error != null;
public bool FullyPlaced => Valid && PartsRequested > 0 && PartsPlaced >= PartsRequested;
+
+ /// Aggregate utilization across every plate the engine used:
+ /// total placed drawing area over total plate area, matching
+ /// Plate.Utilization()'s per-plate definition summed across the job.
public double Utilization => Valid && PlateArea > 0 ? PlacedArea / PlateArea : 0;
}
}
diff --git a/OpenNest.Benchmark/NestValidator.cs b/OpenNest.Benchmark/NestValidator.cs
index 4b0b276..baec8cc 100644
--- a/OpenNest.Benchmark/NestValidator.cs
+++ b/OpenNest.Benchmark/NestValidator.cs
@@ -13,52 +13,41 @@ namespace OpenNest.Benchmark
}
///
- /// Validates a placed layout against the benchmark rules: every part must lie
- /// within the plate's work area, every pair of parts must be at least
- /// PartSpacing apart, and no drawing may have more parts placed than requested.
- /// Geometry checks work on arbitrary (concave, holed) polygons by reusing the
- /// same world-space extraction Part.Intersects uses internally, so no engine
- /// gets an advantage or penalty from shape complexity.
+ /// Validates a (possibly multi-plate) placed layout against the benchmark
+ /// rules: on every plate, every part must lie within that plate's work
+ /// area and every pair of parts must be at least PartSpacing apart; across
+ /// all plates combined, no drawing may have more parts placed than
+ /// requested (the quantity limit is a property of the whole order, not of
+ /// any one plate). Geometry checks work on arbitrary (concave, holed)
+ /// polygons by reusing the same world-space extraction Part.Intersects
+ /// uses internally, so no engine gets an advantage or penalty from shape
+ /// complexity.
///
public static class NestValidator
{
- public static ValidationResult Validate(List parts, Plate plate, BenchmarkJob job)
+ public static ValidationResult Validate(List<(Plate Plate, List Parts)> plateRuns, BenchmarkJob job)
{
var result = new ValidationResult();
+ var allParts = plateRuns.SelectMany(pr => pr.Parts).ToList();
- if (parts == null || parts.Count == 0)
+ if (allParts.Count == 0)
return result;
- ValidateQuantities(parts, job, result);
- ValidateBounds(parts, plate, result);
- ValidateAreaBudget(parts, plate, result);
- ValidateSpacing(parts, plate.PartSpacing, result);
+ ValidateQuantities(allParts, job, result);
+
+ foreach (var (plate, parts) in plateRuns)
+ {
+ if (parts.Count == 0)
+ continue;
+
+ ValidateBounds(parts, plate, result);
+ ValidateAreaBudget(parts, plate, result);
+ ValidateSpacing(parts, plate.PartSpacing, result);
+ }
return result;
}
- ///
- /// Hard mathematical backstop: non-overlapping parts confined to the work
- /// area can never have a combined area greater than the work area itself.
- /// This catches overlap that the polygon-based ValidateSpacing check can
- /// miss - Collision.HasOverlap (and Part.Intersects, which uses the same
- /// algorithm) has been observed to return false negatives on real,
- /// complex production geometry, so this check does not depend on it.
- ///
- private static void ValidateAreaBudget(List parts, Plate plate, ValidationResult result)
- {
- var workArea = plate.WorkArea();
- var budget = workArea.Width * workArea.Length;
- var placedArea = parts.Sum(p => p.BaseDrawing.Area);
-
- if (placedArea > budget + Tolerance.Epsilon)
- {
- result.Violations.Add(
- $"Combined placed area ({placedArea:F2}) exceeds the work area ({budget:F2}) - " +
- "parts must overlap even though the polygon overlap check did not flag a pair");
- }
- }
-
private static void ValidateQuantities(List parts, BenchmarkJob job, ValidationResult result)
{
var allowed = job.Requests.ToDictionary(r => r.Drawing.Id, r => r.Quantity);
@@ -77,7 +66,7 @@ namespace OpenNest.Benchmark
if (placed > max)
{
var name = parts.First(p => p.BaseDrawing.Id == drawingId).BaseDrawing.Name;
- result.Violations.Add($"'{name}': placed {placed} but only {max} were requested");
+ result.Violations.Add($"'{name}': placed {placed} across all plates but only {max} were requested");
}
}
}
@@ -98,11 +87,35 @@ 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");
+ $"'{part.BaseDrawing.Name}' at ({part.Location.X:F2},{part.Location.Y:F2}) falls outside the work area " +
+ $"of a {plate.Size} plate");
}
}
}
+ ///
+ /// Hard mathematical backstop: non-overlapping parts confined to the
+ /// work area can never have a combined area greater than the work
+ /// area itself. This catches overlap that the polygon-based
+ /// ValidateSpacing check can miss - Collision.HasOverlap (and
+ /// Part.Intersects, which uses the same algorithm) has been observed
+ /// to return false negatives on real, complex production geometry, so
+ /// this check does not depend on it.
+ ///
+ private static void ValidateAreaBudget(List parts, Plate plate, ValidationResult result)
+ {
+ var workArea = plate.WorkArea();
+ var budget = workArea.Width * workArea.Length;
+ var placedArea = parts.Sum(p => p.BaseDrawing.Area);
+
+ if (placedArea > budget + Tolerance.Epsilon)
+ {
+ result.Violations.Add(
+ $"Combined placed area ({placedArea:F2}) on a {plate.Size} plate exceeds its work area ({budget:F2}) - " +
+ "parts must overlap even though the polygon overlap check did not flag a pair");
+ }
+ }
+
private static void ValidateSpacing(List parts, double spacing, ValidationResult result)
{
var worldPolygons = new Polygon[parts.Count];
diff --git a/OpenNest.Benchmark/Program.cs b/OpenNest.Benchmark/Program.cs
index b3954bb..4a40bc1 100644
--- a/OpenNest.Benchmark/Program.cs
+++ b/OpenNest.Benchmark/Program.cs
@@ -57,6 +57,13 @@ static class BenchmarkConsole
}
Console.WriteLine($"Loaded {jobs.Count} job(s) from '{options.InputPath}'");
+
+ foreach (var job in jobs)
+ {
+ var sizes = string.Join(", ", job.CandidateSizes.Select(s => s.ToString(1)));
+ Console.WriteLine($" {job.Name}: {job.Requests.Count} drawing(s), {job.TotalRequestedQuantity} part(s) requested, candidate sizes: {sizes}");
+ }
+
Console.WriteLine($"Engines: {string.Join(", ", engines.Select(e => e.Name))}");
var results = BenchmarkRunner.Run(jobs, engines);
@@ -133,17 +140,21 @@ static class BenchmarkConsole
{
Console.Error.WriteLine("OpenNest.Benchmark - compare registered nesting engines on a set of .nest files");
Console.Error.WriteLine();
- Console.Error.WriteLine("For each .nest file, every drawing with quantity > 0 is nested (mixed together)");
- Console.Error.WriteLine("onto a fresh plate per sheet size, once per registered engine. Scoring: material");
- Console.Error.WriteLine("utilization first, then (if everything requested was placed) a smaller used");
- Console.Error.WriteLine("bounding box as the tie-break. An invalid layout (out of bounds, overlapping,");
- Console.Error.WriteLine("or over-quantity) scores zero for that job.");
+ Console.Error.WriteLine("For each .nest file, every drawing with quantity > 0 is nested (mixed together),");
+ Console.Error.WriteLine("once per registered engine. This is a full nest, not a single fixed-size plate:");
+ Console.Error.WriteLine("as many plates as needed are created, one at a time, each sized by picking the");
+ Console.Error.WriteLine("smallest candidate sheet size that fits the largest still-unplaced drawing -");
+ Console.Error.WriteLine("applied identically to every engine, since Nest() itself has no say over its");
+ Console.Error.WriteLine("own plate's size. Scoring: aggregate material utilization across every plate");
+ Console.Error.WriteLine("used, then (if everything requested was placed) fewer plates as the tie-break.");
+ Console.Error.WriteLine("An invalid layout (out of bounds, overlapping, or over-quantity) scores zero.");
Console.Error.WriteLine();
Console.Error.WriteLine("Usage:");
Console.Error.WriteLine(" OpenNest.Benchmark [options]");
Console.Error.WriteLine();
Console.Error.WriteLine("Options:");
- Console.Error.WriteLine(" --sheet-sizes W1xL1,W2xL2,... Sweep these plate sizes instead of each file's own");
+ Console.Error.WriteLine(" --sheet-sizes W1xL1,W2xL2,... Candidate sheet-size pool for the whole nest");
+ Console.Error.WriteLine(" (default: the distinct sizes already in each file)");
Console.Error.WriteLine(" --spacing Override part spacing for every job");
Console.Error.WriteLine(" --engines Name1,Name2,... Only benchmark these registered engines (default: all)");
Console.Error.WriteLine(" --csv Write a flat CSV of all results");
diff --git a/OpenNest.Benchmark/Report.cs b/OpenNest.Benchmark/Report.cs
index b2189c8..12fcc21 100644
--- a/OpenNest.Benchmark/Report.cs
+++ b/OpenNest.Benchmark/Report.cs
@@ -9,9 +9,10 @@ namespace OpenNest.Benchmark
{
///
/// Console + CSV reporting for benchmark results. Ranking rule per job:
- /// valid beats invalid; higher utilization wins; if utilization ties and both
- /// engines fully placed every requested part, the smaller used-bounding-box
- /// (more compact remnant) wins. Ties beyond that are a shared win.
+ /// valid beats invalid; higher aggregate utilization wins; if utilization
+ /// ties and both engines fully placed every requested part, fewer plates
+ /// used wins (the multi-plate analogue of "smaller remnant" - both are
+ /// proxies for wasting less material). Ties beyond that are a shared win.
///
public static class Report
{
@@ -27,7 +28,7 @@ namespace OpenNest.Benchmark
var ranked = jobGroup.OrderBy(r => r, Comparer.Create(Compare)).ToList();
var best = ranked.Count > 0 ? ranked[0] : null;
- Console.WriteLine($"{"Engine",-16} {"Result",-9} {"Parts",-10} {"Util%",-8} {"Remnant",-12} {"Time(ms)",-9} Notes");
+ Console.WriteLine($"{"Engine",-16} {"Result",-9} {"Parts",-10} {"Util%",-8} {"Plates",-18} {"Time(ms)",-9} Notes");
foreach (var r in ranked)
{
@@ -36,10 +37,10 @@ namespace OpenNest.Benchmark
var status = r.Crashed ? "CRASH" : r.Valid ? "ok" : "INVALID";
var partsCol = $"{r.PartsPlaced}/{r.PartsRequested}";
var utilCol = r.Valid ? $"{r.Utilization * 100:F1}" : "-";
- var remnantCol = r.Valid ? $"{r.UsedBoundingBoxArea:F0}" : "-";
+ var platesCol = r.PlatesUsed > 0 ? $"{r.PlatesUsed} ({SizeSummary(r.SizeBreakdown)})" : "-";
var notes = r.Crashed ? r.Error : string.Join("; ", r.Violations.Take(2));
- Console.WriteLine($"{marker}{r.EngineName,-15} {status,-9} {partsCol,-10} {utilCol,-8} {remnantCol,-12} {r.ElapsedMs,-9} {notes}");
+ Console.WriteLine($"{marker}{r.EngineName,-15} {status,-9} {partsCol,-10} {utilCol,-8} {platesCol,-18} {r.ElapsedMs,-9} {notes}");
}
}
}
@@ -57,7 +58,9 @@ namespace OpenNest.Benchmark
Jobs = g.Count(),
Valid = g.Count(r => r.Valid),
Crashed = g.Count(r => r.Crashed),
+ FullyPlaced = g.Count(r => r.FullyPlaced),
TotalUtilization = g.Sum(r => r.Utilization),
+ TotalPlates = g.Sum(r => r.PlatesUsed),
TotalTimeMs = g.Sum(r => r.ElapsedMs),
})
.OrderByDescending(e => e.TotalUtilization)
@@ -65,35 +68,43 @@ namespace OpenNest.Benchmark
var wins = CountWins(results);
- Console.WriteLine($"{"Engine",-16} {"Jobs",-6} {"Valid",-7} {"Crashed",-8} {"Wins",-6} {"AvgUtil%",-10} {"TotalTime(ms)",-14}");
+ Console.WriteLine($"{"Engine",-16} {"Jobs",-6} {"Valid",-7} {"Complete",-9} {"Wins",-6} {"AvgUtil%",-10} {"Plates",-8} {"TotalTime(ms)",-14}");
foreach (var e in byEngine)
{
var avgUtil = e.Jobs > 0 ? e.TotalUtilization / e.Jobs * 100 : 0;
var winCount = wins.TryGetValue(e.Engine, out var w) ? w : 0;
- Console.WriteLine($"{e.Engine,-16} {e.Jobs,-6} {e.Valid,-7} {e.Crashed,-8} {winCount,-6} {avgUtil,-10:F1} {e.TotalTimeMs,-14}");
+ Console.WriteLine($"{e.Engine,-16} {e.Jobs,-6} {e.Valid,-7} {e.FullyPlaced,-9} {winCount,-6} {avgUtil,-10:F1} {e.TotalPlates,-8} {e.TotalTimeMs,-14}");
}
}
public static void WriteCsv(string path, List results)
{
var sb = new StringBuilder();
- sb.AppendLine("Job,Engine,Valid,Crashed,PartsPlaced,PartsRequested,Utilization,UsedBoundingBoxArea,ElapsedMs,Notes");
+ sb.AppendLine("Job,Engine,Valid,Crashed,FullyPlaced,PartsPlaced,PartsRequested,Utilization,PlatesUsed,SizeBreakdown,ElapsedMs,Notes");
foreach (var r in results)
{
var notes = r.Crashed ? r.Error : string.Join(" | ", r.Violations);
sb.AppendLine(string.Join(",",
- Csv(r.JobName), Csv(r.EngineName), r.Valid, r.Crashed,
+ Csv(r.JobName), Csv(r.EngineName), r.Valid, r.Crashed, r.FullyPlaced,
r.PartsPlaced, r.PartsRequested,
r.Utilization.ToString("F4", CultureInfo.InvariantCulture),
- r.UsedBoundingBoxArea.ToString("F2", CultureInfo.InvariantCulture),
+ r.PlatesUsed, Csv(SizeSummary(r.SizeBreakdown)),
r.ElapsedMs, Csv(notes)));
}
File.WriteAllText(path, sb.ToString());
}
+ private static string SizeSummary(Dictionary breakdown)
+ {
+ if (breakdown == null || breakdown.Count == 0)
+ return "-";
+
+ return string.Join("; ", breakdown.Select(kv => $"{kv.Key}×{kv.Value}"));
+ }
+
private static string Csv(string value)
{
if (string.IsNullOrEmpty(value))
@@ -124,7 +135,7 @@ namespace OpenNest.Benchmark
}
/// Lower sorts first (better). Valid beats invalid, then higher
- /// utilization, then (if both fully placed) smaller used-bounding-box.
+ /// aggregate utilization, then (if both fully placed) fewer plates used.
private static int Compare(JobResult a, JobResult b)
{
if (a.Valid != b.Valid)
@@ -138,13 +149,8 @@ namespace OpenNest.Benchmark
if (System.Math.Abs(utilDiff) > Epsilon)
return utilDiff > 0 ? 1 : -1;
- if (a.FullyPlaced && b.FullyPlaced)
- {
- var bboxDiff = a.UsedBoundingBoxArea - b.UsedBoundingBoxArea;
-
- if (System.Math.Abs(bboxDiff) > Epsilon)
- return bboxDiff > 0 ? 1 : -1;
- }
+ if (a.FullyPlaced && b.FullyPlaced && a.PlatesUsed != b.PlatesUsed)
+ return a.PlatesUsed > b.PlatesUsed ? 1 : -1;
return 0;
}