From 7becdd2b882585440776f091067bec1092fd6346 Mon Sep 17 00:00:00 2001 From: Sandeep kurien Date: Wed, 22 Jul 2026 13:30:34 +0530 Subject: [PATCH 1/2] Add reproducible 2D benchmark suite --- CHANGELOG.md | 9 + CITATION.cff | 6 +- README.md | 14 +- SHA256SUMS | 36 +- .../2d/v1/cases/2d-exact-square-grid.json | 66 +++ .../cases/2d-irregular-remnant-with-void.json | 75 ++++ datasets/2d/v1/cases/2d-steel-plate-grid.json | 66 +++ datasets/2d/v1/cases/2d-stock-exhausted.json | 66 +++ datasets/2d/v1/cases/2d-wood-panel-grid.json | 66 +++ datasets/2d/v1/manifest.json | 64 +++ methodology/2d-v1.md | 68 +++ package.json | 2 +- schemas/2d-case.schema.json | 53 +++ schemas/2d-manifest.schema.json | 75 ++++ schemas/2d-result.schema.json | 85 ++++ scripts/validate-repository.mjs | 417 +++++++++++++++--- 16 files changed, 1092 insertions(+), 76 deletions(-) create mode 100644 datasets/2d/v1/cases/2d-exact-square-grid.json create mode 100644 datasets/2d/v1/cases/2d-irregular-remnant-with-void.json create mode 100644 datasets/2d/v1/cases/2d-steel-plate-grid.json create mode 100644 datasets/2d/v1/cases/2d-stock-exhausted.json create mode 100644 datasets/2d/v1/cases/2d-wood-panel-grid.json create mode 100644 datasets/2d/v1/manifest.json create mode 100644 methodology/2d-v1.md create mode 100644 schemas/2d-case.schema.json create mode 100644 schemas/2d-manifest.schema.json create mode 100644 schemas/2d-result.schema.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b0f2e7..0f9e123 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to public benchmark suites and sample assets are recorded here. Released dataset inputs are immutable; semantic input changes create a new suite version. +## 1.1.0 - 2026-07-22 + +- Added the initial five-case synthetic 2D nesting suite. +- Added 2D manifest, case, and result schemas plus methodology v1. +- Added validation for polygon area, deterministic seeds, part/sheet quantities, + and expected placement boundaries. +- Defined geometric utilization and estimated unused area separately from + manufacturing waste. + ## 1.0.1 - 2026-07-21 - Enforced LF line endings for generated and checksummed text assets so clean diff --git a/CITATION.cff b/CITATION.cff index 78fe720..0855ce5 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -4,11 +4,11 @@ title: "CutNesting Benchmarks and Sample Files" type: dataset authors: - name: Technodweep -version: 1.0.0 -date-released: 2026-07-21 +version: 1.1.0 +date-released: 2026-07-22 license: CC-BY-4.0 repository-code: "https://github.com/technodweep/cutnesting-benchmarks" -url: "https://cutnesting.com/benchmarks/1d-cutting/" +url: "https://cutnesting.com/benchmarks/" keywords: - cutting stock - nesting diff --git a/README.md b/README.md index 875dbb7..936ad0b 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ an independently validated proof-bearing `optimal` status. - Browse the [1D benchmark dataset](datasets/1d/v1/manifest.json). - Read the [1D methodology](methodology/v1.md) before interpreting waste or efficiency figures. +- Browse the [2D benchmark dataset](datasets/2d/v1/manifest.json) and read its + [area-utilization methodology](methodology/2d-v1.md). - Download [synthetic DXF and DSTV/NC1 samples](samples/README.md). - Use the [CutNesting-compatible CSV and XLSX templates](templates/README.md). - Check the exact [DSTV/NC1 subset](docs/dstv-nc1-supported-subset.md) represented @@ -75,8 +77,18 @@ material accounting, independent validation outcomes, engine and dataset commits, and machine metadata. Development runs remain local diagnostics and must not be copied into marketing claims. +The initial 2D suite contains five deterministic cases covering exact fit, +steel plate, wood sheet, irregular remnant geometry, and finite-stock +exhaustion. Its primary metric is geometric utilization. "Estimated unused +area" is not disposable waste because kerf, lead-ins, pierces, and toolpaths are +outside this benchmark's scope. + The current immutable dataset release is -[v1.0.1](https://github.com/technodweep/cutnesting-benchmarks/releases/tag/v1.0.1). +[v1.1.0](https://github.com/technodweep/cutnesting-benchmarks/releases/tag/v1.1.0). +This release adds the initial public 2D suite without changing the immutable 1D +v1 inputs. Release +[`v1.0.1`](https://github.com/technodweep/cutnesting-benchmarks/releases/tag/v1.0.1) +remains the source for the published 1D result. Release `v1.0.0` is superseded because Windows checkout line-ending conversion prevented byte-for-byte template verification; benchmark case semantics did not change in `v1.0.1`. diff --git a/SHA256SUMS b/SHA256SUMS index 2e318c7..4fb9985 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -1,57 +1,67 @@ 0320ed1ae154dac21719844b3b528eae7c4317145bcfe527047917755f957ddb datasets/1d/v1/cases/1d-kerf-exact-fit.json 053f4fb4b195271d458c8cffb3340dee3b90cbf577a4ade64babf73a559fe06f datasets/1d/v1/cases/1d-steel-fabrication.json +092be2892d2dfd6811947879bacbc17d73163a163b8bee4fa2f7e41379c8697f samples/dxf/unitless-rectangle.dxf 0af41137f18d762612180d83e5829e12b6c08c0cf8e48475ed14ce809af5ceb8 datasets/1d/v1/cases/1d-aluminium-profile.json 0d7fdb8681b884bd548e656d281453537d892269cc430e77531acb989fb8ca7b samples/README.md 10370f0d118b25a5845ab3272b3048d78ed431c85964319da538424d7c8e0787 datasets/1d/v1/manifest.json 110a2ea93050ba4e4c3e2844a3a80360a1834c34fa6c85140caa5ce1d1c5a33d datasets/1d/v1/cases/1d-asymmetric-trim.json +12b54b0d2f112eebbef39d13424b586574ec200a74dcb757c5deb472f77ec0f6 samples/nc1/radius-too-large.nc1 227d419d4d0bb2eeb7165f6aa69c25ac9c80ef0d6496fb8cfc2fb5cdd08a725e datasets/1d/v1/cases/1d-mixed-finite-stock.json 2291cdc07004e2ceeba3b68a061dc705f0c4e0d43a09e8574bd46a37aff08277 templates/1d-wood.csv +26ff9196bfed893627627676cd65f1b79238e04bc9f0a911a031e11a6ca54236 datasets/2d/v1/cases/2d-irregular-remnant-with-void.json +2b95191fd5257e96d6a23fd25b3c5e21c0de53f359fa5711d3773f74db7540e5 schemas/2d-result.schema.json 2e831894024b572c937b6910d640d750d05da7d551cadaf4443caa412ed18dfd datasets/1d/v1/cases/1d-offcut-threshold.json -3538cbbbc6fc2bfafd81a7a0dafbd3541d5de5adf59330eac0dee844272b0bf2 scripts/validate-repository.mjs +340f79e38c1e9c54e4c4f7fa7a10f779595171562b39d69b86b7ce04c3b5a708 samples/nc1/unsupported-profile.nc1 379c6258142310a26b56e19a12090219a7842230c93043add22165d92543ed8a datasets/1d/v1/cases/1d-oversized-cut.json 3872aa21644f029e59717c5d383b8f56cae2ddc46f714569658504c3f9b0e7ba CONTRIBUTING.md 389631286dcf8f56adac4738d308f494a2d5a07739ece075b328f523b2f0dfde templates/2d-sheet-and-plate.xlsx +3c1ac369feffeb30b7661cb30c25415430e348c589c1e5ee8e618a18a0129b88 scripts/validate-repository.mjs 3f9b228cb1790199f4f5d57c0c5273d0bc31afc56085cf84a45f18d62291c55a templates/definitions.json +3fa1679686c4089d3f40cf5c775c44fd13a86c73ae0b8248dc6d09abc4d19b73 datasets/2d/v1/cases/2d-exact-square-grid.json 4075b6de75729e73d7c885871b65ed82977f2cb410da0fda05a8a8ff3acfb12c datasets/1d/v1/cases/1d-high-piece-count.json -416bdf8ab60a5c061d0f03bb3b6b7ca72d3de33a13b0be2ce248d6905590af56 samples/dxf/self-intersection.dxf 469b6ae250e6ed87b69eb74c4a4465a5a115d4f9177f43ff05ea3f8c49305cba docs/dstv-nc1-supported-subset.md +4e6d54ec0510235190e6d0c99adc0723b178bd96eba8cc56432588a7ded33054 datasets/2d/v1/manifest.json 530c7acb6679f73e273f6837c91113de1a4ac4f891340137186237e921238411 templates/1d-pipe.csv 54d511cc31cd3abf7bfcdff8b5375fbf6380d641e0613d6da74315ac35131fb0 templates/1d-aluminium.csv 575c34472b5be7655cec94cbf64e3cf10d4e1fbb48b3dd7379d1497d32f7c9d2 datasets/1d/v1/cases/1d-stock-exhausted.json 5c6a1002bfe7181ff28461564609d19833dd0619600949d5f19fa02adcc61d4d templates/README.md +5e5dfadffe824f61c3245ecfb43e5a765efbc9f33e25398a5eb1bacb3cfb79dc schemas/2d-case.schema.json 61515a9f1f98ce6615cee16851055fb5ee0c6c1e8c8c461a99c960aeaef6a6c8 .gitattributes 62a4cd80cacd5038ee4db3aa59f2eb505f974911f8073f8e3b7aee466b65766a SECURITY.md 672ed6618f1e15138e5a7aa747c2a84671664ec83b5062c17f1d96f763ba9e53 methodology/v1.md 67ea1f3f5aa0366ba04cbfa4d40a7ddb6e51112cfb49e3cbf4b705f2473473b4 datasets/1d/v1/cases/1d-exact-fit.json 68bf7443fae19274bad90ab0ee55f571fb2387b2dbeb3cfa04bba408b1c4f3b5 schemas/1d-result.schema.json -69e469f4aa3f7488fcd4e35c6daf1e07523b9ed23d742610c8c4870c99951edd README.md 7225ad3c3de17ec306e427b4f9880cddc19460f9e90bdb4443269ceb59a05239 datasets/README.md -754fe4464a7ea94ab81eebe10a69f07ccb27abace529301fab6673b2e98a648c samples/nc1/unsupported-profile.nc1 +730ec1858fcd0d8639002409c367b3d49b3627a6a81f7a1b3eb810fc3efbcb75 datasets/2d/v1/cases/2d-steel-plate-grid.json +77dfc7641646c9f915ee54d20bd0b1ea5d66d0c6eeffa8f743e1516deb22791c samples/dxf/self-intersection.dxf 782d465d88d25f1dffd6f9fafa1620b6607aa1f6bf96c73c379f15b17992be0b results/1d/v1.0.1/README.md 7dc97fd0976ecc0a6bcc852fe44c5a13f8bae4b879866a440b14ac180fc25132 results/README.md 8b616e1bb088f6fb56910aa453e74aaae1c5411928f8622b25374c3eec0d1e80 LICENSE 8c899cfafd3434d17c6b3530160924e6b3aae38c8247e02c17d151af0e8bfb4d scripts/generate-templates.mjs -8e4cbbe934b9f3f1ff4478ec7387ba22b042966c3df45b08d2e43cda7f2c771e samples/dxf/unsupported-entity.dxf 95f51b0bbfbcde9d53690fdf5055714a0b7ada002c76b799f907ef0e70c6b361 datasets/1d/v1/cases/1d-pipe-batch.json 988e03e819c1fd5064f1a48e1cf72b9e11a5b1b067630234f4647363b69901f3 docs/repository-contract.md -9cc055698c8c63b83eda5b2bdf21e57b49a80989365730438f9bc1fdeb349c9e samples/dxf/unitless-open-contour.dxf -9e47e498a7f75119e124d12613407228452a0beb4041086db8046020aade65da samples/nc1/radius-too-large.nc1 9e5ade7e935cf7a1a353f477b3b5d01ae01dc0bb4e69da13ce7d9cabf4fd86cb results/1d/v1.0.1/reference-windows-x64-i5-9400f.summary.csv -a34257ca65ac5f44b99f05b2d180931de5796c0f78fea81d29d94987b16717d4 samples/dxf/layer-block-insert.dxf -a4980b24140bd58dda422718e4b189f208715976b670c33f257c224b20c9942f samples/nc1/plate-radius.nc1 +9e7c7dad84df75e01601c22b4afa3458fd1553d0bfb93afb475c951079a18383 samples/dxf/unitless-open-contour.dxf +a02e7292f9f295f91a704467aef00752597f65cf666a3e9ef2131caafc0798b5 package.json +a574cffc3eb42f427a035157e78948adc13d4694907ceaaf0a6765dd6d0df4b7 datasets/2d/v1/cases/2d-wood-panel-grid.json aac5abb1eacaee94944a150db7d0facf886c71a26def8a36e4be7db3137f3bfc .gitignore af57dba5ee88fb4ce9aee2b1d5b6dfbfb784981a43c25c41f8e6ce9cc09cfc5a results/1d/v1.0.1/reference-windows-x64-i5-9400f.json c4aa3a77b37562ab4206221aab32cf68a89c3de33f411533c08547097d61a7a2 templates/1d-steel.csv c680e32b10dee44df1f84b3f57e8c7c75591e01ee2686ffc53ec9ce7e05e25dc datasets/1d/v1/cases/1d-cost-tradeoff.json -c835f63b88d549850553fb6c9ffe01c3c9cfc197b6a3c74334deeb6cc2c8f896 package.json cb1136ac89f569865b72f8af974ec059abe7914013f0c32f1396d8b99e874a28 schemas/1d-case.schema.json cb6c36568a75c8d10b06ec7d8731789e56023b11cc8d498cfca57703c13a9ad1 datasets/1d/v1/cases/1d-wood-furniture.json +cd13e90119f89a461de8d5a7d14bbe96cf8708110172ad2d019da5f376a262b8 CITATION.cff +cec44b1e590e04b98c94fe10b58e7f2ceef9a8e38c44c502fe7fa68c4eb993e4 samples/dxf/layer-block-insert.dxf +d06f3820426f42512831c0bafa3a82dfc26525ed72e3edffec02f53e37ad5e48 samples/nc1/plate-radius.nc1 +d1aabca6e789b514f0b0aa4a1cf11e07e29ab05abd4108543b7c45166f10dc81 CHANGELOG.md d52a2c32095f4035f5d23002f14a2310548ee469f1a9175a8c893362f653fbc6 schemas/manifest.schema.json -da697e0db4946a0c42bb8aa4aa8c65546561ec8434fc52ead383804fb087fcce CHANGELOG.md -e5067d3ddbfbffc4a53bc184199a1ff67c64bac88c83ce3fb9b4549010f40d85 CITATION.cff +d8c42e7b49d05cd04a59481dd71d87654bbf2592ed8cd7cf3cd5bd86b365e432 samples/dxf/unsupported-entity.dxf +da6e8342856133226cad0ca6c6bca1e607497c30a1d016105b7d54f4ce254a70 datasets/2d/v1/cases/2d-stock-exhausted.json e603eb378f7453f5e14ce87bc5ea0285b93f0fbce3a6edb225190df987ebdace samples/manifest.json e6dc9fea66d59eb62cdc747718fc784bb962952ff7501280b15ba6472042fc36 templates/2d-sheet-and-plate.csv +eeb3bc4f2215d6ed7e29a743946c4075fd77f23316cd3cdffe607cc38c99474d schemas/2d-manifest.schema.json f1ce8f5048968c79ad8e7406696389708e3fb51ee08626b23a80916b9e5d1984 LICENSE-DATA.md f27e13aa8a94fc76ce4766ed9f3910ad00e28cfd48d6ff42177e11dc799cc65a scripts/verify-checksums.mjs -f839d9a035286c9dddde94329a0bbffd0d0880eaf985687b1588cf1020c17866 samples/dxf/unitless-rectangle.dxf +f8ee19613ce597319f2cb9b1a89c9dbe1257b01a9742145633bba14170078c0a methodology/2d-v1.md +fa23d582976607d64ce1497aeebdaa43eb43adc68a9f46e9866d316c17b4bfd3 README.md ff9ea556c36d48d767b4d6f87f9aa61e58248e8683fa90d3ee4a8c7ff0d0604a .github/workflows/validate.yml diff --git a/datasets/2d/v1/cases/2d-exact-square-grid.json b/datasets/2d/v1/cases/2d-exact-square-grid.json new file mode 100644 index 0000000..9c2db4e --- /dev/null +++ b/datasets/2d/v1/cases/2d-exact-square-grid.json @@ -0,0 +1,66 @@ +{ + "schemaVersion": "cutnesting.benchmark.case.2d.v1", + "id": "2d-exact-square-grid", + "title": "Exact square grid", + "description": "Four equal square parts exactly cover one square sheet with zero gap and edge margin, providing a minimal area-accounting fixture.", + "difficulty": "small", + "source": "synthetic", + "license": "CC-BY-4.0", + "provenance": "Created by Technodweep as a deterministic exact-area and complete-placement fixture.", + "input": { + "schemaVersion": 1, + "jobId": "benchmark-2d-exact-square-grid", + "unit": "mm", + "deterministicSeed": 2026072201, + "runtimeHost": "localhost", + "lockedPlacements": [], + "parts": [ + { + "id": "square-panel", + "name": "Square panel", + "quantity": 4, + "geometry": { + "outer": [ + { "x": 0, "y": 0 }, + { "x": 500, "y": 0 }, + { "x": 500, "y": 500 }, + { "x": 0, "y": 500 } + ], + "holes": [] + }, + "allowedRotations": [0, 90], + "materialGroup": "synthetic::exact::1", + "priority": 0 + } + ], + "sheets": [ + { + "id": "square-sheet", + "name": "1000 x 1000 sheet", + "width": 1000, + "height": 1000, + "quantity": 1, + "materialGroup": "synthetic::exact::1", + "cost": null, + "isRemnant": false, + "geometry": { + "outer": [ + { "x": 0, "y": 0 }, + { "x": 1000, "y": 0 }, + { "x": 1000, "y": 1000 }, + { "x": 0, "y": 1000 } + ], + "holes": [] + } + } + ], + "options": { + "partGap": 0, + "edgeMargin": 0, + "quality": "balanced", + "objective": "fewest_sheets", + "timeLimitMs": 10000, + "preferRemnants": false + } + } +} diff --git a/datasets/2d/v1/cases/2d-irregular-remnant-with-void.json b/datasets/2d/v1/cases/2d-irregular-remnant-with-void.json new file mode 100644 index 0000000..e7d07e7 --- /dev/null +++ b/datasets/2d/v1/cases/2d-irregular-remnant-with-void.json @@ -0,0 +1,75 @@ +{ + "schemaVersion": "cutnesting.benchmark.case.2d.v1", + "id": "2d-irregular-remnant-with-void", + "title": "Irregular remnant with a void", + "description": "Four square tiles must fit inside a concave L-shaped reusable remnant while avoiding an internal stock void and preserving edge margin.", + "difficulty": "medium", + "source": "synthetic", + "license": "CC-BY-4.0", + "provenance": "Created by Technodweep to validate true polygon area, concave stock, void avoidance and remnant use.", + "input": { + "schemaVersion": 1, + "jobId": "benchmark-2d-irregular-remnant", + "unit": "mm", + "deterministicSeed": 2026072204, + "runtimeHost": "localhost", + "lockedPlacements": [], + "parts": [ + { + "id": "tile", + "name": "Tile", + "quantity": 4, + "geometry": { + "outer": [ + { "x": 0, "y": 0 }, + { "x": 20, "y": 0 }, + { "x": 20, "y": 20 }, + { "x": 0, "y": 20 } + ], + "holes": [] + }, + "allowedRotations": [0, 90], + "materialGroup": "steel::s275::6", + "priority": 0 + } + ], + "sheets": [ + { + "id": "l-remnant", + "name": "L remnant", + "width": 100, + "height": 100, + "quantity": 1, + "materialGroup": "steel::s275::6", + "cost": 0, + "isRemnant": true, + "geometry": { + "outer": [ + { "x": 0, "y": 0 }, + { "x": 100, "y": 0 }, + { "x": 100, "y": 40 }, + { "x": 40, "y": 40 }, + { "x": 40, "y": 100 }, + { "x": 0, "y": 100 } + ], + "holes": [ + [ + { "x": 10, "y": 10 }, + { "x": 20, "y": 10 }, + { "x": 20, "y": 20 }, + { "x": 10, "y": 20 } + ] + ] + } + } + ], + "options": { + "partGap": 2, + "edgeMargin": 1, + "quality": "balanced", + "objective": "reusable_remnants", + "timeLimitMs": 10000, + "preferRemnants": true + } + } +} diff --git a/datasets/2d/v1/cases/2d-steel-plate-grid.json b/datasets/2d/v1/cases/2d-steel-plate-grid.json new file mode 100644 index 0000000..259c20f --- /dev/null +++ b/datasets/2d/v1/cases/2d-steel-plate-grid.json @@ -0,0 +1,66 @@ +{ + "schemaVersion": "cutnesting.benchmark.case.2d.v1", + "id": "2d-steel-plate-grid", + "title": "Steel plate panel grid", + "description": "Twelve repeated steel panels are nested on one 3000 by 1500 millimetre plate with a 10 millimetre part gap and edge margin.", + "difficulty": "medium", + "source": "synthetic", + "license": "CC-BY-4.0", + "provenance": "Created by Technodweep as a realistic-scale synthetic steel plate utilization case.", + "input": { + "schemaVersion": 1, + "jobId": "benchmark-2d-steel-plate-grid", + "unit": "mm", + "deterministicSeed": 2026072202, + "runtimeHost": "localhost", + "lockedPlacements": [], + "parts": [ + { + "id": "steel-panel-730x480", + "name": "Steel panel 730 x 480", + "quantity": 12, + "geometry": { + "outer": [ + { "x": 0, "y": 0 }, + { "x": 730, "y": 0 }, + { "x": 730, "y": 480 }, + { "x": 0, "y": 480 } + ], + "holes": [] + }, + "allowedRotations": [0, 90], + "materialGroup": "steel::s275::6", + "priority": 0 + } + ], + "sheets": [ + { + "id": "steel-plate-3000x1500", + "name": "S275 plate 3000 x 1500 x 6", + "width": 3000, + "height": 1500, + "quantity": 1, + "materialGroup": "steel::s275::6", + "cost": 180, + "isRemnant": false, + "geometry": { + "outer": [ + { "x": 0, "y": 0 }, + { "x": 3000, "y": 0 }, + { "x": 3000, "y": 1500 }, + { "x": 0, "y": 1500 } + ], + "holes": [] + } + } + ], + "options": { + "partGap": 10, + "edgeMargin": 10, + "quality": "balanced", + "objective": "fewest_sheets", + "timeLimitMs": 10000, + "preferRemnants": false + } + } +} diff --git a/datasets/2d/v1/cases/2d-stock-exhausted.json b/datasets/2d/v1/cases/2d-stock-exhausted.json new file mode 100644 index 0000000..5815da6 --- /dev/null +++ b/datasets/2d/v1/cases/2d-stock-exhausted.json @@ -0,0 +1,66 @@ +{ + "schemaVersion": "cutnesting.benchmark.case.2d.v1", + "id": "2d-stock-exhausted", + "title": "Finite sheet stock exhausted", + "description": "Three large panels compete for one finite sheet that can hold only one, requiring two explicit stock-exhausted results.", + "difficulty": "small", + "source": "synthetic", + "license": "CC-BY-4.0", + "provenance": "Created by Technodweep as a deterministic 2D feasibility and unplaced-part reporting boundary.", + "input": { + "schemaVersion": 1, + "jobId": "benchmark-2d-stock-exhausted", + "unit": "mm", + "deterministicSeed": 2026072205, + "runtimeHost": "localhost", + "lockedPlacements": [], + "parts": [ + { + "id": "large-panel", + "name": "Large panel", + "quantity": 3, + "geometry": { + "outer": [ + { "x": 0, "y": 0 }, + { "x": 60, "y": 0 }, + { "x": 60, "y": 60 }, + { "x": 0, "y": 60 } + ], + "holes": [] + }, + "allowedRotations": [0, 90], + "materialGroup": "steel::s275::6", + "priority": 0 + } + ], + "sheets": [ + { + "id": "single-plate", + "name": "Single 100 x 100 plate", + "width": 100, + "height": 100, + "quantity": 1, + "materialGroup": "steel::s275::6", + "cost": 10, + "isRemnant": false, + "geometry": { + "outer": [ + { "x": 0, "y": 0 }, + { "x": 100, "y": 0 }, + { "x": 100, "y": 100 }, + { "x": 0, "y": 100 } + ], + "holes": [] + } + } + ], + "options": { + "partGap": 2, + "edgeMargin": 0, + "quality": "balanced", + "objective": "fewest_sheets", + "timeLimitMs": 10000, + "preferRemnants": false + } + } +} diff --git a/datasets/2d/v1/cases/2d-wood-panel-grid.json b/datasets/2d/v1/cases/2d-wood-panel-grid.json new file mode 100644 index 0000000..4fe87fd --- /dev/null +++ b/datasets/2d/v1/cases/2d-wood-panel-grid.json @@ -0,0 +1,66 @@ +{ + "schemaVersion": "cutnesting.benchmark.case.2d.v1", + "id": "2d-wood-panel-grid", + "title": "Wood sheet panel grid", + "description": "Twelve cabinet-sized panels are nested on one 2440 by 1220 millimetre sheet with a 5 millimetre tool gap and edge margin.", + "difficulty": "medium", + "source": "synthetic", + "license": "CC-BY-4.0", + "provenance": "Created by Technodweep as a realistic-scale synthetic sheet-goods utilization case.", + "input": { + "schemaVersion": 1, + "jobId": "benchmark-2d-wood-panel-grid", + "unit": "mm", + "deterministicSeed": 2026072203, + "runtimeHost": "localhost", + "lockedPlacements": [], + "parts": [ + { + "id": "wood-panel-600x400", + "name": "Cabinet panel 600 x 400", + "quantity": 12, + "geometry": { + "outer": [ + { "x": 0, "y": 0 }, + { "x": 600, "y": 0 }, + { "x": 600, "y": 400 }, + { "x": 0, "y": 400 } + ], + "holes": [] + }, + "allowedRotations": [0, 90], + "materialGroup": "plywood::birch::18", + "priority": 0 + } + ], + "sheets": [ + { + "id": "wood-sheet-2440x1220", + "name": "Birch plywood 2440 x 1220 x 18", + "width": 2440, + "height": 1220, + "quantity": 1, + "materialGroup": "plywood::birch::18", + "cost": 95, + "isRemnant": false, + "geometry": { + "outer": [ + { "x": 0, "y": 0 }, + { "x": 2440, "y": 0 }, + { "x": 2440, "y": 1220 }, + { "x": 0, "y": 1220 } + ], + "holes": [] + } + } + ], + "options": { + "partGap": 5, + "edgeMargin": 5, + "quality": "balanced", + "objective": "fewest_sheets", + "timeLimitMs": 10000, + "preferRemnants": false + } + } +} diff --git a/datasets/2d/v1/manifest.json b/datasets/2d/v1/manifest.json new file mode 100644 index 0000000..6a6a74a --- /dev/null +++ b/datasets/2d/v1/manifest.json @@ -0,0 +1,64 @@ +{ + "schemaVersion": "cutnesting.benchmark.manifest.2d.v1", + "suiteVersion": "1.0.0", + "methodologyVersion": "1.0.0", + "dimension": "2d", + "description": "Synthetic two-dimensional nesting cases covering exact area accounting, realistic sheet and plate grids, irregular remnant geometry and finite-stock failure boundaries.", + "cases": [ + { + "id": "2d-exact-square-grid", + "file": "cases/2d-exact-square-grid.json", + "category": "exact", + "tags": ["exact-fit", "area-accounting", "deterministic"], + "expected": { + "minimumPlacedParts": 4, + "maximumUnplacedParts": 0, + "allowedSolverStatuses": ["completed"] + } + }, + { + "id": "2d-steel-plate-grid", + "file": "cases/2d-steel-plate-grid.json", + "category": "steel", + "tags": ["steel-plate", "part-gap", "edge-margin", "realistic-scale"], + "expected": { + "minimumPlacedParts": 12, + "maximumUnplacedParts": 0, + "allowedSolverStatuses": ["completed"] + } + }, + { + "id": "2d-wood-panel-grid", + "file": "cases/2d-wood-panel-grid.json", + "category": "wood", + "tags": ["sheet-goods", "part-gap", "edge-margin", "realistic-scale"], + "expected": { + "minimumPlacedParts": 12, + "maximumUnplacedParts": 0, + "allowedSolverStatuses": ["completed"] + } + }, + { + "id": "2d-irregular-remnant-with-void", + "file": "cases/2d-irregular-remnant-with-void.json", + "category": "irregular-remnant", + "tags": ["concave-stock", "stock-void", "reusable-remnant"], + "expected": { + "minimumPlacedParts": 4, + "maximumUnplacedParts": 0, + "allowedSolverStatuses": ["completed"] + } + }, + { + "id": "2d-stock-exhausted", + "file": "cases/2d-stock-exhausted.json", + "category": "failure-boundary", + "tags": ["finite-stock", "unplaced-parts", "feasibility"], + "expected": { + "minimumPlacedParts": 1, + "maximumUnplacedParts": 2, + "allowedSolverStatuses": ["completed"] + } + } + ] +} diff --git a/methodology/2d-v1.md b/methodology/2d-v1.md new file mode 100644 index 0000000..8858398 --- /dev/null +++ b/methodology/2d-v1.md @@ -0,0 +1,68 @@ +# CutNesting 2D Benchmark Methodology v1.0.0 + +## Scope + +This methodology measures geometric area utilization, placement feasibility, +solver invariants, determinism, and execution time for CutNesting's +two-dimensional production nesting engine on a fixed synthetic corpus. + +It does not compare products and does not claim that a returned layout is a +global optimum. The suite includes rectangular plate and sheet cases, an exact +fit, irregular remnant geometry with an internal void, and finite-stock +exhaustion. + +## Production engine boundary + +The native runner calls `optimize_nest_native`, a thin native wrapper around +the same `optimize_nest` production entry point compiled to WebAssembly for the +browser product. Native results are labeled `native-rust`; they are not +browser/WASM performance claims. + +## Area definitions + +- `totalSheetArea`: true polygon area of every used sheet, subtracting holes. +- `placedPartArea`: true polygon area of every placed part, subtracting holes. +- `estimatedUnusedArea`: total sheet area minus placed part area. +- `utilizationPercent`: placed part area divided by total sheet area. +- `estimatedUnusedAreaPercent`: 100 minus utilization percentage. + +Estimated unused area is a geometric planning measure. It is not disposable +manufacturing waste: the benchmark does not model kerf width, lead-ins, +pierces, common-line cutting, or generated toolpaths. Gaps and edge margins +constrain placement, but remain part of the estimated unused area. + +## Feasibility and validation + +The independent runner recomputes and checks: + +- Every requested part instance appears exactly once as placed or unplaced. +- Placed and unplaced parts reference declared definitions. +- Sheet and placement areas from polygon coordinates, including holes. +- Per-sheet and summary area accounting and utilization. +- Used-sheet, requested-part, placed-part, and unplaced-part counts. +- The production engine's invariant status and deterministic seed. +- Identical normalized layout output across all measured runs. +- Each case's allowed solver status and placement boundary. + +The production engine performs its own geometry and collision invariants. The +benchmark runner separately checks serialized output and area accounting. + +## Timing + +The default reference run performs three warmups followed by 30 measured +executions per case. Timing surrounds only the synchronous native production +engine call. Parsing and validation occur outside the timed interval. + +Reports include every raw duration plus p50 and p95. Runtime depends on the +recorded hardware and execution target, so it is a technical reference rather +than a cross-machine performance guarantee. + +## Reproducibility + +Each report records suite and methodology versions, source and dataset Git +commits and dirty state, exact dataset tag when available, SHA-256 hashes for +the manifest/cases/schemas, environment details, raw timings, normalized +metrics, the production result, and validation status. + +Public reference results must be generated from a clean source commit against +a clean, immutable public dataset tag. Dirty-tree runs are diagnostic only. diff --git a/package.json b/package.json index 6d5fbb5..7fad55c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cutnesting-benchmarks", - "version": "1.0.1", + "version": "1.1.0", "private": true, "type": "module", "description": "Public benchmark datasets and sample files for cutting and nesting workflows.", diff --git a/schemas/2d-case.schema.json b/schemas/2d-case.schema.json new file mode 100644 index 0000000..1a273c1 --- /dev/null +++ b/schemas/2d-case.schema.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cutnesting.com/schemas/benchmark-case-2d-v1.json", + "title": "CutNesting 2D benchmark case v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schemaVersion", + "id", + "title", + "description", + "difficulty", + "source", + "license", + "provenance", + "input" + ], + "properties": { + "schemaVersion": { "const": "cutnesting.benchmark.case.2d.v1" }, + "id": { "type": "string", "pattern": "^2d-[a-z0-9-]+$" }, + "title": { "type": "string", "minLength": 8 }, + "description": { "type": "string", "minLength": 40 }, + "difficulty": { "enum": ["small", "medium", "large"] }, + "source": { "const": "synthetic" }, + "license": { "const": "CC-BY-4.0" }, + "provenance": { "type": "string", "minLength": 20 }, + "input": { + "type": "object", + "required": [ + "schemaVersion", + "jobId", + "unit", + "deterministicSeed", + "runtimeHost", + "lockedPlacements", + "parts", + "sheets", + "options" + ], + "properties": { + "schemaVersion": { "const": 1 }, + "jobId": { "type": "string", "minLength": 1 }, + "unit": { "enum": ["mm", "cm", "meter", "inch", "feet"] }, + "deterministicSeed": { "type": "integer", "minimum": 0 }, + "runtimeHost": { "const": "localhost" }, + "lockedPlacements": { "type": "array" }, + "parts": { "type": "array", "minItems": 1 }, + "sheets": { "type": "array", "minItems": 1 }, + "options": { "type": "object" } + } + } + } +} diff --git a/schemas/2d-manifest.schema.json b/schemas/2d-manifest.schema.json new file mode 100644 index 0000000..536bdf1 --- /dev/null +++ b/schemas/2d-manifest.schema.json @@ -0,0 +1,75 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cutnesting.com/schemas/benchmark-manifest-2d-v1.json", + "title": "CutNesting 2D benchmark manifest v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schemaVersion", + "suiteVersion", + "methodologyVersion", + "dimension", + "description", + "cases" + ], + "properties": { + "schemaVersion": { "const": "cutnesting.benchmark.manifest.2d.v1" }, + "suiteVersion": { + "type": "string", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" + }, + "methodologyVersion": { + "type": "string", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" + }, + "dimension": { "const": "2d" }, + "description": { "type": "string", "minLength": 40 }, + "cases": { + "type": "array", + "minItems": 5, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "file", "category", "tags", "expected"], + "properties": { + "id": { "type": "string", "pattern": "^2d-[a-z0-9-]+$" }, + "file": { "type": "string", "pattern": "^cases/[a-z0-9-]+\\.json$" }, + "category": { + "enum": [ + "exact", + "steel", + "wood", + "irregular-remnant", + "failure-boundary" + ] + }, + "tags": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { "type": "string", "pattern": "^[a-z0-9-]+$" } + }, + "expected": { + "type": "object", + "additionalProperties": false, + "required": [ + "minimumPlacedParts", + "maximumUnplacedParts", + "allowedSolverStatuses" + ], + "properties": { + "minimumPlacedParts": { "type": "integer", "minimum": 0 }, + "maximumUnplacedParts": { "type": "integer", "minimum": 0 }, + "allowedSolverStatuses": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { "enum": ["completed"] } + } + } + } + } + } + } + } +} diff --git a/schemas/2d-result.schema.json b/schemas/2d-result.schema.json new file mode 100644 index 0000000..cff4f93 --- /dev/null +++ b/schemas/2d-result.schema.json @@ -0,0 +1,85 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://cutnesting.com/schemas/benchmark-result-2d-v1.json", + "title": "CutNesting 2D benchmark run v1", + "type": "object", + "additionalProperties": true, + "required": [ + "schemaVersion", + "suiteVersion", + "methodologyVersion", + "engine", + "timingPolicy", + "cases" + ], + "properties": { + "schemaVersion": { "const": "cutnesting.benchmark.run.2d.v1" }, + "suiteVersion": { "type": "string" }, + "methodologyVersion": { "type": "string" }, + "engine": { + "type": "object", + "required": ["name", "executionTarget", "productionEntryPoint"] + }, + "timingPolicy": { + "type": "object", + "required": ["warmupRuns", "measuredRuns"] + }, + "interpretation": { + "type": "object", + "required": ["primaryMetric", "unusedAreaMetric", "limitation"] + }, + "cases": { + "type": "array", + "minItems": 5, + "items": { + "type": "object", + "required": [ + "caseId", + "solverStatus", + "metrics", + "timing", + "validation", + "productionResult" + ], + "properties": { + "caseId": { "type": "string", "pattern": "^2d-[a-z0-9-]+$" }, + "solverStatus": { "const": "completed" }, + "metrics": { + "type": "object", + "required": [ + "sheetsUsed", + "totalParts", + "placedParts", + "unplacedParts", + "totalSheetArea", + "placedPartArea", + "estimatedUnusedArea", + "utilizationPercent", + "estimatedUnusedAreaPercent" + ] + }, + "timing": { + "type": "object", + "required": ["rawSolveTimeMs", "p50SolveTimeMs", "p95SolveTimeMs"] + }, + "validation": { + "type": "object", + "required": [ + "valid", + "errors", + "areaAccountingValid", + "solverInvariantsValid" + ], + "properties": { + "valid": { "const": true }, + "errors": { "type": "array", "maxItems": 0 }, + "areaAccountingValid": { "const": true }, + "solverInvariantsValid": { "const": true } + } + }, + "productionResult": { "type": "object" } + } + } + } + } +} diff --git a/scripts/validate-repository.mjs b/scripts/validate-repository.mjs index 0a9b8b0..e91e2f4 100644 --- a/scripts/validate-repository.mjs +++ b/scripts/validate-repository.mjs @@ -16,7 +16,9 @@ async function readJson(path) { try { return JSON.parse(await readFile(path, "utf8")); } catch (error) { - failures.push(`${relative(root, path)} is not valid JSON: ${error.message}`); + failures.push( + `${relative(root, path)} is not valid JSON: ${error.message}`, + ); return null; } } @@ -33,14 +35,29 @@ async function validateCases() { const manifest = await readJson(join(suiteDir, "manifest.json")); if (!manifest) return; - check(manifest.schemaVersion === "cutnesting.benchmark.manifest.v1", "Unexpected suite schemaVersion."); - check(manifest.suiteVersion === "1.0.0", "The v1 directory must contain suite version 1.0.0."); - check(manifest.dimension === "1d", "The current manifest must declare dimension 1d."); - check(Array.isArray(manifest.cases) && manifest.cases.length >= 12, "The 1D suite must contain at least 12 cases."); + check( + manifest.schemaVersion === "cutnesting.benchmark.manifest.v1", + "Unexpected suite schemaVersion.", + ); + check( + manifest.suiteVersion === "1.0.0", + "The v1 directory must contain suite version 1.0.0.", + ); + check( + manifest.dimension === "1d", + "The current manifest must declare dimension 1d.", + ); + check( + Array.isArray(manifest.cases) && manifest.cases.length >= 12, + "The 1D suite must contain at least 12 cases.", + ); const ids = new Set(); for (const entry of manifest.cases ?? []) { - check(typeof entry.id === "string" && /^1d-[a-z0-9-]+$/.test(entry.id), `Invalid case id: ${entry.id}`); + check( + typeof entry.id === "string" && /^1d-[a-z0-9-]+$/.test(entry.id), + `Invalid case id: ${entry.id}`, + ); check(!ids.has(entry.id), `Duplicate case id: ${entry.id}`); ids.add(entry.id); @@ -51,36 +68,237 @@ async function validateCases() { if (!data) continue; check(data.id === entry.id, `${entry.id} does not match its case file id.`); - check(data.schemaVersion === "cutnesting.benchmark.case.1d.v1", `${entry.id} has the wrong schemaVersion.`); - check(data.source === "synthetic", `${entry.id} must declare synthetic source data.`); + check( + data.schemaVersion === "cutnesting.benchmark.case.1d.v1", + `${entry.id} has the wrong schemaVersion.`, + ); + check( + data.source === "synthetic", + `${entry.id} must declare synthetic source data.`, + ); check(data.license === "CC-BY-4.0", `${entry.id} must declare CC-BY-4.0.`); - check(typeof data.provenance === "string" && data.provenance.length >= 20, `${entry.id} needs meaningful provenance.`); - check(data.input?.runtimeHost === "localhost", `${entry.id} must use localhost for the benchmark request.`); - check(Array.isArray(data.input?.stock) && data.input.stock.length > 0, `${entry.id} needs stock definitions.`); - check(Array.isArray(data.input?.cuts) && data.input.cuts.length > 0, `${entry.id} needs cut definitions.`); + check( + typeof data.provenance === "string" && data.provenance.length >= 20, + `${entry.id} needs meaningful provenance.`, + ); + check( + data.input?.runtimeHost === "localhost", + `${entry.id} must use localhost for the benchmark request.`, + ); + check( + Array.isArray(data.input?.stock) && data.input.stock.length > 0, + `${entry.id} needs stock definitions.`, + ); + check( + Array.isArray(data.input?.cuts) && data.input.cuts.length > 0, + `${entry.id} needs cut definitions.`, + ); for (const stock of data.input?.stock ?? []) { - check(Number.isFinite(stock.length) && stock.length > 0, `${entry.id} contains invalid stock length.`); - check(stock.quantity === null || (Number.isInteger(stock.quantity) && stock.quantity > 0), `${entry.id} contains invalid stock quantity.`); + check( + Number.isFinite(stock.length) && stock.length > 0, + `${entry.id} contains invalid stock length.`, + ); + check( + stock.quantity === null || + (Number.isInteger(stock.quantity) && stock.quantity > 0), + `${entry.id} contains invalid stock quantity.`, + ); } for (const cut of data.input?.cuts ?? []) { - check(Number.isFinite(cut.length) && cut.length > 0, `${entry.id} contains invalid cut length.`); - check(Number.isInteger(cut.quantity) && cut.quantity > 0, `${entry.id} contains invalid cut quantity.`); + check( + Number.isFinite(cut.length) && cut.length > 0, + `${entry.id} contains invalid cut length.`, + ); + check( + Number.isInteger(cut.quantity) && cut.quantity > 0, + `${entry.id} contains invalid cut quantity.`, + ); + } + check( + entry.expected.minimumUnplacedParts <= + entry.expected.maximumUnplacedParts, + `${entry.id} has an inverted unplaced range.`, + ); + } + + const caseFiles = (await readdir(join(suiteDir, "cases"))).filter((name) => + name.endsWith(".json"), + ); + check( + caseFiles.length === ids.size, + "The cases directory and manifest must contain the same number of JSON cases.", + ); +} + +async function validate2dCases() { + const suiteDir = join(root, "datasets", "2d", "v1"); + const manifest = await readJson(join(suiteDir, "manifest.json")); + if (!manifest) return; + + check( + manifest.schemaVersion === "cutnesting.benchmark.manifest.2d.v1", + "Unexpected 2D suite schemaVersion.", + ); + check( + manifest.suiteVersion === "1.0.0", + "The 2D v1 directory must contain suite version 1.0.0.", + ); + check( + manifest.dimension === "2d", + "The 2D manifest must declare dimension 2d.", + ); + check( + Array.isArray(manifest.cases) && manifest.cases.length >= 5, + "The 2D suite must contain at least five cases.", + ); + + const ids = new Set(); + for (const entry of manifest.cases ?? []) { + check( + typeof entry.id === "string" && /^2d-[a-z0-9-]+$/.test(entry.id), + `Invalid 2D case id: ${entry.id}`, + ); + check(!ids.has(entry.id), `Duplicate 2D case id: ${entry.id}`); + ids.add(entry.id); + + const casePath = safeChild(suiteDir, entry.file); + check(Boolean(casePath), `${entry.id} has an unsafe file path.`); + if (!casePath) continue; + const data = await readJson(casePath); + if (!data) continue; + + check(data.id === entry.id, `${entry.id} does not match its case file id.`); + check( + data.schemaVersion === "cutnesting.benchmark.case.2d.v1", + `${entry.id} has the wrong schemaVersion.`, + ); + check( + data.source === "synthetic", + `${entry.id} must declare synthetic source data.`, + ); + check(data.license === "CC-BY-4.0", `${entry.id} must declare CC-BY-4.0.`); + check( + typeof data.provenance === "string" && data.provenance.length >= 20, + `${entry.id} needs meaningful provenance.`, + ); + check( + data.input?.runtimeHost === "localhost", + `${entry.id} must use localhost for the benchmark request.`, + ); + check( + Number.isInteger(data.input?.deterministicSeed), + `${entry.id} must declare an integer deterministic seed.`, + ); + check( + Array.isArray(data.input?.lockedPlacements) && + data.input.lockedPlacements.length === 0, + `${entry.id} must not depend on locked placements.`, + ); + check( + Array.isArray(data.input?.parts) && data.input.parts.length > 0, + `${entry.id} needs part definitions.`, + ); + check( + Array.isArray(data.input?.sheets) && data.input.sheets.length > 0, + `${entry.id} needs sheet definitions.`, + ); + + let requestedParts = 0; + for (const part of data.input?.parts ?? []) { + check( + Number.isInteger(part.quantity) && part.quantity > 0, + `${entry.id} contains invalid part quantity.`, + ); + requestedParts += Number.isInteger(part.quantity) ? part.quantity : 0; + checkGeometry(part.geometry, `${entry.id} part ${part.id}`); + check( + Array.isArray(part.allowedRotations) && + part.allowedRotations.length > 0, + `${entry.id} part ${part.id} needs allowed rotations.`, + ); + } + for (const sheet of data.input?.sheets ?? []) { + check( + Number.isInteger(sheet.quantity) && sheet.quantity > 0, + `${entry.id} contains invalid sheet quantity.`, + ); + checkGeometry(sheet.geometry, `${entry.id} sheet ${sheet.id}`); } - check(entry.expected.minimumUnplacedParts <= entry.expected.maximumUnplacedParts, `${entry.id} has an inverted unplaced range.`); + check( + Number.isInteger(entry.expected?.minimumPlacedParts) && + entry.expected.minimumPlacedParts >= 0 && + entry.expected.minimumPlacedParts <= requestedParts, + `${entry.id} has an invalid minimum placed count.`, + ); + check( + Number.isInteger(entry.expected?.maximumUnplacedParts) && + entry.expected.maximumUnplacedParts >= 0 && + entry.expected.maximumUnplacedParts <= requestedParts, + `${entry.id} has an invalid maximum unplaced count.`, + ); + check( + Array.isArray(entry.expected?.allowedSolverStatuses) && + entry.expected.allowedSolverStatuses.length > 0, + `${entry.id} needs an allowed solver status.`, + ); } - const caseFiles = (await readdir(join(suiteDir, "cases"))).filter((name) => name.endsWith(".json")); - check(caseFiles.length === ids.size, "The cases directory and manifest must contain the same number of JSON cases."); + const caseFiles = (await readdir(join(suiteDir, "cases"))).filter((name) => + name.endsWith(".json"), + ); + check( + caseFiles.length === ids.size, + "The 2D cases directory and manifest must contain the same number of JSON cases.", + ); +} + +function checkGeometry(geometry, label) { + const outerArea = ringArea(geometry?.outer); + const holeArea = (geometry?.holes ?? []).reduce( + (total, ring) => total + ringArea(ring), + 0, + ); + check( + Number.isFinite(outerArea) && outerArea > 0, + `${label} needs a valid outer polygon.`, + ); + check( + Number.isFinite(holeArea) && holeArea >= 0 && outerArea > holeArea, + `${label} has invalid hole area.`, + ); +} + +function ringArea(points) { + if (!Array.isArray(points) || points.length < 3) return Number.NaN; + let twiceArea = 0; + for (let index = 0; index < points.length; index += 1) { + const current = points[index]; + const next = points[(index + 1) % points.length]; + if (![current?.x, current?.y, next?.x, next?.y].every(Number.isFinite)) { + return Number.NaN; + } + twiceArea += current.x * next.y - next.x * current.y; + } + return Math.abs(twiceArea / 2); } async function validateSamples() { const sampleDir = join(root, "samples"); const manifest = await readJson(join(sampleDir, "manifest.json")); if (!manifest) return; - check(manifest.source === "synthetic", "Sample manifest must declare synthetic source data."); - check(manifest.license === "CC-BY-4.0", "Sample manifest must declare CC-BY-4.0."); - check(Array.isArray(manifest.fixtures) && manifest.fixtures.length >= 8, "Expected at least eight geometry fixtures."); + check( + manifest.source === "synthetic", + "Sample manifest must declare synthetic source data.", + ); + check( + manifest.license === "CC-BY-4.0", + "Sample manifest must declare CC-BY-4.0.", + ); + check( + Array.isArray(manifest.fixtures) && manifest.fixtures.length >= 8, + "Expected at least eight geometry fixtures.", + ); const ids = new Set(); for (const fixture of manifest.fixtures ?? []) { @@ -91,39 +309,65 @@ async function validateSamples() { if (!fixturePath) continue; try { const info = await stat(fixturePath); - check(info.isFile() && info.size > 0, `${fixture.id} must reference a non-empty file.`); + check( + info.isFile() && info.size > 0, + `${fixture.id} must reference a non-empty file.`, + ); } catch { failures.push(`${fixture.id} references a missing file: ${fixture.path}`); } - check(extname(fixture.path).slice(1) === fixture.format, `${fixture.id} format does not match its extension.`); + check( + extname(fixture.path).slice(1) === fixture.format, + `${fixture.id} format does not match its extension.`, + ); } } async function validateTemplates() { const templateDir = join(root, "templates"); - const names = (await readdir(templateDir)).filter((name) => name.endsWith(".csv")); + const names = (await readdir(templateDir)).filter((name) => + name.endsWith(".csv"), + ); check(names.length >= 5, "Expected four 1D CSVs and one 2D CSV."); for (const name of names) { - const [header, ...rows] = (await readFile(join(templateDir, name), "utf8")).trim().split(/\r?\n/); + const [header, ...rows] = (await readFile(join(templateDir, name), "utf8")) + .trim() + .split(/\r?\n/); check(rows.length > 0, `${name} must contain sample rows.`); if (name.startsWith("1d-")) { - check(header === "type,label,length_mm,quantity,cost,priority", `${name} has an incompatible 1D header.`); - check(rows.some((row) => row.startsWith("stock,")), `${name} needs a stock row.`); - check(rows.some((row) => row.startsWith("cut,")), `${name} needs a cut row.`); + check( + header === "type,label,length_mm,quantity,cost,priority", + `${name} has an incompatible 1D header.`, + ); + check( + rows.some((row) => row.startsWith("stock,")), + `${name} needs a stock row.`, + ); + check( + rows.some((row) => row.startsWith("cut,")), + `${name} needs a cut row.`, + ); } else { - check(header === "type,name,width,height,quantity,unit,material,grade,thickness,cost,is_remnant,grain,rotation,priority", `${name} has an incompatible 2D header.`); - check(rows.some((row) => row.startsWith("part,")), `${name} needs a part row.`); - check(rows.some((row) => row.startsWith("stock,")), `${name} needs a stock row.`); + check( + header === + "type,name,width,height,quantity,unit,material,grade,thickness,cost,is_remnant,grain,rotation,priority", + `${name} has an incompatible 2D header.`, + ); + check( + rows.some((row) => row.startsWith("part,")), + `${name} needs a part row.`, + ); + check( + rows.some((row) => row.startsWith("stock,")), + `${name} needs a stock row.`, + ); } } } async function validateResults() { const resultDir = join(root, "results", "1d", "v1.0.1"); - const resultPath = join( - resultDir, - "reference-windows-x64-i5-9400f.json", - ); + const resultPath = join(resultDir, "reference-windows-x64-i5-9400f.json"); const summaryPath = join( resultDir, "reference-windows-x64-i5-9400f.summary.csv", @@ -138,27 +382,49 @@ async function validateResults() { report.schemaVersion === "cutnesting.benchmark.run.1d.v1", "The published 1D result has the wrong schemaVersion.", ); - check(report.suiteVersion === manifest.suiteVersion, "Result and suite versions differ."); + check( + report.suiteVersion === manifest.suiteVersion, + "Result and suite versions differ.", + ); check( report.methodologyVersion === manifest.methodologyVersion, "Result and methodology versions differ.", ); - check(report.engine?.executionTarget === "native-rust", "The reference result must declare native-rust."); - check(report.timingPolicy?.warmupRuns === 3, "The reference result must contain three warmups."); - check(report.timingPolicy?.measuredRuns === 30, "The reference result must contain 30 measured runs."); - check(report.source?.dirty === false, "The reference engine worktree must be clean."); + check( + report.engine?.executionTarget === "native-rust", + "The reference result must declare native-rust.", + ); + check( + report.timingPolicy?.warmupRuns === 3, + "The reference result must contain three warmups.", + ); + check( + report.timingPolicy?.measuredRuns === 30, + "The reference result must contain 30 measured runs.", + ); + check( + report.source?.dirty === false, + "The reference engine worktree must be clean.", + ); check( report.source?.commit === "9f3b06fb90b8ff83742caed3bc796da109751896", "The reference engine commit changed unexpectedly.", ); - check(report.datasetSource?.dirty === false, "The reference dataset worktree must be clean."); - check(report.datasetSource?.ref === "v1.0.1", "The reference result must name dataset tag v1.0.1."); + check( + report.datasetSource?.dirty === false, + "The reference dataset worktree must be clean.", + ); + check( + report.datasetSource?.ref === "v1.0.1", + "The reference result must name dataset tag v1.0.1.", + ); check( report.datasetSource?.commit === "10af572d9e680a9079f59772d5f7693315aee7fa", "The reference dataset commit changed unexpectedly.", ); check( - Array.isArray(report.cases) && report.cases.length === manifest.cases.length, + Array.isArray(report.cases) && + report.cases.length === manifest.cases.length, "The reference result must contain every suite case.", ); @@ -166,15 +432,27 @@ async function validateResults() { const resultIds = new Set(); for (const result of report.cases ?? []) { resultIds.add(result.caseId); - check(expectedIds.has(result.caseId), `Unknown result case: ${result.caseId}`); - check(result.validation?.valid === true, `${result.caseId} did not pass independent validation.`); - check(result.validation?.errors?.length === 0, `${result.caseId} contains validation errors.`); check( - result.timing?.rawSolveTimeMs?.length === report.timingPolicy.measuredRuns, + expectedIds.has(result.caseId), + `Unknown result case: ${result.caseId}`, + ); + check( + result.validation?.valid === true, + `${result.caseId} did not pass independent validation.`, + ); + check( + result.validation?.errors?.length === 0, + `${result.caseId} contains validation errors.`, + ); + check( + result.timing?.rawSolveTimeMs?.length === + report.timingPolicy.measuredRuns, `${result.caseId} has an incomplete timing series.`, ); check( - result.timing?.rawSolveTimeMs?.every((value) => Number.isFinite(value) && value >= 0), + result.timing?.rawSolveTimeMs?.every( + (value) => Number.isFinite(value) && value >= 0, + ), `${result.caseId} contains an invalid timing sample.`, ); @@ -192,12 +470,17 @@ async function validateResults() { check( Math.abs( metric.disposableMaterialLength - - (metric.kerfLossLength + metric.trimLossLength + metric.scrapLeftoverLength), + (metric.kerfLossLength + + metric.trimLossLength + + metric.scrapLeftoverLength), ) <= 0.001, `${result.caseId} has inconsistent disposable material.`, ); } - check(resultIds.size === expectedIds.size, "The reference result contains duplicate or missing cases."); + check( + resultIds.size === expectedIds.size, + "The reference result contains duplicate or missing cases.", + ); const artifactGroups = [ [report.artifacts?.manifest], @@ -206,32 +489,50 @@ async function validateResults() { ]; for (const artifact of artifactGroups.flat().filter(Boolean)) { const artifactPath = safeChild(root, artifact.path); - check(Boolean(artifactPath), `Result artifact has an unsafe path: ${artifact.path}`); + check( + Boolean(artifactPath), + `Result artifact has an unsafe path: ${artifact.path}`, + ); if (!artifactPath) continue; const content = await readFile(artifactPath); const sha256 = createHash("sha256").update(content).digest("hex"); - check(content.length === artifact.bytes, `Result artifact byte count changed: ${artifact.path}`); - check(sha256 === artifact.sha256, `Result artifact hash changed: ${artifact.path}`); + check( + content.length === artifact.bytes, + `Result artifact byte count changed: ${artifact.path}`, + ); + check( + sha256 === artifact.sha256, + `Result artifact hash changed: ${artifact.path}`, + ); } const summaryLines = (await readFile(summaryPath, "utf8")) .trim() .split(/\r?\n/u); - check(summaryLines.length === manifest.cases.length + 1, "The result summary has the wrong row count."); - const summaryIds = new Set(summaryLines.slice(1).map((line) => line.split(",", 1)[0])); check( - summaryIds.size === expectedIds.size && [...expectedIds].every((id) => summaryIds.has(id)), + summaryLines.length === manifest.cases.length + 1, + "The result summary has the wrong row count.", + ); + const summaryIds = new Set( + summaryLines.slice(1).map((line) => line.split(",", 1)[0]), + ); + check( + summaryIds.size === expectedIds.size && + [...expectedIds].every((id) => summaryIds.has(id)), "The result summary must contain every suite case exactly once.", ); } await validateCases(); +await validate2dCases(); await validateSamples(); await validateTemplates(); await validateResults(); if (failures.length > 0) { - console.error(`Repository validation failed with ${failures.length} issue(s):`); + console.error( + `Repository validation failed with ${failures.length} issue(s):`, + ); failures.forEach((failure) => console.error(`- ${failure}`)); process.exitCode = 1; } else { From 071332db0ce93288f2e5bd736d241aa0feecb2bf Mon Sep 17 00:00:00 2001 From: Sandeep kurien Date: Wed, 22 Jul 2026 13:45:27 +0530 Subject: [PATCH 2/2] Fix canonical sample checksums --- SHA256SUMS | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/SHA256SUMS b/SHA256SUMS index 4fb9985..e3fb6af 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -1,17 +1,14 @@ 0320ed1ae154dac21719844b3b528eae7c4317145bcfe527047917755f957ddb datasets/1d/v1/cases/1d-kerf-exact-fit.json 053f4fb4b195271d458c8cffb3340dee3b90cbf577a4ade64babf73a559fe06f datasets/1d/v1/cases/1d-steel-fabrication.json -092be2892d2dfd6811947879bacbc17d73163a163b8bee4fa2f7e41379c8697f samples/dxf/unitless-rectangle.dxf 0af41137f18d762612180d83e5829e12b6c08c0cf8e48475ed14ce809af5ceb8 datasets/1d/v1/cases/1d-aluminium-profile.json 0d7fdb8681b884bd548e656d281453537d892269cc430e77531acb989fb8ca7b samples/README.md 10370f0d118b25a5845ab3272b3048d78ed431c85964319da538424d7c8e0787 datasets/1d/v1/manifest.json 110a2ea93050ba4e4c3e2844a3a80360a1834c34fa6c85140caa5ce1d1c5a33d datasets/1d/v1/cases/1d-asymmetric-trim.json -12b54b0d2f112eebbef39d13424b586574ec200a74dcb757c5deb472f77ec0f6 samples/nc1/radius-too-large.nc1 227d419d4d0bb2eeb7165f6aa69c25ac9c80ef0d6496fb8cfc2fb5cdd08a725e datasets/1d/v1/cases/1d-mixed-finite-stock.json 2291cdc07004e2ceeba3b68a061dc705f0c4e0d43a09e8574bd46a37aff08277 templates/1d-wood.csv 26ff9196bfed893627627676cd65f1b79238e04bc9f0a911a031e11a6ca54236 datasets/2d/v1/cases/2d-irregular-remnant-with-void.json 2b95191fd5257e96d6a23fd25b3c5e21c0de53f359fa5711d3773f74db7540e5 schemas/2d-result.schema.json 2e831894024b572c937b6910d640d750d05da7d551cadaf4443caa412ed18dfd datasets/1d/v1/cases/1d-offcut-threshold.json -340f79e38c1e9c54e4c4f7fa7a10f779595171562b39d69b86b7ce04c3b5a708 samples/nc1/unsupported-profile.nc1 379c6258142310a26b56e19a12090219a7842230c93043add22165d92543ed8a datasets/1d/v1/cases/1d-oversized-cut.json 3872aa21644f029e59717c5d383b8f56cae2ddc46f714569658504c3f9b0e7ba CONTRIBUTING.md 389631286dcf8f56adac4738d308f494a2d5a07739ece075b328f523b2f0dfde templates/2d-sheet-and-plate.xlsx @@ -19,6 +16,7 @@ 3f9b228cb1790199f4f5d57c0c5273d0bc31afc56085cf84a45f18d62291c55a templates/definitions.json 3fa1679686c4089d3f40cf5c775c44fd13a86c73ae0b8248dc6d09abc4d19b73 datasets/2d/v1/cases/2d-exact-square-grid.json 4075b6de75729e73d7c885871b65ed82977f2cb410da0fda05a8a8ff3acfb12c datasets/1d/v1/cases/1d-high-piece-count.json +416bdf8ab60a5c061d0f03bb3b6b7ca72d3de33a13b0be2ce248d6905590af56 samples/dxf/self-intersection.dxf 469b6ae250e6ed87b69eb74c4a4465a5a115d4f9177f43ff05ea3f8c49305cba docs/dstv-nc1-supported-subset.md 4e6d54ec0510235190e6d0c99adc0723b178bd96eba8cc56432588a7ded33054 datasets/2d/v1/manifest.json 530c7acb6679f73e273f6837c91113de1a4ac4f891340137186237e921238411 templates/1d-pipe.csv @@ -33,16 +31,20 @@ 68bf7443fae19274bad90ab0ee55f571fb2387b2dbeb3cfa04bba408b1c4f3b5 schemas/1d-result.schema.json 7225ad3c3de17ec306e427b4f9880cddc19460f9e90bdb4443269ceb59a05239 datasets/README.md 730ec1858fcd0d8639002409c367b3d49b3627a6a81f7a1b3eb810fc3efbcb75 datasets/2d/v1/cases/2d-steel-plate-grid.json -77dfc7641646c9f915ee54d20bd0b1ea5d66d0c6eeffa8f743e1516deb22791c samples/dxf/self-intersection.dxf +754fe4464a7ea94ab81eebe10a69f07ccb27abace529301fab6673b2e98a648c samples/nc1/unsupported-profile.nc1 782d465d88d25f1dffd6f9fafa1620b6607aa1f6bf96c73c379f15b17992be0b results/1d/v1.0.1/README.md 7dc97fd0976ecc0a6bcc852fe44c5a13f8bae4b879866a440b14ac180fc25132 results/README.md 8b616e1bb088f6fb56910aa453e74aaae1c5411928f8622b25374c3eec0d1e80 LICENSE 8c899cfafd3434d17c6b3530160924e6b3aae38c8247e02c17d151af0e8bfb4d scripts/generate-templates.mjs +8e4cbbe934b9f3f1ff4478ec7387ba22b042966c3df45b08d2e43cda7f2c771e samples/dxf/unsupported-entity.dxf 95f51b0bbfbcde9d53690fdf5055714a0b7ada002c76b799f907ef0e70c6b361 datasets/1d/v1/cases/1d-pipe-batch.json 988e03e819c1fd5064f1a48e1cf72b9e11a5b1b067630234f4647363b69901f3 docs/repository-contract.md +9cc055698c8c63b83eda5b2bdf21e57b49a80989365730438f9bc1fdeb349c9e samples/dxf/unitless-open-contour.dxf +9e47e498a7f75119e124d12613407228452a0beb4041086db8046020aade65da samples/nc1/radius-too-large.nc1 9e5ade7e935cf7a1a353f477b3b5d01ae01dc0bb4e69da13ce7d9cabf4fd86cb results/1d/v1.0.1/reference-windows-x64-i5-9400f.summary.csv -9e7c7dad84df75e01601c22b4afa3458fd1553d0bfb93afb475c951079a18383 samples/dxf/unitless-open-contour.dxf a02e7292f9f295f91a704467aef00752597f65cf666a3e9ef2131caafc0798b5 package.json +a34257ca65ac5f44b99f05b2d180931de5796c0f78fea81d29d94987b16717d4 samples/dxf/layer-block-insert.dxf +a4980b24140bd58dda422718e4b189f208715976b670c33f257c224b20c9942f samples/nc1/plate-radius.nc1 a574cffc3eb42f427a035157e78948adc13d4694907ceaaf0a6765dd6d0df4b7 datasets/2d/v1/cases/2d-wood-panel-grid.json aac5abb1eacaee94944a150db7d0facf886c71a26def8a36e4be7db3137f3bfc .gitignore af57dba5ee88fb4ce9aee2b1d5b6dfbfb784981a43c25c41f8e6ce9cc09cfc5a results/1d/v1.0.1/reference-windows-x64-i5-9400f.json @@ -51,17 +53,15 @@ c680e32b10dee44df1f84b3f57e8c7c75591e01ee2686ffc53ec9ce7e05e25dc datasets/1d/v1 cb1136ac89f569865b72f8af974ec059abe7914013f0c32f1396d8b99e874a28 schemas/1d-case.schema.json cb6c36568a75c8d10b06ec7d8731789e56023b11cc8d498cfca57703c13a9ad1 datasets/1d/v1/cases/1d-wood-furniture.json cd13e90119f89a461de8d5a7d14bbe96cf8708110172ad2d019da5f376a262b8 CITATION.cff -cec44b1e590e04b98c94fe10b58e7f2ceef9a8e38c44c502fe7fa68c4eb993e4 samples/dxf/layer-block-insert.dxf -d06f3820426f42512831c0bafa3a82dfc26525ed72e3edffec02f53e37ad5e48 samples/nc1/plate-radius.nc1 d1aabca6e789b514f0b0aa4a1cf11e07e29ab05abd4108543b7c45166f10dc81 CHANGELOG.md d52a2c32095f4035f5d23002f14a2310548ee469f1a9175a8c893362f653fbc6 schemas/manifest.schema.json -d8c42e7b49d05cd04a59481dd71d87654bbf2592ed8cd7cf3cd5bd86b365e432 samples/dxf/unsupported-entity.dxf da6e8342856133226cad0ca6c6bca1e607497c30a1d016105b7d54f4ce254a70 datasets/2d/v1/cases/2d-stock-exhausted.json e603eb378f7453f5e14ce87bc5ea0285b93f0fbce3a6edb225190df987ebdace samples/manifest.json e6dc9fea66d59eb62cdc747718fc784bb962952ff7501280b15ba6472042fc36 templates/2d-sheet-and-plate.csv eeb3bc4f2215d6ed7e29a743946c4075fd77f23316cd3cdffe607cc38c99474d schemas/2d-manifest.schema.json f1ce8f5048968c79ad8e7406696389708e3fb51ee08626b23a80916b9e5d1984 LICENSE-DATA.md f27e13aa8a94fc76ce4766ed9f3910ad00e28cfd48d6ff42177e11dc799cc65a scripts/verify-checksums.mjs +f839d9a035286c9dddde94329a0bbffd0d0880eaf985687b1588cf1020c17866 samples/dxf/unitless-rectangle.dxf f8ee19613ce597319f2cb9b1a89c9dbe1257b01a9742145633bba14170078c0a methodology/2d-v1.md fa23d582976607d64ce1497aeebdaa43eb43adc68a9f46e9866d316c17b4bfd3 README.md ff9ea556c36d48d767b4d6f87f9aa61e58248e8683fa90d3ee4a8c7ff0d0604a .github/workflows/validate.yml