mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-28 16:01:06 -04:00
perf(engine): pool best-fit pair searches by part geometry across jobs
Every Auto Nest run, and every Default-engine run, recomputed each part's best-fit pairs: job snapshots are fresh objects and BestFitCache keyed its entries on Drawing reference, so the Irregular engine paid PairCatalog.Build (about half of a 118-part solve) on every run of the same drawings. BestFitCache now keeps a process-wide, bounded (512 sets, least recently used first) pool of unfiltered candidate sets keyed by the exact content of a PartGeometrySnapshot (mode and every motion value, bit for bit; PartGeometrySnapshot.ContentComparer) and the spacing. Every drawing DrawingJobMapper.CreateDrawing reconstructs from a snapshot declares that geometry (BestFitCache.ShareByGeometry), so the Irregular pairs and blocks, the cutout lattice and the Fill/Default job paths all read and fill the pool. Candidates depend on the program geometry and spacing alone, so a pooled set is the set the search would produce: benchmark layouts, costs and placements are unchanged on the synthetic nests (all engines) and on the 67-drawing, 118-part job. Failed searches are not pooled, Invalidate leaves the pool alone, Clear empties it, and a drawing whose Program is replaced stops vouching for its geometry. Same 118-part job solved twice in one benchmark process (--parallel 1): the second solve now takes 13.1 s after a 33.0 s first; before, 35.6 s after 41.8 s. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
13ab8e02f8
commit
e6c87df372
6 files changed
+520
-8
No files matched your search
@@ -0,0 +1,276 @@
|
||||
using OpenNest.Engine.BestFit;
|
||||
using OpenNest.Engine.Jobs;
|
||||
using OpenNest.Engine.Jobs.Adapters;
|
||||
using OpenNest.Engine.NestingEngines.Irregular;
|
||||
using static OpenNest.Engine.Tests.NestingEngines.JobBuilder;
|
||||
using static OpenNest.Engine.Tests.NestingEngines.Shapes;
|
||||
using Irregular = OpenNest.Engine.NestingEngines.Irregular;
|
||||
|
||||
namespace OpenNest.Engine.Tests.NestingEngines;
|
||||
|
||||
// The best-fit pool is process-wide state and these tests swap the evaluator factory and the
|
||||
// pool capacity, so they run alone.
|
||||
[CollectionDefinition(nameof(BestFitPoolCollection), DisableParallelization = true)]
|
||||
public class BestFitPoolCollection { }
|
||||
|
||||
/// <summary>
|
||||
/// Best-fit candidates for job geometry are pooled by content, so a repeated or re-tuned job of
|
||||
/// the same parts (fresh snapshots, fresh engine instances) does not search again and places
|
||||
/// the parts exactly as the first job did.
|
||||
/// </summary>
|
||||
[Collection(nameof(BestFitPoolCollection))]
|
||||
public class IrregularPairPoolTests
|
||||
{
|
||||
private const double Spacing = 0.25;
|
||||
|
||||
/// <summary>Counts best-fit searches of one geometry by wrapping the evaluator factory.</summary>
|
||||
private sealed class SearchSpy : IDisposable
|
||||
{
|
||||
private readonly Func<Drawing, double, IPairEvaluator>? previous;
|
||||
private readonly PartGeometrySnapshot geometry;
|
||||
private int count;
|
||||
|
||||
public SearchSpy(OpenNest.CNC.Program program)
|
||||
{
|
||||
geometry = PartGeometrySnapshot.FromProgram(program);
|
||||
previous = BestFitCache.CreateEvaluator;
|
||||
BestFitCache.CreateEvaluator = (drawing, _) =>
|
||||
{
|
||||
if (PartGeometrySnapshot.ContentComparer.Equals(
|
||||
PartGeometrySnapshot.FromProgram(drawing.Program), geometry))
|
||||
Interlocked.Increment(ref count);
|
||||
return new PairEvaluator();
|
||||
};
|
||||
}
|
||||
|
||||
public int Count => Volatile.Read(ref count);
|
||||
|
||||
public void Dispose() => BestFitCache.CreateEvaluator = previous!;
|
||||
}
|
||||
|
||||
private sealed class FailingEvaluator : IPairEvaluator
|
||||
{
|
||||
public List<BestFitResult> EvaluateAll(List<PairCandidate> candidates) =>
|
||||
throw new InvalidOperationException("search failed");
|
||||
}
|
||||
|
||||
// Each test uses its own geometry so pooled sets from other tests cannot satisfy it.
|
||||
private static OpenNest.CNC.Program Wedge(double top) => Polyline((0, 0), (8.3, 0), (top, 20.7), (0, 20.7));
|
||||
|
||||
private static (NestJob Job, IReadOnlyList<Irregular.PartType> Types) FreshJob(OpenNest.CNC.Program program, int quantity)
|
||||
{
|
||||
var job = Job(new[] { Part("wedge", program, quantity, RotationPolicy.Automatic) },
|
||||
new[] { Stock("sheet", 17.3, 31.7, spacing: Spacing) });
|
||||
return (job, PartCatalog.Build(job));
|
||||
}
|
||||
|
||||
private static List<(double, double, double, double, double)> Poses(IReadOnlyDictionary<int, IReadOnlyList<PairPose>> pairs) =>
|
||||
pairs.SelectMany(kv => kv.Value.Select(p => (p.A.Rotation, p.B.Rotation, p.Dx, p.Dy, (double)kv.Key))).ToList();
|
||||
|
||||
[Fact]
|
||||
public void PairCatalog_SecondJobOfTheSameGeometry_SkipsTheSearchAndOffersTheSamePairs()
|
||||
{
|
||||
BestFitCache.Clear();
|
||||
var program = Wedge(5.9);
|
||||
using var spy = new SearchSpy(program);
|
||||
|
||||
var (first, firstTypes) = FreshJob(program, 2);
|
||||
var firstPairs = PairCatalog.Build(firstTypes, Spacing, 31.7, 17.3, CancellationToken.None);
|
||||
Assert.NotEmpty(firstPairs[0]);
|
||||
Assert.Equal(1, spy.Count);
|
||||
Assert.True(BestFitCache.IsShared(first.Parts[0].Geometry, Spacing));
|
||||
|
||||
var (second, secondTypes) = FreshJob(program, 2);
|
||||
Assert.NotSame(first.Parts[0].Geometry, second.Parts[0].Geometry);
|
||||
var secondPairs = PairCatalog.Build(secondTypes, Spacing, 31.7, 17.3, CancellationToken.None);
|
||||
|
||||
Assert.Equal(1, spy.Count);
|
||||
Assert.Equal(Poses(firstPairs), Poses(secondPairs));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IrregularEngine_FreshInstanceAndJob_PlacesIdenticallyWithoutSearchingAgain()
|
||||
{
|
||||
BestFitCache.Clear();
|
||||
var program = Wedge(6.1);
|
||||
using var spy = new SearchSpy(program);
|
||||
|
||||
var (first, _) = FreshJob(program, 4);
|
||||
var firstResult = new IrregularNestingEngine().Solve(first);
|
||||
Assert.Equal(NestJobStatus.Complete, firstResult.Status);
|
||||
LayoutAssert.Valid(first, firstResult);
|
||||
Assert.Equal(1, spy.Count);
|
||||
|
||||
var (second, _) = FreshJob(program, 4);
|
||||
var secondResult = new IrregularNestingEngine().Solve(second);
|
||||
|
||||
Assert.Equal(1, spy.Count);
|
||||
Assert.Equal(Placements(firstResult), Placements(secondResult));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PairCatalog_ReusesTheSearchOnlyWhenTheSpacingMatches()
|
||||
{
|
||||
BestFitCache.Clear();
|
||||
var program = Wedge(6.3);
|
||||
using var spy = new SearchSpy(program);
|
||||
|
||||
var (job, types) = FreshJob(program, 2);
|
||||
PairCatalog.Build(types, Spacing, 31.7, 17.3, CancellationToken.None);
|
||||
PairCatalog.Build(types, Spacing, 31.7, 17.3, CancellationToken.None);
|
||||
Assert.Equal(1, spy.Count);
|
||||
Assert.False(BestFitCache.IsShared(job.Parts[0].Geometry, 0.5));
|
||||
|
||||
PairCatalog.Build(types, 0.5, 31.7, 17.3, CancellationToken.None);
|
||||
Assert.Equal(2, spy.Count);
|
||||
Assert.True(BestFitCache.IsShared(job.Parts[0].Geometry, 0.5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SharedCapacity_DropsTheLeastRecentlyUsedGeometryFirst()
|
||||
{
|
||||
BestFitCache.Clear();
|
||||
var capacity = BestFitCache.SharedCapacity;
|
||||
try
|
||||
{
|
||||
BestFitCache.SharedCapacity = 2;
|
||||
var programs = new[] { Wedge(6.5), Wedge(6.7), Wedge(6.9) };
|
||||
var parts = programs.Select(p => Part("w", p, 2, RotationPolicy.Automatic)).ToArray();
|
||||
|
||||
Compute(parts[0]);
|
||||
Compute(parts[1]);
|
||||
Compute(parts[0]); // The first becomes the most recently used.
|
||||
Compute(parts[2]);
|
||||
|
||||
Assert.True(BestFitCache.IsShared(parts[0].Geometry, Spacing));
|
||||
Assert.False(BestFitCache.IsShared(parts[1].Geometry, Spacing));
|
||||
Assert.True(BestFitCache.IsShared(parts[2].Geometry, Spacing));
|
||||
|
||||
using var spy = new SearchSpy(programs[1]);
|
||||
Compute(parts[1]);
|
||||
Assert.Equal(1, spy.Count);
|
||||
}
|
||||
finally
|
||||
{
|
||||
BestFitCache.SharedCapacity = capacity;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SharedCapacityZero_DisablesPoolingWithoutBreakingTheSearch()
|
||||
{
|
||||
BestFitCache.Clear();
|
||||
var capacity = BestFitCache.SharedCapacity;
|
||||
try
|
||||
{
|
||||
BestFitCache.SharedCapacity = 0;
|
||||
var program = Wedge(7.1);
|
||||
var part = Part("w", program, 2, RotationPolicy.Automatic);
|
||||
using var spy = new SearchSpy(program);
|
||||
Assert.NotEmpty(Compute(part));
|
||||
Assert.False(BestFitCache.IsShared(part.Geometry, Spacing));
|
||||
Assert.NotEmpty(Compute(part));
|
||||
Assert.Equal(2, spy.Count);
|
||||
}
|
||||
finally
|
||||
{
|
||||
BestFitCache.SharedCapacity = capacity;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_DropsThePool()
|
||||
{
|
||||
BestFitCache.Clear();
|
||||
var program = Wedge(7.3);
|
||||
var part = Part("w", program, 2, RotationPolicy.Automatic);
|
||||
using var spy = new SearchSpy(program);
|
||||
Compute(part);
|
||||
Assert.True(BestFitCache.IsShared(part.Geometry, Spacing));
|
||||
|
||||
BestFitCache.Clear();
|
||||
|
||||
Assert.False(BestFitCache.IsShared(part.Geometry, Spacing));
|
||||
Compute(part);
|
||||
Assert.Equal(2, spy.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FailedSearch_IsNotPooled()
|
||||
{
|
||||
BestFitCache.Clear();
|
||||
var program = Wedge(7.5);
|
||||
var part = Part("w", program, 2, RotationPolicy.Automatic);
|
||||
var previous = BestFitCache.CreateEvaluator;
|
||||
try
|
||||
{
|
||||
BestFitCache.CreateEvaluator = (_, _) => new FailingEvaluator();
|
||||
Assert.Throws<InvalidOperationException>(() => Compute(part));
|
||||
Assert.False(BestFitCache.IsShared(part.Geometry, Spacing));
|
||||
}
|
||||
finally
|
||||
{
|
||||
BestFitCache.CreateEvaluator = previous!;
|
||||
}
|
||||
|
||||
Assert.NotEmpty(Compute(part));
|
||||
Assert.True(BestFitCache.IsShared(part.Geometry, Spacing));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShareByGeometry_LapsesWhenTheProgramIsReplaced()
|
||||
{
|
||||
BestFitCache.Clear();
|
||||
var program = Wedge(7.7);
|
||||
var part = Part("w", program, 2, RotationPolicy.Automatic);
|
||||
using var spy = new SearchSpy(program);
|
||||
var drawing = DrawingJobMapper.CreateDrawing(part);
|
||||
BestFitCache.GetOrCompute(drawing, 31.7, 17.3, Spacing);
|
||||
Assert.Equal(1, spy.Count);
|
||||
|
||||
drawing.Program = (OpenNest.CNC.Program)drawing.Program.Clone();
|
||||
BestFitCache.GetOrCompute(drawing, 31.7, 17.3, Spacing);
|
||||
|
||||
// The drawing no longer vouches for the geometry, so it searches on its own.
|
||||
Assert.Equal(2, spy.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContentComparer_MatchesExactMotionsOnly()
|
||||
{
|
||||
var comparer = PartGeometrySnapshot.ContentComparer;
|
||||
var a = PartGeometrySnapshot.FromProgram(Wedge(7.9));
|
||||
var b = PartGeometrySnapshot.FromProgram(Wedge(7.9));
|
||||
var c = PartGeometrySnapshot.FromProgram(Wedge(7.9 + 1e-12));
|
||||
|
||||
Assert.NotSame(a, b);
|
||||
Assert.False(a.Equals(b));
|
||||
Assert.True(comparer.Equals(a, b));
|
||||
Assert.Equal(comparer.GetHashCode(a), comparer.GetHashCode(b));
|
||||
Assert.False(comparer.Equals(a, c));
|
||||
|
||||
var zero = PartGeometrySnapshot.FromProgram(Polyline((0, 0), (5, 0), (5, 5)));
|
||||
var negativeZero = PartGeometrySnapshot.FromProgram(Polyline((-0.0, 0), (5, 0), (5, 5)));
|
||||
Assert.False(comparer.Equals(zero, negativeZero));
|
||||
|
||||
var suppressed = Wedge(7.9);
|
||||
((OpenNest.CNC.Motion)suppressed.Codes[1]).Suppressed = true;
|
||||
Assert.False(comparer.Equals(a, PartGeometrySnapshot.FromProgram(suppressed)));
|
||||
}
|
||||
|
||||
private static List<BestFitResult> Compute(NestJobPart part)
|
||||
{
|
||||
var drawing = DrawingJobMapper.CreateDrawing(part);
|
||||
try
|
||||
{
|
||||
return BestFitCache.GetOrCompute(drawing, 31.7, 17.3, Spacing);
|
||||
}
|
||||
finally
|
||||
{
|
||||
BestFitCache.Invalidate(drawing);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<(string, double, double, double)> Placements(NestJobResult result) =>
|
||||
result.Plates.SelectMany(p => p.Placements.Select(q => (q.PartId, q.X, q.Y, q.Rotation))).ToList();
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using OpenNest.Engine.Jobs;
|
||||
|
||||
namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
@@ -13,6 +14,14 @@ namespace OpenNest.Engine.BestFit
|
||||
/// shares them and closed nests or finished solves release theirs. Candidates are computed once
|
||||
/// per spacing and filtered per plate size. An entry is dropped when the drawing's
|
||||
/// <see cref="Drawing.Program"/> instance or canonical angle changes.
|
||||
/// <para>
|
||||
/// A drawing reconstructed from job geometry (<see cref="ShareByGeometry"/>) draws its
|
||||
/// candidates from a process-wide pool keyed by the geometry's exact content and the spacing,
|
||||
/// so every later job of the same geometry, in any engine instance or Auto Nest run, skips the
|
||||
/// search. Candidates depend on the program geometry and spacing alone, so a pooled set is
|
||||
/// the set the search would produce. The pool keeps the <see cref="SharedCapacity"/> most
|
||||
/// recently used sets and outlives the drawings; <see cref="Invalidate"/> never touches it.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class BestFitCache
|
||||
{
|
||||
@@ -21,9 +30,61 @@ namespace OpenNest.Engine.BestFit
|
||||
private static readonly ConditionalWeakTable<Drawing, Entry> _entries = new();
|
||||
private static readonly object _entriesLock = new();
|
||||
|
||||
private static readonly ConditionalWeakTable<Drawing, SharedGeometry> _geometry = new();
|
||||
private static readonly object _poolLock = new();
|
||||
private static readonly LinkedList<PoolEntry> _poolOrder = new();
|
||||
private static readonly Dictionary<(PartGeometrySnapshot Geometry, double Spacing), LinkedListNode<PoolEntry>>
|
||||
_pool = new(new PoolKeyComparer());
|
||||
private static int _sharedCapacity = 512;
|
||||
|
||||
public static Func<Drawing, double, IPairEvaluator> CreateEvaluator { get; set; }
|
||||
public static Func<ISlideComputer> CreateSlideComputer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Candidate sets (one per geometry and spacing) the process-wide pool keeps; the least
|
||||
/// recently used set is dropped first. Zero disables pooling for later computations.
|
||||
/// </summary>
|
||||
public static int SharedCapacity
|
||||
{
|
||||
get => Volatile.Read(ref _sharedCapacity);
|
||||
set
|
||||
{
|
||||
if (value < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(value));
|
||||
lock (_poolLock)
|
||||
{
|
||||
_sharedCapacity = value;
|
||||
Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Declares <paramref name="drawing"/> an exact reconstruction of <paramref name="geometry"/>
|
||||
/// (its program unrotated, as <c>DrawingJobMapper.ToProgram</c> builds it). Its unfiltered
|
||||
/// candidates then come from, and go to, the process-wide pool for that geometry content
|
||||
/// and spacing. The declaration lapses if the drawing's <see cref="Drawing.Program"/>
|
||||
/// instance is replaced.
|
||||
/// </summary>
|
||||
public static void ShareByGeometry(Drawing drawing, PartGeometrySnapshot geometry)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(drawing);
|
||||
ArgumentNullException.ThrowIfNull(geometry);
|
||||
_geometry.AddOrUpdate(drawing, new SharedGeometry(geometry, drawing.Program));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when the pool holds computed candidates for this geometry content and spacing, so
|
||||
/// a drawing sharing it would skip the search.
|
||||
/// </summary>
|
||||
public static bool IsShared(PartGeometrySnapshot geometry, double spacing)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(geometry);
|
||||
lock (_poolLock)
|
||||
return _pool.TryGetValue((geometry, spacing), out var node)
|
||||
&& node.Value.Candidates.IsValueCreated;
|
||||
}
|
||||
|
||||
public static List<BestFitResult> GetOrCompute(
|
||||
Drawing drawing,
|
||||
double plateWidth,
|
||||
@@ -120,10 +181,16 @@ namespace OpenNest.Engine.BestFit
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Drops every drawing's entry and the shared pool.</summary>
|
||||
public static void Clear()
|
||||
{
|
||||
lock (_entriesLock)
|
||||
_entries.Clear();
|
||||
lock (_poolLock)
|
||||
{
|
||||
_pool.Clear();
|
||||
_poolOrder.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private static Entry GetEntry(Drawing drawing)
|
||||
@@ -146,12 +213,17 @@ namespace OpenNest.Engine.BestFit
|
||||
|
||||
private static List<BestFitResult> GetCandidates(Entry entry, Drawing drawing, double spacing)
|
||||
{
|
||||
var shared = SharedGeometryOf(CanonicalFrame.SourceOf(drawing));
|
||||
var lazy = entry.Unfiltered.GetOrAdd(
|
||||
spacing,
|
||||
_ => new Lazy<List<BestFitResult>>(
|
||||
() => ComputeCandidates(drawing, spacing),
|
||||
LazyThreadSafetyMode.ExecutionAndPublication
|
||||
)
|
||||
_ =>
|
||||
{
|
||||
var computation = new Lazy<List<BestFitResult>>(
|
||||
() => ComputeCandidates(drawing, spacing),
|
||||
LazyThreadSafetyMode.ExecutionAndPublication
|
||||
);
|
||||
return shared == null ? computation : PoolGetOrAdd(shared, spacing, computation);
|
||||
}
|
||||
);
|
||||
|
||||
try
|
||||
@@ -162,10 +234,72 @@ namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
// Don't cache the failure; the next caller retries.
|
||||
entry.Unfiltered.TryRemove(new KeyValuePair<double, Lazy<List<BestFitResult>>>(spacing, lazy));
|
||||
if (shared != null)
|
||||
PoolRemove(shared, spacing, lazy);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static PartGeometrySnapshot SharedGeometryOf(Drawing source)
|
||||
{
|
||||
if (source == null || !_geometry.TryGetValue(source, out var shared))
|
||||
return null;
|
||||
// A replaced program no longer matches the declared geometry.
|
||||
return ReferenceEquals(shared.Program, source.Program) ? shared.Geometry : null;
|
||||
}
|
||||
|
||||
/// <summary>The pooled computation for this geometry and spacing, adding
|
||||
/// <paramref name="computation"/> when there is none; a hit becomes most recently used.</summary>
|
||||
private static Lazy<List<BestFitResult>> PoolGetOrAdd(
|
||||
PartGeometrySnapshot geometry,
|
||||
double spacing,
|
||||
Lazy<List<BestFitResult>> computation
|
||||
)
|
||||
{
|
||||
lock (_poolLock)
|
||||
{
|
||||
var key = (geometry, spacing);
|
||||
if (_pool.TryGetValue(key, out var node))
|
||||
{
|
||||
_poolOrder.Remove(node);
|
||||
_poolOrder.AddFirst(node);
|
||||
return node.Value.Candidates;
|
||||
}
|
||||
|
||||
if (_sharedCapacity == 0)
|
||||
return computation;
|
||||
|
||||
node = _poolOrder.AddFirst(new PoolEntry(geometry, spacing, computation));
|
||||
_pool[key] = node;
|
||||
Trim();
|
||||
return computation;
|
||||
}
|
||||
}
|
||||
|
||||
private static void PoolRemove(PartGeometrySnapshot geometry, double spacing, Lazy<List<BestFitResult>> computation)
|
||||
{
|
||||
lock (_poolLock)
|
||||
{
|
||||
var key = (geometry, spacing);
|
||||
if (_pool.TryGetValue(key, out var node) && ReferenceEquals(node.Value.Candidates, computation))
|
||||
{
|
||||
_pool.Remove(key);
|
||||
_poolOrder.Remove(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Caller holds _poolLock.
|
||||
private static void Trim()
|
||||
{
|
||||
while (_poolOrder.Count > _sharedCapacity)
|
||||
{
|
||||
var last = _poolOrder.Last;
|
||||
_pool.Remove((last.Value.Geometry, last.Value.Spacing));
|
||||
_poolOrder.RemoveLast();
|
||||
}
|
||||
}
|
||||
|
||||
private static List<BestFitResult> ComputeCandidates(Drawing drawing, double spacing)
|
||||
{
|
||||
// Operate on the canonical frame so cached pair positions are orientation-invariant.
|
||||
@@ -261,5 +395,28 @@ namespace OpenNest.Engine.BestFit
|
||||
ReferenceEquals(_program, source.Program)
|
||||
&& _sourceAngle == (source.Source?.Angle ?? 0.0);
|
||||
}
|
||||
|
||||
/// <summary>The geometry a drawing was reconstructed from, and the program built then.</summary>
|
||||
private sealed record SharedGeometry(PartGeometrySnapshot Geometry, CNC.Program Program);
|
||||
|
||||
private sealed record PoolEntry(
|
||||
PartGeometrySnapshot Geometry,
|
||||
double Spacing,
|
||||
Lazy<List<BestFitResult>> Candidates
|
||||
);
|
||||
|
||||
private sealed class PoolKeyComparer
|
||||
: IEqualityComparer<(PartGeometrySnapshot Geometry, double Spacing)>
|
||||
{
|
||||
public bool Equals(
|
||||
(PartGeometrySnapshot Geometry, double Spacing) x,
|
||||
(PartGeometrySnapshot Geometry, double Spacing) y
|
||||
) =>
|
||||
x.Spacing.Equals(y.Spacing)
|
||||
&& PartGeometrySnapshot.ContentComparer.Equals(x.Geometry, y.Geometry);
|
||||
|
||||
public int GetHashCode((PartGeometrySnapshot Geometry, double Spacing) key) =>
|
||||
HashCode.Combine(PartGeometrySnapshot.ContentComparer.GetHashCode(key.Geometry), key.Spacing);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Engine.BestFit;
|
||||
|
||||
namespace OpenNest.Engine.Jobs.Adapters;
|
||||
|
||||
@@ -90,6 +91,9 @@ public static class DrawingJobMapper
|
||||
return program;
|
||||
}
|
||||
|
||||
/// <summary>A private drawing of the part for placement code. Its best-fit candidates are
|
||||
/// pooled by geometry content (<see cref="BestFitCache.ShareByGeometry"/>), so the same part
|
||||
/// geometry in a later job, engine instance or Auto Nest run does not search again.</summary>
|
||||
internal static Drawing CreateDrawing(NestJobPart part)
|
||||
{
|
||||
var drawing = new Drawing(part.Id, ToProgram(part.Geometry)) { Priority = part.Priority };
|
||||
@@ -101,6 +105,7 @@ public static class DrawingJobMapper
|
||||
EndAngle = part.Rotation.End,
|
||||
Allow180Equivalent = part.Rotation.Allow180Equivalent,
|
||||
};
|
||||
BestFitCache.ShareByGeometry(drawing, part.Geometry);
|
||||
return drawing;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,9 +30,73 @@ public sealed class PartGeometrySnapshot
|
||||
Motions = NestJob.Own(motions);
|
||||
}
|
||||
|
||||
private int? contentHash;
|
||||
|
||||
public Mode Mode { get; }
|
||||
public IReadOnlyList<PartGeometryMotion> Motions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Equality by exact content: the same mode and the same motions in the same order, every
|
||||
/// value bit for bit (so 0.0 and -0.0 differ), whatever the instances. Snapshots themselves
|
||||
/// still compare by reference, as job identity does; this comparer is for caches whose
|
||||
/// results depend on the geometry alone, such as best-fit candidates.
|
||||
/// </summary>
|
||||
public static IEqualityComparer<PartGeometrySnapshot> ContentComparer { get; } = new ContentEquality();
|
||||
|
||||
private int ContentHash
|
||||
{
|
||||
get
|
||||
{
|
||||
if (contentHash is int cached)
|
||||
return cached;
|
||||
var hash = new HashCode();
|
||||
hash.Add(Mode);
|
||||
foreach (var motion in Motions)
|
||||
{
|
||||
hash.Add(motion.Type);
|
||||
hash.Add(Bits(motion.X));
|
||||
hash.Add(Bits(motion.Y));
|
||||
hash.Add(Bits(motion.CenterX));
|
||||
hash.Add(Bits(motion.CenterY));
|
||||
hash.Add(motion.Rotation);
|
||||
hash.Add(motion.Layer);
|
||||
hash.Add(motion.Suppressed);
|
||||
}
|
||||
var value = hash.ToHashCode();
|
||||
contentHash = value;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
private static long Bits(double value) => BitConverter.DoubleToInt64Bits(value);
|
||||
|
||||
private static bool SameMotion(PartGeometryMotion a, PartGeometryMotion b) =>
|
||||
a.Type == b.Type
|
||||
&& Bits(a.X) == Bits(b.X)
|
||||
&& Bits(a.Y) == Bits(b.Y)
|
||||
&& Bits(a.CenterX) == Bits(b.CenterX)
|
||||
&& Bits(a.CenterY) == Bits(b.CenterY)
|
||||
&& a.Rotation == b.Rotation
|
||||
&& a.Layer == b.Layer
|
||||
&& a.Suppressed == b.Suppressed;
|
||||
|
||||
private sealed class ContentEquality : IEqualityComparer<PartGeometrySnapshot>
|
||||
{
|
||||
public bool Equals(PartGeometrySnapshot x, PartGeometrySnapshot y)
|
||||
{
|
||||
if (ReferenceEquals(x, y))
|
||||
return true;
|
||||
if (x is null || y is null || x.Mode != y.Mode || x.Motions.Count != y.Motions.Count)
|
||||
return false;
|
||||
for (var i = 0; i < x.Motions.Count; i++)
|
||||
if (!SameMotion(x.Motions[i], y.Motions[i]))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public int GetHashCode(PartGeometrySnapshot snapshot) => snapshot.ContentHash;
|
||||
}
|
||||
|
||||
/// <summary>Copies supported motion geometry immediately; later program edits cannot affect this snapshot.</summary>
|
||||
public static PartGeometrySnapshot FromProgram(Program program)
|
||||
{
|
||||
|
||||
@@ -95,8 +95,9 @@ internal static class PairCatalog
|
||||
Func<NestJobPart, Drawing>? drawingFor
|
||||
)
|
||||
{
|
||||
var drawing = drawingFor?.Invoke(type.Part)
|
||||
?? new Drawing(type.Part.Id, DrawingJobMapper.ToProgram(type.Part.Geometry));
|
||||
// Either drawing reads its candidates from the geometry-content pool, so a repeated
|
||||
// or re-tuned job of the same parts skips the best-fit search.
|
||||
var drawing = drawingFor?.Invoke(type.Part) ?? DrawingJobMapper.CreateDrawing(type.Part);
|
||||
List<BestFitResult> fits;
|
||||
try
|
||||
{
|
||||
|
||||
+11
-2
@@ -81,8 +81,17 @@ Any remaining numerical hole is filled only when its entire ring is certified to
|
||||
space, preserving genuine enclosed placement pockets without changing spacing tolerances.
|
||||
|
||||
The pairs and the Fill blocks below read each part through one private drawing per part
|
||||
geometry, owned by the engine instance, so a part's best-fit search runs once per instance:
|
||||
pairs and blocks share it, and so do a pipeline's cutout trial and its ordinary solve.
|
||||
geometry, owned by the engine instance: pairs and blocks share it, and so do a pipeline's cutout
|
||||
trial and its ordinary solve. The best-fit search itself runs once per part geometry and spacing
|
||||
in the whole process: every drawing reconstructed from job geometry
|
||||
(`DrawingJobMapper.CreateDrawing`) takes its unfiltered candidates from a pool in `BestFitCache`
|
||||
keyed by the snapshot's exact content (mode and every motion value, bit for bit) and the spacing,
|
||||
so a repeated or re-tuned Auto Nest of the same drawings, a fresh engine instance, another engine
|
||||
or the cutout pre-pass skips the search and offers the same pairs. Candidates depend on the program
|
||||
geometry and spacing alone, so a pooled set is the set the search would produce and placements are
|
||||
unchanged. The pool keeps the 512 most recently used sets (`BestFitCache.SharedCapacity`), outlives
|
||||
the private drawings (`Invalidate` releases a drawing's entry, not the pool) and is in-memory only:
|
||||
the sets saved in a `.nest` file belong to the caller's drawings and are not consulted here.
|
||||
|
||||
When remaining demand exceeds two, Irregular also offers Fill patterns as optional
|
||||
multi-member candidates, not as solid bounding boxes or a whole-job Fill fallback. It searches
|
||||
|
||||
Reference in new issue
Block a user