Add OpenNest.Benchmark: generic head-to-head engine comparison harness
Loads any .nest file (or folder of them) via NestReader and nests every drawing with quantity > 0 using each registered NestEngineBase, so it works sight-unseen against arbitrary real jobs without any hardcoded geometry. Optionally sweeps a fixed --sheet-sizes list instead of each file's own plate size. - BenchmarkJob/JobLoader build immutable job specs; a fresh Plate and NestItem list is created per (job, engine) run so state never leaks between engines or jobs. - NestValidator rejects a layout if any part falls outside the work area, any two parts are closer than PartSpacing (checked via each part's own world-space polygon inflated by the spacing, so it holds for arbitrary concave/holed geometry, not just bounding boxes), or a drawing gets more parts than requested. - Scoring matches Plate.Utilization() (placed area / full sheet area); ties among fully-placed layouts break on the smaller used bounding box (more usable remnant). - Report prints a per-job ranked breakdown plus a per-engine summary (wins, avg utilization, time), and can write a flat CSV. Verified end-to-end against a synthetic .nest file (not committed) against the four built-in engines; caught a genuine out-of-work-area bug in StripNestEngine in the process.
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.IO;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenNest.Benchmark
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static class JobLoader
|
||||
{
|
||||
public static List<BenchmarkJob> Load(string inputPath, IReadOnlyList<Size> sheetSizeOverrides = null,
|
||||
double? partSpacingOverride = null)
|
||||
{
|
||||
var files = ResolveFiles(inputPath);
|
||||
var jobs = new List<BenchmarkJob>();
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
Nest nest;
|
||||
|
||||
try
|
||||
{
|
||||
nest = new NestReader(file).Read();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[JobLoader] Skipping '{file}': failed to read ({ex.Message})");
|
||||
continue;
|
||||
}
|
||||
|
||||
var requests = BuildRequests(nest);
|
||||
|
||||
if (requests.Count == 0)
|
||||
{
|
||||
Console.Error.WriteLine($"[JobLoader] Skipping '{file}': no drawings with quantity > 0");
|
||||
continue;
|
||||
}
|
||||
|
||||
var template = ResolvePlateTemplate(nest);
|
||||
var sizes = sheetSizeOverrides != null && sheetSizeOverrides.Count > 0
|
||||
? sheetSizeOverrides
|
||||
: ResolveSheetSizes(nest);
|
||||
|
||||
foreach (var size in sizes)
|
||||
{
|
||||
jobs.Add(new BenchmarkJob
|
||||
{
|
||||
SourceFile = file,
|
||||
SheetSizeLabel = size.ToString(1),
|
||||
PlateSize = size,
|
||||
EdgeSpacing = template.EdgeSpacing,
|
||||
PartSpacing = partSpacingOverride ?? template.PartSpacing,
|
||||
Quadrant = template.Quadrant,
|
||||
Requests = requests,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return jobs;
|
||||
}
|
||||
|
||||
private static List<string> ResolveFiles(string inputPath)
|
||||
{
|
||||
if (Directory.Exists(inputPath))
|
||||
{
|
||||
return Directory.GetFiles(inputPath, "*.nest", SearchOption.AllDirectories)
|
||||
.OrderBy(f => f, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
if (File.Exists(inputPath))
|
||||
return new List<string> { inputPath };
|
||||
|
||||
throw new FileNotFoundException($"Benchmark input not found: {inputPath}");
|
||||
}
|
||||
|
||||
private static List<DrawingRequest> BuildRequests(Nest nest)
|
||||
{
|
||||
var requests = new List<DrawingRequest>();
|
||||
|
||||
foreach (var drawing in nest.Drawings)
|
||||
{
|
||||
var qty = drawing.Quantity.Required;
|
||||
|
||||
if (qty <= 0)
|
||||
continue;
|
||||
|
||||
var constraints = drawing.Constraints;
|
||||
|
||||
requests.Add(new DrawingRequest
|
||||
{
|
||||
Drawing = drawing,
|
||||
Quantity = qty,
|
||||
Priority = drawing.Priority,
|
||||
StepAngle = constraints?.StepAngle ?? 0,
|
||||
RotationStart = constraints?.StartAngle ?? 0,
|
||||
RotationEnd = constraints?.EndAngle ?? 0,
|
||||
});
|
||||
}
|
||||
|
||||
return requests;
|
||||
}
|
||||
|
||||
private static (Spacing EdgeSpacing, double PartSpacing, int Quadrant) ResolvePlateTemplate(Nest nest)
|
||||
{
|
||||
var source = nest.Plates?.FirstOrDefault();
|
||||
|
||||
if (source != null)
|
||||
return (source.EdgeSpacing, source.PartSpacing, source.Quadrant);
|
||||
|
||||
var defaults = nest.PlateDefaults;
|
||||
return (defaults.EdgeSpacing, defaults.PartSpacing, defaults.Quadrant);
|
||||
}
|
||||
|
||||
private static List<Size> ResolveSheetSizes(Nest nest)
|
||||
{
|
||||
var sizes = (nest.Plates ?? Enumerable.Empty<Plate>())
|
||||
.Select(p => p.Size)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
if (sizes.Count == 0)
|
||||
sizes.Add(nest.PlateDefaults.Size);
|
||||
|
||||
return sizes;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user