fix(qwen38flashnext): make layouts deterministic and use host services

The gap-fill pass stopped on a 120 ms stopwatch and QWEN_* environment
variables switched strategies, so the same job could nest differently
with machine load or environment. Gap fill now stops after eight failed
insertion sweeps, and the switches are internal properties with the
old defaults. Part reading, work area, rotation angles, scoring and
result assembly now use the host APIs; its collision gate is unchanged.

Synthetic benchmark (5 jobs, salvage 0.5): all valid, cost 7660.01 ->
7572.05, time 839 -> 553 ms. Not yet calibrated on production-size jobs.

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
aj
2026-09-25 09:29:28 -04:00
co-authored by Codex Claude Opus 5.5
parent 0308a862b8
commit 0cdef009f8
6 changed files with 98 additions and 329 deletions
@@ -18,32 +18,25 @@ internal sealed record SheetAttempt(SheetPacker Packer, int StockIndex);
/// </summary>
internal sealed class JobSolver
{
private NestJobResultBuilder _builder = null!;
private readonly NestJob _job;
private readonly PartPreparation _prep;
private readonly Dictionary<string, int> _remaining;
private readonly Dictionary<string, int> _placed;
private readonly Dictionary<string, int> _used;
private readonly List<CommittedSheet> _sheets = new();
private sealed record CommittedSheet(int StockIndex, List<PlacedPart> Placements);
private int _sheetCount;
public JobSolver(NestJob job, PartPreparation prep)
{
_job = job;
_prep = prep;
_remaining = job.Parts.ToDictionary(p => p.Id, p => p.Quantity, StringComparer.Ordinal);
_placed = job.Parts.ToDictionary(p => p.Id, _ => 0, StringComparer.Ordinal);
_used = job.Plates.ToDictionary(s => s.Id, _ => 0, StringComparer.Ordinal);
}
private static readonly bool _diag =
Environment.GetEnvironmentVariable("QWEN_NEST_DIAG") == "1";
internal static bool Diagnostics { get; set; }
private static bool _diag => Diagnostics;
private void Diag(string message)
{
if (_diag)
Console.Error.WriteLine(
$"[qwen] sheets={_sheets.Count} placed={_placed.Values.Sum()} " +
$"[qwen] sheets={_sheetCount} placed={_job.Parts.Sum(p => _builder.Placed(p.Id))} " +
$"mem={GC.GetTotalMemory(false) / 1048576}MB gc0={GC.CollectionCount(0)} " +
$"gc2={GC.CollectionCount(2)} {message}"
);
@@ -51,14 +44,18 @@ internal sealed class JobSolver
public NestJobResult Solve(IProgress<NestJobProgress>? progress, CancellationToken token)
{
_builder = new NestJobResultBuilder(_job, progress);
var reason = NestJobStopReason.Completed;
while (true)
{
token.ThrowIfCancellationRequested();
var outstanding = OutstandingDemands();
if (outstanding.Count == 0)
{
reason = _builder.IsComplete ? NestJobStopReason.Completed : NestJobStopReason.NoPlacementFound;
break;
if (_job.Options.MaxPlates is int cap && _sheets.Count >= cap)
}
if (_job.Options.MaxPlates is int cap && _sheetCount >= cap)
{
reason = NestJobStopReason.PlateLimitReached;
break;
@@ -75,25 +72,16 @@ internal sealed class JobSolver
}
CommitSheet(attempt.Packer);
progress?.Report(
new NestJobProgress(
NestJobStage.PlateCommitted,
_job.Plates[attempt.StockIndex].Id,
_sheets.Count - 1,
_sheets.Count,
_placed.Values.Sum()
)
);
}
return BuildResult(reason);
return _builder.Build(reason);
}
private List<PartModel> OutstandingDemands()
{
var demands = new List<PartModel>();
foreach (var model in _prep.Models)
if (_remaining[model.Id] > 0)
if ((model.Quantity - _builder.Placed(model.Id)) > 0)
demands.Add(model);
// This engine's own ordering: priority first, then the tallest-then-largest
// part first (a part's thinnest orientation extent), then id for determinism.
@@ -189,7 +177,7 @@ internal sealed class JobSolver
for (var index = 0; index < _job.Plates.Count; index++)
{
var stock = _job.Plates[index];
if (stock.Quantity is int quantity && _used[stock.Id] >= quantity)
if (stock.Quantity is int quantity && _builder.SheetsUsed(stock) >= quantity)
continue;
token.ThrowIfCancellationRequested();
@@ -197,21 +185,14 @@ internal sealed class JobSolver
new NestJobProgress(
NestJobStage.EvaluatingCandidate,
stock.Id,
_sheets.Count,
_sheets.Count,
_placed.Values.Sum()
_sheetCount,
_sheetCount,
_job.Parts.Sum(p => _builder.Placed(p.Id))
)
);
var packer = SheetPacker.Create(stock, _prep, index);
var fillWatch = System.Diagnostics.Stopwatch.StartNew();
FillSheet(packer, outstanding, token);
fillWatch.Stop();
if (_diag)
Diag(
$"trial stock {stock.Id}: placed={packer.Placed.Count} " +
$"{fillWatch.ElapsedMilliseconds}ms {packer.DiagStats()}"
);
if (packer.Placed.Count == 0)
continue;
@@ -261,7 +242,7 @@ internal sealed class JobSolver
{
var available = new Dictionary<string, int>(StringComparer.Ordinal);
foreach (var model in outstanding)
available[model.Id] = _remaining[model.Id];
available[model.Id] = (model.Quantity - _builder.Placed(model.Id));
// One drain pass per requirement, in demand order. Gap-filling retries are
// deliberately NOT an unbounded loop: a sheet's failed-insert scans get more
@@ -274,7 +255,6 @@ internal sealed class JobSolver
continue;
if (!packer.CanEverFit(model))
continue;
var modelWatch = System.Diagnostics.Stopwatch.StartNew();
while (available[model.Id] > 0 && !packer.IsFull)
{
token.ThrowIfCancellationRequested();
@@ -282,32 +262,30 @@ internal sealed class JobSolver
break;
available[model.Id]--;
}
modelWatch.Stop();
if (_diag && modelWatch.ElapsedMilliseconds > 200)
Diag($" fill model {model.Id}: placed={packer.Placed.Count} {modelWatch.ElapsedMilliseconds}ms {packer.DiagStats()}");
}
// Gap-fill pass: a part that could not fit between two early placements may
// fit the gaps a later model leaves behind. Failed-insert scans cost about a
// full candidate sweep each, so the pass is hard time-boxed - on a crowded
// sheet the untried budget was measured at minutes per job, well over the
// benchmark's wall; the box keeps worst case near the first pass's cost.
var retryWatch = System.Diagnostics.Stopwatch.StartNew();
// Count failed insertion sweeps, independent of machine speed and load.
// Successful insertions are bounded by the outstanding quantities.
var retries = GapFillFailedInsertBudget;
var retryAgain = true;
while (retryAgain && retryWatch.ElapsedMilliseconds < GapFillMilliseconds)
while (retryAgain && retries > 0)
{
retryAgain = false;
foreach (var model in outstanding)
{
if (available[model.Id] <= 0 || packer.IsFull)
continue;
if (retryWatch.ElapsedMilliseconds >= GapFillMilliseconds)
if (retries <= 0)
break;
while (available[model.Id] > 0)
{
token.ThrowIfCancellationRequested();
if (!packer.TryInsert(model, out _))
{
retries--;
break;
}
available[model.Id]--;
retryAgain = true;
}
@@ -321,18 +299,12 @@ internal sealed class JobSolver
/// the seams; 12% lower job cost than span-first on a real production job), 2 = largest footprint
/// first, 0 = smallest worst-case extent first (original).
/// </summary>
private static readonly int DemandOrderMode =
int.TryParse(Environment.GetEnvironmentVariable("QWEN_DEMAND_ORDER"), out var m)
? m
: 1;
internal static int DemandOrderMode { get; set; } = 1;
/// <summary>Wall-clock budget for one sheet's gap-fill pass.</summary>
private static readonly int GapFillMilliseconds =
int.TryParse(Environment.GetEnvironmentVariable("QWEN_GAPFILL_MS"), out var ms)
? ms
: 120;
/// <summary>Maximum failed insertion sweeps per sheet gap-fill pass.</summary>
internal static int GapFillFailedInsertBudget { get; set; } = 8;
/// <summary>Trial-sheet metrics; costPerArea = plate area / part area placed.</summary>
/// <summary>Trial-sheet metrics; costPerArea = net sheet area / material area placed.</summary>
private readonly record struct TrialScore(
int count,
int priorityHits,
@@ -343,10 +315,9 @@ internal sealed class JobSolver
/// <summary>
/// Greedy trial-comparison mode. Cost-first optimizes the benchmark's cost
/// function (total plate area); count-first is the conservative fill policy.
/// Env override exists for A/B measurement.
/// Internal setting permits controlled A/B tests.
/// </summary>
private static readonly bool CostFirstScoring =
Environment.GetEnvironmentVariable("QWEN_COST_FIRST") != "0";
internal static bool CostFirstScoring { get; set; } = true;
private TrialScore ScoreTrial(SheetPacker packer)
{
@@ -356,7 +327,8 @@ internal sealed class JobSolver
if (placed.Model.Priority < bestPriority)
bestPriority = placed.Model.Priority;
var priorityHits = packer.Placed.Count(p => p.Model.Priority == bestPriority);
var area = packer.Stock.Size.Width * packer.Stock.Size.Length;
var area = NestJobCost.NetSheetArea(_job, new NestJobPlateResult(0, packer.Stock,
packer.Placed.Select(p => new NestJobPlacement(p.Model.Id, 0, p.X, p.Y, p.Orientation.Angle))));
var placedArea = 0.0;
foreach (var placed in packer.Placed)
placedArea += placed.Model.Area;
@@ -366,74 +338,18 @@ internal sealed class JobSolver
private void CommitSheet(SheetPacker packer)
{
_sheets.Add(new CommittedSheet(packer.StockIndex, packer.Placed));
_used[packer.Stock.Id]++;
foreach (var placed in packer.Placed)
{
_placed[placed.Model.Id]++;
_remaining[placed.Model.Id]--;
}
_builder.AddSheet(packer.Stock, packer.Placed.Select(p =>
(p.Model.Id, p.X, p.Y, p.Orientation.Angle)));
_sheetCount++;
}
private bool AnyStockAvailable()
{
foreach (var stock in _job.Plates)
if (stock.Quantity is null || _used[stock.Id] < stock.Quantity.Value)
if (stock.Quantity is null || _builder.SheetsUsed(stock) < stock.Quantity.Value)
return true;
return false;
}
private NestJobResult BuildResult(NestJobStopReason reason)
{
var instanceIndex = new Dictionary<string, int>(StringComparer.Ordinal);
var plates = new List<NestJobPlateResult>();
foreach (var sheet in _sheets)
{
var placements = new List<NestJobPlacement>(sheet.Placements.Count);
foreach (var placed in sheet.Placements)
{
instanceIndex.TryGetValue(placed.Model.Id, out var next);
instanceIndex[placed.Model.Id] = next + 1;
placements.Add(
new NestJobPlacement(
placed.Model.Id,
next,
placed.X,
placed.Y,
placed.Orientation.Angle
)
);
}
plates.Add(
new NestJobPlateResult(plates.Count, _job.Plates[sheet.StockIndex], placements)
);
}
var fulfillment = _job.Parts
.Select(part => new PartFulfillment(
part.Id,
part.Quantity,
_placed[part.Id],
_remaining[part.Id]
))
.ToList();
var stockUsage = _job.Plates
.Select(stock => new StockUsage(
stock.Id,
_used[stock.Id],
stock.Quantity is int quantity ? quantity - _used[stock.Id] : null
))
.ToList();
return new NestJobResult(
reason == NestJobStopReason.Completed
? NestJobStatus.Complete
: NestJobStatus.Incomplete,
reason,
plates,
fulfillment,
stockUsage
);
}
}
@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using OpenNest.Converters;
using OpenNest.Engine.Jobs;
using OpenNest.Engine.Jobs.Adapters;
using OpenNest.Geometry;
using OpenNest.Math;
@@ -53,10 +51,11 @@ internal sealed class PartModel
/// <summary>Material area (perimeter minus holes), from the analytic shapes.</summary>
public double Area { get; }
internal List<double>? Angles { get; set; }
/// <summary>
/// Chord tolerance for the engine's internal collision polygons. Must stay FINER
/// than the validator's OutlineTolerance (0.001): any chord cuts the cap off a
/// than NestTolerances.ValidationOutline (0.001): any chord cuts the cap off a
/// concave arc, and a coarser polygon cuts MORE - so a coarse flattening is a
/// subset of the validator's material in notched regions and admits real spacing
/// violations (observed on arc-heavy PEP parts at 0.02). Finer than the validator,
@@ -70,47 +69,11 @@ internal sealed class PartModel
/// </summary>
public static PartModel? TryCreate(NestJobPart part)
{
try
{
var entities = new List<Entity>();
foreach (
var entity in ConvertProgram.ToGeometry(
DrawingJobMapper.ToProgram(part.Geometry)
)
)
if (SpecialLayers.IsMaterial(entity.Layer))
entities.Add(entity);
if (entities.Count == 0)
return null;
var profile = new ShapeProfile(entities);
if (profile.Perimeter == null)
return null;
profile.NormalizeWinding();
var area = Math.Abs(profile.Perimeter.Area());
foreach (var cutout in profile.Cutouts)
area -= Math.Abs(cutout.Area());
if (!double.IsFinite(area) || area <= Tolerance.Epsilon)
return null;
return new PartModel(
part.Id,
part.Quantity,
part.Priority,
part.Rotation,
profile,
profile.Perimeter,
new List<Shape>(profile.Cutouts),
area
);
}
catch (Exception)
{
// Malformed snapshots are unplaceable, not fatal: report them unplaced so
// the rest of the job still nests.
return null;
}
var geometry = JobPartGeometry.TryRead(part.Geometry);
if (geometry == null || geometry.MaterialArea <= Tolerance.Epsilon) return null;
// Read normalizes winding before the collision and offset preparation below.
return new PartModel(part.Id, part.Quantity, part.Priority, part.Rotation,
geometry.Profile, geometry.Perimeter, geometry.Cutouts.ToList(), geometry.MaterialArea);
}
}
@@ -441,84 +404,15 @@ internal sealed class PartPreparation
/// </summary>
public static List<double> CandidateAngles(PartModel model)
{
var angles = new List<double>();
var policy = model.Rotation;
if (policy.Kind == RotationPolicyKind.Automatic)
{
angles.Add(0);
angles.Add(Math.PI / 2);
angles.Add(Math.PI);
angles.Add(3 * Math.PI / 2);
try
{
var hull = ConvexHull.Compute(
model
.PerimeterShape
.ToPolygonWithTolerance(PartModel.CollisionTolerance, circumscribe: true)
.Vertices
);
var obb = RotatingCalipers.MinimumBoundingRectangle(hull);
var normalized = OpenNest.Math.Angle.NormalizeRad(obb.Angle);
if (normalized > 0.001 && normalized < Math.PI - 0.001)
{
angles.Add(normalized);
angles.Add(OpenNest.Math.Angle.NormalizeRad(normalized + Math.PI));
}
}
catch (Exception)
{
// A calipers failure only costs candidate angles, never correctness.
}
}
else if (policy.Kind == RotationPolicyKind.Fixed)
{
angles.Add(policy.Start);
if (policy.Allow180Equivalent)
angles.Add(policy.Start + Math.PI);
}
else
{
// BoundedSweep: enumerate the exact step grid the policy allows.
var count = (int)Math.Floor((policy.End - policy.Start) / policy.Step + 1e-9);
if (count < 0)
count = 0;
if (count > 4000)
count = 4000;
for (var i = 0; i <= count; i++)
{
angles.Add(policy.Start + i * policy.Step);
if (policy.Allow180Equivalent)
angles.Add(policy.Start + i * policy.Step + Math.PI);
}
}
// Normalize to [0, 2pi), deduplicate, preserve first-seen order (deterministic).
var unique = new List<double>();
foreach (var angle in angles)
{
var normalized = OpenNest.Math.Angle.NormalizeRad(angle);
if (normalized < 0)
normalized += 2 * Math.PI;
var duplicate = false;
foreach (var existing in unique)
if (Math.Abs(SignedDelta(existing, normalized)) < 1e-9)
{
duplicate = true;
break;
}
if (!duplicate)
unique.Add(normalized);
}
return unique;
if (model.Angles != null) return model.Angles;
var angles = model.Rotation.Kind == RotationPolicyKind.Automatic
? RotationCandidates.ForShape(model.Rotation, model.PerimeterShape)
: model.Rotation.EnumerateAngles(maxSamples: 4000);
// Perimeter symmetry does not establish symmetry of the cutouts.
return model.Angles = (model.CutoutShapes.Count == 0
? RotationCandidates.DistinctOutlines(model.PerimeterShape, angles)
: angles).ToList();
}
private static double SignedDelta(double a, double b)
{
var delta = (a - b) % (2 * Math.PI);
if (delta > Math.PI)
delta -= 2 * Math.PI;
if (delta < -Math.PI)
delta += 2 * Math.PI;
return delta;
}
}
@@ -115,12 +115,11 @@ internal sealed class SheetPacker
StockIndex = stockIndex;
Preparation = prep;
Spacing = stock.PartSpacing;
var left = stock.Quadrant is 1 or 4 ? 0.0 : -stock.Size.Length;
var bottom = stock.Quadrant is 1 or 2 ? 0.0 : -stock.Size.Width;
_workLeft = left + stock.EdgeSpacing.Left;
_workBottom = bottom + stock.EdgeSpacing.Bottom;
_workRight = left + stock.Size.Length - stock.EdgeSpacing.Right;
_workTop = bottom + stock.Size.Width - stock.EdgeSpacing.Top;
var work = stock.WorkArea;
_workLeft = work.Left;
_workBottom = work.Bottom;
_workRight = work.Right;
_workTop = work.Top;
WorkWidth = _workRight - _workLeft;
WorkHeight = _workTop - _workBottom;
@@ -159,7 +158,7 @@ internal sealed class SheetPacker
foreach (var angle in PartPreparation.CandidateAngles(model))
{
var orientation = Preparation.Oriented(model, angle, 0);
if (orientation.Width <= WorkWidth + 1e-9 && orientation.Height <= WorkHeight + 1e-9)
if (Stock.Fits(orientation.Width, orientation.Height))
return true;
}
return false;
@@ -647,8 +646,7 @@ internal sealed class SheetPacker
}
}
private static readonly bool VerifyFastClear =
Environment.GetEnvironmentVariable("QWEN_VERIFY_FASTCLEAR") == "1";
internal static bool VerifyFastClear { get; set; }
internal long DiagFastClearMismatch;
internal long DiagTriMismatch;