Files
OpenNest-Engines/OpenNest.Engine.Qwen38FlashNext/README.md
T
ajandClaude Opus 5.5 aa0e6f967e feat(qwen38flashnext): snapshot first working engine as a baseline
Qwen3.8-Flash-Next reached a working engine and is now optimizing it.
Optimization passes can regress, so this preserves the first version
that passes all acceptance tests (13/13 against OpenNest master,
including the model's own rotated-spacing regression test) for
comparison and rollback. Snapshot taken 2026-09-24 12:00 from
hermes.lan:/home/aj/src/Qwen38FlashNext; the run is still in progress,
so this stays off master until it finishes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 12:00:53 -04:00

113 lines
6.6 KiB
Markdown

# OpenNest.Engine.Qwen38FlashNext
An independent `INestingEngine` implementation. It must not be a wrapper, ensemble, or
selector over OpenNest's built-in engines. `Solve()` must not call, instantiate, or
delegate to any existing `INestingEngine` (`StockLadderNestingEngine`,
`FixedStrategyNestingEngine`), `NestingEngineRegistry`, `NestJobRunner`, or the whole-plate
nesters/fillers behind `PlateNesterFactory` (`DefaultPlateNester`, `StripPlateNester`,
`RemnantPlateNester`, `PlateFillService`, `DefaultPlateFiller`, ...). It must also never run
several of them and keep the best result.
The decisions that make it an engine must be yours: which sheet(s) to use, which parts go
where and in what order, which pattern/strategy to apply to which region, and when to stop.
## Allowed building blocks
Reuse is encouraged. These are tools you drive, composed by your own decision logic:
- `OpenNest.Core` geometry: `Polygon`, `Shape`, `BoundingBox`, `Vector`, `Box`, `ConvexHull`,
`ConvexDecomposition`, `RotatingCalipers`, `Collision`, `NoFitPolygon`, `ShapeProfile`,
`SpatialQuery`.
- Fill and pattern components in `OpenNest.Engine.Fill`: `FillLinear`, `FillExtents`,
`PairFiller`, `ShrinkFiller`, `RemnantFiller`/`RemnantFinder`, `Compactor`, `FillScore`,
`Pattern`/`PatternTiler`, `PartBoundary`, `RotationAnalysis`, `AngleCandidateBuilder`,
`BestCombination`.
- `OpenNest.Engine.BestFit` (`BestFitFinder`, `PairEvaluator`, ...), `RectanglePacking`,
`CirclePacking`.
If you find a faster or better way to do something a shared component already does (for
example linear patterning), implement it inside this engine's own project and leave the
shared code untouched. Do not edit `OpenNest.Core`, `OpenNest.Engine`, or
`OpenNest.Benchmark`. Call it out in your report (what it replaces, why it is better,
measured numbers) so it can be generalized and upstreamed for every engine later.
## Algorithm
Bottom-left greedy insertion over convex No-Fit-Polygons, with an exact material-clearance
gate, driven sheet by sheet by a greedy demand scheduler. All geometry math is the engine's
own (`Engine/`); it calls no built-in nester, filler, or runner.
- **`PartPreparation`** rebuilds each snapshot into a closed contour topology (perimeter +
cutouts; rapids/scribe marks dropped), flattens it circumscribed (the collision polygon
always contains the true material), and caches per-(part, angle, spacing) geometry: bounds,
convex hull, and the spacing-inflated outline **rotated into that orientation's frame**
(offset commutes with rotation; an unrotated inflation tests the candidate against the
material of a different angle - this was a real overlap bug, caught by
`RotatedConcavePartsKeepSpacingAtFixedAngles`). Candidate angles are the policy angles, or
0/90/180/270 plus the rotating-calipers minimum-bounding-rectangle angle for automatic
rotation.
- **`SheetPacker`** places one part instance at a time. Per already-placed part it builds a
convex NFP as `placedHull (+) disk(spacing) (+) reflect(candidateHull)` via its own
Minkowski edge-merge (`Convex.cs`; the merge picks the more-clockwise frontier edge, an
inverted comparison here corrupts every non-parallel sum into a self-intersecting contour),
then enumerates corner-point candidates: anchor work-box corners, NFP vertices, and
NFP-edge/box-line slides, tried in ascending bottom-left order. Because the NFP is
hull-based it only *certifies* clearance when both parts are convex solids with uncapped
hulls; everything else falls through to the exact gate - placed material inflated by the
spacing (holes shrunk, closed holes treated solid) versus the candidate's raw material with
holes subtracted, the same inflation rule the benchmark validator uses, so interlocking
concave parts are placed legally where the convex NFP alone would reject them. A uniform
spatial grid keeps the pair tests near-constant as the sheet fills, and an overlap memo
keyed by world pose collapses repeated clipper work across stock trials.
- **`JobSolver`** walks demands in its own order (priority, then smallest worst-case
orientation extent, then area, then id) and drains each greedily. For the next sheet it
trials *every* available stock size independently and commits the trial placing the most
instances, breaking ties by priority coverage then sheet area; lost trials change no job
state. The job stops on met demand, exhausted stock, no further placement, or the plate
cap. Deterministic: identical input, identical layout.
Trade-offs: greedy BLFG insertion leaves some of the density interlocking-pair and
compaction pipelines find on regular jobs, and every stock size is trialled per sheet
(O(sheets x stocks x fill)); on the real 69-drawing/219-part PT75 job below that costs
~125 s against the benchmark's 5-minute per-solve timeout. In exchange it places arcs,
concaves, and holed parts under one uniform gate with no per-shape-class special cases.
## Benchmark results
`P260805-10-PT75-corrected.nest` (69 drawings, 219 parts, sizes 60x120/72x120/60x96/48x144,
spacing 0.3, `--parallel 1`): **valid, 219/219 placed, 31 plates, 80.2% utilization,
cost 214848**, ~125 s. The same run's Baseline layout scores INVALID (over-quantity and a
spacing violation in the source file), and StockLadder crashes on a drawing whose geometry
has no usable closed edges - the engine's per-part try/catch reports such parts unplaced
instead of failing the job.
## Tests
`tests/` holds starter acceptance tests. Every layout is checked by the benchmark's own
`NestValidator` (bounds, spacing, quantities, stock, rotation), so a passing test means the
benchmark will accept the layout. They fail until `Solve()` is implemented. Keep them and
add engine-specific tests next to them.
```bash
dotnet test OpenNest.Engine.Qwen38FlashNext/tests/OpenNest.Engine.Qwen38FlashNext.Tests.csproj
```
## Build and benchmark
The project is a plugin outside `OpenNest.sln`. `OpenNest.Benchmark` loads plugin engines
from an `Engines/` folder next to its own build output:
```bash
dotnet build OpenNest.Engine.Qwen38FlashNext/OpenNest.Engine.Qwen38FlashNext.csproj -c Release
dotnet build <OpenNest>/OpenNest.Benchmark/OpenNest.Benchmark.csproj -c Release
mkdir -p <OpenNest>/OpenNest.Benchmark/bin/Release/net8.0/Engines
cp OpenNest.Engine.Qwen38FlashNext/bin/Release/net8.0/OpenNest.Engine.Qwen38FlashNext.dll <OpenNest>/OpenNest.Benchmark/bin/Release/net8.0/Engines/
dotnet <OpenNest>/OpenNest.Benchmark/bin/Release/net8.0/OpenNest.Benchmark.dll <path-to-.nest-or-folder> --parallel 1
```
`<OpenNest>` is the OpenNest checkout root. Your engine shows up in the report under its
CLR type name (`Qwen38FlashNextNestingEngine`), competing on equal footing against the built-in
engines.