Files
OpenNest/OpenNest.Engine/NestingEngines/CandidateScore.cs
T
ajandClaude Opus 5.5 33f60d3dcb feat(engine): break equal-cost ties toward the larger free offcut
Without salvage credit (desktop Auto Nest on a single plate size) every
complete one-sheet layout costs the same, so engines kept whichever layout
came first. N26-23S nested 27.86" long when turning two parts fits it in
21.3".

NestJobCost.CompactnessOffcut measures the largest full-width/full-length
edge offcut as salvage does, whatever the salvage rate. Default and Circles
(CandidateScore), Rectangles and Circles sheet choice (SheetByStock) and
Irregular sheet and plan ranking use it only after cost, so no cost changes.
Sheet-level keys round to 1e-6 to keep ties platform-independent.

Rectangles layout digests are re-captured: each changed fixture keeps its
sheets, parts and cost and leaves a larger offcut.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 20:18:57 -04:00

42 lines
2.1 KiB
C#

#nullable enable
using System.Linq;
using OpenNest.Engine.Jobs;
namespace OpenNest.Engine.NestingEngines;
/// <summary>Ranks whole-job candidate results: a valid layout before an invalid one, then the
/// fewest unplaced parts, then the lowest <see cref="NestJobCost"/>, then the largest
/// <see cref="NestJobCost.CompactnessOffcut(NestJob, NestJobResult)"/> (without salvage credit every
/// one-sheet layout costs the same); remaining ties keep candidate order.</summary>
internal sealed record CandidateScore(
NestJobResult Result, bool Valid, int Unplaced, double Cost, double Offcut, int Order, bool Priced)
{
public static CandidateScore Of(NestJob job, NestJobResult result, int order)
{
var valid = NestLayoutCheck.Violations(job, result).Count == 0;
var placed = result.Plates.Sum(p => p.Placements.Count);
var unplaced = System.Math.Max(0, job.Parts.Sum(p => p.Quantity) - placed);
// Cost needs every placement to name a known part, which only a valid result guarantees.
var cost = valid ? NestJobCost.Evaluate(job, result) : double.PositiveInfinity;
var offcut = valid ? NestJobCost.CompactnessOffcut(job, result) : 0;
return new CandidateScore(result, valid, unplaced, cost, offcut, order, NestJobCost.UsesExplicitCosts(job));
}
public bool IsBetterThan(CandidateScore other)
{
if (Valid != other.Valid)
return Valid;
if (Unplaced != other.Unplaced)
return Unplaced < other.Unplaced;
if (Priced && Cost != other.Cost)
return Cost < other.Cost;
var scale = System.Math.Max(1, System.Math.Max(System.Math.Abs(Cost), System.Math.Abs(other.Cost)));
if (double.IsFinite(Cost) && double.IsFinite(other.Cost) && System.Math.Abs(Cost - other.Cost) > 1e-9 * scale)
return Cost < other.Cost;
var offcutScale = System.Math.Max(1, System.Math.Max(Offcut, other.Offcut));
if (System.Math.Abs(Offcut - other.Offcut) > 1e-9 * offcutScale)
return Offcut > other.Offcut;
return Order < other.Order;
}
}