mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-26 22:01:06 -04:00
perf(engine): speed up round cutouts and re-plan Circles' last sheets
- The cutout router uses the circle packer alone for a round hole whose inserts are all round. Lattice and NFP never placed more there, and when demand exceeded what fit they spent 20+ minutes on one job. - The cutout pre-pass skips a hole whose area is smaller than an insert's outline, so bolt holes no longer run the searches. - Circles re-packs its last sheet, and its last two sheets together, onto each single stock and keeps the result when the whole job gets cheaper. On 14 real round-part jobs costs are unchanged and pipeline time drops from 262 s to 144 s for Circles and 355 s to 245 s for Default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0e4fd5f90f
commit
f01b2cec67
4 files changed
+69
-23
No files matched your search
@@ -53,6 +53,9 @@ internal sealed class CutoutPipelinePrepass
|
||||
var requirements = original.Parts.ToDictionary(p => p.Id);
|
||||
var round = original.Parts.ToDictionary(p => p.Id, p => geometry[p.Id] is { } g
|
||||
&& Item.Create(g).FillRatio >= CutoutCirclePacker.MinFillRatio);
|
||||
// An insert's outline cannot fit a hole of smaller area; bolt holes skip the searches.
|
||||
var outline = original.Parts.ToDictionary(p => p.Id, p => geometry[p.Id] is { } g
|
||||
? System.Math.Abs(g.Perimeter.Area()) : double.PositiveInfinity);
|
||||
var bundles = new Dictionary<string, Bundle>(StringComparer.Ordinal);
|
||||
var byFrame = new Dictionary<string, List<NestJobPart>>(StringComparer.Ordinal);
|
||||
// Smallest first: a frame can only hold smaller parts, so its candidate inner bundles
|
||||
@@ -86,9 +89,10 @@ internal sealed class CutoutPipelinePrepass
|
||||
for (var hole = 0; hole < profile.Cutouts.Count; hole++)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var holeArea = System.Math.Abs(profile.Cutouts[hole].Area());
|
||||
var eligible = offers
|
||||
.Where(p => p.Id != frame.Id && available[p.Id] > 0
|
||||
&& p.Priority == frame.Priority
|
||||
&& p.Priority == frame.Priority && outline[p.Id] <= holeArea
|
||||
&& (!bundles.TryGetValue(p.Id, out var b)
|
||||
|| (b.FrameId != frame.Id && b.Levels < MaxLevels)))
|
||||
.Select(p => new NestJobPart(p.Id, p.Geometry, available[p.Id],
|
||||
@@ -99,7 +103,7 @@ internal sealed class CutoutPipelinePrepass
|
||||
? Array.Empty<NestJobPart>()
|
||||
: eligible.Where(p => round[p.Id] && geometry[p.Id]!.Cutouts.Count > 0).ToArray();
|
||||
if (flat.Length == 0 && nested.Length == 0)
|
||||
break;
|
||||
continue;
|
||||
foreach (var pose in Propose(frame, hole, flat, nested))
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
@@ -133,6 +137,7 @@ internal sealed class CutoutPipelinePrepass
|
||||
offers.Add(proxy);
|
||||
requirements.Add(proxyId, proxy);
|
||||
geometry.Add(proxyId, profile);
|
||||
outline.Add(proxyId, outline[frame.Id]);
|
||||
round.Add(proxyId, round[frame.Id]);
|
||||
remaining.Add(proxyId, 1);
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ namespace OpenNest.Engine.Jobs.Cutouts;
|
||||
/// <summary>Internal frame-local cutout proposal. Never changes demand, stock or a live plate.</summary>
|
||||
/// <remarks>Area ratio selects search order, not placement permission. The small-insert path
|
||||
/// prefers a shifted partial lattice; the middle compares it with occupied-aware NFP search;
|
||||
/// the large-insert path tries NFP first. A round hole whose inserts are all round also gets a
|
||||
/// circle-in-circle proposal for the whole list, kept only when it places more copies.
|
||||
/// the large-insert path tries NFP first. A round hole whose inserts are all round gets only the
|
||||
/// circle-in-circle proposal for the whole list.
|
||||
/// All accepted poses are clearance-checked.
|
||||
/// Fill's equal-score pose nondeterminism remains PM c98c21bd.</remarks>
|
||||
internal static class CutoutRouter
|
||||
@@ -50,13 +50,11 @@ internal static class CutoutRouter
|
||||
throw new ArgumentException("Requirement IDs must be distinct from the frame and each other.",
|
||||
nameof(inserts));
|
||||
|
||||
// Nothing can place more than all demand, so a complete circle packing skips the
|
||||
// lattice and NFP searches, which are slow on many round inserts.
|
||||
var circles = CutoutCirclePacker.Applies(hole, inserts)
|
||||
? CutoutCirclePacker.Fill(frame, cutoutIndex, inserts, spacing, token)
|
||||
: null;
|
||||
if (circles != null && circles.Count == inserts.Sum(p => (long)System.Math.Max(0, p.Quantity)))
|
||||
return Circles(circles);
|
||||
// A round hole holding only round inserts is a circle packing problem: the lattice and
|
||||
// NFP searches never placed more there, and they are slow on round inserts, most of all
|
||||
// when demand exceeds what fits (every surplus copy is searched for and not found).
|
||||
if (CutoutCirclePacker.Applies(hole, inserts))
|
||||
return Circles(CutoutCirclePacker.Fill(frame, cutoutIndex, inserts, spacing, token));
|
||||
|
||||
var accepted = new List<NestJobPlacement>();
|
||||
var observed = new List<(NestJobPlacement Pose, bool Lattice)>();
|
||||
@@ -138,8 +136,6 @@ internal static class CutoutRouter
|
||||
}
|
||||
}
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (circles != null && circles.Count > accepted.Count)
|
||||
return Circles(circles);
|
||||
foreach (var (pose, lattice) in observed)
|
||||
observeAccepted?.Invoke(pose, lattice);
|
||||
return accepted.ToArray();
|
||||
|
||||
@@ -23,7 +23,9 @@ namespace OpenNest.Engine.NestingEngines.Circles;
|
||||
/// and left-bottom (columns), plain and seeded with a hexagonal lattice of the type with the most
|
||||
/// remaining area, and bottom-left over a square lattice of it; the candidate with the lowest
|
||||
/// estimated whole-job cost wins. The whole job runs once with that free choice and once
|
||||
/// preferring each offered stock, and the best result by <see cref="CandidateScore"/> is kept.
|
||||
/// preferring each offered stock, and the best result by <see cref="CandidateScore"/> is kept;
|
||||
/// its last sheet, and its last two sheets together, are then re-packed onto each single stock
|
||||
/// and replaced when that lowers the whole job's cost.
|
||||
/// Deterministic: no clocks or randomness; the only stop besides completion is the host token.
|
||||
/// </summary>
|
||||
public sealed class CirclesNestingEngine : INestingEngine
|
||||
@@ -71,19 +73,60 @@ public sealed class CirclesNestingEngine : INestingEngine
|
||||
var forward = progress == null ? null : new CandidateProgress(progress);
|
||||
CandidateScore? best = null;
|
||||
var runs = new NestPlateStock?[] { null }.Concat(job.Plates.Count > 1 ? job.Plates : Array.Empty<NestPlateStock>());
|
||||
var order = 0;
|
||||
var run = 0;
|
||||
foreach (var preferred in runs)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var result = SheetByStock.Run(job, demand, remaining.ToArray(), Pack, forward, token,
|
||||
preferred: preferred);
|
||||
var scored = CandidateScore.Of(job, result, order++);
|
||||
var scored = CandidateScore.Of(job, result, run++);
|
||||
if (best == null || scored.IsBetterThan(best))
|
||||
best = scored;
|
||||
}
|
||||
CandidateProgress.ReportCommits(best!.Result, progress);
|
||||
best = ReplanTail(best!);
|
||||
CandidateProgress.ReportCommits(best.Result, progress);
|
||||
return best.Result;
|
||||
|
||||
// The last sheet is packed from whatever demand is left, often on a larger sheet than
|
||||
// it needs: re-pack the last sheet, then the last two together, onto each single stock.
|
||||
CandidateScore ReplanTail(CandidateScore chosen)
|
||||
{
|
||||
var index = types.ToDictionary(t => t.Id, t => t.Index, StringComparer.Ordinal);
|
||||
for (var tail = 1; tail <= System.Math.Min(2, chosen.Result.Plates.Count); tail++)
|
||||
{
|
||||
var sheets = chosen.Result.Plates;
|
||||
var keep = sheets.Take(sheets.Count - tail).ToList();
|
||||
var replaced = sheets.Skip(keep.Count).ToList();
|
||||
var left = new int[types.Count];
|
||||
foreach (var pose in replaced.SelectMany(p => p.Placements))
|
||||
left[index[pose.PartId]]++;
|
||||
var count = left.Sum();
|
||||
var replacedCost = replaced.Sum(p => NestJobCost.NetSheetCost(job, p));
|
||||
foreach (var stock in job.Plates)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (stock.Quantity is int available && keep.Count(p => p.StockId == stock.Id) >= available)
|
||||
continue;
|
||||
foreach (var layout in Pack(stock, left))
|
||||
{
|
||||
// Only a complete, cheaper tail can lower the whole job's cost.
|
||||
if (layout.Poses.Count < count || layout.Envelope is not { } envelope
|
||||
|| NestJobCost.NetSheetCost(job.Options, stock, envelope) >= replacedCost)
|
||||
continue;
|
||||
var builder = new NestJobResultBuilder(job);
|
||||
foreach (var sheet in keep)
|
||||
builder.AddSheet(job.Plates.Single(s => s.Id == sheet.StockId),
|
||||
sheet.Placements.Select(p => (p.PartId, p.X, p.Y, p.Rotation)));
|
||||
builder.AddSheet(stock, layout.Poses);
|
||||
var scored = CandidateScore.Of(job, builder.Build(chosen.Result.StopReason), run++);
|
||||
if (scored.IsBetterThan(chosen))
|
||||
chosen = scored;
|
||||
}
|
||||
}
|
||||
}
|
||||
return chosen;
|
||||
}
|
||||
|
||||
IEnumerable<SheetLayout> Pack(NestPlateStock stock, IReadOnlyList<int> left)
|
||||
{
|
||||
if (!types.Any(t => left[t.Index] > 0 && DiscPacker.Fits(stock, t)))
|
||||
|
||||
@@ -13,7 +13,7 @@ in `OpenNest.Engine/NestingEngines/<Name>/`, its tests in `OpenNest.Engine.Tests
|
||||
|---|---|---|
|
||||
| Default | Any job; used when no engine is named | Runs Irregular, Rectangles and Circles, checks each layout with the layout check and keeps the best: valid first, then fewest unplaced parts, then lowest salvage-credited cost; ties keep the earlier engine |
|
||||
| Rectangles | Plain and near-rectangular plates | Each part packed as the box of its material at its minimum-area rotation, using a maximal-rectangles free list; stock chosen sheet by sheet by salvage-credited look-ahead cost |
|
||||
| Circles | Round parts: blanks, washers, flanges, gears by their tip circle | Each part packed as its minimum enclosing circle at centers tangent to two of walls, placed discs and clamp keep-outs, with hexagonal and square lattice seeds; bids only when every part is round; stock chosen as Rectangles does, plus whole-job runs preferring each stock |
|
||||
| Circles | Round parts: blanks, washers, flanges, gears by their tip circle | Each part packed as its minimum enclosing circle at centers tangent to two of walls, placed discs and clamp keep-outs, with hexagonal and square lattice seeds; bids only when every part is round; stock chosen as Rectangles does, plus whole-job runs preferring each stock and a tail re-plan |
|
||||
| Irregular | Irregular profiles | No-fit-polygon frontier packing with gap filling and best-fit pairs, six whole-job strategy variants and a tail re-plan |
|
||||
| FlatBar | Flat bar: parts about as wide as the bar | Each stock read as a bar and cut in one row: the CutList cut-list packer and a best-fill search on part lengths less their self-nesting gain, then a slide-left pass that nests shaped ends and refills freed length; stock chosen bar by bar by whole-plan cost |
|
||||
| StockLadder | Caller-supplied stock ladders | Constrained-first fill with equivalent-demand area repacking |
|
||||
@@ -209,7 +209,9 @@ ones. Sheets and stock are chosen by the loop Rectangles uses (`NestingEngines/S
|
||||
salvage-credited look-ahead cost, priority first, and repeated completed layouts. That per-sheet
|
||||
estimate can take a large sheet first and still need a second, so Circles runs the whole job once
|
||||
with a free stock choice and once preferring each offered stock, and keeps the best result as
|
||||
Default ranks candidates (valid, fewest unplaced, lowest cost).
|
||||
Default ranks candidates (valid, fewest unplaced, lowest cost). The last sheet, and the last two
|
||||
sheets together, are then re-packed onto each single stock and replaced when that lowers the
|
||||
whole job's cost, so a small remainder does not finish on a large sheet.
|
||||
|
||||
## Filling cutouts
|
||||
|
||||
@@ -248,14 +250,14 @@ list as discs: largest first, at centers tangent to two of the hole wall and the
|
||||
or at the hole's center. It tries a center-out pass, so a single insert sits concentric, and a
|
||||
wall-first pass, which reaches the optimal counts for up to nine equal discs. Each gap is the
|
||||
spacing plus twice `NestTolerances.ValidationOutline`, and every pose passes `NestLayoutCheck`.
|
||||
If it places all demand, the router skips the lattice and NFP searches, which are slow on many
|
||||
round inserts. Otherwise it keeps whichever proposal places more copies, and the existing
|
||||
proposals win ties.
|
||||
Its result is the router's proposal for that hole: the lattice and NFP searches never placed
|
||||
more there and were slow on round inserts, most of all when demand exceeds what fits.
|
||||
`CutoutPipelinePrepass` builds quantity-one proxy instances for eligible
|
||||
closed-cutout frames. It considers legal frame rotations without outer-perimeter symmetry
|
||||
reduction, shifts insert rotation policies into the local frame, and reserves certified router
|
||||
poses by original requirement ID. It can fill multiple holes and repeated/different frame
|
||||
requirements; inserts with a different priority stay independent. Frames are bundled smallest
|
||||
requirements; inserts with a different priority stay independent, and a hole smaller in area than
|
||||
an insert's outline (a bolt hole) is not searched for it. Frames are bundled smallest
|
||||
first, so a larger frame's round cutout can take, through `CutoutCirclePacker` only, a round part
|
||||
with its own cutouts (a washer with an empty bore) or an earlier bundle as one insert: a flange
|
||||
holds a washer that holds a smaller washer that holds a slug. Chains stop at four parts deep
|
||||
|
||||
Reference in new issue
Block a user