mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-10 06:24:14 -04:00
fix(irregular): charge nested block-fill cost against the work budget
BlockCatalog's PrepareBlocks pre-pass ran a full independent NFP-based pack (PrivatePlateFill.Run) for up to 8 attempts per part type, but that cost never touched the engine's WorkCounter/WorkBudget. For jobs with several part types at double-digit quantities, these uncounted nested solves could burn the entire 5-minute timeout inside the first sheet of the first strategy variant, so Solve() never returned and nothing was ever committed. Charge quantity^2 * outline vertex count against the shared counter before each nested fill, and stop issuing new block proposals once the budget is exhausted, so the existing effort governor actually sees and bounds this cost. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
32db086384
commit
eda4a846f0
3 files changed
+20
-10
No files matched your search
@@ -19,7 +19,7 @@ public class IrregularBlockTests
|
||||
var job = Job(new[] { Part("ell", LShape(9, 7, 3), quantity, RotationPolicy.Automatic) },
|
||||
new[] { Stock("sheet", 40, 60, spacing: 0.25) });
|
||||
var types = PartCatalog.Build(job);
|
||||
using var catalog = new BlockCatalog(0.25, types, NoPairs);
|
||||
using var catalog = new BlockCatalog(0.25, types, NoPairs, new WorkCounter(), long.MaxValue);
|
||||
Assert.Empty(catalog.Get(types[0], quantity, new Box(0, 0, 60, 40), CancellationToken.None));
|
||||
Assert.Equal(0, catalog.PreparationCount);
|
||||
}
|
||||
@@ -31,7 +31,7 @@ public class IrregularBlockTests
|
||||
new[] { Stock("sheet", 40, 60, spacing: 0.25) });
|
||||
var types = PartCatalog.Build(job);
|
||||
var original = types[0].Orientations.ToArray();
|
||||
using var catalog = new BlockCatalog(0.25, types, NoPairs);
|
||||
using var catalog = new BlockCatalog(0.25, types, NoPairs, new WorkCounter(), long.MaxValue);
|
||||
var block = catalog.Get(types[0], 7, new Box(0, 0, 60, 40), CancellationToken.None);
|
||||
Assert.Equal(7, block.Count);
|
||||
Assert.Equal(original, types[0].Orientations);
|
||||
@@ -79,7 +79,7 @@ public class IrregularBlockTests
|
||||
var job = Job(new[] { Part("ell", LShape(9, 7, 3), 44, RotationPolicy.Automatic) }, new[] { stock });
|
||||
var types = PartCatalog.Build(job);
|
||||
var pairs = PairCatalog.Build(types, 0.25, 40, 30, CancellationToken.None);
|
||||
using var blocks = new BlockCatalog(0.25, types, pairs);
|
||||
using var blocks = new BlockCatalog(0.25, types, pairs, new WorkCounter(), long.MaxValue);
|
||||
var before = new FrontierPacker(types, new NoFitCache(0.25), pairs, stock,
|
||||
PackAxis.X, 1, new WorkCounter()).Fill(new[] { 44 }, CancellationToken.None);
|
||||
var after = new FrontierPacker(types, new NoFitCache(0.25), pairs, stock,
|
||||
@@ -97,7 +97,7 @@ public class IrregularBlockTests
|
||||
var stock = Stock("sheet", 3, 3, spacing: 0.25);
|
||||
var job = Job(new[] { Rectangle("box", 2, 2, 3) }, new[] { stock });
|
||||
var types = PartCatalog.Build(job);
|
||||
using var blocks = new BlockCatalog(0.25, types, NoPairs);
|
||||
using var blocks = new BlockCatalog(0.25, types, NoPairs, new WorkCounter(), long.MaxValue);
|
||||
Assert.Empty(blocks.Get(types[0], 3, new Box(0, 0, 3, 3), CancellationToken.None));
|
||||
Assert.Equal(0, blocks.PreparationCount);
|
||||
var fill = new FrontierPacker(types, new NoFitCache(0.25), NoPairs, stock,
|
||||
@@ -158,7 +158,7 @@ public class IrregularBlockTests
|
||||
{
|
||||
var job = Job(new[] { Rectangle("box", 2, 2, 3) }, new[] { Stock("sheet", 10, 20) });
|
||||
var types = PartCatalog.Build(job);
|
||||
using var catalog = new BlockCatalog(0.25, types, NoPairs);
|
||||
using var catalog = new BlockCatalog(0.25, types, NoPairs, new WorkCounter(), long.MaxValue);
|
||||
Assert.Throws<OperationCanceledException>(() =>
|
||||
catalog.Get(types[0], 3, new Box(0, 0, 20, 10), new CancellationToken(true)));
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ namespace OpenNest.Engine.NestingEngines.Irregular;
|
||||
internal sealed class BlockCatalog : IDisposable
|
||||
{
|
||||
private readonly double spacing;
|
||||
private readonly WorkCounter counter;
|
||||
private readonly long budget;
|
||||
private readonly Dictionary<int, Drawing> drawings = new();
|
||||
private readonly Dictionary<int, List<Orientation>> orientations = new();
|
||||
private readonly Dictionary<int, int> attempts = new();
|
||||
@@ -24,9 +26,11 @@ internal sealed class BlockCatalog : IDisposable
|
||||
private readonly Dictionary<(int Type, int Quantity, double Length, double Width), IReadOnlyList<Placed>> cache = new();
|
||||
|
||||
public BlockCatalog(double spacing, IReadOnlyList<PartType> types,
|
||||
IReadOnlyDictionary<int, IReadOnlyList<PairPose>> pairs)
|
||||
IReadOnlyDictionary<int, IReadOnlyList<PairPose>> pairs, WorkCounter counter, long budget)
|
||||
{
|
||||
this.spacing = spacing;
|
||||
this.counter = counter;
|
||||
this.budget = budget;
|
||||
foreach (var type in types)
|
||||
{
|
||||
var poses = type.Orientations.ToList();
|
||||
@@ -65,7 +69,7 @@ internal sealed class BlockCatalog : IDisposable
|
||||
var key = (type.Index, quantity, rectangle.Length, rectangle.Width);
|
||||
if (cache.TryGetValue(key, out var cached))
|
||||
return cached;
|
||||
if (rectangle.Area() < 3 * type.Area || attempts.GetValueOrDefault(type.Index) >= 8)
|
||||
if (rectangle.Area() < 3 * type.Area || attempts.GetValueOrDefault(type.Index) >= 8 || counter.Value >= budget)
|
||||
return Array.Empty<Placed>();
|
||||
attempts[type.Index] = attempts.GetValueOrDefault(type.Index) + 1;
|
||||
var result = Build(type, quantity, rectangle, token);
|
||||
@@ -75,6 +79,11 @@ internal sealed class BlockCatalog : IDisposable
|
||||
|
||||
private IReadOnlyList<Placed> Build(PartType type, int quantity, Box rectangle, CancellationToken token)
|
||||
{
|
||||
// PrivatePlateFill.Run below is a full independent NFP-based pack whose own operations
|
||||
// never touch this solve's effort meter; charge it here using the same factors - demand
|
||||
// and outline complexity - that drive its real cost, so the shared budget actually sees it.
|
||||
var vertices = type.Orientations.Count > 0 ? type.Orientations[0].Outline.Count : 1;
|
||||
counter.Add((long)quantity * quantity * System.Math.Max(1, vertices));
|
||||
if (!drawings.TryGetValue(type.Index, out var drawing))
|
||||
drawings[type.Index] = drawing = DrawingJobMapper.CreateDrawing(type.Part);
|
||||
try
|
||||
|
||||
@@ -48,7 +48,7 @@ public sealed class IrregularNestingEngine : INestingEngine
|
||||
ArgumentNullException.ThrowIfNull(job);
|
||||
token.ThrowIfCancellationRequested();
|
||||
var types = PartCatalog.Build(job);
|
||||
using var solver = new Solver(job, types, progress, token);
|
||||
using var solver = new Solver(job, types, progress, token, WorkBudget);
|
||||
|
||||
// Pair-only orientations may fit stock even when the sampled single poses do not.
|
||||
// Demand that neither a single nor a pair can fit is reported unplaced.
|
||||
@@ -86,7 +86,8 @@ public sealed class IrregularNestingEngine : INestingEngine
|
||||
NestJob job,
|
||||
IReadOnlyList<PartType> types,
|
||||
IProgress<NestJobProgress>? progress,
|
||||
CancellationToken token
|
||||
CancellationToken token,
|
||||
long workBudget
|
||||
) : IDisposable
|
||||
{
|
||||
public void Dispose()
|
||||
@@ -104,7 +105,7 @@ public sealed class IrregularNestingEngine : INestingEngine
|
||||
{
|
||||
var spacing = System.Math.Max(0, stock.PartSpacing);
|
||||
if (!blocks.TryGetValue(spacing, out var found))
|
||||
blocks[spacing] = found = new BlockCatalog(spacing, types, PairsFor(stock));
|
||||
blocks[spacing] = found = new BlockCatalog(spacing, types, PairsFor(stock), Work, workBudget);
|
||||
return found;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user