fix(engine): reuse clamp-removal probes across identical stock

Offering several identical sheets (same size, spacing and clamps) multiplied
every feasibility probe, so eight full-sheet parts on four 48x120 options
with the eight-clamp rail spent the 1000-probe budget and recovered nothing
although each part fits with its clamps removed. A probe's answer is now
reused for the same part, engine and physically identical stock.

Delta review (s3b-delta-review.md) policy gate 3; PM 0e655fdb-a99a-40d8-968a-1863d8068c3e
This commit is contained in:
aj committed 2026-10-13 18:15:50 -04:00
1 parent 64ebda2f6f
commit a1914e147a
2 files changed
+50 -2

No files matched your search

@@ -333,4 +333,30 @@ public class ClampRemovalRepairTests
Assert.All(result.Raw.Plates.Where(p => p.StockId.Contains("removed")), p => Assert.Single(p.Stock.ClampZones));
Assert.Equal(2, result.Raw.StockUsage.Sum(u => u.Used));
}
// Delta review policy gate 3: identical offered stock (four 48x120 sheets with the same
// eight-clamp rail) must not multiply the probe count, or a moderate job of full-sheet
// parts runs out of budget although every part fits with its clamps removed.
[Fact]
public void IdenticalStockOptionsShareTheirProbes()
{
var rail = new ClampRail(1.5, 0.625, new[] { 6.0, 21.0, 36.0, 51.0, 66.0, 81.0, 96.0, 111.0 });
var size = new Size(48, 120);
var stocks = Enumerable.Range(0, 4)
.Select(i => new NestPlateStock($"s{i}", size, null, 0, default, 1, null, rail.ResolveFor(size, 1)) { ClampRail = rail })
.ToArray();
var parts = Enumerable.Range(0, 8)
.Select(i => JobBuilder.Rectangle($"full-{i}", w: 120, h: 48, count: 1, x: i * 1e-3))
.ToArray();
var job = new NestJob(parts, stocks);
var engine = new OpenNest.Engine.NestingEngines.Default.DefaultNestingEngine();
var raw = engine.Solve(job);
Assert.Empty(raw.Plates);
var outcome = ClampRemovalRecovery.TryRecover(job, raw, engine, default);
Assert.NotNull(outcome);
Assert.Equal(NestJobStatus.Complete, outcome!.Result.Status);
Assert.Equal(8, outcome.Result.Plates.Count);
Assert.All(outcome.Result.Plates, sheet => Assert.Empty(sheet.Stock.ClampZones));
}
}
+24 -2
View File
@@ -298,7 +298,9 @@ internal static class ClampRemovalRecovery
) => Probe(part, stocks, options, engine, budget, token);
/// <summary>Whether this part, alone and at quantity one, places on an empty sheet of any
/// of the given stock entries with the given engine. Spends one probe from the budget.</summary>
/// of the given stock entries with the given engine. A question already answered for the
/// same part, engine and physically identical stock is reused; otherwise it spends one
/// probe from the budget.</summary>
private static bool Probe(
NestJobPart part,
IReadOnlyList<NestPlateStock> stocks,
@@ -308,6 +310,9 @@ internal static class ClampRemovalRecovery
CancellationToken token
)
{
var key = (engine, part.Id, string.Join(";", stocks.Select(Signature).Distinct(StringComparer.Ordinal)));
if (budget.Answers.TryGetValue(key, out var known))
return known;
budget.Spend();
var probePart = new NestJobPart(part.Id, part.Geometry, 1, part.Priority, part.Rotation);
var probeStock = stocks.Select(s => Unlimited(s)).ToList();
@@ -315,13 +320,30 @@ internal static class ClampRemovalRecovery
var probeJob = new NestJob(new[] { probePart }, probeStock, probeOptions);
token.ThrowIfCancellationRequested();
var result = engine.Solve(probeJob, null, token);
return result != null && result.Status == NestJobStatus.Complete;
var fits = result != null && result.Status == NestJobStatus.Complete;
budget.Answers[key] = fits;
return fits;
}
/// <summary>Everything about a stock entry that can change whether a lone part fits:
/// sheet size, quadrant, spacings and the exact clamp zones (not its id, cost or
/// quantity).</summary>
private static string Signature(NestPlateStock stock)
{
var e = stock.EdgeSpacing;
var zones = string.Join(",", stock.SheetClampZones.Select(z =>
FormattableString.Invariant($"{z.Id}:{z.Bounds.X:R}/{z.Bounds.Y:R}/{z.Bounds.Length:R}/{z.Bounds.Width:R}")));
return FormattableString.Invariant(
$"{stock.Size.Width:R}x{stock.Size.Length:R}|q{stock.Quadrant}|{stock.PartSpacing:R}|{e.Left:R},{e.Bottom:R},{e.Right:R},{e.Top:R}|{zones}");
}
private sealed class ProbeBudget(int solves)
{
private int left = solves;
/// <summary>Answers already paid for, by (engine, part, stock signatures).</summary>
public Dictionary<(INestingEngine, string, string), bool> Answers { get; } = new();
/// <summary>Throws <see cref="ProbeBudgetExhaustedException"/> once spent; recovery
/// then keeps the ordinary result.</summary>
public void Spend()