mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-10 07:34:14 -04:00
perf(cutting): try a bounded entry catalogue before full search
This commit is contained in:
1 parent
d328c5f691
commit
1c6d02c480
4 files changed
+93
-1
No files matched your search
@@ -43,6 +43,10 @@ public sealed class CuttingPlanBatch
|
||||
/// </summary>
|
||||
public const int ExpansionsPerPart = 400;
|
||||
|
||||
// Extra bounded work on hard/refused plates; the full-cap search still gets its whole budget.
|
||||
internal const int FirstPassBudget = 1000;
|
||||
internal const int FirstPassEntries = 8;
|
||||
|
||||
private readonly Entry[] entries;
|
||||
private readonly CuttingParameters ownedParameters;
|
||||
|
||||
@@ -142,7 +146,7 @@ public sealed class CuttingPlanBatch
|
||||
if (entry.Reorder != null)
|
||||
{
|
||||
progress?.Report(new(index, entries.Length, entry.Number, CuttingPlanPhase.Reordering));
|
||||
reorder = CuttingPlanService.Plan(entry.Reorder, token);
|
||||
reorder = PlanReorder(entry.Reorder, token);
|
||||
if (reorder.Status != CuttingPlanStatus.NoSolutionWithinBudget)
|
||||
{
|
||||
plans[index] = new(entry.Plate, entry.Number, WithFallback(entry, reorder, token), null, overlap);
|
||||
@@ -156,6 +160,22 @@ public sealed class CuttingPlanBatch
|
||||
return new(plans, ownedParameters);
|
||||
}
|
||||
|
||||
internal static CuttingPlanResult PlanReorder(CuttingPlanSnapshot snapshot, CancellationToken token,
|
||||
int firstPassBudget = FirstPassBudget)
|
||||
{
|
||||
// A failed first pass is not a proof of impossibility: retain all original
|
||||
// full-cap alternatives and the complete original budget for the retry.
|
||||
if (snapshot.Failure == null && snapshot.Regeneration && snapshot.MaxEntries > FirstPassEntries
|
||||
&& snapshot.ExpansionBudget > firstPassBudget)
|
||||
{
|
||||
var first = CuttingPlanService.Plan(snapshot.WithSearchLimits(firstPassBudget, FirstPassEntries), token);
|
||||
if ((first.Status == CuttingPlanStatus.Ready && first.IndependentlyReplayed)
|
||||
|| first.Status == CuttingPlanStatus.Cancelled)
|
||||
return first;
|
||||
}
|
||||
return CuttingPlanService.Plan(snapshot, token);
|
||||
}
|
||||
|
||||
private static CuttingPlanResult WithFallback(Entry entry, CuttingPlanResult strict, CancellationToken token)
|
||||
{
|
||||
if (strict.Status != CuttingPlanStatus.UnsupportedGeometry || entry.Fallback.Failure != null)
|
||||
|
||||
@@ -93,6 +93,13 @@ public sealed class CuttingPlanSnapshot
|
||||
public int ExpansionBudget { get; }
|
||||
internal CuttingPlanStatus? Failure { get; }
|
||||
internal IReadOnlyList<CuttingPlanFinding> Findings { get; }
|
||||
|
||||
// A worker-only search variant over the same owned capture. Never recapture the live plate:
|
||||
// both proposals retain the identical freshness record and confirmed settings.
|
||||
internal CuttingPlanSnapshot WithSearchLimits(int expansionBudget, int maxEntries) =>
|
||||
new(Placements, StartPoint, expansionBudget, Failure, Findings, Regeneration,
|
||||
PreservePartOrder, maxEntries, ExpansionObserver, PlateState, OwnedParameters,
|
||||
Dependencies, BestEffort);
|
||||
}
|
||||
|
||||
public sealed class FixedProgramPlacement
|
||||
|
||||
@@ -144,6 +144,63 @@ public class CuttingPlanBatchTests
|
||||
Assert.Null(proposal.BuildPreview(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlanReorder_ShortPassReady_RetainsReplayedApplicableProposal()
|
||||
{
|
||||
var plate = Plate(new Nest(), Clean("first", 1, 1), Clean("second", 12, 1));
|
||||
var snapshot = CuttingPlanService.Capture(CuttingPlanRequest.ForPlate(plate,
|
||||
confirmedParameters: ExplicitContourTests.Parameters()));
|
||||
var full = CuttingPlanService.Plan(snapshot);
|
||||
var result = CuttingPlanBatch.PlanReorder(snapshot, CancellationToken.None);
|
||||
|
||||
Assert.Equal(CuttingPlanStatus.Ready, result.Status);
|
||||
Assert.True(result.IndependentlyReplayed);
|
||||
Assert.True(result.Expansions < full.Expansions,
|
||||
$"Short pass {result.Expansions}, full {full.Expansions}");
|
||||
Assert.InRange(result.Expansions, 1, CuttingPlanBatch.FirstPassBudget - 1);
|
||||
Assert.Equal(2, result.ProposedOrder.Count);
|
||||
Assert.All(plate.Parts, part => Assert.False(part.HasManualLeadIns));
|
||||
Assert.Equal(CuttingCommitStatus.Applied, CuttingPlanService.Apply([result]).Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlanReorder_ShortPassExhausted_UsesFullCapAndOriginalBudget()
|
||||
{
|
||||
var plate = Plate(new Nest(), Grid(4));
|
||||
var snapshot = CuttingPlanService.Capture(CuttingPlanRequest.ForPlate(plate,
|
||||
confirmedParameters: ExplicitContourTests.Parameters()));
|
||||
var full = CuttingPlanService.Plan(snapshot);
|
||||
var recovered = CuttingPlanBatch.PlanReorder(snapshot, CancellationToken.None, firstPassBudget: 1);
|
||||
|
||||
Assert.Equal(CuttingPlanStatus.Ready, full.Status);
|
||||
Assert.Equal(CuttingPlanStatus.Ready, recovered.Status);
|
||||
Assert.True(recovered.IndependentlyReplayed);
|
||||
Assert.Equal(full.Expansions, recovered.Expansions);
|
||||
Assert.Equal(full.RapidDistance, recovered.RapidDistance);
|
||||
Assert.Equal(full.ProposedOrder.Select(p => p.SourceOrdinal), recovered.ProposedOrder.Select(p => p.SourceOrdinal));
|
||||
for (var i = 0; i < full.ProposedOrder.Count; i++)
|
||||
Assert.True(ProgramContent.Equal(full.ProposedOrder[i].CopyProgram(), recovered.ProposedOrder[i].CopyProgram()));
|
||||
Assert.All(plate.Parts, part => Assert.False(part.HasManualLeadIns));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlanReorder_CancelledDuringShortPass_DoesNotRetryOrMutate()
|
||||
{
|
||||
var plate = Plate(new Nest(), Clean("first", 1, 1), Clean("second", 12, 1));
|
||||
using var cancellation = new CancellationTokenSource();
|
||||
var observations = 0;
|
||||
var request = new CuttingPlanRequest(plate.Parts, confirmedParameters: ExplicitContourTests.Parameters())
|
||||
{ ExpansionObserver = _ => { observations++; cancellation.Cancel(); } };
|
||||
var snapshot = CuttingPlanService.Capture(request);
|
||||
|
||||
var result = CuttingPlanBatch.PlanReorder(snapshot, cancellation.Token);
|
||||
|
||||
Assert.Equal(CuttingPlanStatus.Cancelled, result.Status);
|
||||
Assert.Equal(1, observations);
|
||||
Assert.Empty(result.ProposedOrder);
|
||||
Assert.All(plate.Parts, part => Assert.False(part.HasManualLeadIns));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Plan_FreeSearchOutOfBudget_KeepsTheCurrentOrderAndSaysSo()
|
||||
{
|
||||
|
||||
@@ -319,6 +319,14 @@ plans every plate that has parts. Both open one dialog built on
|
||||
obstruct it, spacing the parts farther apart. These are suggestions, not guaranteed
|
||||
fixes: replanning runs the same strict checks; an unverified fallback requires
|
||||
separate warning acceptance and is not approval to post or cut.
|
||||
- The desktop free-order batch first tries up to eight entries per contour for at most
|
||||
1,000 expansions. Only a Ready proposal that passes independent replay is used;
|
||||
otherwise it runs the original 16-entry search with its entire plate budget.
|
||||
Cancellation stops both attempts. This short pass can choose a different valid
|
||||
route, and a hard plate can take up to 1,000 extra expansions before the full
|
||||
attempt. The current-order retry still uses its original 16-entry budget.
|
||||
Returned expansion counts describe the retained attempt, not a discarded
|
||||
short pass plus its full-cap retry.
|
||||
- Every plate is captured on the UI thread and checked and planned on a worker. Clean part
|
||||
material is checked for overlaps with the pre-post overlap analyzer; known overlapping parts
|
||||
still block that plate whatever its route. Incomplete checks remain visible warnings, never
|
||||
|
||||
Reference in new issue
Block a user