mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-02 10:11:35 -05:00
feat(engine): choose which edge strips count as offcut
Equal-cost plans break ties by the largest edge offcut, from any of the four edges. A caller that wants the leftover off one way, such as a shop that keeps only end drops, or a benchmark scoring the used sheet height, had no way to say so. On the converted Nestbolt jobs the extended Irregular search then found plans whose larger offcut is a strip down the side, which Nestbolt scores as worse: b-blaz1-strip 3.601 -> 3.867, b-shirts-strip 2.674 -> 2.995. NestJobOptions.OffcutCut (a CutOffAxis, default null) counts only the strips one cut along that axis frees, for salvage credit and the compactness tie-break alike: Horizontal the full-width bottom and top strips, Vertical the full-length sides. PlateRemnant.LargestOffcut takes the axis; NestJobCost, the time limit and clamp-removal recovery pass it on. Unset, nothing changes. Benchmark manifests accept "offcutCut": "horizontal" or "vertical". With horizontal, one CPU, 20 s limit, Default: b-blaz1-strip 3.934 -> 3.333 b-shirts-strip 2.995 -> 2.610 m-box-deep 1.918 -> 1.276 (Nestbolt 1.251) Manifests accept only the words horizontal or vertical (any case); a list such as "horizontal,vertical" is rejected. PlateRemnant rejects an undefined axis. Fixed strategies keep their own no-salvage tie-break and ignore the option; desktop, Console and MCP do not offer it yet. Mutation reds: 14/14 (offcut-mutations.py).
This commit is contained in:
1 parent
63bcd89d35
commit
561f5be06c
13 files changed
+208
-14
No files matched your search
@@ -47,6 +47,10 @@ namespace OpenNest.Benchmark
|
||||
/// <summary>Saved source-nest setting; manifests have no saved salvage rate and use zero.</summary>
|
||||
public double SalvageRate { get; init; }
|
||||
|
||||
/// <summary>Manifest "offcutCut": which edge strips count as a sheet's offcut;
|
||||
/// null for all.</summary>
|
||||
public CutOffAxis? OffcutCut { get; init; }
|
||||
|
||||
/// <summary>Original hand-authored placements, if the source was a .nest with any real parts.</summary>
|
||||
public List<(Plate Plate, List<Part> Parts)> BaselinePlateRuns { get; init; }
|
||||
public List<DrawingRequest> Requests { get; init; }
|
||||
@@ -93,7 +97,8 @@ namespace OpenNest.Benchmark
|
||||
"Fill",
|
||||
maxPlates,
|
||||
salvageRate ?? SalvageRate,
|
||||
minimumSalvageDimension ?? 0
|
||||
minimumSalvageDimension ?? 0,
|
||||
offcutCut: OffcutCut
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -52,9 +52,24 @@ namespace OpenNest.Benchmark
|
||||
PartSpacing = partSpacingOverride ?? manifest.Spacing,
|
||||
Quadrant = manifest.Quadrant,
|
||||
Requests = requests,
|
||||
OffcutCut = OffcutCut(manifest.OffcutCut, manifestPath),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>"horizontal" or "vertical" (any case); absent counts every edge strip.
|
||||
/// </summary>
|
||||
private static CutOffAxis? OffcutCut(string value, string manifestPath) =>
|
||||
value?.ToLowerInvariant() switch
|
||||
{
|
||||
null => null,
|
||||
"horizontal" => CutOffAxis.Horizontal,
|
||||
"vertical" => CutOffAxis.Vertical,
|
||||
_ => throw new InvalidOperationException(
|
||||
$"Manifest '{manifestPath}' has offcutCut \"{value}\"; "
|
||||
+ "use \"horizontal\" or \"vertical\"."
|
||||
),
|
||||
};
|
||||
|
||||
private static Manifest ReadManifest(string manifestPath)
|
||||
{
|
||||
try
|
||||
@@ -206,6 +221,7 @@ namespace OpenNest.Benchmark
|
||||
public double Spacing { get; set; }
|
||||
public double EdgeSpacing { get; set; }
|
||||
public int Quadrant { get; set; } = 1;
|
||||
public string OffcutCut { get; set; }
|
||||
public List<ManifestPart> Parts { get; set; }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Converters;
|
||||
@@ -74,11 +75,16 @@ public sealed class PlateRemnant
|
||||
/// material envelope (<paramref name="left"/> to <paramref name="top"/>) by
|
||||
/// <paramref name="partSpacing"/>, with both sides at least <paramref name="minimum"/>; null
|
||||
/// when none qualifies. Ties keep the first of bottom, top, left, right. This is the salvage
|
||||
/// offcut whole-job costing credits.
|
||||
/// offcut whole-job costing credits. A <paramref name="cut"/> keeps only the strips one cut
|
||||
/// along that axis frees: <see cref="CutOffAxis.Horizontal"/> the full-width bottom and top
|
||||
/// strips, <see cref="CutOffAxis.Vertical"/> the full-length left and right ones.
|
||||
/// </summary>
|
||||
public static Box LargestOffcut(Box workArea, double partSpacing,
|
||||
double left, double bottom, double right, double top, double minimum)
|
||||
double left, double bottom, double right, double top, double minimum,
|
||||
CutOffAxis? cut = null)
|
||||
{
|
||||
if (cut is { } axis && !Enum.IsDefined(axis))
|
||||
throw new ArgumentOutOfRangeException(nameof(cut));
|
||||
var work = workArea;
|
||||
var gap = partSpacing;
|
||||
var candidates = new[]
|
||||
@@ -89,8 +95,11 @@ public sealed class PlateRemnant
|
||||
new Box(right + gap, work.Bottom, work.Right - right - gap, work.Width),
|
||||
};
|
||||
Box best = null;
|
||||
foreach (var candidate in candidates)
|
||||
for (var i = 0; i < candidates.Length; i++)
|
||||
{
|
||||
var candidate = candidates[i];
|
||||
if (cut == CutOffAxis.Horizontal && i >= 2 || cut == CutOffAxis.Vertical && i < 2)
|
||||
continue;
|
||||
if (candidate.Length >= minimum && candidate.Width >= minimum
|
||||
&& (best == null || candidate.Length * candidate.Width > best.Length * best.Width))
|
||||
best = candidate;
|
||||
|
||||
@@ -59,6 +59,42 @@ public class ClampRemovalRecoveryTests
|
||||
Assert.Equal(2, outcome.Job.Plates.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResidualSolvesKeepTheJobsSalvageAndOffcutSettings()
|
||||
{
|
||||
// The residual job is rebuilt for the reduced-clamp stock; it must score offcuts the
|
||||
// way the caller's job does.
|
||||
var part = JobBuilder.Rectangle("p", w: 14, h: 3, count: 1);
|
||||
var job = new NestJob(new[] { part }, new[] { ClampedSheet(Clamp1(), Clamp2()) },
|
||||
new NestJobOptions(salvageRate: 0.5, minimumSalvageDimension: 1,
|
||||
offcutCut: CutOffAxis.Vertical));
|
||||
var seen = new List<NestJobOptions>();
|
||||
var engine = new Recording(seen);
|
||||
|
||||
var outcome = ClampRemovalRecovery.TryRecover(job, Engine.Solve(job), engine, default);
|
||||
|
||||
Assert.NotNull(outcome);
|
||||
// Probe solves use their own unsalvaged options; every solve with the job's salvage
|
||||
// settings is a residual solve and must carry its offcut cut too.
|
||||
var residual = seen.Where(o => o.SalvageRate == 0.5).ToList();
|
||||
Assert.NotEmpty(residual);
|
||||
Assert.All(residual, o =>
|
||||
{
|
||||
Assert.Equal(1, o.MinimumSalvageDimension);
|
||||
Assert.Equal(CutOffAxis.Vertical, o.OffcutCut);
|
||||
});
|
||||
}
|
||||
|
||||
private sealed class Recording(List<NestJobOptions> seen) : INestingEngine
|
||||
{
|
||||
public NestJobResult Solve(NestJob job, IProgress<NestJobProgress> progress = null,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
seen.Add(job.Options);
|
||||
return Engine.Solve(job, progress, token);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemovalFreesSpaceForOtherResidualDemandToo()
|
||||
{
|
||||
|
||||
@@ -59,6 +59,59 @@ public class NestJobCostTests
|
||||
Assert.Equal(0, NestJobCost.CompactnessOffcut(job, new NestJobPlateResult(0, stock, Array.Empty<NestJobPlacement>())));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, 211.5)]
|
||||
[InlineData(CutOffAxis.Horizontal, 139.5)]
|
||||
[InlineData(CutOffAxis.Vertical, 211.5)]
|
||||
public void AnOffcutCutCountsOnlyTheStripsThatCutFrees(CutOffAxis? cut, double expected)
|
||||
{
|
||||
// A 6 x 10 part in the corner of an 18 x 18 work area leaves a full-width strip on top,
|
||||
// 18 x 7.75, and a larger full-length strip on the right, 11.75 x 18.
|
||||
var stock = new NestPlateStock("sheet", new Size(20, 20), partSpacing: 0.25,
|
||||
edgeSpacing: new Spacing(1, 1, 1, 1));
|
||||
var work = stock.WorkArea;
|
||||
var part = new NestJobPart("part",
|
||||
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(6, 10)), 1);
|
||||
var job = new NestJob(new[] { part }, new[] { stock },
|
||||
new NestJobOptions(salvageRate: 1, minimumSalvageDimension: 2, offcutCut: cut));
|
||||
var sheet = new NestJobPlateResult(0, stock,
|
||||
new[] { new NestJobPlacement("part", 0, work.Left, work.Bottom, 0) });
|
||||
var envelope = new Box(work.Left, work.Bottom, 6, 10);
|
||||
|
||||
Assert.Equal(expected, NestJobCost.CompactnessOffcut(job, sheet));
|
||||
Assert.Equal(expected, NestJobCost.CompactnessOffcut(job.Options, stock, envelope));
|
||||
Assert.Equal(400 - expected, NestJobCost.NetSheetArea(job, sheet));
|
||||
Assert.Equal(400 - expected, NestJobCost.NetSheetArea(job.Options, stock, envelope));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, 211.5)]
|
||||
[InlineData(CutOffAxis.Horizontal, 211.5)]
|
||||
[InlineData(CutOffAxis.Vertical, 139.5)]
|
||||
public void AVerticalCutCountsOnlyTheSideStrips(CutOffAxis? cut, double expected)
|
||||
{
|
||||
// The mirror case: a 10 x 6 part leaves 18 x 11.75 on top but only 7.75 x 18 at the side.
|
||||
var stock = new NestPlateStock("sheet", new Size(20, 20), partSpacing: 0.25,
|
||||
edgeSpacing: new Spacing(1, 1, 1, 1));
|
||||
var work = stock.WorkArea;
|
||||
var options = new NestJobOptions(salvageRate: 1, minimumSalvageDimension: 2,
|
||||
offcutCut: cut);
|
||||
var envelope = new Box(work.Left, work.Bottom, 10, 6);
|
||||
|
||||
Assert.Equal(expected, NestJobCost.CompactnessOffcut(options, stock, envelope));
|
||||
Assert.Equal(400 - expected, NestJobCost.NetSheetArea(options, stock, envelope));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnUndefinedAxisIsRejectedByTheRemnantRule() =>
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => PlateRemnant.LargestOffcut(
|
||||
new Box(0, 0, 10, 10), 0, 1, 1, 2, 2, 0, (CutOffAxis)7));
|
||||
|
||||
[Fact]
|
||||
public void AnUndefinedOffcutCutIsRejected() =>
|
||||
Assert.Throws<ArgumentOutOfRangeException>(
|
||||
() => new NestJobOptions(offcutCut: (CutOffAxis)7));
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 5, 100)]
|
||||
[InlineData(0.5, 0, 100)]
|
||||
|
||||
@@ -92,6 +92,23 @@ public class DefaultNestingEngineTests
|
||||
Assert.Equal("first", error.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, 0.0)]
|
||||
[InlineData(CutOffAxis.Horizontal, 1.0)]
|
||||
public void TheOffcutCutDecidesWhichEqualCostLayoutWins(CutOffAxis? cut, double expectedY)
|
||||
{
|
||||
// Stacked squares leave a 6 x 10 strip at the side and only 10 x 1 on top; a raised row
|
||||
// leaves 10 x 5 on top. Counting every strip picks the stack; counting only full-width
|
||||
// strips (a horizontal cut) picks the row.
|
||||
var plain = SquaresJob();
|
||||
var job = new NestJob(plain.Parts, plain.Plates, new NestJobOptions(offcutCut: cut));
|
||||
|
||||
var result = Choose(job,
|
||||
Returns(j => Layout(j, Sheet((0, 0), (0, 5)))), Returns(j => Raised(j, 1)));
|
||||
|
||||
Assert.Equal(expectedY, result.Plates[0].Placements[0].Y);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NearTiesAreChosenInTieBreakOrderWhateverTheRunOrder()
|
||||
{
|
||||
|
||||
@@ -144,7 +144,8 @@ public class IrregularSearchUntilCancelledTests
|
||||
public void ATimeLimitAsksTheEngineToSearchUntilCancelled()
|
||||
{
|
||||
NestJob? seen = null;
|
||||
var options = new NestJobOptions("Fill", maxPlates: 3, salvageRate: 0.25, minimumSalvageDimension: 1.5);
|
||||
var options = new NestJobOptions("Fill", maxPlates: 3, salvageRate: 0.25,
|
||||
minimumSalvageDimension: 1.5, offcutCut: CutOffAxis.Vertical);
|
||||
var plain = MixedJob(false);
|
||||
var job = new NestJob(plain.Parts, plain.Plates, options);
|
||||
var engine = new DeadlineNestingEngine(
|
||||
@@ -165,6 +166,7 @@ public class IrregularSearchUntilCancelledTests
|
||||
Assert.Equal(3, seen.Options.MaxPlates);
|
||||
Assert.Equal(0.25, seen.Options.SalvageRate);
|
||||
Assert.Equal(1.5, seen.Options.MinimumSalvageDimension);
|
||||
Assert.Equal(CutOffAxis.Vertical, seen.Options.OffcutCut);
|
||||
Assert.Equal(job.Parts, seen.Parts);
|
||||
Assert.Equal(job.Plates, seen.Plates);
|
||||
Assert.False(job.Options.SearchUntilCancelled);
|
||||
|
||||
@@ -163,7 +163,8 @@ internal static class ClampRemovalRecovery
|
||||
|
||||
var variant = Clone(group.Removal.Stock, group.Removal.Stock.Id, sheetCap, null);
|
||||
var options = new NestJobOptions(job.Options.PlacementStrategy, platesLeft,
|
||||
job.Options.SalvageRate, job.Options.MinimumSalvageDimension);
|
||||
job.Options.SalvageRate, job.Options.MinimumSalvageDimension,
|
||||
offcutCut: job.Options.OffcutCut);
|
||||
var groupJob = new NestJob(parts, new[] { variant }, options);
|
||||
|
||||
List<NestJobPlateResult> kept = null;
|
||||
|
||||
@@ -35,14 +35,14 @@ public static class NestJobCost
|
||||
return 0;
|
||||
var (left, bottom, right, top) = MaterialEnvelope(job, sheet);
|
||||
return LargestOffcut(sheet.Stock, left, bottom, right, top,
|
||||
System.Math.Max(0, job.Options.MinimumSalvageDimension));
|
||||
System.Math.Max(0, job.Options.MinimumSalvageDimension), job.Options.OffcutCut);
|
||||
}
|
||||
|
||||
/// <summary><see cref="CompactnessOffcut(NestJob, NestJobPlateResult)"/> from an existing
|
||||
/// material-only placed-parts envelope.</summary>
|
||||
public static double CompactnessOffcut(NestJobOptions options, NestPlateStock stock, Box partsEnvelope) =>
|
||||
LargestOffcut(stock, partsEnvelope.Left, partsEnvelope.Bottom, partsEnvelope.Right, partsEnvelope.Top,
|
||||
System.Math.Max(0, options.MinimumSalvageDimension));
|
||||
System.Math.Max(0, options.MinimumSalvageDimension), options.OffcutCut);
|
||||
|
||||
/// <summary>
|
||||
/// An offcut on a 1e-6 grid, for exact tie-break comparisons: differences below it are
|
||||
@@ -88,14 +88,17 @@ public static class NestJobCost
|
||||
var minimum = options.MinimumSalvageDimension;
|
||||
if (options.SalvageRate == 0 || minimum <= 0)
|
||||
return area;
|
||||
return area - options.SalvageRate * LargestOffcut(stock, left, bottom, right, top, minimum);
|
||||
return area - options.SalvageRate
|
||||
* LargestOffcut(stock, left, bottom, right, top, minimum, options.OffcutCut);
|
||||
}
|
||||
|
||||
// No rectangular edge strip is guaranteed inside an outlined sheet, so it earns no credit.
|
||||
private static double LargestOffcut(
|
||||
NestPlateStock stock, double left, double bottom, double right, double top, double minimum) =>
|
||||
NestPlateStock stock, double left, double bottom, double right, double top, double minimum,
|
||||
CutOffAxis? cut) =>
|
||||
stock.Outline == null
|
||||
&& PlateRemnant.LargestOffcut(stock.WorkArea, stock.PartSpacing, left, bottom, right, top, minimum)
|
||||
&& PlateRemnant.LargestOffcut(stock.WorkArea, stock.PartSpacing,
|
||||
left, bottom, right, top, minimum, cut)
|
||||
is { } offcut ? offcut.Length * offcut.Width : 0;
|
||||
|
||||
public static bool UsesExplicitCosts(NestJob job) =>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using OpenNest;
|
||||
|
||||
namespace OpenNest.Engine.Jobs;
|
||||
|
||||
@@ -10,9 +11,12 @@ public sealed class NestJobOptions
|
||||
int? maxPlates = null,
|
||||
double salvageRate = 0,
|
||||
double minimumSalvageDimension = 0,
|
||||
bool searchUntilCancelled = false
|
||||
bool searchUntilCancelled = false,
|
||||
CutOffAxis? offcutCut = null
|
||||
)
|
||||
{
|
||||
if (offcutCut is { } axis && !Enum.IsDefined(axis))
|
||||
throw new ArgumentOutOfRangeException(nameof(offcutCut));
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(placementStrategy);
|
||||
if (maxPlates <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(maxPlates));
|
||||
@@ -25,8 +29,18 @@ public sealed class NestJobOptions
|
||||
SalvageRate = salvageRate;
|
||||
MinimumSalvageDimension = minimumSalvageDimension;
|
||||
SearchUntilCancelled = searchUntilCancelled;
|
||||
OffcutCut = offcutCut;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When set, only the edge strips one cut along this axis frees count as a sheet's offcut, for
|
||||
/// salvage credit and for the compactness tie-break between equal-cost plans:
|
||||
/// <see cref="CutOffAxis.Horizontal"/> keeps full-width strips at the bottom or top (the drop
|
||||
/// off the end of a sheet), <see cref="CutOffAxis.Vertical"/> full-length ones at the sides.
|
||||
/// Null (the default) counts the largest of all four.
|
||||
/// </summary>
|
||||
public CutOffAxis? OffcutCut { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Asks engines that support it to keep searching after their normal search, reporting
|
||||
/// each better plan (<see cref="NestJobStage.PlanImproved"/>) until the solve's token is
|
||||
@@ -39,7 +53,7 @@ public sealed class NestJobOptions
|
||||
|
||||
/// <summary>A copy of these options with <see cref="SearchUntilCancelled"/> set.</summary>
|
||||
public NestJobOptions WithSearchUntilCancelled(bool value) =>
|
||||
new(PlacementStrategy, MaxPlates, SalvageRate, MinimumSalvageDimension, value);
|
||||
new(PlacementStrategy, MaxPlates, SalvageRate, MinimumSalvageDimension, value, OffcutCut);
|
||||
|
||||
/// <summary>Fraction of eligible edge-offcut area credited by <see cref="NestJobCost.NetSheetCost"/> (0..1).</summary>
|
||||
public double SalvageRate { get; }
|
||||
|
||||
@@ -83,6 +83,35 @@ public sealed class JobLoaderManifestTests : IDisposable
|
||||
Assert.Equal(2, job.Requests.Count);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, null)]
|
||||
[InlineData("\"horizontal\"", CutOffAxis.Horizontal)]
|
||||
[InlineData("\"Vertical\"", CutOffAxis.Vertical)]
|
||||
public void OffcutCut_ReachesTheJobOptions(string json, CutOffAxis? expected)
|
||||
{
|
||||
var manifest = json == null ? ValidManifest
|
||||
: ValidManifest.Replace("\"quadrant\": 2,", $"\"quadrant\": 2, \"offcutCut\": {json},");
|
||||
var job = Assert.Single(JobLoader.Load(WriteManifest("bench.json", manifest)));
|
||||
|
||||
Assert.Equal(expected, job.OffcutCut);
|
||||
Assert.Equal(expected, job.BuildNestJob(10).Options.OffcutCut);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("diagonal")]
|
||||
[InlineData("1")]
|
||||
[InlineData("horizontal,vertical")]
|
||||
[InlineData("Horizontal, Vertical")]
|
||||
public void OffcutCut_RejectsAnythingButAnAxisName(string value)
|
||||
{
|
||||
var manifest = ValidManifest.Replace(
|
||||
"\"quadrant\": 2,", $"\"quadrant\": 2, \"offcutCut\": \"{value}\",");
|
||||
var path = WriteManifest("bench.json", manifest);
|
||||
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => JobLoader.Load(path).ToList());
|
||||
Assert.Contains("offcutCut", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Overrides_ReplaceManifestSheetSizesAndSpacing()
|
||||
{
|
||||
|
||||
@@ -485,3 +485,12 @@ full-width or full-length edge offcut, measured as salvage measures it but whate
|
||||
salvage rate (it honors a positive minimum salvage dimension; with none, any offcut counts).
|
||||
Larger wins, summed over a job's sheets. It is never part of the cost, so it cannot outrank a
|
||||
cheaper layout or change a reported cost.
|
||||
|
||||
`NestJobOptions.OffcutCut` narrows which strips count, for salvage credit and this tie-break
|
||||
alike: `CutOffAxis.Horizontal` counts only the full-width strips at the bottom and top (the
|
||||
drop cut off the end of a sheet), `Vertical` only the full-length ones at the sides. Unset, the
|
||||
largest of the four counts. Set it when the leftover must come off one way, for example a
|
||||
shop that keeps only end drops or a benchmark that scores the used length along one axis.
|
||||
Only engine callers (the API and benchmark manifests) can set it so far; desktop, Console and
|
||||
MCP do not offer it, and the desktop's expected-remnant display still shows the largest strip.
|
||||
Fixed strategies keep their own no-salvage tie-break and ignore it.
|
||||
@@ -7,6 +7,6 @@ dotnet run --project OpenNest.Benchmark -- ./benchmark-jobs \
|
||||
--sheet-sizes 48x96,60x120,72x120 --engines Irregular,Rectangles --csv results.csv
|
||||
```
|
||||
|
||||
Supply `--sheet-sizes` for an independent candidate pool; otherwise a saved nest offers only its original sheet sizes, which can bias comparisons. A saved nest's first plate also supplies spacing, quadrant and clamps: its machine clamp rail is resolved for every candidate size, as desktop Auto Nest does, and its baseline layout is checked against the clamps it was made with. Manifests have no clamps. A manifest part rotates freely (automatic rotation: any angle) unless `"allowRotation": false` locks it at 0°, or `"rotations"` lists its only angles in degrees: `[0]`, `[0, 180]` or `[0, 90, 180, 270]`. A saved nest records its salvage rate, but salvage credit applies only with a positive `--min-salvage-dimension`. Without it, every complete one-sheet layout costs the same, as in desktop Auto Nest on a single plate size. The default `--parallel 3` runs solves concurrently and confounds per-engine timing; use `--parallel 1` for timing comparisons. `--progress` logs each solve's start, engine progress, and finish. `--time-limit <seconds>` stops each solve at that wall-clock limit and scores the best valid plan the engine had reported (see [time limits](../nesting-engines.md#time-limits)); a solve with none counts as timed out. Without a limit the harness abandons a solve after five minutes; with one it waits for the limit plus a minute, so the engine's limit always fires first. Retain input hashes, exact commands, validity, fulfillment, costs, and raw results outside source control when comparing revisions; see [fill performance verification](fill-verification.md).
|
||||
Supply `--sheet-sizes` for an independent candidate pool; otherwise a saved nest offers only its original sheet sizes, which can bias comparisons. A saved nest's first plate also supplies spacing, quadrant and clamps: its machine clamp rail is resolved for every candidate size, as desktop Auto Nest does, and its baseline layout is checked against the clamps it was made with. Manifests have no clamps. A manifest part rotates freely (automatic rotation: any angle) unless `"allowRotation": false` locks it at 0°, or `"rotations"` lists its only angles in degrees: `[0]`, `[0, 180]` or `[0, 90, 180, 270]`. A manifest's `"offcutCut": "horizontal"` (or `"vertical"`) sets `NestJobOptions.OffcutCut`, so only full-width (or full-length) edge strips count as offcut; use it to match a benchmark that scores used sheet height (or length). A saved nest records its salvage rate, but salvage credit applies only with a positive `--min-salvage-dimension`. Without it, every complete one-sheet layout costs the same, as in desktop Auto Nest on a single plate size. The default `--parallel 3` runs solves concurrently and confounds per-engine timing; use `--parallel 1` for timing comparisons. `--progress` logs each solve's start, engine progress, and finish. `--time-limit <seconds>` stops each solve at that wall-clock limit and scores the best valid plan the engine had reported (see [time limits](../nesting-engines.md#time-limits)); a solve with none counts as timed out. Without a limit the harness abandons a solve after five minutes; with one it waits for the limit plus a minute, so the engine's limit always fires first. Retain input hashes, exact commands, validity, fulfillment, costs, and raw results outside source control when comparing revisions; see [fill performance verification](fill-verification.md).
|
||||
|
||||
Custom engines are DLLs implementing `INestingEngine` with a public parameterless constructor, loaded from `Engines/` beside the benchmark executable. See [nesting engines](../nesting-engines.md) for built-ins and engine-change rules. Run `dotnet run --project OpenNest.Benchmark -- --help` for all options.
|
||||
Reference in new issue
Block a user