Compare commits
8
Commits
f0fe79f0f1
...
9b69c67572
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b69c67572 | ||
|
|
aa88eee484 | ||
|
|
e0e3b96bed | ||
|
|
424ff15ebc | ||
|
|
9888fe6083 | ||
|
|
a2dcfc7484 | ||
|
|
ae704478af | ||
|
|
ecca71e185 |
@@ -74,12 +74,13 @@ GPU-accelerated pair evaluation for best-fit nesting. `GpuPairEvaluator` impleme
|
|||||||
Training data collection for ML angle prediction. `TrainingDatabase` stores per-angle nesting results in SQLite via EF Core for offline model training.
|
Training data collection for ML angle prediction. `TrainingDatabase` stores per-angle nesting results in SQLite via EF Core for offline model training.
|
||||||
|
|
||||||
### OpenNest.Benchmark (console app, depends on Core + Engine + IO)
|
### OpenNest.Benchmark (console app, depends on Core + Engine + IO)
|
||||||
Compares registered `NestEngineBase` implementations against each other on real `.nest` files. Fully generic — it never hardcodes drawing geometry, just reads whatever drawings/quantities/plate settings each input file already has.
|
Compares registered `INestingEngine` implementations against each other on real `.nest` files. Each engine solves the whole job — it owns its own multi-plate/size strategy rather than being handed one already-sized plate at a time. Fully generic — it never hardcodes drawing geometry, just reads whatever drawings/quantities/plate settings each input file already has.
|
||||||
|
|
||||||
- `JobLoader` builds `BenchmarkJob`s from a `.nest` file or a folder of them via `NestReader`, using every drawing with `Quantity.Required > 0`. `--sheet-sizes` can sweep a fixed list of plate sizes instead of each file's own.
|
- `JobLoader` builds `BenchmarkJob`s from a `.nest` file or a folder of them via `NestReader`, using every drawing with `Quantity.Required > 0`. `--sheet-sizes` can sweep a fixed list of plate sizes instead of each file's own.
|
||||||
- `BenchmarkRunner` gives each (job, engine) pair a fresh `Plate`/`NestItem` list (`BenchmarkJob.CreatePlate()`/`CreateItems()`) so engines can't see each other's mutated state, then calls the engine's `Nest()` and times it.
|
- `BenchmarkJob.BuildNestJob(maxPlates)` converts the job into a `NestJob`: one `NestJobPart` per requested drawing (via `DrawingJobMapper.FromDrawing`) and one `NestPlateStock` per candidate sheet size (unlimited quantity — the engine decides how many of each size it uses).
|
||||||
- `NestValidator` checks the returned layout: every part inside `Plate.WorkArea()`, every pair at least `Plate.PartSpacing` apart (checked geometrically via each part's own world-space polygon, inflated by the spacing — works on arbitrary concave/holed shapes, not just bounding boxes), and no drawing over its requested quantity. An invalid or throwing run scores zero for that job.
|
- `BenchmarkRunner` calls each engine's `INestingEngine.Solve(NestJob)` once per job, under a wall-clock timeout so a runaway or hanging engine can't stall the whole benchmark run, then materializes the result back into legacy `Plate`/`Part` objects via `NestResultMaterializer` for scoring.
|
||||||
- Scoring matches `Plate.Utilization()` (placed drawing area / full sheet area, `Plate.Area()`). If an engine placed every requested part, ties are broken by the smaller used-bounding-box (`Report`'s ranking rule) — a more compact layout leaves a bigger usable remnant.
|
- `NestValidator` checks the returned layout: every part inside `Plate.WorkArea()`, every pair at least `Plate.PartSpacing` apart (checked geometrically via each part's own world-space polygon, inflated by the spacing — works on arbitrary concave/holed shapes, not just bounding boxes), and no drawing over its requested quantity. An invalid, throwing, or timed-out run scores zero for that job.
|
||||||
|
- Scoring matches `Plate.Utilization()` (placed drawing area / full sheet area, `Plate.Area()`). If an engine placed every requested part, ties are broken by fewer plates used (`Report`'s ranking rule) — using fewer sheets to do the same job wastes less material.
|
||||||
- `--engines Name1,Name2` filters to specific registered engines (default: all); `--csv <path>` writes a flat per-job CSV alongside the console report.
|
- `--engines Name1,Name2` filters to specific registered engines (default: all); `--csv <path>` writes a flat per-job CSV alongside the console report.
|
||||||
|
|
||||||
### OpenNest.Mcp (console app, depends on Core + Engine + IO)
|
### OpenNest.Mcp (console app, depends on Core + Engine + IO)
|
||||||
|
|||||||
@@ -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 SolveTimeout = 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,58 @@ 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 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 nestJob = job.BuildNestJob(MaxPlates);
|
||||||
|
var engine = engineInfo.Factory();
|
||||||
|
using var cts = new CancellationTokenSource(SolveTimeout);
|
||||||
|
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 requirements = job.Requests.ToDictionary<DrawingRequest, Drawing, (string Name, int Quantity)>(
|
||||||
|
r => materialized.DrawingsByPartId[r.Drawing.Id.ToString()],
|
||||||
|
r => (r.Drawing.Name, r.Quantity),
|
||||||
|
ReferenceEqualityComparer.Instance);
|
||||||
|
|
||||||
|
var validation = NestValidator.Validate(plateRuns, requirements);
|
||||||
|
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 +97,22 @@ namespace OpenNest.Benchmark
|
|||||||
Valid = false,
|
Valid = false,
|
||||||
PartsRequested = requested,
|
PartsRequested = requested,
|
||||||
ElapsedMs = sw.ElapsedMilliseconds,
|
ElapsedMs = sw.ElapsedMilliseconds,
|
||||||
Error = engineError,
|
Error = $"Timed out after {SolveTimeout.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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,14 @@ namespace OpenNest.Benchmark
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class NestValidator
|
public static class NestValidator
|
||||||
{
|
{
|
||||||
public static ValidationResult Validate(List<(Plate Plate, List<Part> Parts)> plateRuns, BenchmarkJob job)
|
/// <summary>
|
||||||
|
/// requirements maps each materialized part's BaseDrawing (by reference - materialized
|
||||||
|
/// Drawing instances are freshly reconstructed per NestResultMaterializer.Materialize, so
|
||||||
|
/// identity must never be inferred from Name, which is only incidentally seeded from the
|
||||||
|
/// originating NestJobPart id) to its original quantity limit and display name.
|
||||||
|
/// </summary>
|
||||||
|
public static ValidationResult Validate(List<(Plate Plate, List<Part> Parts)> plateRuns,
|
||||||
|
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements)
|
||||||
{
|
{
|
||||||
var result = new ValidationResult();
|
var result = new ValidationResult();
|
||||||
var allParts = plateRuns.SelectMany(pr => pr.Parts).ToList();
|
var allParts = plateRuns.SelectMany(pr => pr.Parts).ToList();
|
||||||
@@ -33,45 +40,46 @@ namespace OpenNest.Benchmark
|
|||||||
if (allParts.Count == 0)
|
if (allParts.Count == 0)
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
ValidateQuantities(allParts, job, result);
|
ValidateQuantities(allParts, requirements, result);
|
||||||
|
|
||||||
foreach (var (plate, parts) in plateRuns)
|
foreach (var (plate, parts) in plateRuns)
|
||||||
{
|
{
|
||||||
if (parts.Count == 0)
|
if (parts.Count == 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
ValidateBounds(parts, plate, result);
|
ValidateBounds(parts, plate, requirements, result);
|
||||||
ValidateAreaBudget(parts, plate, result);
|
ValidateAreaBudget(parts, plate, result);
|
||||||
ValidateSpacing(parts, plate.PartSpacing, result);
|
ValidateSpacing(parts, plate.PartSpacing, requirements, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ValidateQuantities(List<Part> parts, BenchmarkJob job, ValidationResult result)
|
private static void ValidateQuantities(List<Part> parts,
|
||||||
|
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements, ValidationResult result)
|
||||||
{
|
{
|
||||||
var allowed = job.Requests.ToDictionary(r => r.Drawing.Id, r => r.Quantity);
|
|
||||||
var placedCounts = parts
|
var placedCounts = parts
|
||||||
.GroupBy(p => p.BaseDrawing.Id)
|
.GroupBy<Part, Drawing>(p => p.BaseDrawing, ReferenceEqualityComparer.Instance)
|
||||||
.ToDictionary(g => g.Key, g => g.Count());
|
.ToDictionary(g => g.Key, g => g.Count());
|
||||||
|
|
||||||
foreach (var (drawingId, placed) in placedCounts)
|
foreach (var (drawing, placed) in placedCounts)
|
||||||
{
|
{
|
||||||
if (!allowed.TryGetValue(drawingId, out var max))
|
if (!requirements.TryGetValue(drawing, out var requirement))
|
||||||
{
|
{
|
||||||
result.Violations.Add($"Placed drawing id={drawingId} which was not requested for this job");
|
result.Violations.Add($"Placed drawing '{drawing.Name}' which was not requested for this job");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (placed > max)
|
if (placed > requirement.Quantity)
|
||||||
{
|
{
|
||||||
var name = parts.First(p => p.BaseDrawing.Id == drawingId).BaseDrawing.Name;
|
result.Violations.Add(
|
||||||
result.Violations.Add($"'{name}': placed {placed} across all plates but only {max} were requested");
|
$"'{requirement.Name}': placed {placed} across all plates but only {requirement.Quantity} were requested");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ValidateBounds(List<Part> parts, Plate plate, ValidationResult result)
|
private static void ValidateBounds(List<Part> parts, Plate plate,
|
||||||
|
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements, ValidationResult result)
|
||||||
{
|
{
|
||||||
var workArea = plate.WorkArea();
|
var workArea = plate.WorkArea();
|
||||||
|
|
||||||
@@ -87,7 +95,7 @@ namespace OpenNest.Benchmark
|
|||||||
if (outLeft || outBottom || outRight || outTop)
|
if (outLeft || outBottom || outRight || outTop)
|
||||||
{
|
{
|
||||||
result.Violations.Add(
|
result.Violations.Add(
|
||||||
$"'{part.BaseDrawing.Name}' at ({part.Location.X:F2},{part.Location.Y:F2}) falls outside the work area " +
|
$"'{DisplayName(part, requirements)}' at ({part.Location.X:F2},{part.Location.Y:F2}) falls outside the work area " +
|
||||||
$"of a {plate.Size} plate");
|
$"of a {plate.Size} plate");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,7 +124,8 @@ namespace OpenNest.Benchmark
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ValidateSpacing(List<Part> parts, double spacing, ValidationResult result)
|
private static void ValidateSpacing(List<Part> parts, double spacing,
|
||||||
|
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements, ValidationResult result)
|
||||||
{
|
{
|
||||||
var worldPolygons = new Polygon[parts.Count];
|
var worldPolygons = new Polygon[parts.Count];
|
||||||
var inflatedPolygons = new Polygon[parts.Count];
|
var inflatedPolygons = new Polygon[parts.Count];
|
||||||
@@ -140,12 +149,18 @@ namespace OpenNest.Benchmark
|
|||||||
if (Collision.HasOverlap(inflatedPolygons[i], worldPolygons[j]))
|
if (Collision.HasOverlap(inflatedPolygons[i], worldPolygons[j]))
|
||||||
{
|
{
|
||||||
result.Violations.Add(
|
result.Violations.Add(
|
||||||
$"'{parts[i].BaseDrawing.Name}' and '{parts[j].BaseDrawing.Name}' are closer than the required spacing ({spacing:F3})");
|
$"'{DisplayName(parts[i], requirements)}' and '{DisplayName(parts[j], requirements)}' are closer than the required spacing ({spacing:F3})");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Friendly name for a violation message, falling back to the materialized
|
||||||
|
/// Drawing's own Name (the raw partId string) if this part wasn't in requirements at all -
|
||||||
|
/// that mismatch is already reported by ValidateQuantities, so this is display-only.</summary>
|
||||||
|
private static string DisplayName(Part part, IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements) =>
|
||||||
|
requirements.TryGetValue(part.BaseDrawing, out var requirement) ? requirement.Name : part.BaseDrawing.Name;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extracts a part's perimeter as a world-space polygon, optionally inflated
|
/// Extracts a part's perimeter as a world-space polygon, optionally inflated
|
||||||
/// outward by the given spacing, mirroring Part.Intersects' own geometry
|
/// outward by the given spacing, mirroring Part.Intersects' own geometry
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using OpenNest.Benchmark;
|
|||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
return BenchmarkConsole.Run(args);
|
return BenchmarkConsole.Run(args);
|
||||||
@@ -40,7 +41,10 @@ static class BenchmarkConsole
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
var engines = NestEngineRegistry.AvailableEngines;
|
var enginesDir = Path.Combine(AppContext.BaseDirectory, "Engines");
|
||||||
|
NestingEngineRegistry.LoadPlugins(enginesDir);
|
||||||
|
|
||||||
|
var engines = NestingEngineRegistry.AvailableEngines;
|
||||||
|
|
||||||
if (options.EngineNames.Count > 0)
|
if (options.EngineNames.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -51,7 +55,7 @@ static class BenchmarkConsole
|
|||||||
if (engines.Count == 0)
|
if (engines.Count == 0)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("None of the requested engines are registered. Available: " +
|
Console.Error.WriteLine("None of the requested engines are registered. Available: " +
|
||||||
string.Join(", ", NestEngineRegistry.AvailableEngines.Select(e => e.Name)));
|
string.Join(", ", NestingEngineRegistry.AvailableEngines.Select(e => e.Name)));
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,21 +137,21 @@ static class BenchmarkConsole
|
|||||||
Console.Error.WriteLine($"Warning: could not parse sheet size '{token}', skipping");
|
Console.Error.WriteLine($"Warning: could not parse sheet size '{token}', skipping");
|
||||||
}
|
}
|
||||||
|
|
||||||
return sizes;
|
return sizes.Distinct().ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void PrintUsage()
|
private static void PrintUsage()
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("OpenNest.Benchmark - compare registered nesting engines on a set of .nest files");
|
Console.Error.WriteLine("OpenNest.Benchmark - compare registered whole-job nesting engines on a set of .nest files");
|
||||||
Console.Error.WriteLine();
|
Console.Error.WriteLine();
|
||||||
Console.Error.WriteLine("For each .nest file, every drawing with quantity > 0 is nested (mixed together),");
|
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("once per registered INestingEngine. Each engine is handed the full job - every");
|
||||||
Console.Error.WriteLine("as many plates as needed are created, one at a time, each sized by picking the");
|
Console.Error.WriteLine("requested part and the whole pool of candidate sheet sizes - and owns its own");
|
||||||
Console.Error.WriteLine("smallest candidate sheet size that fits the largest still-unplaced drawing -");
|
Console.Error.WriteLine("multi-plate/size strategy: how many plates it uses, of which sizes, and how");
|
||||||
Console.Error.WriteLine("applied identically to every engine, since Nest() itself has no say over its");
|
Console.Error.WriteLine("demand splits across them. Scoring: aggregate material utilization across every");
|
||||||
Console.Error.WriteLine("own plate's size. Scoring: aggregate material utilization across every plate");
|
Console.Error.WriteLine("plate used, then (if everything requested was placed) fewer plates as the");
|
||||||
Console.Error.WriteLine("used, then (if everything requested was placed) fewer plates as the tie-break.");
|
Console.Error.WriteLine("tie-break. An invalid layout (out of bounds, overlapping, or over-quantity), a");
|
||||||
Console.Error.WriteLine("An invalid layout (out of bounds, overlapping, or over-quantity) scores zero.");
|
Console.Error.WriteLine("thrown exception, or a run exceeding its time budget all score zero.");
|
||||||
Console.Error.WriteLine();
|
Console.Error.WriteLine();
|
||||||
Console.Error.WriteLine("Usage:");
|
Console.Error.WriteLine("Usage:");
|
||||||
Console.Error.WriteLine(" OpenNest.Benchmark <file.nest | folder> [options]");
|
Console.Error.WriteLine(" OpenNest.Benchmark <file.nest | folder> [options]");
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using OpenNest.Engine.Fill;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.Tests.Fill;
|
||||||
|
|
||||||
|
public class SortStripsTests
|
||||||
|
{
|
||||||
|
private static Part MakeRectPart(double x, double y, double w, double h)
|
||||||
|
{
|
||||||
|
var pgm = new OpenNest.CNC.Program();
|
||||||
|
pgm.Codes.Add(new OpenNest.CNC.RapidMove(new Vector(0, 0)));
|
||||||
|
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, 0)));
|
||||||
|
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, h)));
|
||||||
|
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, h)));
|
||||||
|
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, 0)));
|
||||||
|
var drawing = new Drawing("rect", pgm);
|
||||||
|
return new Part(drawing, new Vector(x, y));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SortColumnsByHeight_NonUniformGaps_DoesNotExceedOriginalSpan()
|
||||||
|
{
|
||||||
|
// Three columns with non-uniform gaps between them (5, then 1) and heights
|
||||||
|
// ordered so the sort-by-height pass must reorder them (tallest first, then
|
||||||
|
// shortest, then medium). The tallest column's original position leaves a
|
||||||
|
// 5-unit gap to its neighbor; that single sampled gap must not get replayed
|
||||||
|
// as the spacing for the whole staircase once it's no longer the leading pair.
|
||||||
|
var tall = MakeRectPart(0, 0, 10, 30); // Left 0-10, gap of 5 to next
|
||||||
|
var shortCol = MakeRectPart(15, 0, 5, 5); // Left 15-20, gap of 1 to next
|
||||||
|
var medium = MakeRectPart(21, 0, 20, 15); // Left 21-41
|
||||||
|
|
||||||
|
var originalRight = new[] { tall, shortCol, medium }.Max(p => p.BoundingBox.Right);
|
||||||
|
var originalLeft = new[] { tall, shortCol, medium }.Min(p => p.BoundingBox.Left);
|
||||||
|
var originalSpan = originalRight - originalLeft;
|
||||||
|
|
||||||
|
var parts = new List<Part> { tall, shortCol, medium };
|
||||||
|
IterativeShrinkFiller.SortColumnsByHeight(parts, spacing: 1.0);
|
||||||
|
|
||||||
|
var newRight = parts.Max(p => p.BoundingBox.Right);
|
||||||
|
var newLeft = parts.Min(p => p.BoundingBox.Left);
|
||||||
|
var newSpan = newRight - newLeft;
|
||||||
|
|
||||||
|
Assert.True(newSpan <= originalSpan + 1e-9,
|
||||||
|
$"Resequenced columns must not exceed the original footprint: original span {originalSpan}, new span {newSpan}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using OpenNest.Geometry;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.Tests.Jobs;
|
||||||
|
|
||||||
|
public class FixedStrategyNestingEngineTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ForcesConfiguredStrategyRegardlessOfJobOptions()
|
||||||
|
{
|
||||||
|
var engine = new FixedStrategyNestingEngine("Strip");
|
||||||
|
// The job itself declares an unknown strategy; if FixedStrategyNestingEngine
|
||||||
|
// didn't override it, PlateNesterFactory would reject it with NotSupportedException.
|
||||||
|
var job = FiniteStockJobTests.Job(1, new NestJobOptions("Not A Real Strategy"));
|
||||||
|
|
||||||
|
var result = engine.Solve(job);
|
||||||
|
|
||||||
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreservesJobMaxPlates()
|
||||||
|
{
|
||||||
|
var engine = new FixedStrategyNestingEngine("Default");
|
||||||
|
var part = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(100, 100)), 6);
|
||||||
|
var stock = new NestPlateStock("sheet", new Size(220, 220), quantity: null, partSpacing: 2.0,
|
||||||
|
edgeSpacing: new Spacing(5.0, 5.0, 5.0, 5.0), quadrant: 1);
|
||||||
|
var job = new NestJob(new[] { part }, new[] { stock }, new NestJobOptions("Default", maxPlates: 1));
|
||||||
|
|
||||||
|
var result = engine.Solve(job);
|
||||||
|
|
||||||
|
Assert.Equal(NestJobStatus.Incomplete, result.Status);
|
||||||
|
Assert.Equal(NestJobStopReason.PlateLimitReached, result.StopReason);
|
||||||
|
Assert.Single(result.Plates);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RejectsNullOrWhitespaceStrategyAtConstruction()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(() => new FixedStrategyNestingEngine(null));
|
||||||
|
Assert.Throws<ArgumentException>(() => new FixedStrategyNestingEngine(" "));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.Tests.Jobs;
|
||||||
|
|
||||||
|
public class NestingEngineRegistryTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void BuiltInStrategiesAreRegistered()
|
||||||
|
{
|
||||||
|
var names = NestingEngineRegistry.AvailableEngines.Select(e => e.Name).ToList();
|
||||||
|
|
||||||
|
Assert.Contains("Default", names);
|
||||||
|
Assert.Contains("Strip", names);
|
||||||
|
Assert.Contains("Vertical Remnant", names);
|
||||||
|
Assert.Contains("Horizontal Remnant", names);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EachBuiltInFactoryProducesAWorkingEngine()
|
||||||
|
{
|
||||||
|
foreach (var info in NestingEngineRegistry.AvailableEngines)
|
||||||
|
{
|
||||||
|
var engine = info.Factory();
|
||||||
|
var result = engine.Solve(FiniteStockJobTests.Job(1));
|
||||||
|
|
||||||
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DuplicateNameIsSkipped()
|
||||||
|
{
|
||||||
|
var before = NestingEngineRegistry.AvailableEngines.Count;
|
||||||
|
|
||||||
|
NestingEngineRegistry.Register("Default", "duplicate", () => new FixedStrategyNestingEngine("Default"));
|
||||||
|
|
||||||
|
Assert.Equal(before, NestingEngineRegistry.AvailableEngines.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LoadPluginsAgainstMissingDirectoryIsANoOp()
|
||||||
|
{
|
||||||
|
var before = NestingEngineRegistry.AvailableEngines.Count;
|
||||||
|
|
||||||
|
NestingEngineRegistry.LoadPlugins(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()));
|
||||||
|
|
||||||
|
Assert.Equal(before, NestingEngineRegistry.AvailableEngines.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -219,8 +219,10 @@ namespace OpenNest.Engine.Fill
|
|||||||
if (strips.Count <= 1)
|
if (strips.Count <= 1)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var gap = stripMin(strips[1]) - stripMax(strips[0]);
|
// Use the required clearance as the inter-strip gap, not a gap sampled from one
|
||||||
|
// original pair: actual placement gaps vary for irregular/mixed-size geometry, and
|
||||||
|
// replaying a larger sampled gap across every reordered pair can push the trailing
|
||||||
|
// strip past the original (already plate-fitted) footprint.
|
||||||
strips.Sort((a, b) => sortMetric(a).CompareTo(sortMetric(b)));
|
strips.Sort((a, b) => sortMetric(a).CompareTo(sortMetric(b)));
|
||||||
|
|
||||||
var pos = primaryEdge(parts[0].BoundingBox);
|
var pos = primaryEdge(parts[0].BoundingBox);
|
||||||
@@ -236,7 +238,7 @@ namespace OpenNest.Engine.Fill
|
|||||||
part.Offset(offset);
|
part.Offset(offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
pos = stripMax(s) + gap;
|
pos = stripMax(s) + spacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
parts.Clear();
|
parts.Clear();
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace OpenNest;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adapts one fixed IPlateNester strategy to the whole-job INestingEngine contract, so it can compete
|
||||||
|
/// as a full job solver alongside model-submitted engines. Delegates all multi-plate/size selection to
|
||||||
|
/// NestJobRunner; only the placement strategy key is forced, overriding whatever the job itself declared.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class FixedStrategyNestingEngine : INestingEngine
|
||||||
|
{
|
||||||
|
private readonly string strategy;
|
||||||
|
private readonly NestJobRunner runner = new(PlateNesterFactory.Create);
|
||||||
|
|
||||||
|
public FixedStrategyNestingEngine(string strategy)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(strategy))
|
||||||
|
throw new ArgumentException("Strategy cannot be null or whitespace.", nameof(strategy));
|
||||||
|
this.strategy = strategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public NestJobResult Solve(NestJob job, IProgress<NestJobProgress> progress = null, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(job);
|
||||||
|
var forced = new NestJob(job.Parts, job.Plates, new NestJobOptions(strategy, job.Options.MaxPlates));
|
||||||
|
return runner.Solve(forced, progress, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace OpenNest;
|
||||||
|
|
||||||
|
/// <summary>Display metadata plus a fresh-instance factory for one registered whole-job engine.</summary>
|
||||||
|
public class NestingEngineInfo
|
||||||
|
{
|
||||||
|
public NestingEngineInfo(string name, string description, Func<INestingEngine> factory)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Description = description;
|
||||||
|
Factory = factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name { get; }
|
||||||
|
public string Description { get; }
|
||||||
|
public Func<INestingEngine> Factory { get; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace OpenNest;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registry of whole-job INestingEngine implementations, parallel to NestEngineRegistry (which is for
|
||||||
|
/// the legacy single-plate NestEngineBase). The four production strategies are exposed here through
|
||||||
|
/// FixedStrategyNestingEngine so they compete on equal footing with model-submitted engines. Unlike
|
||||||
|
/// NestEngineRegistry, this has no ActiveEngineName/global-selection concept — callers choose an engine
|
||||||
|
/// explicitly from AvailableEngines.
|
||||||
|
/// </summary>
|
||||||
|
public static class NestingEngineRegistry
|
||||||
|
{
|
||||||
|
private static readonly List<NestingEngineInfo> engines = new();
|
||||||
|
|
||||||
|
static NestingEngineRegistry()
|
||||||
|
{
|
||||||
|
Register("Default", "Multi-phase nesting (Linear, Pairs, RectBestFit, Remainder)",
|
||||||
|
() => new FixedStrategyNestingEngine("Default"));
|
||||||
|
|
||||||
|
Register("Strip", "Strip-based nesting for mixed-drawing layouts",
|
||||||
|
() => new FixedStrategyNestingEngine("Strip"));
|
||||||
|
|
||||||
|
Register("Vertical Remnant", "Optimizes for largest right-side vertical drop",
|
||||||
|
() => new FixedStrategyNestingEngine("Vertical Remnant"));
|
||||||
|
|
||||||
|
Register("Horizontal Remnant", "Optimizes for largest top-side horizontal drop",
|
||||||
|
() => new FixedStrategyNestingEngine("Horizontal Remnant"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<NestingEngineInfo> AvailableEngines => engines;
|
||||||
|
|
||||||
|
public static void Register(string name, string description, Func<INestingEngine> factory)
|
||||||
|
{
|
||||||
|
if (engines.Any(e => e.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[NestingEngineRegistry] Duplicate engine '{name}' skipped");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
engines.Add(new NestingEngineInfo(name, description, factory));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Scans *.dll in directory for non-abstract INestingEngine types with a public
|
||||||
|
/// parameterless constructor, registering each under its CLR type name. Mirrors
|
||||||
|
/// NestEngineRegistry.LoadPlugins's per-assembly/per-type isolation: one bad plugin never
|
||||||
|
/// prevents the rest from loading.</summary>
|
||||||
|
public static void LoadPlugins(string directory)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(directory))
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var dll in Directory.GetFiles(directory, "*.dll"))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var assembly = Assembly.LoadFrom(dll);
|
||||||
|
|
||||||
|
foreach (var type in assembly.GetTypes())
|
||||||
|
{
|
||||||
|
if (type.IsAbstract || !typeof(INestingEngine).IsAssignableFrom(type))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var ctor = type.GetConstructor(Type.EmptyTypes);
|
||||||
|
|
||||||
|
if (ctor == null)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[NestingEngineRegistry] Skipping {type.Name}: no parameterless constructor");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Register(type.Name, string.Empty, () => (INestingEngine)ctor.Invoke(null));
|
||||||
|
Debug.WriteLine($"[NestingEngineRegistry] Loaded plugin engine: {type.Name}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[NestingEngineRegistry] Failed to register {type.Name}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[NestingEngineRegistry] Failed to load assembly {Path.GetFileName(dll)}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<InternalsVisibleTo Include="OpenNest.Tests" />
|
<InternalsVisibleTo Include="OpenNest.Tests" />
|
||||||
|
<InternalsVisibleTo Include="OpenNest.Engine.Tests" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ dotnet run --project OpenNest.Console/OpenNest.Console.csproj -- project.zip ext
|
|||||||
|
|
||||||
## Benchmarking Nest Engines
|
## Benchmarking Nest Engines
|
||||||
|
|
||||||
`OpenNest.Benchmark` compares every registered `NestEngineBase` implementation against each other on a set of `.nest` files, scoring by material utilization:
|
`OpenNest.Benchmark` compares every registered `INestingEngine` implementation against each other on a set of `.nest` files, scoring by material utilization. Each engine owns its own multi-plate/size strategy for the whole job — how many plates it uses, of which sizes, and how demand splits across them:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Benchmark all registered engines against every .nest file in a folder
|
# Benchmark all registered engines against every .nest file in a folder
|
||||||
@@ -179,7 +179,9 @@ dotnet run --project OpenNest.Benchmark/OpenNest.Benchmark.csproj -- job.nest \
|
|||||||
--sheet-sizes 48x96,60x96,60x120,72x120,72x144 --engines Default,Astra,Claude --csv results.csv
|
--sheet-sizes 48x96,60x96,60x120,72x120,72x144 --engines Default,Astra,Claude --csv results.csv
|
||||||
```
|
```
|
||||||
|
|
||||||
An engine's layout is rejected (scoring zero for that job) if any part falls outside the work area, any two parts are closer than the required spacing, or a drawing gets more parts placed than requested.
|
An engine's layout is rejected (scoring zero for that job) if any part falls outside the work area, any two parts are closer than the required spacing, or a drawing gets more parts placed than requested. A run that doesn't finish within its time budget also scores zero, as a timeout.
|
||||||
|
|
||||||
|
Custom competitor engines can be added by dropping a DLL implementing `INestingEngine` with a public parameterless constructor into the `Engines/` directory next to the benchmark executable; each one is registered under its own CLR type name. This is a separate plugin contract from the desktop app's `NestEngineRegistry`/`NestEngineBase` (which requires a `(Plate)` constructor) — a `NestEngineBase` plugin dropped into the benchmark's `Engines/` folder is silently skipped, since the benchmark only ever solves whole jobs.
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
@@ -213,7 +215,7 @@ OpenNest.sln
|
|||||||
| **OpenNest.Gpu** | GPU-accelerated bitmap overlap detection for best-fit pair evaluation using ILGPU. |
|
| **OpenNest.Gpu** | GPU-accelerated bitmap overlap detection for best-fit pair evaluation using ILGPU. |
|
||||||
| **OpenNest.Posts.Cincinnati** | Post-processor plugin for Cincinnati CL-707/800/900/940/CLX laser cutting machines. Outputs Cincinnati-format G-code with material library, kerf compensation, and pierce logic. |
|
| **OpenNest.Posts.Cincinnati** | Post-processor plugin for Cincinnati CL-707/800/900/940/CLX laser cutting machines. Outputs Cincinnati-format G-code with material library, kerf compensation, and pierce logic. |
|
||||||
| **OpenNest.Mcp** | MCP (Model Context Protocol) server exposing nesting operations as tools for AI assistants. |
|
| **OpenNest.Mcp** | MCP (Model Context Protocol) server exposing nesting operations as tools for AI assistants. |
|
||||||
| **OpenNest.Benchmark** | Runs every registered nest engine against a set of `.nest` files and scores them by material utilization, so engine implementations can be compared head-to-head. |
|
| **OpenNest.Benchmark** | Runs every registered whole-job nesting engine (`INestingEngine`) against a set of `.nest` files and scores them by material utilization, so competing engines — each owning its own multi-plate strategy — can be compared head-to-head. |
|
||||||
| **OpenNest.Tests** | 89 test files covering core geometry, fill strategies, splitting, bending, BOM import, post-processing, and the API. |
|
| **OpenNest.Tests** | 89 test files covering core geometry, fill strategies, splitting, bending, BOM import, post-processing, and the API. |
|
||||||
|
|
||||||
## Nesting Engines
|
## Nesting Engines
|
||||||
|
|||||||
Reference in New Issue
Block a user