fix(cutting): scale the reorder stall with the blocked part, not the plate's largest

An attempt along a part order stopped backtracking after 8 x entries x
contours expansions without progress, with contours taken as the most of
any part on the plate. One many-holed part therefore let every blocked
part, even a two-contour tab, replay its predecessors' entries for the
whole of that allowance; a plate whose rounds each stalled that long
ran out of budget before the recovery pass that would have finished it.

Along an order the stall now uses the larger contour count of the blocked
part and of the part cut just before it, whose departure a retry changes,
capped by the old limit. The sequence-free recovery pass keeps the plate
maximum. Planning characterization digests are unchanged.

The optional private-fixture test now also runs RecoveryCuttingNestPath
with default lead-ins, and requires that file's plates to plan within
60% of their budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-19 20:54:28 -04:00
1 parent 04a6e0eb50
commit cf32d1cab5
3 files changed
+31 -8

No files matched your search

@@ -21,7 +21,8 @@ internal static class JointCuttingPlanSearch
{
/// <summary>
/// Expansions per entry and contour that a reordering attempt may spend without getting further
/// along its order before it gives up and learns from the part that blocked it.
/// along its order before it gives up and learns from the part that blocked it. Along an order
/// the contours are those of the blocked part and of the part cut just before it.
/// </summary>
internal const int StallExpansionsPerEntry = 8;
private const int MaximumRecoveryHoles = 16;
@@ -276,7 +277,7 @@ internal static class JointCuttingPlanSearch
return attempt;
if (attempt.Advance(node))
progressExpansions = Expansions;
else if (stall is int limit && Expansions - progressExpansions > limit)
else if (stall is int limit && Expansions - progressExpansions > StallLimit(limit, attempt))
{
if (sequence != null || !contourRecovery)
return attempt;
@@ -307,6 +308,19 @@ internal static class JointCuttingPlanSearch
}
}
// Along an order, the stall scales with the part the attempt cannot get past and the part
// cut just before it, whose departure a retry changes: one many-holed part elsewhere on the
// plate must not let every blocked part replay its predecessors' entries for that long.
private int StallLimit(int limit, Attempt attempt)
{
if (attempt.Blocked is not int blocked)
return limit;
var contours = snapshot.Placements[blocked].Prepared?.Count ?? 1;
if (attempt.LastCut is { } last)
contours = System.Math.Max(contours, last.Prepared?.Count ?? 1);
return (int)System.Math.Min(limit, (long)StallExpansionsPerEntry * snapshot.MaxEntries * contours);
}
/// <summary>
/// The NEXT cut's centre that the outside entry should face, or null for the last
/// part. Supplied order: the next not-yet-finished part in that order (the sequence
@@ -918,6 +932,9 @@ internal static class JointCuttingPlanSearch
/// <summary>The deepest whole-part boundary reached; its Previous chain leads back to the root.</summary>
private Node Deepest { get; set; }
/// <summary>The part cut last before the deepest boundary, or null at the start point.</summary>
internal FixedProgramPlacement LastCut => Deepest?.Order is { Length: > 0 } order ? order[^1] : null;
/// <summary>The ordinal of the part the attempt could not get past, or null.</summary>
internal int? Blocked => sequence == null || Deepest == null || Deepest.Order.Length >= sequence.Length ? null
: sequence[Deepest.Order.Length];
@@ -10,11 +10,14 @@ public class ContourRecoveryFileTests
// Optional private inputs: never embed customer geometry or names in the repository.
// RecoveryCuttingNestPath: scribed parts with aligned slot pairs, and a bolted frame boxed
// in by parts cut before it, that the free search only finishes through contour recovery.
// One of its plates also holds a many-holed part that once set every stall, so the plan
// must finish with most of the budget to spare, not just within it.
[SkippableTheory]
[InlineData("InterlockedCuttingNestPath", false)]
[InlineData("InterlockedCuttingNestPath", true)]
[InlineData("RecoveryCuttingNestPath", true)]
public void PlateFinishesThroughDesktopBatchWithoutChangingInput(string key, bool lineLeads)
[InlineData("InterlockedCuttingNestPath", false, 100)]
[InlineData("InterlockedCuttingNestPath", true, 100)]
[InlineData("RecoveryCuttingNestPath", false, 60)]
[InlineData("RecoveryCuttingNestPath", true, 60)]
public void PlateFinishesThroughDesktopBatchWithoutChangingInput(string key, bool lineLeads, int budgetPercent)
{
var path = TestConfig.GetExistingPath(key);
Skip.If(path == null, $"Configure {key} in ignored test-config.json.");
@@ -37,7 +40,8 @@ public class ContourRecoveryFileTests
Assert.True(planned.IsOverlapClear);
Assert.True(planned.Result.IndependentlyReplayed);
Assert.Equal(CuttingPlanStatus.Ready, planned.Result.Status);
Assert.InRange(planned.Result.Expansions, 1, CuttingPlanBatch.PlateBudget(planned.Plate.Parts.Count));
Assert.InRange(planned.Result.Expansions, 1,
CuttingPlanBatch.PlateBudget(planned.Plate.Parts.Count) * budgetPercent / 100);
Assert.Equal(planned.Plate.Parts.Count, planned.PartCount);
var snapshot = planned.Result.Snapshot;
foreach (var part in planned.Result.ProposedOrder)
+3 -1
View File
@@ -196,7 +196,9 @@ along an order that teaches such a rule ends the round; the directed-recovery an
full passes run only when the lighter pass taught nothing. An attempt
stops backtracking after a stall of 8 x entries x contours expansions without
getting further, so it learns instead of retrying every entry combination of the
parts before it. A cutoff is the exception: it is a fixed program and cutting it earlier never
parts before it. The contours are the larger count of the blocked part and the part
cut just before it, whose departure a retry changes, so one many-holed part elsewhere
on the plate does not lengthen every stall. A cutoff is the exception: it is a fixed program and cutting it earlier never
makes its own cut harder, so an approach into a non-separating cutoff that crosses parts already
cut teaches "cut the cutoff before them" at once, without that stall. A rule that would contradict an order already required is skipped.
"Cut before" rules are a heuristic (a part blocked straight after another may be