docs(engines): template on shared services and a determinism rule
New engines start from the shared test kit (the template's tests are a one-line EngineContractTests subclass) and the host APIs, so they don't re-derive geometry reading, work areas or validator tolerances. BENCH-RULES.md now forbids clocks, unseeded randomness and environment variables from influencing placement (budgets count work; wall time only through the host's cancellation token) and lists the kit as read-only. Build-Engines.ps1 deploys only OpenNest.Engine.* folders, and New-Engine.ps1 -IncludeBuildFiles copies the kit. Co-Authored-By: Codex <noreply@openai.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+2
-2
@@ -27,7 +27,7 @@ if (-not (Test-Path (Join-Path $OpenNestRoot 'OpenNest.Benchmark/OpenNest.Benchm
|
|||||||
}
|
}
|
||||||
$OpenNestRoot = (Resolve-Path $OpenNestRoot).Path
|
$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.' }
|
if ($LASTEXITCODE -ne 0) { throw 'OpenNest.Benchmark build failed.' }
|
||||||
|
|
||||||
$deployDir = Join-Path $OpenNestRoot "OpenNest.Benchmark/bin/$Configuration/net8.0/Engines"
|
$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) {
|
foreach ($dir in $projects) {
|
||||||
$csproj = Join-Path $dir.FullName "$($dir.Name).csproj"
|
$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." }
|
if ($LASTEXITCODE -ne 0) { throw "$($dir.Name) build failed." }
|
||||||
|
|
||||||
$dll = Join-Path $dir.FullName "bin/$Configuration/net8.0/$($dir.Name).dll"
|
$dll = Join-Path $dir.FullName "bin/$Configuration/net8.0/$($dir.Name).dll"
|
||||||
|
|||||||
+18
-11
@@ -7,8 +7,8 @@
|
|||||||
placeholder in file names and contents. The new engine builds against OpenNest via
|
placeholder in file names and contents. The new engine builds against OpenNest via
|
||||||
Directory.Build.props, which must sit in <Destination> or a parent folder.
|
Directory.Build.props, which must sit in <Destination> or a parent folder.
|
||||||
|
|
||||||
With -IncludeBuildFiles, Directory.Build.props/.targets are copied into <Destination>
|
With -IncludeBuildFiles, Directory.Build.props/.targets and the shared Engine.Testing
|
||||||
too, so the engine can live outside this repo, e.g. in an Engines/ folder inside an
|
source kit are copied into <Destination>, 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).
|
OpenNest checkout (the props detect that layout on their own).
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
@@ -29,21 +29,28 @@ $target = Join-Path $Destination "OpenNest.Engine.$Name"
|
|||||||
if (Test-Path $target) { throw "'$target' already exists." }
|
if (Test-Path $target) { throw "'$target' already exists." }
|
||||||
|
|
||||||
New-Item -ItemType Directory -Force $Destination | Out-Null
|
New-Item -ItemType Directory -Force $Destination | Out-Null
|
||||||
Copy-Item $template $target -Recurse
|
# A built template contains binary/obj files. Copy only source files, never rewrite binaries.
|
||||||
|
$template = (Resolve-Path $template).Path
|
||||||
Get-ChildItem $target -Recurse -File | ForEach-Object {
|
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)
|
$text = [IO.File]::ReadAllText($_.FullName)
|
||||||
[IO.File]::WriteAllText($_.FullName, $text.Replace('__NAME__', $Name))
|
[IO.File]::WriteAllText($output, $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) }
|
|
||||||
|
|
||||||
if ($IncludeBuildFiles) {
|
if ($IncludeBuildFiles) {
|
||||||
foreach ($file in 'Directory.Build.props', 'Directory.Build.targets') {
|
foreach ($file in 'Directory.Build.props', 'Directory.Build.targets') {
|
||||||
Copy-Item (Join-Path $PSScriptRoot $file) $Destination -Force
|
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"
|
Write-Host "Created $target"
|
||||||
|
|||||||
@@ -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.
|
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
|
## Writing a new engine
|
||||||
|
|
||||||
```powershell
|
```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
|
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
|
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
|
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
|
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
|
```powershell
|
||||||
./New-Engine.ps1 -Name Nova -Destination <OpenNest>/Engines -IncludeBuildFiles
|
./New-Engine.ps1 -Name Nova -Destination <OpenNest>/Engines -IncludeBuildFiles
|
||||||
|
|||||||
@@ -41,6 +41,12 @@ file covers how the run itself works.
|
|||||||
|
|
||||||
## Tests and scoring
|
## 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,
|
- 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
|
skip or delete existing ones. If you believe an existing test is wrong, leave it and explain
|
||||||
why in your report.
|
why in your report.
|
||||||
|
|||||||
+10
-4
@@ -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:
|
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`,
|
- `OpenNest.Core` geometry: `Polygon`, `Shape`, `BoundingBox`, `Vector`, `Box`, `ConvexHull`,
|
||||||
`ConvexDecomposition`, `RotatingCalipers`, `Collision`, `NoFitPolygon`, `ShapeProfile`,
|
`ConvexDecomposition`, `RotatingCalipers`, `Collision`, `NoFitPolygon`, `ShapeProfile`,
|
||||||
`SpatialQuery`.
|
`SpatialQuery`.
|
||||||
@@ -46,10 +50,12 @@ measured numbers) so it can be generalized and upstreamed for every engine later
|
|||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
`tests/` holds starter acceptance tests. Every layout is checked by the benchmark's own
|
`tests/` references the read-only `../Engine.Testing` kit and subclasses
|
||||||
`NestValidator` (bounds, spacing, quantities, stock, rotation), so a passing test means the
|
`EngineContractTests<TEngine>`. `LayoutAssert.Valid` uses `NestLayoutCheck.Violations`,
|
||||||
benchmark will accept the layout. They fail until `Solve()` is implemented. Keep them and
|
the benchmark's shared validation primitive, plus strict bounds and accounting checks.
|
||||||
add engine-specific tests next to them.
|
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
|
```bash
|
||||||
dotnet test OpenNest.Engine.__NAME__/tests/OpenNest.Engine.__NAME__.Tests.csproj
|
dotnet test OpenNest.Engine.__NAME__/tests/OpenNest.Engine.__NAME__.Tests.csproj
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public sealed class __NAME__NestingEngine : INestingEngine
|
|||||||
)
|
)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(job);
|
ArgumentNullException.ThrowIfNull(job);
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
// TODO: implement independent placement logic here.
|
// 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.Plates -> candidate stock sheets (size, spacing, edge spacing, quadrant, quantity)
|
||||||
// job.Options -> job-wide options
|
// job.Options -> job-wide options
|
||||||
//
|
//
|
||||||
// Return a NestJobResult built from NestJobPlateResult (one per used sheet, holding
|
// Read material with JobPartGeometry.TryRead(part.Geometry); use stock.WorkArea/Fits,
|
||||||
// ordered NestJobPlacement values), PartFulfillment (requested vs placed per part id),
|
// RotationCandidates.ForShape and NestJobCost as primitives for your own decisions.
|
||||||
// and StockUsage (sheets used per stock id).
|
// 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.");
|
throw new NotImplementedException("__NAME__ nesting engine placement logic not yet implemented.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Using Include="Xunit" />
|
<Using Include="Xunit" />
|
||||||
|
<ProjectReference Include="../../Engine.Testing/OpenNest.Engine.Testing.csproj" />
|
||||||
<ProjectReference Include="../OpenNest.Engine.__NAME__.csproj" />
|
<ProjectReference Include="../OpenNest.Engine.__NAME__.csproj" />
|
||||||
<!-- The benchmark's NestValidator is the arbiter the engine is scored by. -->
|
|
||||||
<ProjectReference Include="$(OpenNestRoot)OpenNest.Benchmark/OpenNest.Benchmark.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using OpenNest.Benchmark;
|
|
||||||
using OpenNest.CNC;
|
using OpenNest.CNC;
|
||||||
using OpenNest.Engine.Jobs;
|
using OpenNest.Engine.Jobs;
|
||||||
using OpenNest.Engine.Jobs.Adapters;
|
using OpenNest.Engine.Jobs.Adapters;
|
||||||
@@ -10,7 +12,7 @@ using OpenNest.Geometry;
|
|||||||
namespace OpenNest.Engine.__NAME__.Tests;
|
namespace OpenNest.Engine.__NAME__.Tests;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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
|
/// 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.
|
/// Solve() is implemented; add engine-specific tests alongside them.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -30,7 +32,7 @@ public class __NAME__NestingEngineTests
|
|||||||
|
|
||||||
var result = new __NAME__NestingEngine().Solve(job);
|
var result = new __NAME__NestingEngine().Solve(job);
|
||||||
|
|
||||||
AssertValid(job, result);
|
LayoutAssert.Valid(job, result);
|
||||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||||
Assert.Single(result.Plates);
|
Assert.Single(result.Plates);
|
||||||
Assert.Equal(12, result.Plates[0].Placements.Count);
|
Assert.Equal(12, result.Plates[0].Placements.Count);
|
||||||
@@ -55,7 +57,7 @@ public class __NAME__NestingEngineTests
|
|||||||
|
|
||||||
var result = new __NAME__NestingEngine().Solve(job);
|
var result = new __NAME__NestingEngine().Solve(job);
|
||||||
|
|
||||||
AssertValid(job, result);
|
LayoutAssert.Valid(job, result);
|
||||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +68,7 @@ public class __NAME__NestingEngineTests
|
|||||||
|
|
||||||
var result = new __NAME__NestingEngine().Solve(job);
|
var result = new __NAME__NestingEngine().Solve(job);
|
||||||
|
|
||||||
AssertValid(job, result);
|
LayoutAssert.Valid(job, result);
|
||||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||||
Assert.True(result.Plates.Count > 1);
|
Assert.True(result.Plates.Count > 1);
|
||||||
}
|
}
|
||||||
@@ -81,70 +83,11 @@ public class __NAME__NestingEngineTests
|
|||||||
|
|
||||||
var result = new __NAME__NestingEngine().Solve(job);
|
var result = new __NAME__NestingEngine().Solve(job);
|
||||||
|
|
||||||
AssertValid(job, result);
|
LayoutAssert.Valid(job, result);
|
||||||
var huge = Assert.Single(result.Fulfillment, f => f.PartId == "huge");
|
var huge = Assert.Single(result.Fulfillment, f => f.PartId == "huge");
|
||||||
Assert.Equal(1, huge.Unplaced);
|
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<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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class __NAME__ContractTests : EngineContractTests<__NAME__NestingEngine> { }
|
||||||
|
|||||||
Reference in New Issue
Block a user