mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-07 11:55:44 -05:00
fix(autonest): mark ledger substitutes against the best rank matched
An acceptance rule may list sheet grades first, so at plate thickness its best match can be rank 3 (A36 for HR). Auto Nest marked every row above rank 1 a substitute; it now ranks rows against the best rule rank ML matched at the nest's thickness, as ML labels them, so A36 is preferred and only A572 is a substitute. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
079dfaf9ea
commit
610b9dafcb
3 files changed
+50
-5
No files matched your search
@@ -30,8 +30,15 @@ public sealed record LedgerStockRow(
|
||||
/// <summary>The catalog grade and finish the row is cut from, e.g. "A1011 P&O"; null when the ledger sends neither.</summary>
|
||||
public string? Label { get; init; }
|
||||
|
||||
/// <summary>True for stock ML accepts in place of the preferred material (rank above 1).</summary>
|
||||
public bool IsSubstitute => Rank > 1;
|
||||
/// <summary>
|
||||
/// The best rule rank among the specs ML matched at this thickness: the preferred material
|
||||
/// for this request. A rule may list sheet grades first, so at plate thickness its best match
|
||||
/// can be rank 3 and is still not a substitute.
|
||||
/// </summary>
|
||||
public int PreferredRank { get; init; } = 1;
|
||||
|
||||
/// <summary>True for stock ML accepts in place of the preferred material (a rule rank above <see cref="PreferredRank"/>).</summary>
|
||||
public bool IsSubstitute => Rank > PreferredRank;
|
||||
}
|
||||
|
||||
/// <summary>The rows offered to Auto Nest and the counts of ledger items left out.</summary>
|
||||
@@ -156,6 +163,11 @@ public static class LedgerStockMapper
|
||||
}
|
||||
}
|
||||
|
||||
// ML labels substitutes against the best-ranked spec it matched, not against rank 1;
|
||||
// an exact mapping (rank 0) overrides the rule and never makes rule stock a substitute.
|
||||
var ruleRanks = specById.Values.Where(s => s.Rank >= 1).Select(s => s.Rank!.Value)
|
||||
.Concat(entries.Where(e => e.Rank >= 1).Select(e => e.Rank)).ToList();
|
||||
var preferredRank = ruleRanks.Count == 0 ? 1 : ruleRanks.Min();
|
||||
var rows = new List<LedgerStockRow>(entries.Count);
|
||||
// ML already sends rank order; the stable sort keeps ledger order within a rank.
|
||||
foreach (var entry in entries.OrderBy(e => e.Rank))
|
||||
@@ -175,6 +187,7 @@ public static class LedgerStockMapper
|
||||
{
|
||||
ItemLocations = itemLocations,
|
||||
Rank = entry.Rank,
|
||||
PreferredRank = preferredRank,
|
||||
Label = entry.Label,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -87,6 +87,35 @@ public class LedgerStockMapperTests
|
||||
Assert.Equal(new[] { false, false, false, true, true }, mapping.Rows.Select(r => r.IsSubstitute));
|
||||
}
|
||||
|
||||
// Steel/HR lists A1011 grades first; at 3/8 only its A36 (rank 3) and A572 (rank 4) entries
|
||||
// match, so A36 is the preferred plate and only A572 is a substitute, as ML labels them.
|
||||
[Fact]
|
||||
public void SubstitutesAreRankedAgainstTheBestRankMatchedAtThisThickness()
|
||||
{
|
||||
var mapping = LedgerStockMapper.Map(new[]
|
||||
{
|
||||
Item("A36-1", LedgerItemKinds.FullSheet, 60, 120, spec: 30, rank: 3),
|
||||
Item("A36-2", LedgerItemKinds.Drop, 20, 70, spec: 30, rank: 3),
|
||||
Item("GR50-1", LedgerItemKinds.FullSheet, 60, 120, spec: 31, rank: 4),
|
||||
}, Units.Inches, specs: new[] { Spec(30, 3, "A36", null), Spec(31, 4, "A572 GR50", null) });
|
||||
|
||||
Assert.Equal(new[] { false, false, true }, mapping.Rows.Select(r => r.IsSubstitute));
|
||||
Assert.Equal(1, mapping.SubstituteRows);
|
||||
Assert.All(mapping.Rows, r => Assert.Equal(3, r.PreferredRank));
|
||||
}
|
||||
|
||||
// The preferred spec may have nothing on the rack: it still sets the bar for substitutes.
|
||||
[Fact]
|
||||
public void AnOutOfStockPreferredSpecStillMakesLowerRanksSubstitutes()
|
||||
{
|
||||
var mapping = LedgerStockMapper.Map(new[]
|
||||
{
|
||||
Item("PO-1", LedgerItemKinds.FullSheet, 48, 96, spec: 41, rank: 2),
|
||||
}, Units.Inches, specs: new[] { Spec(40, 1, "A1011", "HR"), Spec(41, 2, "A1011", "HRPO") });
|
||||
|
||||
Assert.True(Assert.Single(mapping.Rows).IsSubstitute);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnOlderLedgerWithoutRanksOrGradesIsRankOneAndUnlabeled()
|
||||
{
|
||||
|
||||
@@ -277,8 +277,11 @@ and the thickness converted to inches (rounded to 4 places). ML resolves its own
|
||||
OpenNest material mapping, and OpenNest keeps no copy of it. OpenNest's grade is a shop
|
||||
condition such as HR or P&O; ML's acceptance rule for the material and grade decides which
|
||||
catalog grades and finishes may fill it, ranked by preference. Rank 0 is an exact mapping
|
||||
override, rank 1 the rule's preferred entry, and higher ranks are substitutes, which only
|
||||
ever upgrade (P&O may fill HR, never the reverse). Specs carry `rank`, `materialFamily`,
|
||||
override and rank 1 the rule's first entry. The best rule rank ML matched at the nest's
|
||||
thickness is the preferred material, and higher ranks are substitutes, which only ever
|
||||
upgrade (P&O may fill HR, never the reverse). A rule that lists sheet grades first can
|
||||
match only its plate entries at plate thickness: there A36 at rank 3 is preferred and
|
||||
A572 at rank 4 the substitute, as ML labels them. Specs carry `rank`, `materialFamily`,
|
||||
`grade` and `finish`; items carry `rank` and arrive in rank, location code, item code order.
|
||||
An older ledger that sends none of these still loads: its stock counts as rank 1, unlabeled.
|
||||
The button is enabled when
|
||||
@@ -309,7 +312,7 @@ separate `NestJob` input. `OpenNest.Engine` has no ledger reference.
|
||||
|
||||
**Grid.** A load replaces earlier ledger rows and keeps hand-typed rows. A ledger row's
|
||||
Source shows its label, or "Ledger" when the ledger sends no grade or finish, with
|
||||
"(irr.)" for an irregular drop. A substitute (rank above 1) reads, for example,
|
||||
"(irr.)" for an irregular drop. A substitute (a rank above the preferred one) reads, for example,
|
||||
"A1011 P&O for HR" in amber, and its tooltip names the substitution and rank ahead of the
|
||||
usual item codes and locations. Substitutes load ticked like other rows; untick **Use** to
|
||||
keep them out of the run. Ledger rows' size and cost are read-only. You can lower a ledger
|
||||
|
||||
Reference in new issue
Block a user