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:
aj
2026-09-25 09:29:27 -04:00
co-authored by Codex Claude Opus 5.5
parent c691381f30
commit d0c6af783b
10 changed files with 120 additions and 217 deletions
@@ -14,8 +14,7 @@ public sealed class Gpt6AstraNestingEngine : INestingEngine
NestJobValidator.Validate(job);
var parts = GeometryPreparation.Prepare(job, token);
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 &&
v.Height <= s.Size.Width - s.EdgeSpacing.Top - s.EdgeSpacing.Bottom + 1e-9)).ToArray()).ToArray();
s.Fits(v.Width, v.Height))).ToArray()).ToArray();
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 frontier = new List<Plan> { initial };
@@ -56,7 +55,9 @@ public sealed class Gpt6AstraNestingEngine : INestingEngine
evaluated++;
}
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++)
{
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 sheets = new List<SheetTrial>(state.Sheets) { trial };
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 (IsComplete(next))
{
@@ -97,29 +98,26 @@ public sealed class Gpt6AstraNestingEngine : INestingEngine
}
}
var selected = complete ?? best;
var counts = new int[parts.Length];
var plates = new List<NestJobPlateResult>();
var builder = new NestJobResultBuilder(job, progress);
foreach (var sheet in selected.Sheets)
{
token.ThrowIfCancellationRequested();
var stock = job.Plates[sheet.StockIndex];
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()));
builder.AddSheet(job.Plates[sheet.StockIndex], Poses(sheet));
}
token.ThrowIfCancellationRequested();
var reason = complete != null ? NestJobStopReason.Completed :
selected.Sheets.Count >= (job.Options.MaxPlates ?? int.MaxValue) ? NestJobStopReason.PlateLimitReached :
!Enumerable.Range(0, job.Plates.Count).Any(s => Available(selected, s)) ? NestJobStopReason.StockExhausted :
NestJobStopReason.NoPlacementFound;
return new(complete != null ? NestJobStatus.Complete : NestJobStatus.Incomplete, reason, plates,
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])));
return builder.Build(reason);
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 IsComplete(Plan p) => parts.Select((part, i) => p.Counts[i] == part.Requirement.Quantity).All(v => v);