mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-02 11:21:40 -05:00
Equal-cost plans break ties by the largest edge offcut, from any of the four edges. A caller that wants the leftover off one way, such as a shop that keeps only end drops, or a benchmark scoring the used sheet height, had no way to say so. On the converted Nestbolt jobs the extended Irregular search then found plans whose larger offcut is a strip down the side, which Nestbolt scores as worse: b-blaz1-strip 3.601 -> 3.867, b-shirts-strip 2.674 -> 2.995. NestJobOptions.OffcutCut (a CutOffAxis, default null) counts only the strips one cut along that axis frees, for salvage credit and the compactness tie-break alike: Horizontal the full-width bottom and top strips, Vertical the full-length sides. PlateRemnant.LargestOffcut takes the axis; NestJobCost, the time limit and clamp-removal recovery pass it on. Unset, nothing changes. Benchmark manifests accept "offcutCut": "horizontal" or "vertical". With horizontal, one CPU, 20 s limit, Default: b-blaz1-strip 3.934 -> 3.333 b-shirts-strip 2.995 -> 2.610 m-box-deep 1.918 -> 1.276 (Nestbolt 1.251) Manifests accept only the words horizontal or vertical (any case); a list such as "horizontal,vertical" is rejected. PlateRemnant rejects an undefined axis. Fixed strategies keep their own no-salvage tie-break and ignore the option; desktop, Console and MCP do not offer it yet. Mutation reds: 14/14 (offcut-mutations.py).
124 lines
5.2 KiB
C#
124 lines
5.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using OpenNest.Engine.Jobs;
|
|
using OpenNest.Engine.Jobs.Adapters;
|
|
using OpenNest.Geometry;
|
|
|
|
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; }
|
|
|
|
/// <summary>Source plate's machine clamp rail, resolved for each candidate size as Auto Nest does; null when none.</summary>
|
|
public ClampRail ClampRail { get; init; }
|
|
|
|
/// <summary>Source plate's explicit clamp zones when it has no rail, offered unchanged on every size.</summary>
|
|
public IReadOnlyList<ClampZone> ClampZones { get; init; } = Array.Empty<ClampZone>();
|
|
|
|
/// <summary>Saved source-nest setting; manifests have no saved salvage rate and use zero.</summary>
|
|
public double SalvageRate { get; init; }
|
|
|
|
/// <summary>Manifest "offcutCut": which edge strips count as a sheet's offcut;
|
|
/// null for all.</summary>
|
|
public CutOffAxis? OffcutCut { get; init; }
|
|
|
|
/// <summary>Original hand-authored placements, if the source was a .nest with any real parts.</summary>
|
|
public List<(Plate Plate, List<Part> Parts)> BaselinePlateRuns { 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. Stock comes from
|
|
/// the same template builder as desktop Auto Nest, so clamps match it.
|
|
/// </summary>
|
|
public NestJob BuildNestJob(
|
|
int maxPlates,
|
|
double? salvageRate = null,
|
|
double? minimumSalvageDimension = null
|
|
)
|
|
{
|
|
var parts = Requests.Select(r =>
|
|
DrawingJobMapper.FromDrawing(r.Drawing.Id.ToString(), r.Drawing, r.Quantity)
|
|
);
|
|
// No options would make the builder offer the template itself; an empty pool stays empty.
|
|
var stock = CandidateSizes.Count == 0
|
|
? Array.Empty<NestPlateStock>()
|
|
: NestStockBuilder.FromTemplate(
|
|
StockTemplate(),
|
|
CandidateSizes.Select(size => new PlateOption { Width = size.Width, Length = size.Length }).ToList()
|
|
);
|
|
return new NestJob(
|
|
parts,
|
|
stock,
|
|
new NestJobOptions(
|
|
"Fill",
|
|
maxPlates,
|
|
salvageRate ?? SalvageRate,
|
|
minimumSalvageDimension ?? 0,
|
|
offcutCut: OffcutCut
|
|
)
|
|
);
|
|
}
|
|
|
|
/// <summary>A plate carrying this job's spacing, quadrant and clamps, sized to the largest candidate.</summary>
|
|
private Plate StockTemplate()
|
|
{
|
|
var template = new Plate(CandidateSizes.MaxBy(size => size.Width * size.Length))
|
|
{
|
|
PartSpacing = PartSpacing,
|
|
EdgeSpacing = EdgeSpacing,
|
|
Quadrant = Quadrant,
|
|
};
|
|
if (ClampRail != null)
|
|
template.SetClampRail(ClampRail);
|
|
else
|
|
foreach (var zone in ClampZones)
|
|
template.ClampZones.Add(zone);
|
|
return template;
|
|
}
|
|
}
|
|
}
|