fix(benchmark): rank by sheet cost so engines can't game the score

The benchmark is about to be used as the objective for LLM-designed
engines, and several gaps would have rewarded the wrong behavior:

- Ranking was utilization-first, so dropping awkward parts raised the
  score. Rank valid > fully placed > cost > plates, where cost is
  salvage-credited sheet area plus a largest-sheet penalty per unplaced
  part; placing a part is never scored worse than omitting it.
- Salvage rate was ignored in scoring; cost now uses EstimateNetArea,
  recomputed from job geometry rather than trusted from the engine.
- Rotation constraints were never validated. Add RotationPolicy.Allows
  (shared with NestJobPlacementValidator) and check every placement.
- Returned sheets were trusted, so an engine could loosen spacing or
  invent a size. Sheets must now match offered stock.
- Part-in-part placements were flagged as overlaps; spacing now accounts
  for cutouts, with an X-sorted sweep to prune distant pairs.
- Summary averaged per-job percentages; it now sums areas and cost.
- --spacing and sheet sizes parsed with the current culture.
- Warn when .nest jobs offer only their original sheet sizes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
aj
2026-09-22 15:05:43 -04:00
co-authored by Claude Opus 5.5
parent 98c4929d14
commit 57e9f625b6
13 changed files with 669 additions and 84 deletions
+23
View File
@@ -81,6 +81,29 @@ namespace OpenNest.Benchmark
return jobs;
}
/// <summary>Parses "WxL" with invariant-culture numbers, so "48.5x96" means the
/// same thing on every machine (Size.Parse follows the current culture).</summary>
public static bool TryParseSheetSize(string text, out Size size)
{
size = default;
var dims = text?.Split('x', 'X');
if (dims == null || dims.Length != 2)
return false;
var style = System.Globalization.NumberStyles.Float;
var culture = System.Globalization.CultureInfo.InvariantCulture;
if (
!double.TryParse(dims[0].Trim(), style, culture, out var width)
|| !double.TryParse(dims[1].Trim(), style, culture, out var length)
)
return false;
size = new Size(width, length);
return true;
}
private static List<string> ResolveFiles(string inputPath)
{
if (Directory.Exists(inputPath))