Files
OpenNest/OpenNest.Benchmark/BenchmarkJob.cs
T
ajandClaude Opus 5.5 57e9f625b6 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>
2026-09-22 15:05:43 -04:00

84 lines
3.2 KiB
C#

using System.Collections.Generic;
using System.IO;
using System.Linq;
using OpenNest.Geometry;
using OpenNest.Engine.Jobs;
using OpenNest.Engine.Jobs.Adapters;
namespace OpenNest.Benchmark
{
/// <summary>
/// One request to nest a specific drawing, with the quantity and rotation
/// constraints pulled from its source .nest file.
/// </summary>
public class DrawingRequest
{
public Drawing Drawing { get; init; }
public int Quantity { get; init; }
public int Priority { get; init; }
public double StepAngle { get; init; }
public double RotationStart { get; init; }
public double RotationEnd { get; init; }
}
/// <summary>
/// An immutable specification for one benchmark job: the full set of
/// drawings/quantities that must be nested, and the pool of sheet sizes the
/// engine may draw from while doing it. A single run may use several
/// plates - possibly of different sizes - to place everything, the same
/// way a real production job spreads across whatever plates it needs
/// rather than being handed one fixed-size sheet.
/// </summary>
public class BenchmarkJob
{
public string SourceFile { get; init; }
public List<Size> CandidateSizes { get; init; }
public Spacing EdgeSpacing { get; init; }
public double PartSpacing { get; init; }
public int Quadrant { get; init; }
public List<DrawingRequest> Requests { get; init; }
public string Name => Path.GetFileNameWithoutExtension(SourceFile);
public int TotalRequestedQuantity => Requests.Sum(r => r.Quantity);
/// <summary>Sheet area charged per unplaced part: the largest candidate
/// sheet. Any single part that fits the stock at all fits on one such
/// sheet, so placing a part is never scored worse than leaving it out.</summary>
public double UnplacedPartPenalty =>
CandidateSizes.Count == 0 ? 0 : CandidateSizes.Max(s => s.Width * s.Length);
/// <summary>
/// Builds the whole-job request this job represents: one NestJobPart per
/// requested drawing, and one NestPlateStock per candidate sheet size
/// (unlimited quantity - the engine under test decides how many of each
/// size it actually uses, and how demand splits across plates). The
/// engine owns its own multi-plate/size strategy; this harness no
/// longer picks plate sizes on the engine's behalf.
/// </summary>
public NestJob BuildNestJob(
int maxPlates,
double salvageRate = 0,
double minimumSalvageDimension = 0
)
{
var parts = Requests.Select(r =>
DrawingJobMapper.FromDrawing(r.Drawing.Id.ToString(), r.Drawing, r.Quantity)
);
var stock = CandidateSizes.Select(size => new NestPlateStock(
size.ToString(1),
size,
null,
PartSpacing,
EdgeSpacing,
Quadrant
));
return new NestJob(
parts,
stock,
new NestJobOptions("Default", maxPlates, salvageRate, minimumSalvageDimension)
);
}
}
}