Files
OpenNest/OpenNest.Tests/CuttingPlanning/ContourRecoveryFileTests.cs
T
ajandClaude Opus 5.5 cf32d1cab5 fix(cutting): scale the reorder stall with the blocked part, not the plate's largest
An attempt along a part order stopped backtracking after 8 x entries x
contours expansions without progress, with contours taken as the most of
any part on the plate. One many-holed part therefore let every blocked
part, even a two-contour tab, replay its predecessors' entries for the
whole of that allowance; a plate whose rounds each stalled that long
ran out of budget before the recovery pass that would have finished it.

Along an order the stall now uses the larger contour count of the blocked
part and of the part cut just before it, whose departure a retry changes,
capped by the old limit. The sequence-free recovery pass keeps the plate
maximum. Planning characterization digests are unchanged.

The optional private-fixture test now also runs RecoveryCuttingNestPath
with default lead-ins, and requires that file's plates to plan within
60% of their budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 20:54:28 -04:00

114 lines
6.1 KiB
C#

using System.Security.Cryptography;
using OpenNest.CNC.CuttingStrategy;
using OpenNest.Engine.CuttingPlanning;
using OpenNest.IO;
namespace OpenNest.Tests.CuttingPlanning;
public class ContourRecoveryFileTests
{
// Optional private inputs: never embed customer geometry or names in the repository.
// RecoveryCuttingNestPath: scribed parts with aligned slot pairs, and a bolted frame boxed
// in by parts cut before it, that the free search only finishes through contour recovery.
// One of its plates also holds a many-holed part that once set every stall, so the plan
// must finish with most of the budget to spare, not just within it.
[SkippableTheory]
[InlineData("InterlockedCuttingNestPath", false, 100)]
[InlineData("InterlockedCuttingNestPath", true, 100)]
[InlineData("RecoveryCuttingNestPath", false, 60)]
[InlineData("RecoveryCuttingNestPath", true, 60)]
public void PlateFinishesThroughDesktopBatchWithoutChangingInput(string key, bool lineLeads, int budgetPercent)
{
var path = TestConfig.GetExistingPath(key);
Skip.If(path == null, $"Configure {key} in ignored test-config.json.");
var bytes = SHA256.HashData(File.ReadAllBytes(path!));
var nest = new NestReader(path).Read();
var plates = nest.Plates.Where(p => p.Parts.Count > 0).ToArray();
Assert.NotEmpty(plates);
var parts = plates.SelectMany(p => p.Parts).ToArray();
var programs = parts.Select(p => ExplicitContourTests.Fingerprint(p.Program)).ToArray();
var poses = parts.Select(p => (p.Location, p.Rotation, p.LeadInsLocked)).ToArray();
var parameters = Parameters(lineLeads);
var proposal = CuttingPlanBatch.Capture(plates, parameters, preservePartOrder: false).Plan();
Assert.True(proposal.CanApply, string.Join(Environment.NewLine, proposal.Describe("in")));
Assert.False(proposal.RequiresWarningAcceptance);
Assert.Equal(plates.Length, proposal.Plates.Count);
foreach (var planned in proposal.Plates)
{
Assert.True(planned.IsOverlapClear);
Assert.True(planned.Result.IndependentlyReplayed);
Assert.Equal(CuttingPlanStatus.Ready, planned.Result.Status);
Assert.InRange(planned.Result.Expansions, 1,
CuttingPlanBatch.PlateBudget(planned.Plate.Parts.Count) * budgetPercent / 100);
Assert.Equal(planned.Plate.Parts.Count, planned.PartCount);
var snapshot = planned.Result.Snapshot;
foreach (var part in planned.Result.ProposedOrder)
{
Assert.Equal(snapshot.Placements[part.SourceOrdinal].Prepared.Count, part.ContourChoices.Count);
Assert.Equal(part.ContourChoices.Count, part.ContourChoices.Select(c => c.ContourOrdinal).Distinct().Count());
}
var replay = CuttingPlanService.ReplayPrograms(snapshot, planned.Result.ProposedOrder, 0, default);
Assert.Equal(CuttingPlanStatus.Ready, replay.Status);
Assert.True(replay.IndependentlyReplayed);
Assert.Empty(replay.Findings);
var bounded = CuttingPlanService.Plan(snapshot.WithSearchLimits(1, 16));
Assert.Equal(CuttingPlanStatus.NoSolutionWithinBudget, bounded.Status);
Assert.Equal(1, bounded.Expansions);
Assert.Empty(bounded.ProposedOrder);
Assert.False(bounded.IndependentlyReplayed);
}
using var cancellation = new CancellationTokenSource();
cancellation.Cancel();
var cancelled = CuttingPlanBatch.Capture(plates, parameters, false).Plan(token: cancellation.Token);
Assert.True(cancelled.IsCancelled);
Assert.False(cancelled.CanApplyWithWarnings);
Assert.Equal(parts, plates.SelectMany(p => p.Parts));
Assert.Equal(programs, parts.Select(p => ExplicitContourTests.Fingerprint(p.Program)));
Assert.Equal(poses, parts.Select(p => (p.Location, p.Rotation, p.LeadInsLocked)));
Assert.Equal(bytes, SHA256.HashData(File.ReadAllBytes(path)));
}
// Optional private input: a plate of holed parts whose preferred hole chains used to fail on
// cutouts left back across themselves, spending the whole default budget before a new order.
[SkippableFact]
public void HoledPlatesFindANewOrderWithinTheDefaultBudget()
{
var path = TestConfig.GetExistingPath("HoledReorderNestPath");
Skip.If(path == null, "Configure HoledReorderNestPath in ignored test-config.json.");
var bytes = SHA256.HashData(File.ReadAllBytes(path!));
var nest = new NestReader(path).Read();
// Locked programs are fixed emissions: those plates yield unverified proposals by design.
var plates = nest.Plates.Where(p => p.Parts.Count > 0 && p.Parts.All(part => !part.LeadInsLocked)).ToArray();
Skip.If(plates.Length == 0, "The configured nest has no plate of unlocked parts.");
var parts = plates.SelectMany(p => p.Parts).ToArray();
var programs = parts.Select(p => ExplicitContourTests.Fingerprint(p.Program)).ToArray();
var proposal = CuttingPlanBatch.Capture(plates, Parameters(lineLeads: true), preservePartOrder: false).Plan();
Assert.True(proposal.CanApply, string.Join(Environment.NewLine, proposal.Describe("in")));
foreach (var planned in proposal.Plates)
{
Assert.False(planned.KeptCurrentOrder);
Assert.True(planned.IsReady);
Assert.InRange(planned.Result.Expansions, 1, CuttingPlanBatch.PlateBudget(planned.Plate.Parts.Count));
}
Assert.Equal(programs, parts.Select(p => ExplicitContourTests.Fingerprint(p.Program)));
Assert.Equal(bytes, SHA256.HashData(File.ReadAllBytes(path)));
}
private static CuttingParameters Parameters(bool lineLeads)
{
var parameters = new CuttingParameters();
if (lineLeads)
{
parameters.ExternalLeadIn = new LineLeadIn { Length = 0.15, ApproachAngle = 90 };
parameters.InternalLeadIn = new LineLeadIn { Length = 0.15, ApproachAngle = 90 };
parameters.ArcCircleLeadIn = new LineLeadIn { Length = 0.15, ApproachAngle = 90 };
}
return parameters;
}
}