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:
aj
2026-09-25 09:29:27 -04:00
co-authored by Codex Claude Opus 5.5
parent d0c6af783b
commit 0308a862b8
8 changed files with 77 additions and 335 deletions
+20 -10
View File
@@ -13,11 +13,12 @@ Every placement decision (which part, which rotation, where, on which sheet) com
coarsened for arc-heavy parts until the outline is ≤ ~64 vertices, capped at 0.1% of part size).
- Candidate rotations come from the part's `RotationPolicy`: for `Automatic`, the four right
angles plus the two orientations that axis-align the minimum-area bounding rectangle
(`RotatingCalipers`); for sweeps, up to 8 evenly spaced legal steps. Point-symmetric duplicates are dropped.
(`RotatingCalipers`); for sweeps, the host policy grid truncated by the engine's orientation limit. Point-symmetric duplicates are dropped.
- Each orientation gets a **footprint**: outline inflated (miter joins, so it contains the exact
round offset) by `(spacing + 0.003) / 2 + chordTolerance`. Two parts respect the spacing
when their footprints don't overlap. The 0.003 covers `NestValidator` flattening each arc
within 0.001 of true (0.002 for a pair), plus Clipper's 1e-4 grid on both sides.
round offset) by `spacing / 2 + NestTolerances.SafeClearanceMargin(chordTolerance) / 2`
plus four Clipper grid units per footprint. The extra grid allowance preserves this
engine's established contact points through repeated NFP Boolean operations; removing it
increased mixed-job cost from 1586.94 to 1589.81 in the migration check.
- **No-fit polygons** between footprints come from Clipper2 Minkowski sums: an O(n+m)
edge merge for convex pairs, and for concave pairs the boundary sweep ∪ (A + p₀) ∪ (−B + a₀).
The last two terms cover "B inside A" and "B swallows A". NFPs are cached per orientation pair.
@@ -27,14 +28,14 @@ Every placement decision (which part, which rotation, where, on which sheet) com
legal reference points: the inner-fit rectangle minus the NFPs of everything placed. Each
placement subtracts one translated NFP from each region (in parallel, which stays deterministic).
Regions only shrink, and an empty region is retired for the rest of the sheet.
- At every step all remaining types × orientations compete (there is no fixed placement sequence):
- At every step the lowest-number priority with a feasible placement wins; peer types × orientations compete (there is no fixed placement sequence):
1. **Gap fill:** if any part fits without pushing the packing front forward, place the
*largest* such part at its lowest point.
2. **Advance:** otherwise place the part with the least front advance per `area^β`, i.e. the
most material coverage for the sheet length it consumes.
- The front sweeps along X or Y, which leaves one full-width offcut strip for salvage credit.
**3. Whole job (`Opus55NestingEngine`, `SheetEconomics`)**
**3. Whole job (`Opus55NestingEngine`, `NestJobCost`)**
- Sheet by sheet, every available stock size is trial-filled. The trial with the lowest
*estimated whole-job cost* (its net area, plus the remaining demand priced at the best
efficiency any trial achieved) is committed. This lets a sheet that finishes the job beat a
@@ -55,8 +56,7 @@ Every placement decision (which part, which rotation, where, on which sheet) com
| `FrontierPacker.cs` | One-sheet fill: free regions and the gap-fill/advance choice rule |
| `NoFitCache.cs` | Spacing footprints and cached NFPs (Clipper2 Minkowski) |
| `PartCatalog.cs` | Snapshot → perimeter polygon per allowed orientation |
| `SheetEconomics.cs` | Net-area objective with salvage credit |
| `tests/` | xUnit suite. Layouts are judged by `OpenNest.Benchmark.NestValidator` |
| `tests/` | xUnit suite. Layouts are judged by `Engine.Testing.LayoutAssert` and `NestLayoutCheck` |
## Build / test
@@ -91,7 +91,17 @@ The engine reports as `Opus55NestingEngine`.
distinct parts: `48 / partCount`, minimum 2). Free-angle rotations aren't explored beyond the MBR alignment.
- **Greedy core:** there is no order/permutation search. The variants and tail re-plan are the only
search, and density on small mixed jobs trails what an interlocking-pair filler can reach.
- **`NestJobPart.Priority` is ignored**, and progress reports only `EvaluatingCandidate`
per trial and `PlateCommitted` at the end, with no finer-grained progress.
- **Priority is enforced during placement** (lower number first). Progress reports
`EvaluatingCandidate` per trial and `PlateCommitted` at the end, with no finer-grained progress.
- Parts whose geometry has no readable closed perimeter, or that fit no offered stock at any
allowed rotation, are reported unplaced (`NoPlacementFound`) instead of failing the job.
## Shared services migration
`JobPartGeometry.TryRead` supplies the perimeter. `ForShape(policy, perimeter, limit)`
retains the engine's orientation cap and `DistinctOutlines` drops perimeter symmetry.
Stock `WorkArea`/`Fits`, host salvage scoring and `NestJobResultBuilder` replace copied
plumbing. The frontier, NFP cache, variant work budget and tail improvement remain local.
The shared contract suite exposed and now guards lower-number priority precedence.
Every old engine-specific test remains. All five salvage benchmark costs and validity
match baseline; see [PR 5 results](../MIGRATION-PR5.md).