mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-11 20:14:14 -04:00
fix(cutting): try endpoint alternatives before hole combinations
This commit is contained in:
1 parent
7622d63556
commit
64459f7bc8
3 files changed
+126
-12
No files matched your search
@@ -10,8 +10,9 @@ using OpenNest.Geometry;
|
||||
namespace OpenNest.Engine.CuttingPlanning;
|
||||
|
||||
/// <summary>
|
||||
/// 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"/>;
|
||||
/// Plans whole parts and their emitted contour prefixes along a part order. Multi-part holed
|
||||
/// requests first try one ranked hole chain per endpoint, then retain full backtracking.
|
||||
/// A preserved order stays fixed. 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. Once
|
||||
/// nothing new can be learned, the remaining budget goes to a full search over every ready part.
|
||||
@@ -32,24 +33,39 @@ internal static class JointCuttingPlanSearch
|
||||
{
|
||||
var walk = new Walk(snapshot, token);
|
||||
var count = snapshot.Placements.Count;
|
||||
var maxContours = snapshot.Placements.Max(p => p.Prepared?.Count ?? 1);
|
||||
var stall = StallExpansionsPerEntry * snapshot.MaxEntries * maxContours;
|
||||
var preferEndpoints = count > 1 && maxContours > 1;
|
||||
try
|
||||
{
|
||||
if (snapshot.PreservePartOrder)
|
||||
{
|
||||
var kept = walk.Follow(Enumerable.Range(0, count).ToArray(), null, null);
|
||||
var keptSequence = Enumerable.Range(0, count).ToArray();
|
||||
if (preferEndpoints)
|
||||
{
|
||||
var preferred = walk.Follow(keptSequence, stall, null, preferredOnly: true);
|
||||
if (preferred.Order != null)
|
||||
return walk.Ready(preferred.Order);
|
||||
}
|
||||
var kept = walk.Follow(keptSequence, null, null);
|
||||
return kept.Order != null ? walk.Ready(kept.Order) : walk.Exhausted();
|
||||
}
|
||||
|
||||
var centres = snapshot.Placements.Select(Centre).ToArray();
|
||||
var prerequisites = Enumerable.Range(0, count)
|
||||
.Select(i => new HashSet<int>(snapshot.Dependencies.PrerequisitesOf(i))).ToArray();
|
||||
var maxContours = snapshot.Placements.Max(p => p.Prepared?.Count ?? 1);
|
||||
var stall = StallExpansionsPerEntry * snapshot.MaxEntries * maxContours;
|
||||
|
||||
var sequence = CuttingPartOrder.Plan(Enumerable.Range(0, count).ToArray(), centres,
|
||||
snapshot.StartPoint, prerequisites, token);
|
||||
Node resume = null;
|
||||
while (true)
|
||||
{
|
||||
if (preferEndpoints)
|
||||
{
|
||||
var preferred = walk.Follow(sequence, stall, resume, preferredOnly: true);
|
||||
if (preferred.Order != null)
|
||||
return walk.Ready(preferred.Order);
|
||||
}
|
||||
var attempt = walk.Follow(sequence, stall, resume);
|
||||
if (attempt.Order != null)
|
||||
return walk.Ready(attempt.Order);
|
||||
@@ -173,7 +189,7 @@ internal static class JointCuttingPlanSearch
|
||||
/// 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>
|
||||
internal Attempt Follow(int[] sequence, int? stall, Node resume)
|
||||
internal Attempt Follow(int[] sequence, int? stall, Node resume, bool preferredOnly = false)
|
||||
{
|
||||
var root = resume ?? new Node([], snapshot.StartPoint, new ReleasedContourState(), null, null);
|
||||
var attempt = new Attempt(sequence);
|
||||
@@ -194,7 +210,7 @@ internal static class JointCuttingPlanSearch
|
||||
progressExpansions = Expansions;
|
||||
else if (stall is int limit && Expansions - progressExpansions > limit)
|
||||
return attempt;
|
||||
frame.Children ??= OrderedChildren(node, sequence, attempt);
|
||||
frame.Children ??= OrderedChildren(node, sequence, attempt, preferredOnly);
|
||||
if (frame.Next == frame.Children.Length)
|
||||
{
|
||||
stack.Pop();
|
||||
@@ -243,9 +259,9 @@ 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)
|
||||
private Edge[] OrderedChildren(Node node, int[] sequence, Attempt attempt, bool preferredOnly)
|
||||
{
|
||||
var edges = Expand(node, sequence, attempt).ToList();
|
||||
var edges = Expand(node, sequence, attempt, preferredOnly).ToList();
|
||||
if (edges.Count <= 1)
|
||||
return edges.ToArray();
|
||||
// Stable source order: the minimum incremental rapid per source, ties ordinal.
|
||||
@@ -263,7 +279,7 @@ internal static class JointCuttingPlanSearch
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private IEnumerable<Edge> Expand(Node node, int[] sequence, Attempt attempt)
|
||||
private IEnumerable<Edge> Expand(Node node, int[] sequence, Attempt attempt, bool preferredOnly)
|
||||
{
|
||||
IEnumerable<FixedProgramPlacement> sources;
|
||||
if (node.Active is { } active)
|
||||
@@ -315,6 +331,11 @@ internal static class JointCuttingPlanSearch
|
||||
var boundary = node.Active?.Boundary ?? node;
|
||||
var contours = choices.Length == prepared.Count - 1 ? new[] { prepared.PerimeterOrdinal }
|
||||
: Enumerable.Range(0, prepared.PerimeterOrdinal).Where(c => !choices.Any(e => e.ContourOrdinal == c));
|
||||
// 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 (preferredOnly && preference != null)
|
||||
contours = contours.OrderBy(c => Array.IndexOf(preference.Route, c)).Take(1);
|
||||
foreach (var contour in contours)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
@@ -346,7 +367,8 @@ internal static class JointCuttingPlanSearch
|
||||
}
|
||||
if (entries.Count == 0)
|
||||
continue;
|
||||
for (var entry = 0; entry < entries.Count; entry++)
|
||||
var entryCount = preferredOnly && node.Active?.Perimeter != null ? System.Math.Min(1, entries.Count) : entries.Count;
|
||||
for (var entry = 0; entry < entryCount; entry++)
|
||||
{
|
||||
CountExpansion(source); // Before emission/native queries, including rejected candidates.
|
||||
var prefix = choices.Append(entries[entry]).ToArray();
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.CNC.CuttingPlanning;
|
||||
using OpenNest.CNC.CuttingStrategy;
|
||||
using OpenNest.Diagnostics;
|
||||
using OpenNest.Engine.CuttingPlanning;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.CuttingPlanning;
|
||||
|
||||
public class HoleEndpointBudgetTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void LaterFixedApproachTriesOtherEndpointsBeforeHoleCombinations(bool preserveOrder)
|
||||
{
|
||||
var parameters = HoleLookAheadTests.Parameters();
|
||||
var clean = HoleLookAheadTests.MixedHoles();
|
||||
foreach (var centre in new[] { new Vector(4, 8), new Vector(8, 8) })
|
||||
{
|
||||
clean.MoveTo(centre.X + 0.75, centre.Y);
|
||||
clean.ArcTo(centre.X + 0.75, centre.Y, centre.X, centre.Y, RotationType.CCW);
|
||||
}
|
||||
var first = new Part(new Drawing("four holes", clean));
|
||||
var nextClean = LeadPathValidationTests.Rectangle(0, 0, 2, 2);
|
||||
nextClean.MoveTo(-22, 10);
|
||||
nextClean.Codes.Add(new LinearMove(-21, 10) { Layer = LayerType.Scribe });
|
||||
nextClean.MoveTo(0, 10);
|
||||
nextClean.Codes.Add(new LinearMove(1, 10) { Layer = LayerType.Scribe });
|
||||
var next = new Part(new Drawing("fixed marked part", nextClean), new Vector(20, 2));
|
||||
var nextPrepared = PreparedContours.Capture(nextClean, parameters);
|
||||
Assert.True(next.RestoreLeadInProgram(nextPrepared.Emit(
|
||||
[nextPrepared.ClosestEntry(0, new Vector(3, 1))]), true));
|
||||
var before = new[] { first, next }.Select(p => ExplicitContourTests.Fingerprint(p.Program)).ToArray();
|
||||
var start = new Vector(-2, 4);
|
||||
var snapshot = CuttingPlanService.Capture(new CuttingPlanRequest([first, next], start,
|
||||
confirmedParameters: parameters, preservePartOrder: preserveOrder));
|
||||
var source = snapshot.Placements[0];
|
||||
var prepared = source.Prepared;
|
||||
var adapter = new ContourEntryFeasibility(prepared, source.Location, source.Material,
|
||||
snapshot.Placements.Select(p => p.Material).ToArray());
|
||||
var target = new Vector(21, 3);
|
||||
var outside = ContourEntrySelection.Select(prepared.AutomaticEntryCandidatesWithFallbacks(4, target)
|
||||
.RankTowardNextCut(target, start), c => adapter.Check(c.Choice)).Choices[0];
|
||||
var centres = prepared.HoleCentres().Select(c => c ?? Vector.Zero).ToArray();
|
||||
var route = CuttingHoleOrder.Plan(new[] { 0, 1, 2, 3 }, centres, start,
|
||||
PreferredContourEntries.Pierce(prepared, outside, default));
|
||||
var preferred = PreferredContourEntries.TryPlan(prepared, outside, route, centres, start,
|
||||
c => adapter.Check(c.Choice));
|
||||
Assert.True(preferred.IsPreferred, preferred.Reason);
|
||||
var execution = ExecutionMotionReader.Read(prepared.Emit(preferred.HoleChoices.Append(outside).ToArray()),
|
||||
Vector.Zero, start, default);
|
||||
var checker = new ReleasedContourState();
|
||||
Assert.Empty(checker.Check(execution, start, 1));
|
||||
Assert.Contains(checker.Check(snapshot.Placements[1].Execution, execution.DeparturePoint, 2),
|
||||
f => f.Kind == PostVerificationKind.RapidCrossing);
|
||||
|
||||
var result = CuttingPlanService.Plan(snapshot);
|
||||
|
||||
Assert.True(result.Status == CuttingPlanStatus.Ready,
|
||||
$"{result.Status}, {result.Expansions} expansions: " + string.Join("; ", result.Findings.Select(f => f.Message)));
|
||||
Assert.True(result.IndependentlyReplayed);
|
||||
Assert.Equal(new[] { 0, 1 }, result.ProposedOrder.Select(p => p.SourceOrdinal));
|
||||
Assert.Equal(5, result.ProposedOrder[0].ContourChoices.Count);
|
||||
Assert.Equal(4, result.ProposedOrder[0].ContourChoices[^1].ContourOrdinal);
|
||||
Assert.NotEqual(outside.Point, result.ProposedOrder[0].ContourChoices[^1].Point);
|
||||
Assert.InRange(result.Expansions, 1, 20000);
|
||||
var replay = CuttingPlanService.ReplayPrograms(snapshot, result.ProposedOrder, 0, default);
|
||||
Assert.Equal(CuttingPlanStatus.Ready, replay.Status);
|
||||
Assert.True(replay.IndependentlyReplayed);
|
||||
Assert.Equal(before, new[] { first, next }.Select(p => ExplicitContourTests.Fingerprint(p.Program)));
|
||||
Assert.Equal(before[1], ExplicitContourTests.Fingerprint(result.ProposedOrder[1].CopyProgram()));
|
||||
|
||||
var bounded = CuttingPlanService.Plan(new CuttingPlanRequest([first, next], start,
|
||||
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 cancelled = new CancellationTokenSource();
|
||||
cancelled.Cancel();
|
||||
Assert.Equal(CuttingPlanStatus.Cancelled, CuttingPlanService.Plan(snapshot, cancelled.Token).Status);
|
||||
Assert.Equal(before, new[] { first, next }.Select(p => ExplicitContourTests.Fingerprint(p.Program)));
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,14 @@ the final replay enforce them:
|
||||
With regeneration, the bounded deterministic search plans internal contour order
|
||||
and native entry candidates part by part along a whole-part order. Internal
|
||||
contours precede their own perimeter; parts remain contiguous. Backtracking can
|
||||
revisit an earlier entry when a later part cannot be reached safely.
|
||||
revisit an earlier entry when a later part cannot be reached safely. For multi-part
|
||||
requests containing regenerated holes, it first tries one ranked hole chain for
|
||||
each outside endpoint. This lets a later blocked approach change the previous
|
||||
part's departure without first exhausting combinations of its earlier holes.
|
||||
This preferred pass uses the same emitted-motion checks, shared expansion budget
|
||||
and stall limit as the retained search; on failure the full entry/hole-order
|
||||
backtracking pass remains available. Single-part and no-hole requests keep their
|
||||
existing search order.
|
||||
|
||||
A preserved order is followed as given. Otherwise the order is an open
|
||||
travelling-salesman path over part centres from the start point: nearest neighbour,
|
||||
|
||||
Reference in new issue
Block a user