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:
@@ -10,8 +10,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
<ProjectReference Include="../../Engine.Testing/OpenNest.Engine.Testing.csproj" />
|
||||
<ProjectReference Include="../OpenNest.Engine.Opus55.csproj" />
|
||||
<!-- The benchmark's NestValidator is the arbiter the engine is scored by. -->
|
||||
<ProjectReference Include="$(OpenNestRoot)OpenNest.Benchmark/OpenNest.Benchmark.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using OpenNest.Engine.Testing;
|
||||
using static OpenNest.Engine.Testing.JobBuilder;
|
||||
using static OpenNest.Engine.Testing.Shapes;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Benchmark;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Engine.Jobs;
|
||||
using OpenNest.Engine.Jobs.Adapters;
|
||||
@@ -18,7 +20,7 @@ public class Opus55NestingEngineTests
|
||||
|
||||
var result = new Opus55NestingEngine().Solve(job);
|
||||
|
||||
AssertValid(job, result);
|
||||
LayoutAssert.Valid(job, result);
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
Assert.Single(result.Plates);
|
||||
Assert.Equal(12, result.Plates[0].Placements.Count);
|
||||
@@ -44,7 +46,7 @@ public class Opus55NestingEngineTests
|
||||
|
||||
var result = new Opus55NestingEngine().Solve(job);
|
||||
|
||||
AssertValid(job, result);
|
||||
LayoutAssert.Valid(job, result);
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
}
|
||||
|
||||
@@ -55,7 +57,7 @@ public class Opus55NestingEngineTests
|
||||
|
||||
var result = new Opus55NestingEngine().Solve(job);
|
||||
|
||||
AssertValid(job, result);
|
||||
LayoutAssert.Valid(job, result);
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
}
|
||||
|
||||
@@ -70,7 +72,7 @@ public class Opus55NestingEngineTests
|
||||
|
||||
var result = new Opus55NestingEngine().Solve(job);
|
||||
|
||||
AssertValid(job, result);
|
||||
LayoutAssert.Valid(job, result);
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
}
|
||||
|
||||
@@ -84,7 +86,7 @@ public class Opus55NestingEngineTests
|
||||
|
||||
var result = new Opus55NestingEngine().Solve(job);
|
||||
|
||||
AssertValid(job, result);
|
||||
LayoutAssert.Valid(job, result);
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
Assert.Equal("small", Assert.Single(result.Plates).StockId);
|
||||
}
|
||||
@@ -96,7 +98,7 @@ public class Opus55NestingEngineTests
|
||||
|
||||
var result = new Opus55NestingEngine().Solve(job);
|
||||
|
||||
AssertValid(job, result);
|
||||
LayoutAssert.Valid(job, result);
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
Assert.True(result.Plates.Count > 1);
|
||||
Assert.Equal(25, result.Plates.Sum(p => p.Placements.Count));
|
||||
@@ -120,7 +122,7 @@ public class Opus55NestingEngineTests
|
||||
|
||||
var result = new Opus55NestingEngine().Solve(job);
|
||||
|
||||
AssertValid(job, result);
|
||||
LayoutAssert.Valid(job, result);
|
||||
foreach (var placement in result.Plates.SelectMany(p => p.Placements))
|
||||
{
|
||||
var policy = placement.PartId == "fixed" ? fixedPolicy : sweep;
|
||||
@@ -138,7 +140,7 @@ public class Opus55NestingEngineTests
|
||||
|
||||
var result = new Opus55NestingEngine().Solve(job);
|
||||
|
||||
AssertValid(job, result);
|
||||
LayoutAssert.Valid(job, result);
|
||||
Assert.Equal(NestJobStatus.Incomplete, result.Status);
|
||||
Assert.Equal(NestJobStopReason.NoPlacementFound, result.StopReason);
|
||||
Assert.Equal(1, result.Fulfillment.Single(f => f.PartId == "huge").Unplaced);
|
||||
@@ -152,7 +154,7 @@ public class Opus55NestingEngineTests
|
||||
|
||||
var result = new Opus55NestingEngine().Solve(job);
|
||||
|
||||
AssertValid(job, result);
|
||||
LayoutAssert.Valid(job, result);
|
||||
Assert.Equal(NestJobStopReason.StockExhausted, result.StopReason);
|
||||
Assert.Equal(2, result.Plates.Count);
|
||||
var usage = Assert.Single(result.StockUsage);
|
||||
@@ -171,7 +173,7 @@ public class Opus55NestingEngineTests
|
||||
|
||||
var result = new Opus55NestingEngine().Solve(job);
|
||||
|
||||
AssertValid(job, result);
|
||||
LayoutAssert.Valid(job, result);
|
||||
Assert.Single(result.Plates);
|
||||
Assert.Equal(NestJobStopReason.PlateLimitReached, result.StopReason);
|
||||
}
|
||||
@@ -188,7 +190,7 @@ public class Opus55NestingEngineTests
|
||||
var first = new Opus55NestingEngine().Solve(Build());
|
||||
var second = new Opus55NestingEngine().Solve(Build());
|
||||
|
||||
Assert.Equal(Describe(first), Describe(second));
|
||||
Assert.Equal(System.Text.Json.JsonSerializer.Serialize(first), System.Text.Json.JsonSerializer.Serialize(second));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -204,7 +206,7 @@ public class Opus55NestingEngineTests
|
||||
|
||||
var result = new Opus55NestingEngine().Solve(job);
|
||||
|
||||
AssertValid(job, result);
|
||||
LayoutAssert.Valid(job, result);
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
Assert.Equal(2, Assert.Single(result.Plates).Placements.Count);
|
||||
}
|
||||
@@ -216,86 +218,6 @@ public class Opus55NestingEngineTests
|
||||
Assert.IsAssignableFrom<INestingEngine>(engine);
|
||||
}
|
||||
|
||||
// ---- helpers -------------------------------------------------------------------------
|
||||
|
||||
private static string Describe(NestJobResult result) =>
|
||||
string.Join(
|
||||
"|",
|
||||
result.Plates.Select(p =>
|
||||
p.StockId + ":" + string.Join(",", p.Placements.Select(x => $"{x.PartId}#{x.InstanceIndex}@{x.X:R},{x.Y:R},{x.Rotation:R}"))
|
||||
)
|
||||
);
|
||||
|
||||
private static void AssertValid(NestJob job, NestJobResult result)
|
||||
{
|
||||
var materialized = NestResultMaterializer.Materialize(job, result);
|
||||
var runs = materialized.Nest.Plates.Select(plate => (Plate: plate, Parts: plate.Parts.ToList())).ToList();
|
||||
var requirements = job.Parts.ToDictionary<NestJobPart, Drawing, (string Name, int Quantity)>(
|
||||
p => materialized.DrawingsByPartId[p.Id],
|
||||
p => (p.Id, p.Quantity),
|
||||
ReferenceEqualityComparer.Instance
|
||||
);
|
||||
var validation = NestValidator.Validate(runs, requirements);
|
||||
NestValidator.ValidateAgainstJob(job, result, job.Parts.ToDictionary(p => p.Id, p => p.Id), validation);
|
||||
Assert.True(validation.Valid, string.Join(Environment.NewLine, validation.Violations));
|
||||
|
||||
foreach (var f in result.Fulfillment)
|
||||
Assert.Equal(f.Requested, f.Placed + f.Unplaced);
|
||||
}
|
||||
|
||||
private static NestJob Job(NestJobPart[] parts, NestPlateStock[] stock, NestJobOptions? options = null) =>
|
||||
new(parts, stock, options);
|
||||
|
||||
private static NestJobPart Part(string id, Program program, int quantity, RotationPolicy? rotation = null) =>
|
||||
new(id, PartGeometrySnapshot.FromProgram(program), quantity, 0, rotation);
|
||||
|
||||
/// <param name="width">Y extent.</param>
|
||||
/// <param name="length">X extent.</param>
|
||||
private static NestPlateStock Stock(
|
||||
string id,
|
||||
double width,
|
||||
double length,
|
||||
double spacing = 0,
|
||||
Spacing edge = default,
|
||||
int quadrant = 1,
|
||||
int? quantity = null
|
||||
) => new(id, new Size(width, length), quantity, spacing, edge, quadrant);
|
||||
|
||||
private static Program Polyline(params (double X, double Y)[] points)
|
||||
{
|
||||
var program = new Program();
|
||||
program.Codes.Add(new RapidMove(points[0].X, points[0].Y));
|
||||
foreach (var (x, y) in points.Skip(1))
|
||||
program.Codes.Add(new LinearMove(x, y));
|
||||
program.Codes.Add(new LinearMove(points[0].X, points[0].Y));
|
||||
return program;
|
||||
}
|
||||
|
||||
private static Program Rectangle(double w, double h) => Polyline((0, 0), (w, 0), (w, h), (0, h));
|
||||
|
||||
private static Program Triangle(double w, double h) => Polyline((0, 0), (w, 0), (w * 0.3, h));
|
||||
|
||||
private static Program LShape(double w, double h, double t) => Polyline((0, 0), (w, 0), (w, t), (t, t), (t, h), (0, h));
|
||||
|
||||
private static Program Disc(double r)
|
||||
{
|
||||
var program = new Program();
|
||||
program.Codes.Add(new RapidMove(r, 0));
|
||||
program.Codes.Add(new ArcMove(-r, 0, 0, 0, RotationType.CCW));
|
||||
program.Codes.Add(new ArcMove(r, 0, 0, 0, RotationType.CCW));
|
||||
return program;
|
||||
}
|
||||
|
||||
/// <summary>Stadium: two semicircular ends joined by straight sides, offset from the origin.</summary>
|
||||
private static Program Obround(double length, double width)
|
||||
{
|
||||
var r = width / 2;
|
||||
var program = new Program();
|
||||
program.Codes.Add(new RapidMove(1 + r, 1));
|
||||
program.Codes.Add(new LinearMove(1 + length - r, 1));
|
||||
program.Codes.Add(new ArcMove(1 + length - r, 1 + width, 1 + length - r, 1 + r, RotationType.CCW));
|
||||
program.Codes.Add(new LinearMove(1 + r, 1 + width));
|
||||
program.Codes.Add(new ArcMove(1 + r, 1, 1 + r, 1 + r, RotationType.CCW));
|
||||
return program;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class Opus55ContractTests : EngineContractTests<Opus55NestingEngine> { }
|
||||
|
||||
Reference in New Issue
Block a user