refactor(gpt6astra): use host geometry, tolerances and layout checks
Gpt6Astra reverse-engineered the validator: hand-tuned paddings and a copied check sequence (ValidationOverlap) to match its rounding. It now reads parts with JobPartGeometry, takes clearance from NestTolerances, checks candidates with NestLayoutCheck.Clears, and assembles results with NestJobResultBuilder and NestJobCost; its tests use the shared kit. Its contact search, beam search and extra Automatic angles are unchanged. Synthetic benchmark (5 jobs, salvage 0.5): all valid, 2 sheets each, cost 5574.07 -> 5470.07; time 1871 -> 2400 ms from the stricter shared check on arc-heavy jobs. Co-Authored-By: Codex <noreply@openai.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
using Clipper2Lib;
|
using Clipper2Lib;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Engine.Jobs;
|
||||||
using M = System.Math;
|
using M = System.Math;
|
||||||
|
|
||||||
namespace OpenNest.Engine.Gpt6Astra;
|
namespace OpenNest.Engine.Gpt6Astra;
|
||||||
@@ -47,7 +48,9 @@ internal sealed class ContactGeometry
|
|||||||
}
|
}
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
var delta = spacing + stationary.ContactError + moving.ContactError
|
var delta = spacing + stationary.ContactError + moving.ContactError
|
||||||
+ (stationary.Curved || moving.Curved ? 0.003 : spacing > 0 ? 0.0003 : 0);
|
+ (stationary.Curved || moving.Curved
|
||||||
|
? NestTolerances.SafeClearanceMargin(NestTolerances.ValidationOutline)
|
||||||
|
: spacing > 0 ? NestTolerances.SafeClearanceMargin(0) : 0);
|
||||||
if (delta > 0) paths = Clipper.InflatePaths(paths, delta, JoinType.Round,
|
if (delta > 0) paths = Clipper.InflatePaths(paths, delta, JoinType.Round,
|
||||||
EndType.Polygon, 2, GeometryPrecision.Digits, 0.00001);
|
EndType.Polygon, 2, GeometryPrecision.Digits, 0.00001);
|
||||||
// Bound cache residency for jobs with many distinct rotation pairs.
|
// Bound cache residency for jobs with many distinct rotation pairs.
|
||||||
|
|||||||
@@ -11,16 +11,17 @@ internal sealed record SheetTrial(int StockIndex, int[] Counts, List<PackedShape
|
|||||||
/// <summary>Searches vertices of the available translation region and exact-fit contacts.</summary>
|
/// <summary>Searches vertices of the available translation region and exact-fit contacts.</summary>
|
||||||
internal sealed class ContactPlacer(PreparedPart[] parts, ContactGeometry geometry, CancellationToken token)
|
internal sealed class ContactPlacer(PreparedPart[] parts, ContactGeometry geometry, CancellationToken token)
|
||||||
{
|
{
|
||||||
|
private static readonly double GridUnit = M.Pow(10, -NestTolerances.ClipperPrecision);
|
||||||
private readonly Dictionary<(int, int, double, double, double, double, double), bool> validationCache = new();
|
private readonly Dictionary<(int, int, double, double, double, double, double), bool> validationCache = new();
|
||||||
private double validationOriginX;
|
private double validationOriginX;
|
||||||
private double validationOriginY;
|
private double validationOriginY;
|
||||||
|
|
||||||
internal SheetTrial Pack(int stockIndex, NestPlateStock stock, int[] committed, int[] flexibility, int mode)
|
internal SheetTrial Pack(int stockIndex, NestPlateStock stock, int[] committed, int[] flexibility, int mode)
|
||||||
{
|
{
|
||||||
validationOriginX = (stock.Quadrant is 1 or 4 ? 0 : -stock.Size.Length) + stock.EdgeSpacing.Left;
|
validationOriginX = stock.WorkArea.Left;
|
||||||
validationOriginY = (stock.Quadrant is 1 or 2 ? 0 : -stock.Size.Width) + stock.EdgeSpacing.Bottom;
|
validationOriginY = stock.WorkArea.Bottom;
|
||||||
var width = stock.Size.Length - stock.EdgeSpacing.Left - stock.EdgeSpacing.Right;
|
var width = stock.WorkArea.Length;
|
||||||
var height = stock.Size.Width - stock.EdgeSpacing.Bottom - stock.EdgeSpacing.Top;
|
var height = stock.WorkArea.Width;
|
||||||
var counts = (int[])committed.Clone();
|
var counts = (int[])committed.Clone();
|
||||||
var placed = new List<PackedShape>();
|
var placed = new List<PackedShape>();
|
||||||
var spaces = new Dictionary<int, SearchSpace>();
|
var spaces = new Dictionary<int, SearchSpace>();
|
||||||
@@ -112,10 +113,10 @@ internal sealed class ContactPlacer(PreparedPart[] parts, ContactGeometry geomet
|
|||||||
// separately, then validate against material, not the outer envelope.
|
// separately, then validate against material, not the outer envelope.
|
||||||
foreach (var hole in other.Variant.Material.Where(p => !Clipper.IsPositive(p)))
|
foreach (var hole in other.Variant.Material.Where(p => !Clipper.IsPositive(p)))
|
||||||
{
|
{
|
||||||
var l = hole.Min(p => p.x) + other.X + spacing + 0.0004;
|
var l = hole.Min(p => p.x) + other.X + spacing + (4 * GridUnit);
|
||||||
var b = hole.Min(p => p.y) + other.Y + spacing + 0.0004;
|
var b = hole.Min(p => p.y) + other.Y + spacing + (4 * GridUnit);
|
||||||
var r = hole.Max(p => p.x) + other.X - spacing - moving.Width - 0.0004;
|
var r = hole.Max(p => p.x) + other.X - spacing - moving.Width - (4 * GridUnit);
|
||||||
var t = hole.Max(p => p.y) + other.Y - spacing - moving.Height - 0.0004;
|
var t = hole.Max(p => p.y) + other.Y - spacing - moving.Height - (4 * GridUnit);
|
||||||
if (r < l || t < b) continue;
|
if (r < l || t < b) continue;
|
||||||
Add(l, b); Add(r, b); Add(l, t); Add(r, t); Add((l + r) / 2, (b + t) / 2);
|
Add(l, b); Add(r, b); Add(l, t); Add(r, t); Add((l + r) / 2, (b + t) / 2);
|
||||||
// Box corners miss the useful interior of circular and rounded holes.
|
// Box corners miss the useful interior of circular and rounded holes.
|
||||||
@@ -144,8 +145,8 @@ internal sealed class ContactPlacer(PreparedPart[] parts, ContactGeometry geomet
|
|||||||
// Exact contacts can be invalid only after the host's four-decimal
|
// Exact contacts can be invalid only after the host's four-decimal
|
||||||
// polygon rounding. Try nearby outward contacts without changing angle.
|
// polygon rounding. Try nearby outward contacts without changing angle.
|
||||||
foreach (var (dx, dy) in new (double, double)[] {
|
foreach (var (dx, dy) in new (double, double)[] {
|
||||||
(0.0003, 0), (0, 0.0003), (0.0003, 0.0003), (-0.0003, 0),
|
((3 * GridUnit), 0), (0, (3 * GridUnit)), ((3 * GridUnit), (3 * GridUnit)), (-(3 * GridUnit), 0),
|
||||||
(0, -0.0003), (-0.0003, 0.0003), (0.0003, -0.0003), (-0.0003, -0.0003) })
|
(0, -(3 * GridUnit)), (-(3 * GridUnit), (3 * GridUnit)), ((3 * GridUnit), -(3 * GridUnit)), (-(3 * GridUnit), -(3 * GridUnit)) })
|
||||||
{
|
{
|
||||||
var nudged = pose with { X = pose.X + dx, Y = pose.Y + dy };
|
var nudged = pose with { X = pose.X + dx, Y = pose.Y + dy };
|
||||||
if (nudged.X < 0 || nudged.Y < 0 || nudged.X > maxX || nudged.Y > maxY) continue;
|
if (nudged.X < 0 || nudged.Y < 0 || nudged.X > maxX || nudged.Y > maxY) continue;
|
||||||
@@ -202,10 +203,9 @@ internal sealed class ContactPlacer(PreparedPart[] parts, ContactGeometry geomet
|
|||||||
private bool Valid(PackedShape candidate, List<PackedShape> placed, double spacing)
|
private bool Valid(PackedShape candidate, List<PackedShape> placed, double spacing)
|
||||||
{
|
{
|
||||||
PathsD? material = null;
|
PathsD? material = null;
|
||||||
PathsD? validationMaterial = null;
|
|
||||||
foreach (var other in placed)
|
foreach (var other in placed)
|
||||||
{
|
{
|
||||||
var gap = spacing + (candidate.Variant.Curved || other.Variant.Curved ? 0.003 : 0.0001);
|
var gap = spacing + (candidate.Variant.Curved || other.Variant.Curved ? NestTolerances.SafeClearanceMargin(NestTolerances.ValidationOutline) : NestTolerances.SafeClearanceMargin(0));
|
||||||
if (candidate.X >= other.X + other.Variant.Width + gap ||
|
if (candidate.X >= other.X + other.Variant.Width + gap ||
|
||||||
other.X >= candidate.X + candidate.Variant.Width + gap ||
|
other.X >= candidate.X + candidate.Variant.Width + gap ||
|
||||||
candidate.Y >= other.Y + other.Variant.Height + gap ||
|
candidate.Y >= other.Y + other.Variant.Height + gap ||
|
||||||
@@ -223,46 +223,24 @@ internal sealed class ContactPlacer(PreparedPart[] parts, ContactGeometry geomet
|
|||||||
var obstacle = GeometryPrecision.Translate(other.Variant.Halo(spacing), other.X, other.Y);
|
var obstacle = GeometryPrecision.Translate(other.Variant.Halo(spacing), other.X, other.Y);
|
||||||
var overlap = Clipper.Intersect(material, obstacle, FillRule.NonZero, GeometryPrecision.Digits);
|
var overlap = Clipper.Intersect(material, obstacle, FillRule.NonZero, GeometryPrecision.Digits);
|
||||||
if (M.Abs(Clipper.Area(overlap)) > 1e-8) return false;
|
if (M.Abs(Clipper.Area(overlap)) > 1e-8) return false;
|
||||||
validationMaterial ??= GeometryPrecision.Translate(candidate.Variant.ValidationRegion(0), candidate.X, candidate.Y);
|
|
||||||
var validationObstacle = GeometryPrecision.Translate(other.Variant.ValidationRegion(spacing), other.X, other.Y);
|
|
||||||
if (M.Abs(Clipper.Area(Clipper.Intersect(validationMaterial, validationObstacle,
|
|
||||||
FillRule.NonZero, GeometryPrecision.Digits))) > 1e-8) return false;
|
|
||||||
if (spacing == 0 || candidate.Variant.Material.Count > 1 || other.Variant.Material.Count > 1)
|
|
||||||
{
|
|
||||||
var outerIntersection = Clipper.Intersect(
|
|
||||||
new PathsD(validationMaterial.Where(Clipper.IsPositive)),
|
|
||||||
new PathsD(validationObstacle.Where(Clipper.IsPositive)), FillRule.NonZero, GeometryPrecision.Digits);
|
|
||||||
if (spacing != 0 && M.Abs(Clipper.Area(outerIntersection)) <= 1e-8) continue;
|
|
||||||
var key = (candidate.Variant.Id, other.Variant.Id, spacing,
|
var key = (candidate.Variant.Id, other.Variant.Id, spacing,
|
||||||
candidate.X + validationOriginX, candidate.Y + validationOriginY,
|
candidate.X + validationOriginX, candidate.Y + validationOriginY,
|
||||||
other.X + validationOriginX, other.Y + validationOriginY);
|
other.X + validationOriginX, other.Y + validationOriginY);
|
||||||
if (!validationCache.TryGetValue(key, out var collides))
|
if (!validationCache.TryGetValue(key, out var collides))
|
||||||
{
|
{
|
||||||
collides = ValidationOverlap(
|
NestJobPlacement Pose(PackedShape p) => new("check", 0,
|
||||||
GeometryPrecision.Translate(validationMaterial, validationOriginX, validationOriginY),
|
validationOriginX + p.X - p.Variant.OriginX,
|
||||||
GeometryPrecision.Translate(validationObstacle, validationOriginX, validationOriginY));
|
validationOriginY + p.Y - p.Variant.OriginY, p.Variant.Angle);
|
||||||
|
// Equal-left ties follow commit order, just as the full-layout check does.
|
||||||
|
collides = !NestLayoutCheck.Clears(other.Variant.Geometry, Pose(other),
|
||||||
|
candidate.Variant.Geometry, Pose(candidate), spacing);
|
||||||
if (validationCache.Count >= 4096) validationCache.Clear();
|
if (validationCache.Count >= 4096) validationCache.Clear();
|
||||||
validationCache[key] = collides;
|
validationCache[key] = collides;
|
||||||
}
|
}
|
||||||
if (collides) return false;
|
if (collides) return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
private static bool ValidationOverlap(PathsD a, PathsD b)
|
|
||||||
{
|
|
||||||
var holesA = a.Where(p => !Clipper.IsPositive(p)).Select(ClipperBridge.ToPolygon).ToList();
|
|
||||||
var holesB = b.Where(p => !Clipper.IsPositive(p)).Select(ClipperBridge.ToPolygon).ToList();
|
|
||||||
foreach (var outerA in a.Where(Clipper.IsPositive))
|
|
||||||
foreach (var outerB in b.Where(Clipper.IsPositive))
|
|
||||||
{
|
|
||||||
var pa = ClipperBridge.ToPolygon(outerA);
|
|
||||||
var pb = ClipperBridge.ToPolygon(outerB);
|
|
||||||
// The benchmark orders by world-space left bound before clipping.
|
|
||||||
if (pa.Left <= pb.Left ? Collision.HasOverlap(pa, pb, holesA, holesB) :
|
|
||||||
Collision.HasOverlap(pb, pa, holesB, holesA)) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ public sealed class Gpt6AstraNestingEngine : INestingEngine
|
|||||||
NestJobValidator.Validate(job);
|
NestJobValidator.Validate(job);
|
||||||
var parts = GeometryPreparation.Prepare(job, token);
|
var parts = GeometryPreparation.Prepare(job, token);
|
||||||
var fit = parts.Select(p => job.Plates.Select(s => p.Variants.Any(v =>
|
var fit = parts.Select(p => job.Plates.Select(s => p.Variants.Any(v =>
|
||||||
v.Width <= s.Size.Length - s.EdgeSpacing.Left - s.EdgeSpacing.Right + 1e-9 &&
|
s.Fits(v.Width, v.Height))).ToArray()).ToArray();
|
||||||
v.Height <= s.Size.Width - s.EdgeSpacing.Top - s.EdgeSpacing.Bottom + 1e-9)).ToArray()).ToArray();
|
|
||||||
var placer = new ContactPlacer(parts, new ContactGeometry(), token);
|
var placer = new ContactPlacer(parts, new ContactGeometry(), token);
|
||||||
var initial = new Plan(new int[parts.Length], new int[job.Plates.Count], new List<SheetTrial>(), 0);
|
var initial = new Plan(new int[parts.Length], new int[job.Plates.Count], new List<SheetTrial>(), 0);
|
||||||
var frontier = new List<Plan> { initial };
|
var frontier = new List<Plan> { initial };
|
||||||
@@ -56,7 +55,9 @@ public sealed class Gpt6AstraNestingEngine : INestingEngine
|
|||||||
evaluated++;
|
evaluated++;
|
||||||
}
|
}
|
||||||
if (trial.Shapes.Count == 0) continue;
|
if (trial.Shapes.Count == 0) continue;
|
||||||
var sheetCost = job.Plates[s].Size.Length * job.Plates[s].Size.Width;
|
var sheetCost = NestJobCost.NetSheetArea(job,
|
||||||
|
new NestJobPlateResult(0, job.Plates[s], Poses(trial).Select(p =>
|
||||||
|
new NestJobPlacement(p.PartId, 0, p.X, p.Y, p.Rotation))));
|
||||||
for (var p = 0; p < parts.Length; p++)
|
for (var p = 0; p < parts.Length; p++)
|
||||||
{
|
{
|
||||||
var delivered = trial.Counts[p] - state.Counts[p];
|
var delivered = trial.Counts[p] - state.Counts[p];
|
||||||
@@ -65,7 +66,7 @@ public sealed class Gpt6AstraNestingEngine : INestingEngine
|
|||||||
var used = (int[])state.Used.Clone(); used[s]++;
|
var used = (int[])state.Used.Clone(); used[s]++;
|
||||||
var sheets = new List<SheetTrial>(state.Sheets) { trial };
|
var sheets = new List<SheetTrial>(state.Sheets) { trial };
|
||||||
var next = new Plan(trial.Counts, used, sheets,
|
var next = new Plan(trial.Counts, used, sheets,
|
||||||
state.Cost + job.Plates[s].Size.Length * job.Plates[s].Size.Width);
|
state.Cost + sheetCost);
|
||||||
if (BetterFulfillment(next, best)) best = next;
|
if (BetterFulfillment(next, best)) best = next;
|
||||||
if (IsComplete(next))
|
if (IsComplete(next))
|
||||||
{
|
{
|
||||||
@@ -97,29 +98,26 @@ public sealed class Gpt6AstraNestingEngine : INestingEngine
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var selected = complete ?? best;
|
var selected = complete ?? best;
|
||||||
var counts = new int[parts.Length];
|
var builder = new NestJobResultBuilder(job, progress);
|
||||||
var plates = new List<NestJobPlateResult>();
|
|
||||||
foreach (var sheet in selected.Sheets)
|
foreach (var sheet in selected.Sheets)
|
||||||
{
|
{
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
var stock = job.Plates[sheet.StockIndex];
|
builder.AddSheet(job.Plates[sheet.StockIndex], Poses(sheet));
|
||||||
var x = (stock.Quadrant is 1 or 4 ? 0 : -stock.Size.Length) + stock.EdgeSpacing.Left;
|
|
||||||
var y = (stock.Quadrant is 1 or 2 ? 0 : -stock.Size.Width) + stock.EdgeSpacing.Bottom;
|
|
||||||
var placements = sheet.Shapes.Select(p => new NestJobPlacement(job.Parts[p.Variant.Part].Id,
|
|
||||||
counts[p.Variant.Part]++, x + p.X - p.Variant.OriginX,
|
|
||||||
y + p.Y - p.Variant.OriginY, p.Variant.Angle)).ToArray();
|
|
||||||
plates.Add(new(plates.Count, stock, placements));
|
|
||||||
progress?.Report(new(NestJobStage.PlateCommitted, stock.Id, plates.Count - 1,
|
|
||||||
plates.Count, counts.Sum()));
|
|
||||||
}
|
}
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
var reason = complete != null ? NestJobStopReason.Completed :
|
var reason = complete != null ? NestJobStopReason.Completed :
|
||||||
selected.Sheets.Count >= (job.Options.MaxPlates ?? int.MaxValue) ? NestJobStopReason.PlateLimitReached :
|
selected.Sheets.Count >= (job.Options.MaxPlates ?? int.MaxValue) ? NestJobStopReason.PlateLimitReached :
|
||||||
!Enumerable.Range(0, job.Plates.Count).Any(s => Available(selected, s)) ? NestJobStopReason.StockExhausted :
|
!Enumerable.Range(0, job.Plates.Count).Any(s => Available(selected, s)) ? NestJobStopReason.StockExhausted :
|
||||||
NestJobStopReason.NoPlacementFound;
|
NestJobStopReason.NoPlacementFound;
|
||||||
return new(complete != null ? NestJobStatus.Complete : NestJobStatus.Incomplete, reason, plates,
|
return builder.Build(reason);
|
||||||
job.Parts.Select((p, i) => new PartFulfillment(p.Id, p.Quantity, counts[i], p.Quantity - counts[i])),
|
|
||||||
job.Plates.Select((s, i) => new StockUsage(s.Id, selected.Used[i], s.Quantity - selected.Used[i])));
|
IEnumerable<(string PartId, double X, double Y, double Rotation)> Poses(SheetTrial sheet)
|
||||||
|
{
|
||||||
|
var work = job.Plates[sheet.StockIndex].WorkArea;
|
||||||
|
return sheet.Shapes.Select(p => (job.Parts[p.Variant.Part].Id,
|
||||||
|
work.Left + p.X - p.Variant.OriginX, work.Bottom + p.Y - p.Variant.OriginY,
|
||||||
|
p.Variant.Angle));
|
||||||
|
}
|
||||||
|
|
||||||
bool Available(Plan p, int s) => p.Used[s] < (job.Plates[s].Quantity ?? int.MaxValue);
|
bool Available(Plan p, int s) => p.Used[s] < (job.Plates[s].Quantity ?? int.MaxValue);
|
||||||
bool IsComplete(Plan p) => parts.Select((part, i) => p.Counts[i] == part.Requirement.Quantity).All(v => v);
|
bool IsComplete(Plan p) => parts.Select((part, i) => p.Counts[i] == part.Requirement.Quantity).All(v => v);
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
using Clipper2Lib;
|
using Clipper2Lib;
|
||||||
using OpenNest.Converters;
|
|
||||||
using OpenNest.Engine.Jobs;
|
using OpenNest.Engine.Jobs;
|
||||||
using OpenNest.Engine.Jobs.Adapters;
|
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using M = System.Math;
|
using M = System.Math;
|
||||||
|
|
||||||
@@ -25,24 +23,11 @@ internal sealed class ShapeVariant
|
|||||||
internal required double ContactError { get; init; }
|
internal required double ContactError { get; init; }
|
||||||
internal required Polygon Hull { get; init; }
|
internal required Polygon Hull { get; init; }
|
||||||
internal required bool Convex { get; init; }
|
internal required bool Convex { get; init; }
|
||||||
internal required ShapeProfile ValidationProfile { get; init; }
|
internal required JobPartGeometry Geometry { get; init; }
|
||||||
internal bool BoxLike => Material.Count == 1 && GridAligned(OriginX) && GridAligned(OriginY) &&
|
internal bool BoxLike => Material.Count == 1 && GridAligned(OriginX) && GridAligned(OriginY) &&
|
||||||
GridAligned(Width) && GridAligned(Height) &&
|
GridAligned(Width) && GridAligned(Height) &&
|
||||||
M.Abs(Outline.Area() - Width * Height) < 1e-8 * M.Max(1, Width * Height);
|
M.Abs(Outline.Area() - Width * Height) < 1e-8 * M.Max(1, Width * Height);
|
||||||
private static bool GridAligned(double x) => M.Abs(x - M.Round(x * 10000) / 10000) < 1e-9;
|
private static bool GridAligned(double x) => M.Abs(x - M.Round(x * 10000) / 10000) < 1e-9;
|
||||||
private readonly Dictionary<double, PathsD> validationRegions = new();
|
|
||||||
|
|
||||||
internal PathsD ValidationRegion(double spacing)
|
|
||||||
{
|
|
||||||
if (validationRegions.TryGetValue(spacing, out var cached)) return cached;
|
|
||||||
// Match the external validator's sequence: flatten/round in the original
|
|
||||||
// rotated snapshot frame, then translate. Rounding after normalization is
|
|
||||||
// not equivalent at a zero-clearance contact.
|
|
||||||
var region = ClipperBridge.OffsetForValidation(ValidationProfile, spacing, 0.001);
|
|
||||||
var paths = new PathsD(region.Outers.Select(p => ClipperBridge.ToPath(p, true)));
|
|
||||||
paths.AddRange(region.Holes.Select(p => ClipperBridge.ToPath(p, false)));
|
|
||||||
return validationRegions[spacing] = GeometryPrecision.Translate(paths, -OriginX, -OriginY);
|
|
||||||
}
|
|
||||||
private readonly Dictionary<double, PathsD> halos = new();
|
private readonly Dictionary<double, PathsD> halos = new();
|
||||||
|
|
||||||
internal PathsD Halo(double spacing)
|
internal PathsD Halo(double spacing)
|
||||||
@@ -50,7 +35,7 @@ internal sealed class ShapeVariant
|
|||||||
if (halos.TryGetValue(spacing, out var cached)) return cached;
|
if (halos.TryGetValue(spacing, out var cached)) return cached;
|
||||||
// Raw outlines already circumscribe curves; the extra clearance covers independent
|
// Raw outlines already circumscribe curves; the extra clearance covers independent
|
||||||
// flattenings after pose materialization and the validator's four-decimal grid.
|
// flattenings after pose materialization and the validator's four-decimal grid.
|
||||||
var delta = spacing + (Curved ? 0.0021 : spacing > 0 ? 0.00015 : 0);
|
var delta = spacing + (Curved ? NestTolerances.SafeClearanceMargin(NestTolerances.ValidationOutline) : spacing > 0 ? NestTolerances.SafeClearanceMargin(0) : 0);
|
||||||
return halos[spacing] = delta == 0 ? Material : Clipper.InflatePaths(Material, delta,
|
return halos[spacing] = delta == 0 ? Material : Clipper.InflatePaths(Material, delta,
|
||||||
JoinType.Round, EndType.Polygon, 2, GeometryPrecision.Digits, 0.00001);
|
JoinType.Round, EndType.Polygon, 2, GeometryPrecision.Digits, 0.00001);
|
||||||
}
|
}
|
||||||
@@ -77,17 +62,19 @@ internal static class GeometryPreparation
|
|||||||
return job.Parts.Select((part, index) =>
|
return job.Parts.Select((part, index) =>
|
||||||
{
|
{
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
var entities = ConvertProgram.ToGeometry(DrawingJobMapper.ToProgram(part.Geometry))
|
var geometry = JobPartGeometry.Read(part.Geometry);
|
||||||
.Where(e => SpecialLayers.IsMaterial(e.Layer)).ToList();
|
var baseProfile = geometry.Profile;
|
||||||
// Input validation has established that open marks lie inside material. They
|
var closed = new[] { geometry.Perimeter }.Concat(geometry.Cutouts)
|
||||||
// must not be interpreted as holes by ShapeProfile.
|
.SelectMany(shape => shape.Entities).ToList();
|
||||||
var closed = ShapeBuilder.GetShapes(entities).Where(s => s.IsClosed())
|
var area = geometry.MaterialArea;
|
||||||
.SelectMany(s => s.Entities).ToList();
|
|
||||||
var baseProfile = new ShapeProfile(closed);
|
|
||||||
var area = baseProfile.Perimeter.Area() - baseProfile.Cutouts.Sum(h => h.Area());
|
|
||||||
var variants = new List<ShapeVariant>();
|
var variants = new List<ShapeVariant>();
|
||||||
var keys = new HashSet<string>(StringComparer.Ordinal);
|
var keys = new HashSet<string>(StringComparer.Ordinal);
|
||||||
foreach (var angle in Angles(part.Rotation, baseProfile))
|
var angles = Angles(part.Rotation, baseProfile).ToArray();
|
||||||
|
// The host symmetry primitive compares perimeters only. Keep full-material
|
||||||
|
// signatures for holed parts, whose cutouts can break perimeter symmetry.
|
||||||
|
var distinct = baseProfile.Cutouts.Count == 0
|
||||||
|
? RotationCandidates.DistinctOutlines(baseProfile.Perimeter, angles) : angles;
|
||||||
|
foreach (var angle in distinct)
|
||||||
{
|
{
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
var rotated = closed.Select(e => { var copy = e.Clone(); copy.Rotate(angle); return copy; }).ToList();
|
var rotated = closed.Select(e => { var copy = e.Clone(); copy.Rotate(angle); return copy; }).ToList();
|
||||||
@@ -97,15 +84,14 @@ internal static class GeometryPreparation
|
|||||||
var h = rotated.Max(e => e.Top) - y;
|
var h = rotated.Max(e => e.Top) - y;
|
||||||
if (!double.IsFinite(w) || !double.IsFinite(h) || w <= 0 || h <= 0)
|
if (!double.IsFinite(w) || !double.IsFinite(h) || w <= 0 || h <= 0)
|
||||||
throw new ArgumentException($"Unusable rotated bounds: {part.Id}.");
|
throw new ArgumentException($"Unusable rotated bounds: {part.Id}.");
|
||||||
var validationProfile = new ShapeProfile(rotated.Select(e => e.Clone()).ToList());
|
|
||||||
foreach (var e in rotated) e.Offset(-x, -y);
|
foreach (var e in rotated) e.Offset(-x, -y);
|
||||||
var profile = new ShapeProfile(rotated);
|
var profile = new ShapeProfile(rotated);
|
||||||
var material = ClipperBridge.ToRegion(profile, 0.001, circumscribe: true);
|
var material = ClipperBridge.ToRegion(profile, NestTolerances.ValidationOutline, circumscribe: true);
|
||||||
// Circular/symmetric parts should not multiply identical NFP work. Compare
|
// Circular/symmetric parts should not multiply identical NFP work. Compare
|
||||||
// normalized closed contours, including holes, independent of start vertex.
|
// normalized closed contours, including holes, independent of start vertex.
|
||||||
var key = string.Join("|", material.Select(Canonical).Order(StringComparer.Ordinal));
|
var key = string.Join("|", material.Select(Canonical).Order(StringComparer.Ordinal));
|
||||||
if (!keys.Add(key)) continue;
|
if (!keys.Add(key)) continue;
|
||||||
var outline = ClipperBridge.Flatten(profile.Perimeter, 0.001, circumscribe: true);
|
var outline = ClipperBridge.Flatten(profile.Perimeter, NestTolerances.ValidationOutline, circumscribe: true);
|
||||||
var hull = ConvexHull.Compute(outline.Vertices);
|
var hull = ConvexHull.Compute(outline.Vertices);
|
||||||
var convex = M.Abs(hull.Area() - outline.Area()) < 1e-7 * M.Max(1, hull.Area());
|
var convex = M.Abs(hull.Area() - outline.Area()) < 1e-7 * M.Max(1, hull.Area());
|
||||||
// Concave Minkowski sums have quadratic input size. Only the contact
|
// Concave Minkowski sums have quadratic input size. Only the contact
|
||||||
@@ -118,7 +104,7 @@ internal static class GeometryPreparation
|
|||||||
OriginX = x, OriginY = y, Width = w, Height = h,
|
OriginX = x, OriginY = y, Width = w, Height = h,
|
||||||
Curved = rotated.Any(e => e is Arc or Circle), Material = material,
|
Curved = rotated.Any(e => e is Arc or Circle), Material = material,
|
||||||
Outline = outline, ContactOutline = contactOutline, ContactError = contactError,
|
Outline = outline, ContactOutline = contactOutline, ContactError = contactError,
|
||||||
Hull = hull, Convex = convex, ValidationProfile = validationProfile });
|
Hull = hull, Convex = convex, Geometry = geometry });
|
||||||
}
|
}
|
||||||
var ordered = variants.OrderBy(v => M.Round(v.Width * v.Height, 7)).ToArray();
|
var ordered = variants.OrderBy(v => M.Round(v.Width * v.Height, 7)).ToArray();
|
||||||
if (part.Rotation.Kind == RotationPolicyKind.Automatic && ordered.Length > 8)
|
if (part.Rotation.Kind == RotationPolicyKind.Automatic && ordered.Length > 8)
|
||||||
@@ -130,8 +116,7 @@ internal static class GeometryPreparation
|
|||||||
// discard every fitting orientation merely because its envelope is larger.
|
// discard every fitting orientation merely because its envelope is larger.
|
||||||
foreach (var stock in job.Plates)
|
foreach (var stock in job.Plates)
|
||||||
{
|
{
|
||||||
bool Fits(ShapeVariant v) => v.Width <= stock.Size.Length - stock.EdgeSpacing.Left - stock.EdgeSpacing.Right + 1e-9 &&
|
bool Fits(ShapeVariant v) => stock.Fits(v.Width, v.Height);
|
||||||
v.Height <= stock.Size.Width - stock.EdgeSpacing.Top - stock.EdgeSpacing.Bottom + 1e-9;
|
|
||||||
if (!shortlist.Any(Fits)) shortlist.AddRange(all.Where(Fits).Take(4));
|
if (!shortlist.Any(Fits)) shortlist.AddRange(all.Where(Fits).Take(4));
|
||||||
}
|
}
|
||||||
ordered = shortlist.DistinctBy(v => v.Id).ToArray();
|
ordered = shortlist.DistinctBy(v => v.Id).ToArray();
|
||||||
@@ -151,7 +136,7 @@ internal static class GeometryPreparation
|
|||||||
|
|
||||||
private static IEnumerable<double> Angles(RotationPolicy policy, ShapeProfile profile)
|
private static IEnumerable<double> Angles(RotationPolicy policy, ShapeProfile profile)
|
||||||
{
|
{
|
||||||
var values = new List<double>();
|
var values = new List<double>(RotationCandidates.ForShape(policy, profile.Perimeter));
|
||||||
if (policy.Kind == RotationPolicyKind.Automatic)
|
if (policy.Kind == RotationPolicyKind.Automatic)
|
||||||
{
|
{
|
||||||
// All half-turns matter for asymmetric parts, unlike envelope-only packing.
|
// All half-turns matter for asymmetric parts, unlike envelope-only packing.
|
||||||
@@ -163,19 +148,7 @@ internal static class GeometryPreparation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
values = policy.EnumerateAngles().ToList();
|
||||||
var last = policy.Kind == RotationPolicyKind.Fixed ? 0 : M.Floor((policy.End - policy.Start) / policy.Step);
|
|
||||||
if (!double.IsFinite(last)) last = 720;
|
|
||||||
var samples = (int)M.Min(720, last);
|
|
||||||
for (var i = 0; i <= samples; i++)
|
|
||||||
{
|
|
||||||
var k = samples == 0 ? 0 : M.Floor(last * ((double)i / samples));
|
|
||||||
var angle = policy.Start + k * policy.Step;
|
|
||||||
if (!double.IsFinite(angle) || !policy.Allows(angle)) continue;
|
|
||||||
values.Add(angle);
|
|
||||||
if (policy.Allow180Equivalent) values.Add(angle + M.PI);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var seen = new HashSet<long>();
|
var seen = new HashSet<long>();
|
||||||
foreach (var value in values)
|
foreach (var value in values)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,15 +11,14 @@ sheet's usable translation rectangle exposes contact positions where another par
|
|||||||
This permits overlapping bounding rectangles, complementary triangle pairs, staggered circles,
|
This permits overlapping bounding rectangles, complementary triangle pairs, staggered circles,
|
||||||
concave interlocking, and insertion into straight-edged and curved holes.
|
concave interlocking, and insertion into straight-edged and curved holes.
|
||||||
|
|
||||||
1. Validate immutable job input. Reconstruct owned analytic entities with `DrawingJobMapper`
|
1. Validate immutable job input. Read owned analytic material with `JobPartGeometry.Read`. Closed contours define material; internal open marks do not become
|
||||||
and `ConvertProgram`. Closed contours define material; internal open marks do not become
|
|
||||||
holes. Preserve the snapshot's origin when converting normalized placements back to poses.
|
holes. Preserve the snapshot's origin when converting normalized placements back to poses.
|
||||||
2. Prepare rotated outlines and material regions with holes, using conservative curve flattening.
|
2. Prepare rotated outlines and material regions with holes, using conservative curve flattening.
|
||||||
Automatic angles combine 15-degree samples over a full turn with orientations aligned to the
|
Automatic angles extend `RotationCandidates.ForShape` with 15-degree samples over a full turn with orientations aligned to the
|
||||||
longest straight edges. Symmetric duplicates are removed. Prefer up to 16 orientations whose
|
longest straight edges. Symmetric duplicates are removed. Prefer up to 16 orientations whose
|
||||||
envelope area is within 8% of the minimum; retain additional orientations when needed to fit
|
envelope area is within 8% of the minimum; retain additional orientations when needed to fit
|
||||||
a candidate stock. Fixed and bounded rotation policies remain enforced. Bounded sweeps use
|
a candidate stock. Fixed and bounded rotation policies remain enforced. Bounded sweeps use
|
||||||
up to 721 integer step indices, including permitted half-turn equivalents.
|
up to 720 base samples through `RotationPolicy.EnumerateAngles`, including permitted half-turn equivalents.
|
||||||
3. Process high-priority parts first (a lower `Priority` number ranks higher, as in the host),
|
3. Process high-priority parts first (a lower `Priority` number ranks higher, as in the host),
|
||||||
then parts fitting fewer available stock types, then larger envelopes. Larger frames precede inserts. Search every retained orientation for each instance.
|
then parts fitting fewer available stock types, then larger envelopes. Larger frames precede inserts. Search every retained orientation for each instance.
|
||||||
4. Build cached Minkowski/no-fit regions. Convex pairs use Core's linear convex NFP primitive;
|
4. Build cached Minkowski/no-fit regions. Convex pairs use Core's linear convex NFP primitive;
|
||||||
@@ -40,7 +39,7 @@ concave interlocking, and insertion into straight-edged and curved holes.
|
|||||||
area lower bound prunes plans only once a complete cheaper plan exists. After 24 evaluated
|
area lower bound prunes plans only once a complete cheaper plan exists. After 24 evaluated
|
||||||
trials only one directional objective is used; after 64, beam width reduces to two.
|
trials only one directional objective is used; after 64, beam width reduces to two.
|
||||||
Work counts, not elapsed time or randomness, control search breadth.
|
Work counts, not elapsed time or randomness, control search breadth.
|
||||||
7. Select a complete plan with lowest purchased area, breaking equal-cost ties by sheet count.
|
7. Select a complete plan with lowest `NestJobCost.NetSheetArea` including salvage, breaking equal-cost ties by sheet count.
|
||||||
If no complete plan is found, maximize fulfilled counts by priority, then minimize cost.
|
If no complete plan is found, maximize fulfilled counts by priority, then minimize cost.
|
||||||
Emit committed-sheet progress, contiguous per-part instance indices, inventory, fulfillment
|
Emit committed-sheet progress, contiguous per-part instance indices, inventory, fulfillment
|
||||||
and the contract's job-level stop reason. Cancellation throws without returning a partial job.
|
and the contract's job-level stop reason. Cancellation throws without returning a partial job.
|
||||||
@@ -53,10 +52,11 @@ corrects curved-hole validation; the placement algorithm remains entirely in Gpt
|
|||||||
## Precision and safety
|
## Precision and safety
|
||||||
|
|
||||||
Analytic rotated bounds govern sheet containment. Material curves are conservatively flattened
|
Analytic rotated bounds govern sheet containment. Material curves are conservatively flattened
|
||||||
at 0.001 job units. Positive configuration-space spacing includes 0.0003 extra units for non-rectangular
|
at `NestTolerances.ValidationOutline`. Contact generation and material halos both use
|
||||||
straight outlines; curved outlines reserve 0.003 extra units even at zero spacing, accounting for offset/chord error and the
|
`SafeClearanceMargin` so proposed contacts pass the same clearance gate. Axis-aligned
|
||||||
benchmark validator's four-decimal grid. Axis-aligned rectangle contacts preserve exact requested
|
rectangle contacts preserve exact requested spacing. `NestLayoutCheck.Clears` replaces
|
||||||
spacing. Actual material intersection checks backstop candidate construction. Both straight-edged
|
the copied validator construction and collision sequence, with world-pose caching.
|
||||||
|
Actual material intersection checks backstop candidate construction. Both straight-edged
|
||||||
and curved holes are available for insertion. The shared collision routine now subtracts hole
|
and curved holes are available for insertion. The shared collision routine now subtracts hole
|
||||||
triangles into disjoint fragments with consistent half-space clipping, resolving the reproduced
|
triangles into disjoint fragments with consistent half-space clipping, resolving the reproduced
|
||||||
curved-hole false positive. See the benchmark report for regression results.
|
curved-hole false positive. See the benchmark report for regression results.
|
||||||
@@ -123,3 +123,15 @@ non-cardinal rotations, hole insertion, automatic diagonal-only stock fits, all
|
|||||||
curves, incremental geometry, determinism, inventory, cancellation and stock-plan regressions.
|
curves, incremental geometry, determinism, inventory, cancellation and stock-plan regressions.
|
||||||
All 34 synthetic/generated benchmark cases and all four repository-DXF cases were valid and complete.
|
All 34 synthetic/generated benchmark cases and all four repository-DXF cases were valid and complete.
|
||||||
Existing nullable warnings originate from the benchmark validator linked into the test project.
|
Existing nullable warnings originate from the benchmark validator linked into the test project.
|
||||||
|
|
||||||
|
## Shared services migration
|
||||||
|
|
||||||
|
Stock bounds and fit checks use the host stock primitives; committed results and progress
|
||||||
|
use `NestJobResultBuilder`. The test project shares `Engine.Testing` and no longer links
|
||||||
|
benchmark source. The synthetic console also validates through `NestLayoutCheck` and
|
||||||
|
reports `NestJobCost.Evaluate`. Automatic 15-degree/edge sampling, beam search, contact
|
||||||
|
placement, exact rectangle handling, and full-material symmetry signatures for holed
|
||||||
|
parts remain engine-owned. The host symmetry helper compares only perimeters.
|
||||||
|
|
||||||
|
The five salvage benchmarks stayed valid and complete. Cost fell from 5574.07 to
|
||||||
|
5470.07 overall (no job worsened at report precision); see [PR 5 results](../MIGRATION-PR5.md).
|
||||||
|
|||||||
@@ -2,6 +2,5 @@
|
|||||||
<PropertyGroup><OutputType>Exe</OutputType><Nullable>disable</Nullable></PropertyGroup>
|
<PropertyGroup><OutputType>Exe</OutputType><Nullable>disable</Nullable></PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="../OpenNest.Engine.Gpt6Astra.csproj" />
|
<ProjectReference Include="../OpenNest.Engine.Gpt6Astra.csproj" />
|
||||||
<Compile Include="$(OpenNestRoot)OpenNest.Benchmark/NestValidator.cs" Link="NestValidator.cs" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ using OpenNest.CNC;
|
|||||||
using OpenNest.Engine.Jobs;
|
using OpenNest.Engine.Jobs;
|
||||||
using OpenNest.Engine.Jobs.Adapters;
|
using OpenNest.Engine.Jobs.Adapters;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Benchmark;
|
|
||||||
using CncProgram = OpenNest.CNC.Program;
|
using CncProgram = OpenNest.CNC.Program;
|
||||||
|
|
||||||
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
|
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
|
||||||
@@ -30,10 +29,8 @@ if (args.Contains("--diagnose-ring"))
|
|||||||
new NestJobPlacement("insert", 0, x + offset, y, 0) }) },
|
new NestJobPlacement("insert", 0, x + offset, y, 0) }) },
|
||||||
new[] { new PartFulfillment("ring", 1, 1, 0), new PartFulfillment("insert", 1, 1, 0) },
|
new[] { new PartFulfillment("ring", 1, 1, 0), new PartFulfillment("insert", 1, 1, 0) },
|
||||||
new[] { new StockUsage("s", 1, 0) });
|
new[] { new StockUsage("s", 1, 0) });
|
||||||
var materialized = NestResultMaterializer.Materialize(j, result);
|
var violations = NestLayoutCheck.Violations(j, result);
|
||||||
var check = NestValidator.Validate(materialized.Nest.Plates.Select(p => (p, p.Parts.ToList())).ToList(),
|
Console.WriteLine($"q={quadrant} offset={offset} valid={violations.Count == 0}: {string.Join(';', violations)}");
|
||||||
j.Parts.ToDictionary(p => materialized.DrawingsByPartId[p.Id], p => (p.Id, p.Quantity)));
|
|
||||||
Console.WriteLine($"q={quadrant} offset={offset} valid={check.Valid}: {string.Join(';', check.Violations)}");
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -73,7 +70,7 @@ Add("tail", new[] { Part("rect", Rect(6, 4), 17) }, new[] {
|
|||||||
Add("plate-cap", new[] { Part("r", Rect(5, 5), 10) }, new[] {
|
Add("plate-cap", new[] { Part("r", Rect(5, 5), 10) }, new[] {
|
||||||
new NestPlateStock("small", new Size(10, 10)), new NestPlateStock("large", new Size(20, 20)) }, new NestJobOptions(maxPlates: 1));
|
new NestPlateStock("small", new Size(10, 10)), new NestPlateStock("large", new Size(20, 20)) }, new NestJobOptions(maxPlates: 1));
|
||||||
cases.AddRange(OpenNest.Engine.Gpt6Astra.Benchmarks.GeneratedCases.Create());
|
cases.AddRange(OpenNest.Engine.Gpt6Astra.Benchmarks.GeneratedCases.Create());
|
||||||
Console.WriteLine("case,valid,placed,requested,sheets,area,milliseconds");
|
Console.WriteLine("case,valid,placed,requested,sheets,cost,milliseconds");
|
||||||
foreach (var (name, job) in cases)
|
foreach (var (name, job) in cases)
|
||||||
{
|
{
|
||||||
if (args.Length > 1 && !name.Contains(args[1], StringComparison.OrdinalIgnoreCase)) continue;
|
if (args.Length > 1 && !name.Contains(args[1], StringComparison.OrdinalIgnoreCase)) continue;
|
||||||
@@ -82,13 +79,10 @@ foreach (var (name, job) in cases)
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = engine.Solve(job, token: cts.Token); sw.Stop();
|
var result = engine.Solve(job, token: cts.Token); sw.Stop();
|
||||||
var nest = NestResultMaterializer.Materialize(job, result);
|
var violations = NestLayoutCheck.Violations(job, result);
|
||||||
var validation = NestValidator.Validate(nest.Nest.Plates.Select(p => (p, p.Parts.ToList())).ToList(),
|
if (violations.Count != 0) Environment.ExitCode = 1;
|
||||||
job.Parts.ToDictionary(p => nest.DrawingsByPartId[p.Id], p => (p.Id, p.Quantity)));
|
Console.WriteLine($"{name},{violations.Count == 0},{result.Fulfillment.Sum(f => f.Placed)},{job.Parts.Sum(p => p.Quantity)},{result.Plates.Count},{NestJobCost.Evaluate(job, result)},{sw.ElapsedMilliseconds}");
|
||||||
NestValidator.ValidateAgainstJob(job, result, job.Parts.ToDictionary(p => p.Id, p => p.Id), validation);
|
foreach (var violation in violations.Take(4)) Console.Error.WriteLine($"{name}: {violation}");
|
||||||
if (!validation.Valid) Environment.ExitCode = 1;
|
|
||||||
Console.WriteLine($"{name},{validation.Valid},{result.Fulfillment.Sum(f => f.Placed)},{job.Parts.Sum(p => p.Quantity)},{result.Plates.Count},{result.Plates.Sum(p => p.Stock.Size.Length * p.Stock.Size.Width)},{sw.ElapsedMilliseconds}");
|
|
||||||
foreach (var violation in validation.Violations.Take(4)) Console.Error.WriteLine($"{name}: {violation}");
|
|
||||||
}
|
}
|
||||||
catch (Exception ex) { Environment.ExitCode = 1; Console.WriteLine($"{name},ERROR,,,,,{sw.ElapsedMilliseconds}"); Console.Error.WriteLine(ex); }
|
catch (Exception ex) { Environment.ExitCode = 1; Console.WriteLine($"{name},ERROR,,,,,{sw.ElapsedMilliseconds}"); Console.Error.WriteLine(ex); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ claim of performance on an unseen competition dataset.
|
|||||||
## Synthetic cases
|
## Synthetic cases
|
||||||
|
|
||||||
Both versions were run on exactly the same programmatically generated geometry and stock.
|
Both versions were run on exactly the same programmatically generated geometry and stock.
|
||||||
The driver validates materialized output with `OpenNest.Benchmark.NestValidator`, including
|
The driver validates materialized output with `NestLayoutCheck.Violations`, including
|
||||||
quantity, stock settings, rotation, material overlap and spacing. Timings cover `Solve` only,
|
quantity, stock settings, rotation, material overlap and spacing. Timings cover `Solve` only,
|
||||||
exclude external validation, and are single-run observations rather than stable distributions.
|
exclude external validation, and are single-run observations rather than stable distributions.
|
||||||
Every contact result is valid and complete. The baseline is valid but incomplete on `plate-cap`.
|
Every contact result is valid and complete. The baseline is valid but incomplete on `plate-cap`.
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
using OpenNest.Engine.Testing;
|
||||||
|
using static OpenNest.Engine.Testing.JobBuilder;
|
||||||
|
using static OpenNest.Engine.Testing.Shapes;
|
||||||
using OpenNest.CNC;
|
using OpenNest.CNC;
|
||||||
using OpenNest.Converters;
|
using OpenNest.Converters;
|
||||||
using OpenNest.Engine.Jobs;
|
using OpenNest.Engine.Jobs;
|
||||||
@@ -23,7 +26,7 @@ public class Gpt6AstraNestingEngineTests
|
|||||||
var before = job.Parts.Select(p => p.Geometry.Motions.ToArray()).ToArray();
|
var before = job.Parts.Select(p => p.Geometry.Motions.ToArray()).ToArray();
|
||||||
var result = new Gpt6AstraNestingEngine().Solve(job);
|
var result = new Gpt6AstraNestingEngine().Solve(job);
|
||||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||||
Validate(job, result);
|
LayoutAssert.Valid(job, result);
|
||||||
for (var i = 0; i < job.Parts.Count; i++) Assert.Equal(before[i], job.Parts[i].Geometry.Motions);
|
for (var i = 0; i < job.Parts.Count; i++) Assert.Equal(before[i], job.Parts[i].Geometry.Motions);
|
||||||
var again = new Gpt6AstraNestingEngine().Solve(job);
|
var again = new Gpt6AstraNestingEngine().Solve(job);
|
||||||
Assert.Equal(result.Plates.SelectMany(p => p.Placements), again.Plates.SelectMany(p => p.Placements));
|
Assert.Equal(result.Plates.SelectMany(p => p.Placements), again.Plates.SelectMany(p => p.Placements));
|
||||||
@@ -37,7 +40,7 @@ public class Gpt6AstraNestingEngineTests
|
|||||||
new NestPlateStock("large", new Size(20, 20)), new NestPlateStock("small", new Size(2, 2)) });
|
new NestPlateStock("large", new Size(20, 20)), new NestPlateStock("small", new Size(2, 2)) });
|
||||||
var result = new Gpt6AstraNestingEngine().Solve(job);
|
var result = new Gpt6AstraNestingEngine().Solve(job);
|
||||||
Assert.Equal("small", Assert.Single(result.Plates).StockId);
|
Assert.Equal("small", Assert.Single(result.Plates).StockId);
|
||||||
Validate(job, result);
|
LayoutAssert.Valid(job, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@@ -50,7 +53,7 @@ public class Gpt6AstraNestingEngineTests
|
|||||||
var result = new Gpt6AstraNestingEngine().Solve(job);
|
var result = new Gpt6AstraNestingEngine().Solve(job);
|
||||||
Assert.Equal(reason, result.StopReason);
|
Assert.Equal(reason, result.StopReason);
|
||||||
Assert.Equal(2, Assert.Single(result.Fulfillment).Unplaced);
|
Assert.Equal(2, Assert.Single(result.Fulfillment).Unplaced);
|
||||||
Validate(job, result);
|
LayoutAssert.Valid(job, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -83,7 +86,7 @@ public class Gpt6AstraNestingEngineTests
|
|||||||
}, new[] { new NestPlateStock("s", new Size(20, 20), partSpacing: 0.4) });
|
}, new[] { new NestPlateStock("s", new Size(20, 20), partSpacing: 0.4) });
|
||||||
var result = new Gpt6AstraNestingEngine().Solve(job);
|
var result = new Gpt6AstraNestingEngine().Solve(job);
|
||||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||||
Validate(job, result);
|
LayoutAssert.Valid(job, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -130,7 +133,7 @@ public class Gpt6AstraNestingEngineTests
|
|||||||
}, new[] { new NestPlateStock("s", new Size(20, 30), partSpacing: 0.2) });
|
}, new[] { new NestPlateStock("s", new Size(20, 30), partSpacing: 0.2) });
|
||||||
var result = new Gpt6AstraNestingEngine().Solve(job);
|
var result = new Gpt6AstraNestingEngine().Solve(job);
|
||||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||||
Validate(job, result);
|
LayoutAssert.Valid(job, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -148,7 +151,7 @@ public class Gpt6AstraNestingEngineTests
|
|||||||
});
|
});
|
||||||
var result = new Gpt6AstraNestingEngine().Solve(job);
|
var result = new Gpt6AstraNestingEngine().Solve(job);
|
||||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||||
Validate(job, result);
|
LayoutAssert.Valid(job, result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +165,7 @@ public class Gpt6AstraNestingEngineTests
|
|||||||
var result = new Gpt6AstraNestingEngine().Solve(job);
|
var result = new Gpt6AstraNestingEngine().Solve(job);
|
||||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||||
Assert.Single(result.Plates);
|
Assert.Single(result.Plates);
|
||||||
Validate(job, result);
|
LayoutAssert.Valid(job, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -176,7 +179,7 @@ public class Gpt6AstraNestingEngineTests
|
|||||||
var result = new Gpt6AstraNestingEngine().Solve(job);
|
var result = new Gpt6AstraNestingEngine().Solve(job);
|
||||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||||
Assert.Single(result.Plates);
|
Assert.Single(result.Plates);
|
||||||
Validate(job, result);
|
LayoutAssert.Valid(job, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -188,7 +191,7 @@ public class Gpt6AstraNestingEngineTests
|
|||||||
var result = new Gpt6AstraNestingEngine().Solve(job);
|
var result = new Gpt6AstraNestingEngine().Solve(job);
|
||||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||||
Assert.Equal("large", Assert.Single(result.Plates).StockId);
|
Assert.Equal("large", Assert.Single(result.Plates).StockId);
|
||||||
Validate(job, result);
|
LayoutAssert.Valid(job, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -198,7 +201,7 @@ public class Gpt6AstraNestingEngineTests
|
|||||||
new[] { new NestPlateStock("s", new Size(8, 8), 1) });
|
new[] { new NestPlateStock("s", new Size(8, 8), 1) });
|
||||||
var result = new Gpt6AstraNestingEngine().Solve(job);
|
var result = new Gpt6AstraNestingEngine().Solve(job);
|
||||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||||
Validate(job, result);
|
LayoutAssert.Valid(job, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -210,7 +213,7 @@ public class Gpt6AstraNestingEngineTests
|
|||||||
new[] { new NestPlateStock("s", new Size(24, 48), partSpacing: 0.15) });
|
new[] { new NestPlateStock("s", new Size(24, 48), partSpacing: 0.15) });
|
||||||
var result = new Gpt6AstraNestingEngine().Solve(job);
|
var result = new Gpt6AstraNestingEngine().Solve(job);
|
||||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||||
Validate(job, result);
|
LayoutAssert.Valid(job, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -222,7 +225,7 @@ public class Gpt6AstraNestingEngineTests
|
|||||||
var result = new Gpt6AstraNestingEngine().Solve(job);
|
var result = new Gpt6AstraNestingEngine().Solve(job);
|
||||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||||
Assert.True(result.Plates.Sum(p => p.Stock.Size.Length * p.Stock.Size.Width) <= 3600);
|
Assert.True(result.Plates.Sum(p => p.Stock.Size.Length * p.Stock.Size.Width) <= 3600);
|
||||||
Validate(job, result);
|
LayoutAssert.Valid(job, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -232,7 +235,7 @@ public class Gpt6AstraNestingEngineTests
|
|||||||
new[] { new NestPlateStock("s", new Size(4.25, 4.25), 1, 0.25) });
|
new[] { new NestPlateStock("s", new Size(4.25, 4.25), 1, 0.25) });
|
||||||
var result = new Gpt6AstraNestingEngine().Solve(job);
|
var result = new Gpt6AstraNestingEngine().Solve(job);
|
||||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||||
Validate(job, result);
|
LayoutAssert.Valid(job, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@@ -250,7 +253,7 @@ public class Gpt6AstraNestingEngineTests
|
|||||||
}, new[] { new NestPlateStock("s", new Size(20, 25), partSpacing: spacing) });
|
}, new[] { new NestPlateStock("s", new Size(20, 25), partSpacing: spacing) });
|
||||||
var result = new Gpt6AstraNestingEngine().Solve(job);
|
var result = new Gpt6AstraNestingEngine().Solve(job);
|
||||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||||
Validate(job, result);
|
LayoutAssert.Valid(job, result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,7 +274,7 @@ public class Gpt6AstraNestingEngineTests
|
|||||||
var result = new Gpt6AstraNestingEngine().Solve(job);
|
var result = new Gpt6AstraNestingEngine().Solve(job);
|
||||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||||
Assert.Equal(2, Assert.Single(result.Plates).Placements.Count);
|
Assert.Equal(2, Assert.Single(result.Plates).Placements.Count);
|
||||||
Validate(job, result);
|
LayoutAssert.Valid(job, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -287,7 +290,7 @@ public class Gpt6AstraNestingEngineTests
|
|||||||
var result = new Gpt6AstraNestingEngine().Solve(job, token: cancellation.Token);
|
var result = new Gpt6AstraNestingEngine().Solve(job, token: cancellation.Token);
|
||||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||||
Assert.Single(result.Plates);
|
Assert.Single(result.Plates);
|
||||||
Validate(job, result);
|
LayoutAssert.Valid(job, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -300,76 +303,19 @@ public class Gpt6AstraNestingEngineTests
|
|||||||
rotation: RotationPolicy.Fixed(0)) },
|
rotation: RotationPolicy.Fixed(0)) },
|
||||||
new[] { new NestPlateStock("s", new Size(10.4, 20.6), 1, partSpacing: 0.2) });
|
new[] { new NestPlateStock("s", new Size(10.4, 20.6), 1, partSpacing: 0.2) });
|
||||||
var result = new Gpt6AstraNestingEngine().Solve(job);
|
var result = new Gpt6AstraNestingEngine().Solve(job);
|
||||||
Validate(job, result);
|
LayoutAssert.Valid(job, result);
|
||||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||||
Assert.Equal(2, Assert.Single(result.Plates).Placements.Count);
|
Assert.Equal(2, Assert.Single(result.Plates).Placements.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Program NotchedPartWithEtch()
|
|
||||||
{
|
|
||||||
var p = new Program();
|
|
||||||
p.MoveTo(0, 0); p.LineTo(10, 0); p.LineTo(10, 4); p.LineTo(8, 4); p.LineTo(8, 6);
|
|
||||||
p.LineTo(10, 6); p.LineTo(10, 10); p.LineTo(0, 10); p.LineTo(0, 0);
|
|
||||||
p.MoveTo(7.5, 5);
|
|
||||||
p.Codes.Add(new LinearMove(9, 5) { Layer = LayerType.Scribe });
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class CallbackProgress(Action<NestJobProgress> callback) : IProgress<NestJobProgress>
|
private sealed class CallbackProgress(Action<NestJobProgress> callback) : IProgress<NestJobProgress>
|
||||||
{ public void Report(NestJobProgress value) => callback(value); }
|
{ public void Report(NestJobProgress value) => callback(value); }
|
||||||
|
|
||||||
private static NestJobPart Rectangle(string id, double w, double h, int count,
|
|
||||||
RotationPolicy? rotation = null, double x = 0, double y = 0)
|
|
||||||
{
|
|
||||||
var p = new Program();
|
|
||||||
p.MoveTo(x, y); p.LineTo(x + w, y); p.LineTo(x + w, y + h);
|
|
||||||
p.LineTo(x, y + h); p.LineTo(x, y);
|
|
||||||
return new(id, PartGeometrySnapshot.FromProgram(p), count, rotation: rotation ?? RotationPolicy.Fixed(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void Validate(NestJob job, NestJobResult result)
|
|
||||||
{
|
|
||||||
var materialized = NestResultMaterializer.Materialize(job, result);
|
|
||||||
var requirements = job.Parts.ToDictionary(p => materialized.DrawingsByPartId[p.Id],
|
|
||||||
p => (Name: p.Id, Quantity: p.Quantity));
|
|
||||||
var validation = OpenNest.Benchmark.NestValidator.Validate(
|
|
||||||
materialized.Nest.Plates.Select(p => (p, p.Parts.ToList())).ToList(), requirements);
|
|
||||||
OpenNest.Benchmark.NestValidator.ValidateAgainstJob(job, result,
|
|
||||||
job.Parts.ToDictionary(p => p.Id, p => p.Id), validation);
|
|
||||||
Assert.True(validation.Valid, string.Join("; ", validation.Violations));
|
|
||||||
foreach (var sheet in result.Plates)
|
|
||||||
{
|
|
||||||
var s = sheet.Stock;
|
|
||||||
var left = (s.Quadrant is 1 or 4 ? 0 : -s.Size.Length) + s.EdgeSpacing.Left;
|
|
||||||
var bottom = (s.Quadrant is 1 or 2 ? 0 : -s.Size.Width) + s.EdgeSpacing.Bottom;
|
|
||||||
var right = left + s.Size.Length - s.EdgeSpacing.Left - s.EdgeSpacing.Right;
|
|
||||||
var top = bottom + s.Size.Width - s.EdgeSpacing.Bottom - s.EdgeSpacing.Top;
|
|
||||||
foreach (var pose in sheet.Placements)
|
|
||||||
{
|
|
||||||
var part = job.Parts.Single(p => p.Id == pose.PartId);
|
|
||||||
Assert.True(part.Rotation.Allows(pose.Rotation));
|
|
||||||
var geometry = ConvertProgram.ToGeometry(DrawingJobMapper.ToProgram(part.Geometry))
|
|
||||||
.Where(e => SpecialLayers.IsMaterial(e.Layer)).ToArray();
|
|
||||||
foreach (var entity in geometry) { entity.Rotate(pose.Rotation); entity.Offset(pose.X, pose.Y); }
|
|
||||||
var b = (L: geometry.Min(e => e.Left), B: geometry.Min(e => e.Bottom),
|
|
||||||
R: geometry.Max(e => e.Right), T: geometry.Max(e => e.Top));
|
|
||||||
Assert.True(b.L >= left - 1e-7 && b.B >= bottom - 1e-7 && b.R <= right + 1e-7 && b.T <= top + 1e-7);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foreach (var part in job.Parts)
|
|
||||||
{
|
|
||||||
var placed = result.Plates.SelectMany(s => s.Placements).Where(p => p.PartId == part.Id).ToArray();
|
|
||||||
Assert.Equal(Enumerable.Range(0, placed.Length), placed.Select(p => p.InstanceIndex).Order());
|
|
||||||
var fulfillment = result.Fulfillment.Single(f => f.PartId == part.Id);
|
|
||||||
Assert.Equal(placed.Length, fulfillment.Placed);
|
|
||||||
Assert.Equal(part.Quantity, fulfillment.Placed + fulfillment.Unplaced);
|
|
||||||
}
|
|
||||||
foreach (var usage in result.StockUsage)
|
|
||||||
{
|
|
||||||
var stock = job.Plates.Single(s => s.Id == usage.StockId);
|
|
||||||
Assert.Equal(result.Plates.Count(s => s.StockId == stock.Id), usage.Used);
|
|
||||||
Assert.Equal(stock.Quantity - usage.Used, usage.Remaining);
|
|
||||||
Assert.True(usage.Remaining is null or >= 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class Gpt6AstraContractTests : EngineContractTests<Gpt6AstraNestingEngine> { }
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Using Include="Xunit" />
|
<Using Include="Xunit" />
|
||||||
<Compile Include="$(OpenNestRoot)OpenNest.Benchmark/NestValidator.cs" Link="NestValidator.cs" />
|
<ProjectReference Include="../../Engine.Testing/OpenNest.Engine.Testing.csproj" />
|
||||||
<ProjectReference Include="../OpenNest.Engine.Gpt6Astra.csproj" />
|
<ProjectReference Include="../OpenNest.Engine.Gpt6Astra.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
Reference in New Issue
Block a user