mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-06 10:42:12 -04:00
fix(cutting): fall back to a full order search when learning stops
Review found a plate the previous search planned that the tour now refused. Left leads in on its left and right on its right, so right straight after left crosses left and left straight after right crosses right. Learning "right before left" then contradicted "left before right" and the search returned ConstraintConflict, although cutting a third part above them in between is safe. A blocked approach only proves that one part cannot follow the parts cut so far from that position, not a global order, so learned rules stay a heuristic. Once nothing new can be learned, the remaining budget now goes to a full search over every ready part, nearest first (the search used before the tour). A rule contradicting an order already required is still skipped rather than ending learning early.
This commit is contained in:
1 parent
c21f687987
commit
4e6419fd2c
3 files changed
+43
-9
No files matched your search
@@ -13,7 +13,8 @@ namespace OpenNest.Engine.CuttingPlanning;
|
||||
/// Plans whole parts and their emitted contour prefixes along a part order. A preserved order is
|
||||
/// followed with full backtracking. Otherwise the order comes from <see cref="CuttingPartOrder"/>;
|
||||
/// when a part on it cannot be reached without crossing parts already cut, the search learns
|
||||
/// "cut this part before those", keeps the parts cut before them and re-plans the rest.
|
||||
/// "cut this part before those", keeps the parts cut before them and re-plans the rest. Once
|
||||
/// nothing new can be learned, the remaining budget goes to a full search over every ready part.
|
||||
/// </summary>
|
||||
internal static class JointCuttingPlanSearch
|
||||
{
|
||||
@@ -52,7 +53,12 @@ internal static class JointCuttingPlanSearch
|
||||
if (attempt.Order != null)
|
||||
return walk.Ready(attempt.Order);
|
||||
if (!Learn(attempt, prerequisites))
|
||||
return walk.Exhausted();
|
||||
{
|
||||
// "Cut before" rules are a heuristic: a part blocked straight after another can
|
||||
// still be reachable via a third. Search every order with what budget remains.
|
||||
var full = walk.Follow(null, null, null);
|
||||
return full.Order != null ? walk.Ready(full.Order) : walk.Exhausted();
|
||||
}
|
||||
// Back up to just before the earliest part the blocked approach crossed, keep the
|
||||
// parts cut before it, and re-plan the rest from where the tool is at that point.
|
||||
var back = attempt.Crossed.Min(part => Array.IndexOf(sequence, part));
|
||||
@@ -154,7 +160,8 @@ internal static class JointCuttingPlanSearch
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Follows <paramref name="sequence"/> from <paramref name="resume"/> (or the start point).
|
||||
/// Follows <paramref name="sequence"/> from <paramref name="resume"/> (or the start point);
|
||||
/// a null sequence tries every dependency-ready part, nearest first.
|
||||
/// With a stall limit the attempt ends once that many expansions pass without getting
|
||||
/// further along the order; it never backtracks behind its starting node.
|
||||
/// </summary>
|
||||
@@ -199,8 +206,11 @@ internal static class JointCuttingPlanSearch
|
||||
else
|
||||
{
|
||||
var finished = node.Order.Select(o => o.SourceOrdinal).ToHashSet();
|
||||
var next = snapshot.Placements[sequence[node.Order.Length]];
|
||||
sources = snapshot.Dependencies.IsReady(next.SourceOrdinal, finished) ? [next] : [];
|
||||
sources = sequence == null
|
||||
? snapshot.Placements.Where(p => !finished.Contains(p.SourceOrdinal)
|
||||
&& snapshot.Dependencies.IsReady(p.SourceOrdinal, finished))
|
||||
: snapshot.Dependencies.IsReady(sequence[node.Order.Length], finished)
|
||||
? [snapshot.Placements[sequence[node.Order.Length]]] : [];
|
||||
}
|
||||
foreach (var source in sources)
|
||||
{
|
||||
@@ -296,7 +306,7 @@ internal static class JointCuttingPlanSearch
|
||||
private Node Deepest { get; set; }
|
||||
|
||||
/// <summary>The ordinal of the part the attempt could not get past, or null.</summary>
|
||||
internal int? Blocked => Deepest == null || Deepest.Order.Length >= sequence.Length ? null
|
||||
internal int? Blocked => sequence == null || Deepest == null || Deepest.Order.Length >= sequence.Length ? null
|
||||
: sequence[Deepest.Order.Length];
|
||||
|
||||
/// <summary>Parts whose completed contours the blocked part's motions crossed (it can be among them).</summary>
|
||||
|
||||
@@ -76,6 +76,24 @@ public class ReorderSearchTests
|
||||
Assert.True(result.IndependentlyReplayed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FreeOrder_LearnedOrderContradicts_FallsBackToTheFullSearch()
|
||||
{
|
||||
// Left leads on its left, right on its right: right straight after left crosses left, and
|
||||
// left straight after right crosses right, so "cut before" rules contradict each other.
|
||||
// Going via the part above, which leaves downward, reaches right safely.
|
||||
var left = LeftLeadRectangle("left", 0, 0, 2, 2);
|
||||
var right = LeftLeadRectangle("right", 4, 0, 2, 2, mirror: true);
|
||||
var via = LeftLeadRectangle("via", -1, 5, 2, 2, departure: new Vector(-0.25, -0.25));
|
||||
|
||||
var result = CuttingPlanService.Plan(new CuttingPlanRequest([left, right, via],
|
||||
confirmedParameters: ExplicitContourTests.Parameters()));
|
||||
|
||||
Assert.True(result.Status == CuttingPlanStatus.Ready, Describe(result));
|
||||
Assert.True(result.IndependentlyReplayed);
|
||||
Assert.Equal(new[] { left, via, right }, result.ProposedOrder.Select(p => p.SourcePart));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FreeOrder_NoSafeOrder_RefusesWithTheCrossing()
|
||||
{
|
||||
@@ -107,9 +125,9 @@ public class ReorderSearchTests
|
||||
private static (double, double) Key(Part part) => (part.Location.X, part.Location.Y);
|
||||
|
||||
// A locked rectangle whose lead-in and lead-out sit 0.25 outside its left edge (its right
|
||||
// edge when mirrored), so the tool departs on that side.
|
||||
// edge when mirrored), so the tool departs on that side unless a final rapid moves it on.
|
||||
private static Part LeftLeadRectangle(string name, double x, double y, double width, double height,
|
||||
bool mirror = false)
|
||||
bool mirror = false, Vector? departure = null)
|
||||
{
|
||||
var clean = LeadPathValidationTests.Rectangle(0, 0, width, height);
|
||||
var part = new Part(new Drawing(name, clean), new Vector(x, y));
|
||||
@@ -131,6 +149,8 @@ public class ReorderSearchTests
|
||||
}
|
||||
placed.LineTo(edge, height / 2);
|
||||
placed.Codes.Add(new LinearMove(outside, height / 2) { Layer = LayerType.Leadout });
|
||||
if (departure is { } end)
|
||||
placed.MoveTo(end.X, end.Y);
|
||||
Assert.True(part.RestoreLeadInProgram(placed, true));
|
||||
return part;
|
||||
}
|
||||
|
||||
@@ -88,7 +88,11 @@ before those", backs up to just before the earliest of them and re-plans the res
|
||||
from the tool position there; parts cut before that point are kept. 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. When nothing new can be learned the result is a refusal.
|
||||
parts before it. 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
|
||||
goes to a full search that tries every ready part, nearest first; only when that
|
||||
also fails is the result a refusal.
|
||||
|
||||
Candidates use native closest points, vertices, midpoints and circle angles in
|
||||
stable order, capped by `maxEntries`. Circle rounding, clamping, corner resolution
|
||||
|
||||
Reference in new issue
Block a user