Files
OpenNest-Engines/OpenNest.Engine.Qwen38FlashNext/Engine/JobSolver.cs
T
ajandClaude Opus 5.5 aa0e6f967e feat(qwen38flashnext): snapshot first working engine as a baseline
Qwen3.8-Flash-Next reached a working engine and is now optimizing it.
Optimization passes can regress, so this preserves the first version
that passes all acceptance tests (13/13 against OpenNest master,
including the model's own rotated-spacing regression test) for
comparison and rollback. Snapshot taken 2026-09-24 12:00 from
hermes.lan:/home/aj/src/Qwen38FlashNext; the run is still in progress,
so this stays off master until it finishes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 12:00:53 -04:00

315 lines
11 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using OpenNest.Engine.Jobs;
namespace OpenNest.Engine.Qwen38FlashNext.Engine;
using Math = System.Math;
internal sealed record SheetAttempt(SheetPacker Packer, int StockIndex);
/// <summary>
/// Whole-job decision layer: which stock the next sheet uses, the order parts are
/// demanded in, when a sheet is finished, and when the job stops. Every placement
/// inside a sheet comes from <see cref="SheetPacker"/>; nothing here delegates to a
/// built-in engine, nester, filler, or runner.
/// </summary>
internal sealed class JobSolver
{
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);
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";
private void Diag(string message)
{
if (_diag)
Console.Error.WriteLine(
$"[qwen] sheets={_sheets.Count} placed={_placed.Values.Sum()} " +
$"mem={GC.GetTotalMemory(false) / 1048576}MB gc0={GC.CollectionCount(0)} " +
$"gc2={GC.CollectionCount(2)} {message}"
);
}
public NestJobResult Solve(IProgress<NestJobProgress>? progress, CancellationToken token)
{
var reason = NestJobStopReason.Completed;
while (true)
{
token.ThrowIfCancellationRequested();
var outstanding = OutstandingDemands();
if (outstanding.Count == 0)
break;
if (_job.Options.MaxPlates is int cap && _sheets.Count >= cap)
{
reason = NestJobStopReason.PlateLimitReached;
break;
}
var attempt = BestNextSheet(outstanding, progress, token);
Diag($"nextSheet -> {(attempt == null ? "none" : $"stock {_job.Plates[attempt.StockIndex].Id} placed {attempt.Packer.Placed.Count}")}");
if (attempt == null)
{
reason = AnyStockAvailable()
? NestJobStopReason.NoPlacementFound
: NestJobStopReason.StockExhausted;
break;
}
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);
}
private List<PartModel> OutstandingDemands()
{
var demands = new List<PartModel>();
foreach (var model in _prep.Models)
if (_remaining[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.
demands.Sort(
(a, b) =>
{
var byPriority = a.Priority.CompareTo(b.Priority);
if (byPriority != 0)
return byPriority;
var bySpan = MinimumMaxSpan(b).CompareTo(MinimumMaxSpan(a));
if (bySpan != 0)
return bySpan;
var byArea = b.Area.CompareTo(a.Area);
if (byArea != 0)
return byArea;
return string.CompareOrdinal(a.Id, b.Id);
}
);
return demands;
}
private double MinimumMaxSpan(PartModel model)
{
if (!_minimumSpan.TryGetValue(model.Id, out var span))
{
span = double.MaxValue;
foreach (var angle in PartPreparation.CandidateAngles(model))
{
var orientation = _prep.Oriented(model, angle, 0);
var worst = Math.Max(orientation.Width, orientation.Height);
if (worst < span)
span = worst;
}
_minimumSpan[model.Id] = span;
}
return span;
}
private readonly Dictionary<string, double> _minimumSpan = new(StringComparer.Ordinal);
/// <summary>
/// Packs every available stock size independently and commits the best trial:
/// most instances first, then highest priority coverage, then the smallest sheet
/// area (the cost function the benchmark scores), then input order.
/// </summary>
private SheetAttempt? BestNextSheet(
List<PartModel> outstanding,
IProgress<NestJobProgress>? progress,
CancellationToken token
)
{
SheetAttempt? best = null;
(int count, int priorityHits, double area) bestScore = default;
for (var index = 0; index < _job.Plates.Count; index++)
{
var stock = _job.Plates[index];
if (stock.Quantity is int quantity && _used[stock.Id] >= quantity)
continue;
token.ThrowIfCancellationRequested();
progress?.Report(
new NestJobProgress(
NestJobStage.EvaluatingCandidate,
stock.Id,
_sheets.Count,
_sheets.Count,
_placed.Values.Sum()
)
);
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;
var score = ScoreTrial(packer);
if (
best == null
|| score.count > bestScore.count
|| (score.count == bestScore.count && score.priorityHits > bestScore.priorityHits)
|| (
score.count == bestScore.count
&& score.priorityHits == bestScore.priorityHits
&& score.area < bestScore.area
)
)
{
best = new SheetAttempt(packer, index);
bestScore = score;
}
}
return best;
}
/// <summary>
/// This engine's fill policy for one sheet: walk the demand order and drain each
/// requirement greedily; a requirement that cannot place any more instances is
/// skipped (never aborts the sheet) and retried on the next sheet. Consumes a
/// local copy of demand - losing this trial must not change job state.
/// </summary>
private void FillSheet(SheetPacker packer, List<PartModel> outstanding, CancellationToken token)
{
var available = new Dictionary<string, int>(StringComparer.Ordinal);
foreach (var model in outstanding)
available[model.Id] = _remaining[model.Id];
foreach (var model in outstanding)
{
if (available[model.Id] <= 0)
continue;
if (!packer.CanEverFit(model))
continue;
var modelWatch = System.Diagnostics.Stopwatch.StartNew();
while (available[model.Id] > 0 && !packer.IsFull)
{
token.ThrowIfCancellationRequested();
if (!packer.TryInsert(model, out _))
break;
available[model.Id]--;
}
modelWatch.Stop();
if (_diag && modelWatch.ElapsedMilliseconds > 200)
Diag($" fill model {model.Id}: placed={packer.Placed.Count} {modelWatch.ElapsedMilliseconds}ms");
}
}
private (int count, int priorityHits, double area) ScoreTrial(SheetPacker packer)
{
var count = packer.Placed.Count;
var bestPriority = int.MaxValue;
foreach (var placed in packer.Placed)
if (placed.Model.Priority < bestPriority)
bestPriority = placed.Model.Priority;
var priorityHits = packer.Placed.Count(p => p.Model.Priority == bestPriority);
return (count, priorityHits, packer.Stock.Size.Width * packer.Stock.Size.Length);
}
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]--;
}
}
private bool AnyStockAvailable()
{
foreach (var stock in _job.Plates)
if (stock.Quantity is null || _used[stock.Id] < 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(sheet.StockIndex, _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
);
}
}