perf: parallelize PairFiller candidates and add GridDedup
- Evaluate pair candidates in parallel batches instead of sequentially - Add GridDedup to skip duplicate pattern/direction/workArea combos across PairFiller and StripeFiller strategies - Replace crude 30% remnant area estimate with L-shaped geometry calculation using actual grid extents and max utilization - Move FillStrategyRegistry.SetEnabled to outer evaluation loop to avoid repeated enable/disable per remnant fill Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,75 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.Fill;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tracks evaluated grid configurations so duplicate pattern/direction/workArea
|
||||||
|
/// combinations can be skipped across fill strategies.
|
||||||
|
/// </summary>
|
||||||
|
public class GridDedup
|
||||||
|
{
|
||||||
|
public const string SharedStateKey = "GridDedup";
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<GridKey, byte> _seen = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true if this configuration has NOT been seen before (i.e., should be evaluated).
|
||||||
|
/// Returns false if it's a duplicate.
|
||||||
|
/// </summary>
|
||||||
|
public bool TryAdd(Box patternBox, Box workArea, NestDirection dir)
|
||||||
|
{
|
||||||
|
var key = new GridKey(patternBox, workArea, dir);
|
||||||
|
return _seen.TryAdd(key, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Count => _seen.Count;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or creates a GridDedup from FillContext.SharedState.
|
||||||
|
/// </summary>
|
||||||
|
public static GridDedup GetOrCreate(System.Collections.Generic.Dictionary<string, object> sharedState)
|
||||||
|
{
|
||||||
|
if (sharedState.TryGetValue(SharedStateKey, out var existing))
|
||||||
|
return (GridDedup)existing;
|
||||||
|
|
||||||
|
var dedup = new GridDedup();
|
||||||
|
sharedState[SharedStateKey] = dedup;
|
||||||
|
return dedup;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly struct GridKey : IEquatable<GridKey>
|
||||||
|
{
|
||||||
|
private readonly int _patternW, _patternL, _workW, _workL, _dir;
|
||||||
|
|
||||||
|
public GridKey(Box patternBox, Box workArea, NestDirection dir)
|
||||||
|
{
|
||||||
|
_patternW = (int)System.Math.Round(patternBox.Width * 10);
|
||||||
|
_patternL = (int)System.Math.Round(patternBox.Length * 10);
|
||||||
|
_workW = (int)System.Math.Round(workArea.Width * 10);
|
||||||
|
_workL = (int)System.Math.Round(workArea.Length * 10);
|
||||||
|
_dir = (int)dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(GridKey other) =>
|
||||||
|
_patternW == other._patternW && _patternL == other._patternL &&
|
||||||
|
_workW == other._workW && _workL == other._workL &&
|
||||||
|
_dir == other._dir;
|
||||||
|
|
||||||
|
public override bool Equals(object obj) => obj is GridKey other && Equals(other);
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
unchecked
|
||||||
|
{
|
||||||
|
var hash = _patternW;
|
||||||
|
hash = hash * 397 ^ _patternL;
|
||||||
|
hash = hash * 397 ^ _workW;
|
||||||
|
hash = hash * 397 ^ _workL;
|
||||||
|
hash = hash * 397 ^ _dir;
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ using System.Collections.Generic;
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using OpenNest.Engine;
|
using OpenNest.Engine;
|
||||||
|
|
||||||
namespace OpenNest.Engine.Fill
|
namespace OpenNest.Engine.Fill
|
||||||
@@ -32,13 +33,15 @@ namespace OpenNest.Engine.Fill
|
|||||||
private readonly Size plateSize;
|
private readonly Size plateSize;
|
||||||
private readonly double partSpacing;
|
private readonly double partSpacing;
|
||||||
private readonly IFillComparer comparer;
|
private readonly IFillComparer comparer;
|
||||||
|
private readonly GridDedup dedup;
|
||||||
|
|
||||||
public PairFiller(Plate plate, IFillComparer comparer = null)
|
public PairFiller(Plate plate, IFillComparer comparer = null, GridDedup dedup = null)
|
||||||
{
|
{
|
||||||
this.plate = plate;
|
this.plate = plate;
|
||||||
this.plateSize = plate.Size;
|
this.plateSize = plate.Size;
|
||||||
this.partSpacing = plate.PartSpacing;
|
this.partSpacing = plate.PartSpacing;
|
||||||
this.comparer = comparer ?? new DefaultFillComparer();
|
this.comparer = comparer ?? new DefaultFillComparer();
|
||||||
|
this.dedup = dedup ?? new GridDedup();
|
||||||
}
|
}
|
||||||
|
|
||||||
public PairFillResult Fill(NestItem item, Box workArea,
|
public PairFillResult Fill(NestItem item, Box workArea,
|
||||||
@@ -68,20 +71,41 @@ namespace OpenNest.Engine.Fill
|
|||||||
List<Part> best = null;
|
List<Part> best = null;
|
||||||
var sinceImproved = 0;
|
var sinceImproved = 0;
|
||||||
var effectiveWorkArea = workArea;
|
var effectiveWorkArea = workArea;
|
||||||
|
var batchSize = System.Math.Max(2, Environment.ProcessorCount);
|
||||||
|
|
||||||
|
var maxUtilization = candidates.Count > 0 ? candidates.Max(c => c.Utilization) : 1.0;
|
||||||
|
var partBox = drawing.Program.BoundingBox();
|
||||||
|
var partArea = System.Math.Max(partBox.Width * partBox.Length, 1);
|
||||||
|
|
||||||
|
FillStrategyRegistry.SetEnabled("Pairs", "RectBestFit", "Extents", "Linear");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
for (var i = 0; i < candidates.Count; i++)
|
for (var batchStart = 0; batchStart < candidates.Count; batchStart += batchSize)
|
||||||
{
|
{
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var filled = EvaluateCandidate(candidates[i], drawing, effectiveWorkArea, token);
|
var batchEnd = System.Math.Min(batchStart + batchSize, candidates.Count);
|
||||||
|
var batchCount = batchEnd - batchStart;
|
||||||
|
var batchWorkArea = effectiveWorkArea;
|
||||||
|
var minCountToBeat = best?.Count ?? 0;
|
||||||
|
|
||||||
if (comparer.IsBetter(filled, best, effectiveWorkArea))
|
var results = new List<Part>[batchCount];
|
||||||
|
Parallel.For(0, batchCount,
|
||||||
|
new ParallelOptions { CancellationToken = token },
|
||||||
|
j =>
|
||||||
{
|
{
|
||||||
best = filled;
|
results[j] = EvaluateCandidate(
|
||||||
|
candidates[batchStart + j], drawing, batchWorkArea,
|
||||||
|
minCountToBeat, maxUtilization, partArea, token);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (var j = 0; j < batchCount; j++)
|
||||||
|
{
|
||||||
|
if (comparer.IsBetter(results[j], best, effectiveWorkArea))
|
||||||
|
{
|
||||||
|
best = results[j];
|
||||||
sinceImproved = 0;
|
sinceImproved = 0;
|
||||||
effectiveWorkArea = TryReduceWorkArea(filled, targetCount, workArea, effectiveWorkArea);
|
effectiveWorkArea = TryReduceWorkArea(best, targetCount, workArea, effectiveWorkArea);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -94,12 +118,13 @@ namespace OpenNest.Engine.Fill
|
|||||||
PlateNumber = plateNumber,
|
PlateNumber = plateNumber,
|
||||||
Parts = best,
|
Parts = best,
|
||||||
WorkArea = workArea,
|
WorkArea = workArea,
|
||||||
Description = $"Pairs: {i + 1}/{candidates.Count} candidates, best = {best?.Count ?? 0} parts",
|
Description = $"Pairs: {batchStart + j + 1}/{candidates.Count} candidates, best = {best?.Count ?? 0} parts",
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (i + 1 >= EarlyExitMinTried && sinceImproved >= EarlyExitStaleLimit)
|
if (batchEnd >= EarlyExitMinTried && sinceImproved >= EarlyExitStaleLimit)
|
||||||
{
|
{
|
||||||
Debug.WriteLine($"[PairFiller] Early exit at {i + 1}/{candidates.Count} — no improvement in last {sinceImproved} candidates");
|
Debug.WriteLine($"[PairFiller] Early exit at {batchEnd}/{candidates.Count} — no improvement in last {sinceImproved} candidates");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,6 +133,10 @@ namespace OpenNest.Engine.Fill
|
|||||||
{
|
{
|
||||||
Debug.WriteLine("[PairFiller] Cancelled mid-phase, using results so far");
|
Debug.WriteLine("[PairFiller] Cancelled mid-phase, using results so far");
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
FillStrategyRegistry.SetEnabled(null);
|
||||||
|
}
|
||||||
|
|
||||||
Debug.WriteLine($"[PairFiller] Best pair result: {best?.Count ?? 0} parts");
|
Debug.WriteLine($"[PairFiller] Best pair result: {best?.Count ?? 0} parts");
|
||||||
return best ?? new List<Part>();
|
return best ?? new List<Part>();
|
||||||
@@ -151,7 +180,8 @@ namespace OpenNest.Engine.Fill
|
|||||||
}
|
}
|
||||||
|
|
||||||
private List<Part> EvaluateCandidate(BestFitResult candidate, Drawing drawing,
|
private List<Part> EvaluateCandidate(BestFitResult candidate, Drawing drawing,
|
||||||
Box workArea, CancellationToken token)
|
Box workArea, int minCountToBeat, double maxUtilization, double partArea,
|
||||||
|
CancellationToken token)
|
||||||
{
|
{
|
||||||
var pairParts = candidate.BuildParts(drawing);
|
var pairParts = candidate.BuildParts(drawing);
|
||||||
var angles = BuildTilingAngles(candidate);
|
var angles = BuildTilingAngles(candidate);
|
||||||
@@ -168,6 +198,9 @@ namespace OpenNest.Engine.Fill
|
|||||||
var engine = new FillLinear(workArea, partSpacing);
|
var engine = new FillLinear(workArea, partSpacing);
|
||||||
foreach (var dir in new[] { NestDirection.Horizontal, NestDirection.Vertical })
|
foreach (var dir in new[] { NestDirection.Horizontal, NestDirection.Vertical })
|
||||||
{
|
{
|
||||||
|
if (!dedup.TryAdd(pattern.BoundingBox, workArea, dir))
|
||||||
|
continue;
|
||||||
|
|
||||||
var gridParts = engine.Fill(pattern, dir);
|
var gridParts = engine.Fill(pattern, dir);
|
||||||
if (gridParts != null && gridParts.Count > 0)
|
if (gridParts != null && gridParts.Count > 0)
|
||||||
grids.Add((gridParts, dir));
|
grids.Add((gridParts, dir));
|
||||||
@@ -180,17 +213,34 @@ namespace OpenNest.Engine.Fill
|
|||||||
// Sort by count descending so we try the best grids first
|
// Sort by count descending so we try the best grids first
|
||||||
grids.Sort((a, b) => b.Parts.Count.CompareTo(a.Parts.Count));
|
grids.Sort((a, b) => b.Parts.Count.CompareTo(a.Parts.Count));
|
||||||
|
|
||||||
|
// Early abort: if the best grid + optimistic remnant can't beat the global best, skip Phase 2
|
||||||
|
if (minCountToBeat > 0)
|
||||||
|
{
|
||||||
|
var topCount = grids[0].Parts.Count;
|
||||||
|
var optimisticRemnant = EstimateRemnantUpperBound(
|
||||||
|
grids[0].Parts, workArea, maxUtilization, partArea);
|
||||||
|
if (topCount + optimisticRemnant <= minCountToBeat)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[PairFiller] Skipping candidate: grid {topCount} + estimate {optimisticRemnant} <= best {minCountToBeat}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Phase 2: try remnant for each grid, skip if grid is too far behind
|
// Phase 2: try remnant for each grid, skip if grid is too far behind
|
||||||
List<Part> best = null;
|
List<Part> best = null;
|
||||||
var maxRemnantEstimate = EstimateMaxRemnantParts(drawing, workArea);
|
|
||||||
|
|
||||||
foreach (var (gridParts, dir) in grids)
|
foreach (var (gridParts, dir) in grids)
|
||||||
{
|
{
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
// If this grid + max possible remnant can't beat current best, skip
|
// If this grid + max possible remnant can't beat current best, skip
|
||||||
if (best != null && gridParts.Count + maxRemnantEstimate <= best.Count)
|
if (best != null)
|
||||||
|
{
|
||||||
|
var remnantBound = EstimateRemnantUpperBound(
|
||||||
|
gridParts, workArea, maxUtilization, partArea);
|
||||||
|
if (gridParts.Count + remnantBound <= best.Count)
|
||||||
break; // sorted descending, so remaining are even smaller
|
break; // sorted descending, so remaining are even smaller
|
||||||
|
}
|
||||||
|
|
||||||
var remnantParts = FillRemnant(gridParts, drawing, workArea, token);
|
var remnantParts = FillRemnant(gridParts, drawing, workArea, token);
|
||||||
List<Part> total;
|
List<Part> total;
|
||||||
@@ -212,12 +262,20 @@ namespace OpenNest.Engine.Fill
|
|||||||
return best;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int EstimateMaxRemnantParts(Drawing drawing, Box workArea)
|
private int EstimateRemnantUpperBound(List<Part> gridParts, Box workArea,
|
||||||
|
double maxUtilization, double partArea)
|
||||||
{
|
{
|
||||||
var partBox = drawing.Program.BoundingBox();
|
var gridBox = ((IEnumerable<IBoundable>)gridParts).GetBoundingBox();
|
||||||
var partArea = System.Math.Max(partBox.Width * partBox.Length, 1);
|
|
||||||
var remnantArea = workArea.Area() * 0.3; // remnant is at most ~30% of work area
|
// L-shaped remnant: top strip (full width) + right strip (grid height only)
|
||||||
return (int)(remnantArea / partArea) + 1;
|
var topHeight = System.Math.Max(0, workArea.Top - gridBox.Top);
|
||||||
|
var rightWidth = System.Math.Max(0, workArea.Right - gridBox.Right);
|
||||||
|
|
||||||
|
var topArea = workArea.Width * topHeight;
|
||||||
|
var rightArea = rightWidth * System.Math.Min(gridBox.Top - workArea.Y, workArea.Length);
|
||||||
|
var remnantArea = topArea + rightArea;
|
||||||
|
|
||||||
|
return (int)(remnantArea * maxUtilization / partArea) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Part> FillRemnant(List<Part> gridParts, Drawing drawing,
|
private List<Part> FillRemnant(List<Part> gridParts, Drawing drawing,
|
||||||
@@ -263,9 +321,6 @@ namespace OpenNest.Engine.Fill
|
|||||||
return cachedResult;
|
return cachedResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
FillStrategyRegistry.SetEnabled("Pairs", "RectBestFit", "Extents", "Linear");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var remnantEngine = NestEngineRegistry.Create(plate);
|
var remnantEngine = NestEngineRegistry.Create(plate);
|
||||||
var item = new NestItem { Drawing = drawing };
|
var item = new NestItem { Drawing = drawing };
|
||||||
var parts = remnantEngine.Fill(item, remnantBox, null, token);
|
var parts = remnantEngine.Fill(item, remnantBox, null, token);
|
||||||
@@ -281,11 +336,6 @@ namespace OpenNest.Engine.Fill
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
FillStrategyRegistry.SetEnabled(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<double> BuildTilingAngles(BestFitResult candidate)
|
private static List<double> BuildTilingAngles(BestFitResult candidate)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ public class StripeFiller
|
|||||||
private readonly FillContext _context;
|
private readonly FillContext _context;
|
||||||
private readonly NestDirection _primaryAxis;
|
private readonly NestDirection _primaryAxis;
|
||||||
private readonly IFillComparer _comparer;
|
private readonly IFillComparer _comparer;
|
||||||
|
private readonly GridDedup _dedup;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// When true, only complete stripes are placed — no partial rows/columns.
|
/// When true, only complete stripes are placed — no partial rows/columns.
|
||||||
@@ -38,6 +39,7 @@ public class StripeFiller
|
|||||||
_context = context;
|
_context = context;
|
||||||
_primaryAxis = primaryAxis;
|
_primaryAxis = primaryAxis;
|
||||||
_comparer = context.Policy?.Comparer ?? new DefaultFillComparer();
|
_comparer = context.Policy?.Comparer ?? new DefaultFillComparer();
|
||||||
|
_dedup = GridDedup.GetOrCreate(context.SharedState);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Part> Fill()
|
public List<Part> Fill()
|
||||||
@@ -115,6 +117,10 @@ public class StripeFiller
|
|||||||
var rotatedPattern = FillHelpers.BuildRotatedPattern(pairParts, angle);
|
var rotatedPattern = FillHelpers.BuildRotatedPattern(pairParts, angle);
|
||||||
var perpDim = GetDimension(rotatedPattern.BoundingBox, perpAxis);
|
var perpDim = GetDimension(rotatedPattern.BoundingBox, perpAxis);
|
||||||
var stripeBox = MakeStripeBox(workArea, perpDim, primaryAxis);
|
var stripeBox = MakeStripeBox(workArea, perpDim, primaryAxis);
|
||||||
|
|
||||||
|
if (!_dedup.TryAdd(rotatedPattern.BoundingBox, workArea, primaryAxis))
|
||||||
|
return null;
|
||||||
|
|
||||||
var stripeEngine = new FillLinear(stripeBox, spacing);
|
var stripeEngine = new FillLinear(stripeBox, spacing);
|
||||||
var stripeParts = stripeEngine.Fill(rotatedPattern, primaryAxis);
|
var stripeParts = stripeEngine.Fill(rotatedPattern, primaryAxis);
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ namespace OpenNest.Engine.Strategies
|
|||||||
public List<Part> Fill(FillContext context)
|
public List<Part> Fill(FillContext context)
|
||||||
{
|
{
|
||||||
var comparer = context.Policy?.Comparer;
|
var comparer = context.Policy?.Comparer;
|
||||||
var filler = new PairFiller(context.Plate, comparer);
|
var dedup = GridDedup.GetOrCreate(context.SharedState);
|
||||||
|
var filler = new PairFiller(context.Plate, comparer, dedup);
|
||||||
var result = filler.Fill(context.Item, context.WorkArea,
|
var result = filler.Fill(context.Item, context.WorkArea,
|
||||||
context.PlateNumber, context.Token, context.Progress);
|
context.PlateNumber, context.Token, context.Progress);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user