fix(planning): learn to reach a sheet separator from beyond the parts in its way

A sheet separator is cut after everything, so when the approach to its
pierce crossed parts already cut, the usual "cut it before them" rule
contradicted that and the search learned nothing. It fell back to the
full search, which spent the budget or found a route that climbed the
column and jumped back down past the cut parts (N26-23M).

When every "cut the blocked part first" rule would contradict a required
order, learn the other way round: cut the part the approach left from
before the parts it crossed, so the tool arrives from beyond them. A
separator blocked this way now ends the attempt at once, like any
cutoff whose rule can be learned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-27 14:36:26 -04:00
1 parent 207f126b9e
commit f4317bc52f
3 files changed
+69 -19

No files matched your search

@@ -196,26 +196,35 @@ internal static class JointCuttingPlanSearch
}
// "Cut the blocked part before every part whose cut contour its approach crossed", unless that
// would contradict an order already required. False when nothing new was learned.
// would contradict an order already required. When every such rule would (a sheet separator
// is cut after everything), the blocked part cannot come earlier, so the tool must arrive
// from beyond those parts instead: "cut the part it left from before them". False when
// nothing new was learned.
private static bool Learn(Attempt attempt, HashSet<int>[] prerequisites)
{
if (attempt.Blocked is not int blocked)
return false;
var learned = false;
foreach (var crossed in attempt.Crossed.Order())
{
if (crossed == blocked || prerequisites[crossed].Contains(blocked) || Precedes(crossed, blocked, prerequisites))
continue;
prerequisites[crossed].Add(blocked);
learned = true;
}
return learned;
var rules = Rules(attempt, prerequisites).ToArray();
foreach (var (first, then) in rules)
prerequisites[then].Add(first);
return rules.Length != 0;
}
// True when a crossing of the blocked part's approach would teach a new order rule.
private static bool CanLearn(Attempt attempt, HashSet<int>[] prerequisites) =>
attempt.Blocked is int blocked && attempt.Crossed.Any(crossed => crossed != blocked
&& !prerequisites[crossed].Contains(blocked) && !Precedes(crossed, blocked, prerequisites));
Rules(attempt, prerequisites).Any();
// The rules Learn would add, as (cut first, cut then) pairs.
private static IEnumerable<(int First, int Then)> Rules(Attempt attempt, HashSet<int>[] prerequisites)
{
if (attempt.Blocked is not int blocked)
return [];
var before = New(blocked);
return before.Length != 0 || attempt.LastCut?.SourceOrdinal is not int departed ? before : New(departed);
(int, int)[] New(int first) => attempt.Crossed.Order()
.Where(crossed => crossed != blocked && crossed != first && !prerequisites[crossed].Contains(first)
&& !Precedes(crossed, first, prerequisites))
.Select(crossed => (first, crossed)).ToArray();
}
// True when 'first' must already come before 'second' through the prerequisite chain.
private static bool Precedes(int first, int second, HashSet<int>[] prerequisites)
@@ -334,10 +343,10 @@ internal static class JointCuttingPlanSearch
}
// A cutoff is a fixed program cut the same way wherever it goes, so replanning
// the parts before it can only change where its approach starts. Cutting it
// earlier never makes its own cut harder; learn that rather than stall here.
// earlier never makes its own cut harder, and a sheet separator that cannot come
// earlier needs the approach from elsewhere; learn that rather than stall here.
if (prerequisites != null && sequence != null && attempt.Blocked is int blocked
&& snapshot.Placements[blocked] is { IsCutOff: true, SeparatesSheet: false }
&& CanLearn(attempt, prerequisites))
&& snapshot.Placements[blocked].IsCutOff && CanLearn(attempt, prerequisites))
return attempt;
if (attempt.Advance(node))
progressExpansions = Expansions;
@@ -79,6 +79,43 @@ public class CuttingDependencyTests
&& f.Message.Contains("before the sheet is separated"));
}
// A column of two wide parts under three narrow ones, beside a sheet separator at X 12.5
// pierced at (12.5, 0). Climbing the column ends at the top, where the straight approach to
// that pierce crosses the wide parts, and the separator cannot be cut any earlier. The search
// learns the other way round, the part the approach left from before the parts it crossed,
// and so finishes beside the pierce. It used to learn nothing and spend its whole budget.
[Fact]
public void Plan_ApproachIntoASeparatorCrossingCutParts_LearnsToLeaveFromBeyondThem()
{
var (plate, column, separator) = ColumnBesideSeparator();
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate,
confirmedParameters: ExplicitContourTests.Parameters()));
Assert.True(result.Status == CuttingPlanStatus.Ready, Describe(result));
var order = result.ProposedOrder.Select(o => o.SourcePart).ToList();
Assert.Same(separator, order[^1]);
Assert.Contains(order[^2], column[..2]);
Assert.True(result.Expansions < 400, Describe(result));
}
private static (Plate, Part[] Column, Part Separator) ColumnBesideSeparator()
{
var nest = new Nest();
var plate = nest.CreatePlate();
plate.Size = new Size(100, 100);
Part[] column =
[
Rectangle(2, 2, 10, 10), Rectangle(2, 14, 10, 10),
Rectangle(2, 26, 5, 10), Rectangle(2, 38, 5, 10), Rectangle(2, 50, 5, 10),
];
foreach (var part in column)
plate.Parts.Add(part);
plate.CutOffs.Add(new CutOff(new Vector(12.5, 0), CutOffAxis.Vertical));
plate.RegenerateCutOffs(new CutOffSettings());
return (plate, column, plate.Parts.Single(p => p.BaseDrawing.IsCutOff));
}
// The tour cuts B, then the locked rectangle A (leaving right), then the cutoff pierced at
// (10, 0) below-left of A: that approach crosses A. Replanning B's entries can never fix it,
// so the search learns "cutoff first" at once instead of retrying each of B's entries.
+6 -2
View File
@@ -218,7 +218,10 @@ from the start point. Its 2-opt and Or-opt passes would grow with the square of
part count. Replans after a learned rule use the plain tour. If a part on that order cannot be
reached without crossing parts already cut, the search learns "cut this part
before those", backs up to just before the earliest of them and re-plans the rest
from the tool position there; parts cut before that point are kept. The first pass
from the tool position there; parts cut before that point are kept. When the part
cannot come earlier (a sheet separator is cut after everything), it learns the other
way round, "cut the part the approach left from before those", so the tool arrives
from beyond them. The first pass
along an order that teaches such a rule ends the round; the directed-recovery and
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
@@ -227,7 +230,8 @@ parts before it. The contours are the larger count of the blocked part and the p
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 teaches "cut the cutoff before them" at once, without that stall, and one into a sheet
separator teaches its other-way-round rule at once. 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
reachable via a third), so once nothing new can be learned the remaining budget
first tries sequence-free recovery, visiting dependency-ready parts lazily by