fix(opus55): honor part priority and use host scoring and tolerances
Opus55 ignored NestJobPart.Priority, so the shared contract test (lower number wins scarce stock) failed; lower-number priority now precedes its placement score. SheetEconomics is replaced by the host's NestJobCost so it optimizes exactly what the benchmark scores, and its footprint margin comes from NestTolerances.SafeClearanceMargin plus four Clipper grid units - the same 0.003 total as before, which keeps its contact points. Synthetic benchmark (5 jobs, salvage 0.5): all valid, cost unchanged at 5452.79, time 611 -> 456 ms. Co-Authored-By: Codex <noreply@openai.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,17 +19,6 @@ namespace OpenNest.Engine.Opus55;
|
||||
/// </summary>
|
||||
public sealed class Opus55NestingEngine : INestingEngine
|
||||
{
|
||||
/// <summary>
|
||||
/// Extra clearance beyond the stock's part spacing, in job units. Footprints already contain
|
||||
/// the true outline grown by half the clearance (chord tolerance is added separately), so this
|
||||
/// only has to cover the validator's side: NestValidator flattens each perimeter within
|
||||
/// 0.001 of the true arc (OutlineTolerance), so two true arcs can read up to 0.002 closer
|
||||
/// than they are. The remaining 0.001 absorbs the 1e-4 Clipper grid on both sides (footprint
|
||||
/// offset, Minkowski union, free-region difference, the validator's own offset); the
|
||||
/// validator's round-join chord error (0.0001) only ever makes it more lenient.
|
||||
/// </summary>
|
||||
internal const double ClearanceMargin = 0.003;
|
||||
|
||||
/// <summary>Strategy variants, tried in order: (front direction, area exponent beta).</summary>
|
||||
private static readonly (PackAxis Axis, double Beta)[] Variants =
|
||||
{
|
||||
@@ -54,6 +43,7 @@ public sealed class Opus55NestingEngine : INestingEngine
|
||||
)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(job);
|
||||
token.ThrowIfCancellationRequested();
|
||||
var types = PartCatalog.Build(job);
|
||||
var solver = new Solver(job, types, progress, token);
|
||||
|
||||
@@ -63,7 +53,7 @@ public sealed class Opus55NestingEngine : INestingEngine
|
||||
{
|
||||
var placeable = job.Plates.Any(stock =>
|
||||
stock.Quantity != 0
|
||||
&& type.Orientations.Any(o => FrontierPacker.Fits(o, FrontierPacker.WorkArea(stock)))
|
||||
&& type.Orientations.Any(o => stock.Fits(o.Width, o.Height))
|
||||
);
|
||||
demand[type.Index] = placeable ? type.Part.Quantity : 0;
|
||||
}
|
||||
@@ -99,7 +89,7 @@ public sealed class Opus55NestingEngine : INestingEngine
|
||||
|
||||
public WorkCounter Work { get; } = new();
|
||||
|
||||
private double Penalty => job.Plates.Count == 0 ? 0 : job.Plates.Max(SheetEconomics.SheetArea);
|
||||
private double Penalty => NestJobCost.UnplacedPartPenalty(job);
|
||||
|
||||
public Plan Plan(int[] demand, PackAxis axis, double beta)
|
||||
{
|
||||
@@ -126,7 +116,7 @@ public sealed class Opus55NestingEngine : INestingEngine
|
||||
var prefix = sheets.Take(sheets.Count - k).ToList();
|
||||
var tail = sheets.Skip(sheets.Count - k).ToList();
|
||||
var tailParts = tail.Sum(s => s.Parts.Count);
|
||||
var tailNet = tail.Sum(s => SheetEconomics.NetArea(job.Options, s));
|
||||
var tailNet = tail.Sum(s => NetArea(job.Options, s));
|
||||
var tailDemand = new int[types.Count];
|
||||
foreach (var part in tail.SelectMany(s => s.Parts))
|
||||
tailDemand[part.Orientation.TypeIndex]++;
|
||||
@@ -158,7 +148,7 @@ public sealed class Opus55NestingEngine : INestingEngine
|
||||
|
||||
private NoFitCache CacheFor(NestPlateStock stock)
|
||||
{
|
||||
var clearance = System.Math.Max(0, stock.PartSpacing) + ClearanceMargin;
|
||||
var clearance = System.Math.Max(0, stock.PartSpacing);
|
||||
if (!caches.TryGetValue(clearance, out var cache))
|
||||
caches[clearance] = cache = new NoFitCache(clearance);
|
||||
return cache;
|
||||
@@ -209,7 +199,7 @@ public sealed class Opus55NestingEngine : INestingEngine
|
||||
var packer = new FrontierPacker(types, CacheFor(stock), stock, axis, beta, Work);
|
||||
var fill = packer.Fill(remaining, token);
|
||||
if (fill.Parts.Count > 0)
|
||||
trials.Add((fill, SheetEconomics.NetArea(job.Options, fill)));
|
||||
trials.Add((fill, NetArea(job.Options, fill)));
|
||||
}
|
||||
|
||||
if (trials.Count == 0)
|
||||
@@ -243,36 +233,23 @@ public sealed class Opus55NestingEngine : INestingEngine
|
||||
|
||||
private sealed record Run(IReadOnlyList<SheetFill> Sheets, double Net, NestJobStopReason Reason);
|
||||
|
||||
private static NestJobResult BuildResult(
|
||||
NestJob job,
|
||||
IReadOnlyList<PartType> types,
|
||||
Plan plan,
|
||||
IProgress<NestJobProgress>? progress
|
||||
)
|
||||
private static NestJobResult BuildResult(NestJob job, IReadOnlyList<PartType> types,
|
||||
Plan plan, IProgress<NestJobProgress>? progress)
|
||||
{
|
||||
var placed = new int[types.Count];
|
||||
var plates = new List<NestJobPlateResult>(plan.Sheets.Count);
|
||||
var committedParts = 0;
|
||||
var builder = new NestJobResultBuilder(job, progress);
|
||||
foreach (var sheet in plan.Sheets)
|
||||
{
|
||||
var placements = sheet.Parts.Select(p =>
|
||||
{
|
||||
var type = types[p.Orientation.TypeIndex];
|
||||
return new NestJobPlacement(type.Part.Id, placed[type.Index]++, p.X, p.Y, p.Orientation.Rotation);
|
||||
});
|
||||
plates.Add(new NestJobPlateResult(plates.Count, sheet.Stock, placements.ToList()));
|
||||
committedParts += sheet.Parts.Count;
|
||||
progress?.Report(new NestJobProgress(NestJobStage.PlateCommitted, sheet.Stock.Id, plates.Count - 1, plates.Count, committedParts));
|
||||
}
|
||||
builder.AddSheet(sheet.Stock, sheet.Parts.Select(p =>
|
||||
(types[p.Orientation.TypeIndex].Part.Id, p.X, p.Y, p.Orientation.Rotation)));
|
||||
return builder.Build(plan.Reason);
|
||||
}
|
||||
|
||||
var fulfillment = types.Select(t => new PartFulfillment(t.Part.Id, t.Part.Quantity, placed[t.Index], t.Part.Quantity - placed[t.Index]));
|
||||
var usage = job.Plates.Select(stock =>
|
||||
{
|
||||
var count = plan.Sheets.Count(s => ReferenceEquals(s.Stock, stock));
|
||||
return new StockUsage(stock.Id, count, stock.Quantity - count);
|
||||
});
|
||||
var status = plan.Unplaced == 0 ? NestJobStatus.Complete : NestJobStatus.Incomplete;
|
||||
return new NestJobResult(status, plan.Reason, plates, fulfillment.ToList(), usage.ToList());
|
||||
private static double NetArea(NestJobOptions options, SheetFill fill)
|
||||
{
|
||||
if (fill.Parts.Count == 0) return fill.Stock.Area;
|
||||
var left = fill.Parts.Min(p => p.Left);
|
||||
var bottom = fill.Parts.Min(p => p.Bottom);
|
||||
return NestJobCost.NetSheetArea(options, fill.Stock, new OpenNest.Geometry.Box(left, bottom,
|
||||
fill.Parts.Max(p => p.Right) - left, fill.Parts.Max(p => p.Top) - bottom));
|
||||
}
|
||||
|
||||
private sealed record Plan(IReadOnlyList<SheetFill> Sheets, double Cost, int Unplaced, NestJobStopReason Reason)
|
||||
|
||||
Reference in New Issue
Block a user