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
);
}
}