mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-02 17:11:35 -05:00
Under a time limit, Irregular often finished in about a second and left
the rest unused: on the converted Nestbolt b-jakobs1 job it used five
sheets where four fit.
NestJobOptions.SearchUntilCancelled asks an engine to keep searching
after its normal search until the token is cancelled.
DeadlineNestingEngine sets it. Irregular then re-runs its six variants
with seeded per-type weights on its placement rule (seeds 1, 2, 3, ...,
each weight within e^+-0.4), reports each plan that beats its best,
then re-plans that plan's tail and reports any gain. It skips this when
the token cannot be cancelled, nobody listens for progress, nothing is
placeable, or the job has one part type, so unlimited solves are
unchanged.
Default now runs Irregular last (Rectangles, Circles, Irregular) so
Circles still runs, and chooses only after every candidate has run, in
tie-break order, so the run order never changes the choice. Unlimited
choices match the old run order {Rectangles, Irregular, Circles} except
when three candidates are near-tied within the 1e-9 comparison
tolerance without being transitively ordered (only on all-round jobs,
where Circles bids).
b-jakobs1 on one CPU, 10 s limit: 5 sheets (4.500) -> 4 (3.751).
211 lines
8.2 KiB
C#
211 lines
8.2 KiB
C#
#nullable enable
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Runtime.ExceptionServices;
|
|
using System.Threading;
|
|
using OpenNest.Engine.Jobs;
|
|
using OpenNest.Engine.NestingEngines.Circles;
|
|
using OpenNest.Engine.NestingEngines.Irregular;
|
|
using OpenNest.Engine.NestingEngines.Rectangles;
|
|
|
|
namespace OpenNest.Engine.NestingEngines.Default;
|
|
|
|
/// <summary>
|
|
/// The engine used when the caller names none. It runs each candidate engine on the whole job,
|
|
/// checks every result with <see cref="NestLayoutCheck"/> and returns the best one: a valid
|
|
/// layout before an invalid one, then the fewest unplaced parts, then the lowest
|
|
/// <see cref="NestJobCost"/>; remaining ties keep candidate order.
|
|
///
|
|
/// Candidates are Irregular, then Rectangles, then Circles. Neither of the first two wins every
|
|
/// job: Rectangles packs each part as its box, which is often best for plain plates and is a
|
|
/// cheap, valid fallback for the rest. Circles bids only when every part is round, and otherwise
|
|
/// returns everything unplaced at once.
|
|
/// A candidate that throws or returns no result is skipped; when every candidate throws, the
|
|
/// exception of the first in tie-break order is rethrown. Cancellation stops the search.
|
|
/// Candidates' plate commits are not forwarded; the chosen result's commits are reported once it
|
|
/// is selected.
|
|
///
|
|
/// Candidates' whole-job plans (<see cref="NestJobStage.PlanImproved"/>) are scored the same way
|
|
/// and reported only when one beats every plan reported so far, so a later candidate never shows a
|
|
/// worse plan after a better one. A candidate result that beats them is reported too, and the
|
|
/// chosen result is always the last plan reported.
|
|
///
|
|
/// Rectangles, the quick and always-valid fallback, runs first, then Circles, then Irregular, so
|
|
/// a caller that stops the search early, such as <see cref="DeadlineNestingEngine"/>, already
|
|
/// holds a valid plan, and Irregular can search until cancelled
|
|
/// (<see cref="NestJobOptions.SearchUntilCancelled"/>) last. The choice is made after all have
|
|
/// run, in candidate (tie-break) order, so the run order never changes it.
|
|
///
|
|
/// Deterministic: candidates run one after another and the choice uses no clock.
|
|
/// </summary>
|
|
public sealed class DefaultNestingEngine : INestingEngine
|
|
{
|
|
private readonly IReadOnlyList<Func<INestingEngine>> candidates;
|
|
private readonly IReadOnlyList<int> runOrder;
|
|
|
|
public DefaultNestingEngine()
|
|
: this(
|
|
new Func<INestingEngine>[]
|
|
{
|
|
() => new IrregularNestingEngine(),
|
|
() => new RectanglesNestingEngine(),
|
|
() => new CirclesNestingEngine(),
|
|
},
|
|
new[] { 1, 2, 0 })
|
|
{
|
|
}
|
|
|
|
/// <summary>Test seam: candidate engines in tie-break order, run in that order.</summary>
|
|
internal DefaultNestingEngine(IReadOnlyList<Func<INestingEngine>> candidates)
|
|
: this(candidates, Enumerable.Range(0, candidates?.Count ?? 0).ToArray())
|
|
{
|
|
}
|
|
|
|
/// <summary>Test seam: candidate engines in tie-break order, and the order to run them in
|
|
/// (a permutation of their indexes); the run order affects progress, never the choice.
|
|
/// </summary>
|
|
internal DefaultNestingEngine(
|
|
IReadOnlyList<Func<INestingEngine>> candidates,
|
|
IReadOnlyList<int> runOrder
|
|
)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(candidates);
|
|
ArgumentNullException.ThrowIfNull(runOrder);
|
|
if (candidates.Count == 0)
|
|
throw new ArgumentException(
|
|
"At least one candidate engine is required.",
|
|
nameof(candidates)
|
|
);
|
|
if (
|
|
runOrder.Count != candidates.Count
|
|
|| runOrder.Distinct().Count() != candidates.Count
|
|
|| runOrder.Any(i => i < 0 || i >= candidates.Count)
|
|
)
|
|
throw new ArgumentException(
|
|
"The run order must list every candidate index once.",
|
|
nameof(runOrder)
|
|
);
|
|
this.candidates = candidates;
|
|
this.runOrder = runOrder;
|
|
}
|
|
|
|
/// <summary>Candidate indexes in the order they run.</summary>
|
|
internal IReadOnlyList<int> RunOrder => runOrder;
|
|
|
|
public NestJobResult Solve(
|
|
NestJob job,
|
|
IProgress<NestJobProgress>? progress = null,
|
|
CancellationToken token = default
|
|
)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(job);
|
|
token.ThrowIfCancellationRequested();
|
|
var forward = progress == null ? null : new PlanFilter(job, progress, token);
|
|
var scores = new CandidateScore?[candidates.Count];
|
|
ExceptionDispatchInfo? firstFailure = null;
|
|
var firstFailureOrder = int.MaxValue;
|
|
|
|
foreach (var order in runOrder)
|
|
{
|
|
token.ThrowIfCancellationRequested();
|
|
NestJobResult? result;
|
|
if (forward != null)
|
|
forward.Order = order;
|
|
try
|
|
{
|
|
result = candidates[order]().Solve(job, forward, token);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// A canceled solve is rethrown by the token checks around each candidate.
|
|
if (order < firstFailureOrder)
|
|
{
|
|
firstFailure = ExceptionDispatchInfo.Capture(ex);
|
|
firstFailureOrder = order;
|
|
}
|
|
continue;
|
|
}
|
|
if (result == null)
|
|
continue;
|
|
|
|
var scored = CandidateScore.Of(job, result, order);
|
|
scores[order] = scored;
|
|
forward?.Offer(scored);
|
|
}
|
|
|
|
// Choose in tie-break order, whatever order the candidates ran in.
|
|
CandidateScore? best = null;
|
|
foreach (var scored in scores)
|
|
if (scored != null && (best == null || scored.IsBetterThan(best)))
|
|
best = scored;
|
|
|
|
token.ThrowIfCancellationRequested();
|
|
if (best == null)
|
|
{
|
|
firstFailure?.Throw();
|
|
throw new InvalidOperationException("No candidate engine returned a result.");
|
|
}
|
|
|
|
forward?.Finish(best);
|
|
CandidateProgress.ReportCommits(best.Result, progress);
|
|
return best.Result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Forwards candidate progress except plate commits, and replaces candidates' raw plan reports
|
|
/// with Default's own best plan so far by <see cref="CandidateScore"/>.
|
|
/// </summary>
|
|
private sealed class PlanFilter(NestJob job, IProgress<NestJobProgress> inner, CancellationToken token)
|
|
: IProgress<NestJobProgress>
|
|
{
|
|
private CandidateScore? shown;
|
|
|
|
/// <summary>Candidate order of the plans now arriving, for score ties.</summary>
|
|
public int Order { get; set; }
|
|
|
|
public void Report(NestJobProgress value)
|
|
{
|
|
if (value == null || value.Stage == NestJobStage.PlateCommitted)
|
|
return;
|
|
if (value.Stage != NestJobStage.PlanImproved)
|
|
{
|
|
inner.Report(value);
|
|
return;
|
|
}
|
|
if (value.Plan == null)
|
|
return;
|
|
CandidateScore plan;
|
|
try
|
|
{
|
|
plan = CandidateScore.Of(job, value.Plan, Order);
|
|
}
|
|
catch (Exception) when (!token.IsCancellationRequested)
|
|
{
|
|
// A plan that cannot be scored is not shown; throwing would fail the candidate.
|
|
return;
|
|
}
|
|
Offer(plan);
|
|
}
|
|
|
|
/// <summary>Reports the plan when it beats every plan reported so far.</summary>
|
|
public void Offer(CandidateScore plan)
|
|
{
|
|
if (shown != null && !plan.IsBetterThan(shown))
|
|
return;
|
|
shown = plan;
|
|
CandidateProgress.ReportPlan(plan.Result, inner);
|
|
}
|
|
|
|
/// <summary>Makes the chosen result the last plan reported. A candidate's plan can outscore
|
|
/// every returned result (its engine judged a later plan better), so this may follow it.</summary>
|
|
public void Finish(CandidateScore chosen)
|
|
{
|
|
if (shown != null && CandidateProgress.SameLayout(shown.Result, chosen.Result))
|
|
return;
|
|
shown = chosen;
|
|
CandidateProgress.ReportPlan(chosen.Result, inner);
|
|
}
|
|
}
|
|
}
|