mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-10 07:34:14 -04:00
perf(cutting): generate ranked alternatives only when visited
This commit is contained in:
1 parent
e897258a85
commit
d328c5f691
3 files changed
+118
-28
No files matched your search
@@ -196,29 +196,39 @@ internal static class JointCuttingPlanSearch
|
||||
var progressExpansions = Expansions;
|
||||
var stack = new Stack<Frame>();
|
||||
stack.Push(new(root));
|
||||
while (stack.Count != 0)
|
||||
try
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var frame = stack.Peek();
|
||||
var node = frame.Node;
|
||||
if (node.Order.Length == snapshot.Placements.Count)
|
||||
while (stack.Count != 0)
|
||||
{
|
||||
attempt.Order = node.Order;
|
||||
return attempt;
|
||||
token.ThrowIfCancellationRequested();
|
||||
var frame = stack.Peek();
|
||||
var node = frame.Node;
|
||||
if (node.Order.Length == snapshot.Placements.Count)
|
||||
{
|
||||
attempt.Order = node.Order;
|
||||
return attempt;
|
||||
}
|
||||
if (attempt.Advance(node))
|
||||
progressExpansions = Expansions;
|
||||
else if (stall is int limit && Expansions - progressExpansions > limit)
|
||||
return attempt;
|
||||
frame.Children ??= OrderedChildren(node, sequence, attempt, preferredOnly).GetEnumerator();
|
||||
if (!frame.Children.MoveNext())
|
||||
{
|
||||
stack.Pop().Children.Dispose();
|
||||
continue;
|
||||
}
|
||||
stack.Push(new(frame.Children.Current.Node));
|
||||
}
|
||||
if (attempt.Advance(node))
|
||||
progressExpansions = Expansions;
|
||||
else if (stall is int limit && Expansions - progressExpansions > limit)
|
||||
return attempt;
|
||||
frame.Children ??= OrderedChildren(node, sequence, attempt, preferredOnly);
|
||||
if (frame.Next == frame.Children.Length)
|
||||
{
|
||||
stack.Pop();
|
||||
continue;
|
||||
}
|
||||
stack.Push(new(frame.Children[frame.Next++].Node));
|
||||
return attempt;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Ready, stalled, cancelled and budget-exhausted searches can all leave
|
||||
// suspended siblings. Release their captured prefixes on every exit.
|
||||
foreach (var frame in stack)
|
||||
frame.Children?.Dispose();
|
||||
}
|
||||
return attempt;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -259,8 +269,14 @@ internal static class JointCuttingPlanSearch
|
||||
/// one part and contour stage the automatic rank leads — OrderBy(Distance) alone
|
||||
/// would undo the look-ahead facing. Legacy (unranked) children keep distance order.
|
||||
/// </summary>
|
||||
private Edge[] OrderedChildren(Node node, int[] sequence, Attempt attempt, bool preferredOnly)
|
||||
private IEnumerable<Edge> OrderedChildren(Node node, int[] sequence, Attempt attempt, bool preferredOnly)
|
||||
{
|
||||
// A prescribed next part (or an active part's next contour) has a single
|
||||
// source. Expand already yields its contour/entry rank order, so emit and
|
||||
// validate a sibling only when DFS reaches it. Free whole-part selection
|
||||
// still needs every source's distances before it can rank them.
|
||||
if (sequence != null || node.Active != null)
|
||||
return Expand(node, sequence, attempt, preferredOnly);
|
||||
var edges = Expand(node, sequence, attempt, preferredOnly).ToList();
|
||||
if (edges.Count <= 1)
|
||||
return edges.ToArray();
|
||||
@@ -334,8 +350,10 @@ internal static class JointCuttingPlanSearch
|
||||
// First try one ranked hole chain per endpoint. A later-part failure then
|
||||
// changes the endpoint before replaying all earlier hole combinations.
|
||||
// The retained pass below still searches every entry and hole order.
|
||||
if (preference != null)
|
||||
contours = contours.OrderBy(c => Array.IndexOf(preference.Route, c)).ThenBy(c => c);
|
||||
if (preferredOnly && preference != null)
|
||||
contours = contours.OrderBy(c => Array.IndexOf(preference.Route, c)).Take(1);
|
||||
contours = contours.Take(1);
|
||||
foreach (var contour in contours)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
@@ -593,7 +611,6 @@ internal static class JointCuttingPlanSearch
|
||||
private sealed class Frame(Node node)
|
||||
{
|
||||
internal Node Node { get; } = node;
|
||||
internal Edge[] Children { get; set; }
|
||||
internal int Next { get; set; }
|
||||
internal IEnumerator<Edge> Children { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using OpenNest.CNC.CuttingPlanning;
|
||||
using OpenNest.Engine.CuttingPlanning;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.CuttingPlanning;
|
||||
|
||||
public class LazyCuttingSearchTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void RankedRouteDoesNotSpendBudgetEmittingUnusedSiblings(bool preserveOrder)
|
||||
{
|
||||
var parts = Parts();
|
||||
var programs = parts.Select(p => p.Program).ToArray();
|
||||
var parameters = ExplicitContourTests.Parameters();
|
||||
var snapshot = CuttingPlanService.Capture(new CuttingPlanRequest(parts, new Vector(-2, -2),
|
||||
expansionBudget: 36, confirmedParameters: parameters, preservePartOrder: preserveOrder));
|
||||
var result = CuttingPlanService.Plan(snapshot);
|
||||
Assert.Equal(CuttingPlanStatus.Ready, result.Status);
|
||||
Assert.True(result.IndependentlyReplayed);
|
||||
Assert.Equal(new[] { 0, 1 }, result.ProposedOrder.Select(p => p.SourceOrdinal));
|
||||
Assert.InRange(result.Expansions, 1, 36);
|
||||
var replay = CuttingPlanService.ReplayPrograms(snapshot, result.ProposedOrder, 0, default);
|
||||
Assert.Equal(CuttingPlanStatus.Ready, replay.Status);
|
||||
Assert.True(replay.IndependentlyReplayed);
|
||||
Assert.Equal(programs, parts.Select(p => p.Program));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void LazyTraversalKeepsBudgetAndMidSearchCancellationFailClosed(bool preserveOrder)
|
||||
{
|
||||
var parts = Parts();
|
||||
var programs = parts.Select(p => p.Program).ToArray();
|
||||
var parameters = ExplicitContourTests.Parameters();
|
||||
var bounded = CuttingPlanService.Plan(new CuttingPlanRequest(parts, new Vector(-2, -2),
|
||||
expansionBudget: 1, confirmedParameters: parameters, preservePartOrder: preserveOrder));
|
||||
Assert.Equal(CuttingPlanStatus.NoSolutionWithinBudget, bounded.Status);
|
||||
Assert.Equal(1, bounded.Expansions);
|
||||
Assert.Empty(bounded.ProposedOrder);
|
||||
Assert.False(bounded.IndependentlyReplayed);
|
||||
using var cancellation = new CancellationTokenSource();
|
||||
var cancelled = CuttingPlanService.Plan(new CuttingPlanRequest(parts, new Vector(-2, -2),
|
||||
confirmedParameters: parameters, preservePartOrder: preserveOrder)
|
||||
{ ExpansionObserver = n => { if (n == 10) cancellation.Cancel(); } }, cancellation.Token);
|
||||
Assert.Equal(CuttingPlanStatus.Cancelled, cancelled.Status);
|
||||
Assert.Equal(10, cancelled.Expansions);
|
||||
Assert.Empty(cancelled.ProposedOrder);
|
||||
Assert.False(cancelled.IndependentlyReplayed);
|
||||
Assert.Equal(programs, parts.Select(p => p.Program));
|
||||
}
|
||||
|
||||
private static Part[] Parts() =>
|
||||
[
|
||||
new(new Drawing("first", LeadPathValidationTests.Rectangle(0, 0, 10, 10)), Vector.Zero),
|
||||
new(new Drawing("second", LeadPathValidationTests.Rectangle(0, 0, 10, 10)), new Vector(20, 0)),
|
||||
];
|
||||
}
|
||||
+18
-5
@@ -146,11 +146,24 @@ other curve already touches, while a contact anywhere else on the line still ref
|
||||
|
||||
Source parts rank by modeled travel (material-centre distance at a holed-part
|
||||
boundary); within a part, preferred contour order and facing-entry rank precede
|
||||
travel. Ties use stable source/contour/entry ordinals. Hash values and drawing names are not tie breakers. The expansion budget
|
||||
counts rejected candidates and frontier ranking as well as accepted moves, before
|
||||
emission; it is not a wall-clock timeout. Callers can cancel. Exhaustion may occur
|
||||
before already-generated siblings are traversed; it returns a refusal, not an
|
||||
unranked fallback or a proof of geometric impossibility.
|
||||
travel. Ties use stable source/contour/entry ordinals. Hash values and drawing names
|
||||
are not tie breakers.
|
||||
|
||||
When the next whole part is fixed by the tour or a part is already active, the
|
||||
search generates and checks contour alternatives on demand in contour/entry rank
|
||||
order. It does not emit every sibling before trying the first. Backtracking still
|
||||
visits the remaining alternatives when needed; none is pruned. The free whole-part
|
||||
fallback still evaluates all ready sources before ranking their travel distances.
|
||||
|
||||
The expansion budget counts evaluated candidates and frontier ranking, including
|
||||
rejections, before emission; it is not a wall-clock timeout. An unvisited sibling
|
||||
consumes no additional DFS-prefix emission work or per-sibling expansion charge;
|
||||
entry-catalogue prechecks still emit isolated contours and consume budget.
|
||||
Consequently a bounded search can
|
||||
reach a different frontier and report different rejected approaches than eager
|
||||
expansion did. All visited candidates retain their lead/rapid checks, and a selected
|
||||
plan still receives fresh independent replay. Callers can cancel. Exhaustion returns
|
||||
a refusal, not an unranked fallback or proof of geometric impossibility.
|
||||
|
||||
## Automatic outside entries and look-ahead
|
||||
|
||||
|
||||
Reference in new issue
Block a user