mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-15 01:21:55 -04:00
213 lines
9.8 KiB
C#
213 lines
9.8 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using OpenNest.Engine.Jobs;
|
|
using OpenNest.Engine.Jobs.Adapters;
|
|
using static OpenNest.Engine.Tests.NestingEngines.JobBuilder;
|
|
|
|
namespace OpenNest.Engine.Tests.NestingEngines;
|
|
|
|
/// <summary>Physical accounting/continuation contract, run independently for each repeating engine.</summary>
|
|
public abstract class RepeatEngineSafetyTests
|
|
{
|
|
private protected abstract INestingEngine Engine(Action? packed = null, Action<RepeatBatch>? batches = null);
|
|
|
|
[Theory]
|
|
[InlineData(5)]
|
|
[InlineData(1)]
|
|
public void MixedScarcityContinuesToDifferentResidualLayoutInOneSolve(int urgentCount)
|
|
{
|
|
var job = Mixed(urgentCount, 8);
|
|
var batches = new List<RepeatBatch>();
|
|
var result = Engine(batches: batches.Add).Solve(job);
|
|
LayoutAssert.Valid(job, result);
|
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
|
Assert.Equal(urgentCount, batches[0].Copies);
|
|
Assert.True(batches.Count > 1);
|
|
Assert.Equal(urgentCount, result.Fulfillment.Single(f => f.PartId == "a").Placed);
|
|
Assert.Equal(8, result.Fulfillment.Single(f => f.PartId == "b").Placed);
|
|
Assert.All(result.Plates.Take(urgentCount), p => Assert.Equal(new[] { "a", "b" }, p.Placements.Select(v => v.PartId)));
|
|
Assert.All(result.Plates.Skip(urgentCount), p => Assert.All(p.Placements, v => Assert.Equal("b", v.PartId)));
|
|
}
|
|
|
|
[Fact]
|
|
public void SmallDemandThatFitsTheRemnantIsPlacedBeforeRepeating()
|
|
{
|
|
var job = Mixed(10, 10);
|
|
var batches = new List<RepeatBatch>();
|
|
var result = Engine(batches: batches.Add).Solve(job);
|
|
LayoutAssert.Valid(job, result);
|
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
|
Assert.Equal(10, Assert.Single(batches).Copies);
|
|
Assert.All(result.Plates, p => Assert.Equal(new[] { "a", "b" }, p.Placements.Select(v => v.PartId)));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(2, null, NestJobStopReason.StockExhausted)]
|
|
[InlineData(null, 2, NestJobStopReason.PlateLimitReached)]
|
|
[InlineData(3, 2, NestJobStopReason.PlateLimitReached)]
|
|
public void FiniteStockAndMaxPlatesCountPhysicalCopies(int? stockCount, int? cap, NestJobStopReason reason)
|
|
{
|
|
var job = Job([Rectangle("a", 9, 9, 40)], [Stock("s", 20, 20, spacing: 0.2, quantity: stockCount)], new NestJobOptions(maxPlates: cap));
|
|
var batches = new List<RepeatBatch>();
|
|
var result = Engine(batches: batches.Add).Solve(job);
|
|
LayoutAssert.Valid(job, result);
|
|
Assert.Equal(reason, result.StopReason);
|
|
Assert.Equal(2, result.Plates.Count);
|
|
Assert.Equal(8, Assert.Single(result.Fulfillment).Placed);
|
|
Assert.Equal(2, Assert.Single(batches).Copies);
|
|
Assert.Equal(stockCount - 2, Assert.Single(result.StockUsage).Remaining);
|
|
}
|
|
|
|
[Fact]
|
|
public void CompetingSameSizeStockEntriesCommitSinglyUntilOneRemains()
|
|
{
|
|
var job = Job([Rectangle("a", 9, 9, 16)], [Stock("one", 20, 20, spacing: 0.2, quantity: 2), Stock("two", 20, 20, spacing: 0.2, quantity: 2)]);
|
|
var batches = new List<RepeatBatch>();
|
|
var calls = 0;
|
|
var result = Engine(() => calls++, batches.Add).Solve(job);
|
|
LayoutAssert.Valid(job, result);
|
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
|
Assert.Equal(new[] { 1, 1, 2 }, batches.Select(b => b.Copies));
|
|
Assert.Equal(new[] { 2, 2, 1 }, batches.Select(b => b.AvailableStockEntries));
|
|
Assert.All(result.StockUsage, s => Assert.Equal(2, s.Used));
|
|
Assert.True(calls >= 5); // Two competing trials twice, then the genuinely sole remaining stock.
|
|
}
|
|
|
|
[Fact]
|
|
public void SameNamedDrawingsRetainExactDistinctRequirementIds()
|
|
{
|
|
var a = new Drawing("same", Shapes.Rectangle(6, 9));
|
|
var b = new Drawing("same", Shapes.Rectangle(3, 9)) { Priority = 1 };
|
|
var job = Job([DrawingJobMapper.FromDrawing("a", a, 5), DrawingJobMapper.FromDrawing("A", b, 8)], [Stock("s", 10, 10, spacing: 0.2)]);
|
|
var result = Engine().Solve(job);
|
|
LayoutAssert.Valid(job, result);
|
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
|
Assert.Equal(5, result.Fulfillment.Single(f => f.PartId == "a").Placed);
|
|
Assert.Equal(8, result.Fulfillment.Single(f => f.PartId == "A").Placed);
|
|
Assert.Equal(0, a.Quantity.Nested);
|
|
Assert.Equal(0, b.Quantity.Nested);
|
|
}
|
|
|
|
[Fact]
|
|
public void SalvageOffcutDoesNotPreventRepeatingCompletedJobSearch()
|
|
{
|
|
var stock = new NestPlateStock("s", new OpenNest.Geometry.Size(10, 10), null, 0.2, default, 1, cost: 20);
|
|
var job = Job([Rectangle("a", 6, 9, 10)], [stock], new NestJobOptions(salvageRate: 0.5, minimumSalvageDimension: 1));
|
|
var batches = new List<RepeatBatch>();
|
|
var result = Engine(batches: batches.Add).Solve(job);
|
|
LayoutAssert.Valid(job, result);
|
|
Assert.Equal(10, Assert.Single(batches).Copies);
|
|
Assert.Equal(10, result.Plates.Count);
|
|
Assert.Equal(162, NestJobCost.Evaluate(job, result), precision: 8);
|
|
Assert.True(NestJobCost.NetSheetCost(job, result.Plates[0]) < stock.Cost);
|
|
}
|
|
|
|
[Fact]
|
|
public void OversizedRequirementRemainsUnplacedWhileUsefulDemandRepeats()
|
|
{
|
|
var job = Job([Rectangle("huge", 100, 100, 4), Rectangle("small", 6, 9, 5)], [Stock("s", 10, 10, spacing: 0.2)]);
|
|
var batches = new List<RepeatBatch>();
|
|
var result = Engine(batches: batches.Add).Solve(job);
|
|
LayoutAssert.Valid(job, result);
|
|
Assert.Equal(NestJobStatus.Incomplete, result.Status);
|
|
Assert.Equal(4, result.Fulfillment.Single(f => f.PartId == "huge").Unplaced);
|
|
Assert.Equal(5, result.Fulfillment.Single(f => f.PartId == "small").Placed);
|
|
Assert.Contains(batches, b => b.Copies == 5);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false)]
|
|
[InlineData(true)]
|
|
public void NoFitOrUnavailableStockProducesNoEmptyRepeat(bool unavailable)
|
|
{
|
|
var job = Job([Rectangle("a", 100, 100, 2)], [Stock("s", 10, 10, quantity: unavailable ? 0 : null)]);
|
|
var batches = new List<RepeatBatch>();
|
|
var result = Engine(batches: batches.Add).Solve(job);
|
|
LayoutAssert.Valid(job, result);
|
|
Assert.Empty(result.Plates);
|
|
Assert.Empty(batches);
|
|
Assert.Equal(2, Assert.Single(result.Fulfillment).Unplaced);
|
|
}
|
|
|
|
[Fact]
|
|
public void CancellationAtRepeatDecisionCannotReturnAProposal()
|
|
{
|
|
using var cancellation = new CancellationTokenSource();
|
|
var job = Job([Rectangle("a", 9, 9, 40)], [Stock("s", 20, 20, spacing: 0.2)]);
|
|
var observed = 0;
|
|
var engine = Engine(batches: b => { Assert.Equal(10, b.Copies); observed++; cancellation.Cancel(); });
|
|
Assert.Throws<OperationCanceledException>(() => engine.Solve(job, token: cancellation.Token));
|
|
Assert.Equal(1, observed);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(1)]
|
|
[InlineData(10)]
|
|
public void CancellationDuringPhysicalExpansionIncludingLastCopyIsObserved(int cancelAfter)
|
|
{
|
|
using var cancellation = new CancellationTokenSource();
|
|
var committed = new List<NestJobProgress>();
|
|
var progress = new InlineProgress(p =>
|
|
{
|
|
if (p.Stage != NestJobStage.PlateCommitted) return;
|
|
committed.Add(p);
|
|
if (p.CommittedPlates == cancelAfter) cancellation.Cancel();
|
|
});
|
|
var job = Job([Rectangle("a", 9, 9, 40)], [Stock("s", 20, 20, spacing: 0.2)]);
|
|
Assert.Throws<OperationCanceledException>(() => Engine().Solve(job, progress, cancellation.Token));
|
|
Assert.Equal(cancelAfter, committed.Count);
|
|
Assert.Equal(cancelAfter * 4, committed.Last().CommittedParts);
|
|
}
|
|
|
|
[Fact]
|
|
public void SuccessfulProgressRemainsPhysicalAndOrdered()
|
|
{
|
|
var committed = new List<NestJobProgress>();
|
|
var job = Job([Rectangle("a", 9, 9, 40)], [Stock("s", 20, 20, spacing: 0.2)]);
|
|
var result = Engine().Solve(job, new InlineProgress(p => { if (p.Stage == NestJobStage.PlateCommitted) committed.Add(p); }));
|
|
LayoutAssert.Valid(job, result);
|
|
Assert.Equal(Enumerable.Range(1, 10), committed.Select(p => p.CommittedPlates));
|
|
Assert.Equal(Enumerable.Range(1, 10).Select(i => i * 4), committed.Select(p => p.CommittedParts));
|
|
}
|
|
|
|
[Fact]
|
|
public void UnreadableGeometryIsNotTurnedIntoAnEmptyRepeat()
|
|
{
|
|
var job = Job([Part("unsupported", new OpenNest.CNC.Program(), 12)], [Stock("s", 10, 10)]);
|
|
var batches = new List<RepeatBatch>();
|
|
var result = Engine(batches: batches.Add).Solve(job);
|
|
Assert.Equal(NestJobStatus.Incomplete, result.Status);
|
|
Assert.Equal(12, Assert.Single(result.Fulfillment).Unplaced);
|
|
Assert.Empty(result.Plates);
|
|
Assert.Empty(batches);
|
|
Assert.Equal(0, Assert.Single(result.StockUsage).Used);
|
|
}
|
|
|
|
[Fact]
|
|
public void CancellationInsideTheRealSheetSearchNeverCertifiesARepeat()
|
|
{
|
|
using var cancellation = new CancellationTokenSource();
|
|
var job = Job([Rectangle("a", 9, 9, 40)], [Stock("s", 20, 20, spacing: 0.2)]);
|
|
var batches = new List<RepeatBatch>();
|
|
var packed = 0;
|
|
var progress = new InlineProgress(p =>
|
|
{
|
|
if (p.Stage == NestJobStage.EvaluatingCandidate) cancellation.Cancel();
|
|
});
|
|
Assert.ThrowsAny<OperationCanceledException>(() => Engine(() => packed++, batches.Add).Solve(job, progress, cancellation.Token));
|
|
Assert.Equal(0, packed);
|
|
Assert.Empty(batches);
|
|
}
|
|
|
|
private static NestJob Mixed(int a, int b) => Job(
|
|
[Part("a", Shapes.Rectangle(6, 9), a, RotationPolicy.Fixed(0)), Part("b", Shapes.Rectangle(3, 9), b, RotationPolicy.Fixed(0), priority: 1)],
|
|
[Stock("s", 10, 10, spacing: 0.2)]);
|
|
|
|
private sealed class InlineProgress(Action<NestJobProgress> report) : IProgress<NestJobProgress>
|
|
{
|
|
public void Report(NestJobProgress value) => report(value);
|
|
}
|
|
}
|