diff --git a/Build-Engines.ps1 b/Build-Engines.ps1 index 237507a..703601e 100644 --- a/Build-Engines.ps1 +++ b/Build-Engines.ps1 @@ -27,7 +27,7 @@ if (-not (Test-Path (Join-Path $OpenNestRoot 'OpenNest.Benchmark/OpenNest.Benchm } $OpenNestRoot = (Resolve-Path $OpenNestRoot).Path -dotnet build (Join-Path $OpenNestRoot 'OpenNest.Benchmark/OpenNest.Benchmark.csproj') -c $Configuration +dotnet build (Join-Path $OpenNestRoot 'OpenNest.Benchmark/OpenNest.Benchmark.csproj') -c $Configuration -m:1 -nr:false if ($LASTEXITCODE -ne 0) { throw 'OpenNest.Benchmark build failed.' } $deployDir = Join-Path $OpenNestRoot "OpenNest.Benchmark/bin/$Configuration/net8.0/Engines" @@ -38,7 +38,7 @@ $projects = Get-ChildItem $PSScriptRoot -Directory -Filter 'OpenNest.Engine.*' | foreach ($dir in $projects) { $csproj = Join-Path $dir.FullName "$($dir.Name).csproj" - dotnet build $csproj -c $Configuration "-p:OpenNestRoot=$OpenNestRoot/" + dotnet build $csproj -c $Configuration "-p:OpenNestRoot=$OpenNestRoot/" -m:1 -nr:false if ($LASTEXITCODE -ne 0) { throw "$($dir.Name) build failed." } $dll = Join-Path $dir.FullName "bin/$Configuration/net8.0/$($dir.Name).dll" diff --git a/New-Engine.ps1 b/New-Engine.ps1 index f744c78..88d6358 100644 --- a/New-Engine.ps1 +++ b/New-Engine.ps1 @@ -7,8 +7,8 @@ placeholder in file names and contents. The new engine builds against OpenNest via Directory.Build.props, which must sit in or a parent folder. - With -IncludeBuildFiles, Directory.Build.props/.targets are copied into - too, so the engine can live outside this repo, e.g. in an Engines/ folder inside an + With -IncludeBuildFiles, Directory.Build.props/.targets and the shared Engine.Testing + source kit are copied into , so the engine can live outside this repo, e.g. in an Engines/ folder inside an OpenNest checkout (the props detect that layout on their own). .EXAMPLE @@ -29,21 +29,28 @@ $target = Join-Path $Destination "OpenNest.Engine.$Name" if (Test-Path $target) { throw "'$target' already exists." } New-Item -ItemType Directory -Force $Destination | Out-Null -Copy-Item $template $target -Recurse - -Get-ChildItem $target -Recurse -File | ForEach-Object { - $text = [IO.File]::ReadAllText($_.FullName) - [IO.File]::WriteAllText($_.FullName, $text.Replace('__NAME__', $Name)) -} -# Deepest paths first so renaming a folder never invalidates a pending child path. -Get-ChildItem $target -Recurse | Where-Object Name -like '*__NAME__*' | - Sort-Object { $_.FullName.Length } -Descending | - ForEach-Object { Rename-Item $_.FullName $_.Name.Replace('__NAME__', $Name) } +# A built template contains binary/obj files. Copy only source files, never rewrite binaries. +$template = (Resolve-Path $template).Path +Get-ChildItem $template -Recurse -File | + Where-Object { $_.FullName.Substring($template.Length) -notmatch '[\\/](bin|obj)[\\/]' } | + ForEach-Object { + $relative = $_.FullName.Substring($template.Length + 1).Replace('__NAME__', $Name) + $output = Join-Path $target $relative + New-Item -ItemType Directory -Force (Split-Path $output -Parent) | Out-Null + $text = [IO.File]::ReadAllText($_.FullName) + [IO.File]::WriteAllText($output, $text.Replace('__NAME__', $Name)) + } if ($IncludeBuildFiles) { foreach ($file in 'Directory.Build.props', 'Directory.Build.targets') { Copy-Item (Join-Path $PSScriptRoot $file) $Destination -Force } + # The acceptance tests reference the shared, read-only kit beside the engine. + $kitTarget = Join-Path $Destination 'Engine.Testing' + New-Item -ItemType Directory -Force $kitTarget | Out-Null + Get-ChildItem (Join-Path $PSScriptRoot 'Engine.Testing') -File | + Where-Object { $_.Extension -in '.cs', '.csproj' } | + Copy-Item -Destination $kitTarget -Force } Write-Host "Created $target" diff --git a/README.md b/README.md index 234fc6b..71c4419 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,12 @@ dotnet test OpenNest.Engine.Opus55/tests/OpenNest.Engine.Opus55.Tests.csproj Each engine's README covers its algorithm and benchmark results. +`Engine.Testing/` contains shared xUnit contract tests, shapes, job builders and layout +assertions backed by `NestLayoutCheck`. It is a test dependency, not a plugin; +`Build-Engines.ps1` only deploys projects in `OpenNest.Engine.*` directories. All engines +require the shared-services APIs in the sibling host checkout. See +[the PR 5 migration report](MIGRATION-PR5.md) for validation and benchmark results. + ## Writing a new engine ```powershell @@ -41,10 +47,10 @@ Each engine's README covers its algorithm and benchmark results. This copies `_Template/` to `OpenNest.Engine.Nova/`: an `INestingEngine` stub, a README spelling out what counts as an independent engine, `BENCH-RULES.md` (how a model's run works: workspace limits, git, the real-part archive, reporting), and starter acceptance tests checked by -the benchmark's own `NestValidator` (they fail until `Solve()` is implemented). +the shared `NestLayoutCheck` (they fail until `Solve()` is implemented). To work inside an OpenNest checkout instead, stamp it into an `Engines/` folder there and -bring the shared build files along; they detect that layout automatically: +bring the shared build files and test kit along; they detect that layout automatically: ```powershell ./New-Engine.ps1 -Name Nova -Destination /Engines -IncludeBuildFiles diff --git a/_Template/BENCH-RULES.md b/_Template/BENCH-RULES.md index e3b4d1d..ec61407 100644 --- a/_Template/BENCH-RULES.md +++ b/_Template/BENCH-RULES.md @@ -41,6 +41,12 @@ file covers how the run itself works. ## Tests and scoring +- `Engine.Testing/` is the shared, read-only test kit supplied beside engine folders in + bench copies. Reference it for shapes, job construction, layout assertions and inherited + engine contract tests; do not copy or edit it during an engine optimization run. +- Placement must be deterministic: no clocks, unseeded randomness or environment variables + may influence placement. Budgets count work. Wall time may stop work only through the + host's cancellation token. Diagnostics must not affect placement decisions. - Keep the starter tests in `tests/` and keep them passing. Add tests; do not weaken, skip or delete existing ones. If you believe an existing test is wrong, leave it and explain why in your report. diff --git a/_Template/README.md b/_Template/README.md index 7188d85..7877d08 100644 --- a/_Template/README.md +++ b/_Template/README.md @@ -18,6 +18,10 @@ real-part drawing archive, tests and your final report. Reuse is encouraged. These are tools you drive, composed by your own decision logic: +- `OpenNest.Engine.Jobs`: `JobPartGeometry`, stock `WorkArea`/`Area`/`Fits`, + `RotationPolicy.EnumerateAngles`, `RotationCandidates`, `NestJobCost`, `NestTolerances`, + `NestLayoutCheck`, and `NestJobResultBuilder`. These prepare geometry, check and account + for decisions made by your algorithm; they do not choose placements. - `OpenNest.Core` geometry: `Polygon`, `Shape`, `BoundingBox`, `Vector`, `Box`, `ConvexHull`, `ConvexDecomposition`, `RotatingCalipers`, `Collision`, `NoFitPolygon`, `ShapeProfile`, `SpatialQuery`. @@ -46,10 +50,12 @@ measured numbers) so it can be generalized and upstreamed for every engine later ## 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. +`tests/` references the read-only `../Engine.Testing` kit and subclasses +`EngineContractTests`. `LayoutAssert.Valid` uses `NestLayoutCheck.Violations`, +the benchmark's shared validation primitive, plus strict bounds and accounting checks. +The acceptance tests fail until `Solve()` is implemented. Keep them and add engine-specific +tests next to them. No clocks, unseeded randomness or environment variables may influence +placement; count work for budgets and honor the host cancellation token for wall time. ```bash dotnet test OpenNest.Engine.__NAME__/tests/OpenNest.Engine.__NAME__.Tests.csproj diff --git a/_Template/__NAME__NestingEngine.cs b/_Template/__NAME__NestingEngine.cs index 59656de..3137dcd 100644 --- a/_Template/__NAME__NestingEngine.cs +++ b/_Template/__NAME__NestingEngine.cs @@ -18,6 +18,7 @@ public sealed class __NAME__NestingEngine : INestingEngine ) { ArgumentNullException.ThrowIfNull(job); + token.ThrowIfCancellationRequested(); // TODO: implement independent placement logic here. // @@ -30,9 +31,12 @@ public sealed class __NAME__NestingEngine : INestingEngine // job.Plates -> candidate stock sheets (size, spacing, edge spacing, quadrant, quantity) // job.Options -> job-wide options // - // Return a NestJobResult built from NestJobPlateResult (one per used sheet, holding - // ordered NestJobPlacement values), PartFulfillment (requested vs placed per part id), - // and StockUsage (sheets used per stock id). + // Read material with JobPartGeometry.TryRead(part.Geometry); use stock.WorkArea/Fits, + // RotationCandidates.ForShape and NestJobCost as primitives for your own decisions. + // var result = new NestJobResultBuilder(job, progress); + // result.AddSheet(stock, poses); // (PartId, X, Y, Rotation); call only on commit. + // return result.Build(NestJobStopReason.NoPlacementFound); + // The builder assigns sheet/instance indices, fulfillment, usage and commit progress. throw new NotImplementedException("__NAME__ nesting engine placement logic not yet implemented."); } diff --git a/_Template/tests/OpenNest.Engine.__NAME__.Tests.csproj b/_Template/tests/OpenNest.Engine.__NAME__.Tests.csproj index 0efd585..45cfc21 100644 --- a/_Template/tests/OpenNest.Engine.__NAME__.Tests.csproj +++ b/_Template/tests/OpenNest.Engine.__NAME__.Tests.csproj @@ -10,8 +10,7 @@ + - - diff --git a/_Template/tests/__NAME__NestingEngineTests.cs b/_Template/tests/__NAME__NestingEngineTests.cs index b120e7a..2d9fe9f 100644 --- a/_Template/tests/__NAME__NestingEngineTests.cs +++ b/_Template/tests/__NAME__NestingEngineTests.cs @@ -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; @@ -10,7 +12,7 @@ using OpenNest.Geometry; namespace OpenNest.Engine.__NAME__.Tests; /// -/// Starter acceptance tests. Every layout is checked by the same NestValidator the benchmark +/// Starter acceptance tests. Every layout is checked by the shared NestLayoutCheck the benchmark /// scores with, so a passing test means the benchmark will accept the layout. They fail until /// Solve() is implemented; add engine-specific tests alongside them. /// @@ -30,7 +32,7 @@ public class __NAME__NestingEngineTests var result = new __NAME__NestingEngine().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); @@ -55,7 +57,7 @@ public class __NAME__NestingEngineTests var result = new __NAME__NestingEngine().Solve(job); - AssertValid(job, result); + LayoutAssert.Valid(job, result); Assert.Equal(NestJobStatus.Complete, result.Status); } @@ -66,7 +68,7 @@ public class __NAME__NestingEngineTests var result = new __NAME__NestingEngine().Solve(job); - AssertValid(job, result); + LayoutAssert.Valid(job, result); Assert.Equal(NestJobStatus.Complete, result.Status); Assert.True(result.Plates.Count > 1); } @@ -81,70 +83,11 @@ public class __NAME__NestingEngineTests var result = new __NAME__NestingEngine().Solve(job); - AssertValid(job, result); + LayoutAssert.Valid(job, result); var huge = Assert.Single(result.Fulfillment, f => f.PartId == "huge"); Assert.Equal(1, huge.Unplaced); } - // ---- helpers ------------------------------------------------------------------------- - - 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( - 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); - - /// Y extent. - /// X extent. - 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; - } } + +public sealed class __NAME__ContractTests : EngineContractTests<__NAME__NestingEngine> { }