mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-05 18:52:11 -04:00
perf(irregular): cap surplus rows in private stripe fills
This commit is contained in:
1 parent
5f67a51bf1
commit
32db086384
9 files changed
+258
-18
No files matched your search
@@ -53,6 +53,13 @@ namespace OpenNest.Engine.Fill
|
||||
/// </summary>
|
||||
public string Label { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When positive, grid fills stop adding perpendicular rows once they hold at least
|
||||
/// this many parts; a first row that already holds them is returned on its own. The
|
||||
/// first row is always complete. Zero fills the whole work area.
|
||||
/// </summary>
|
||||
public int MaxParts { get; init; }
|
||||
|
||||
private static Vector MakeOffset(NestDirection direction, double distance)
|
||||
{
|
||||
return direction == NestDirection.Horizontal
|
||||
@@ -195,7 +202,12 @@ namespace OpenNest.Engine.Fill
|
||||
/// patterns, also adds individual parts from the next incomplete copy
|
||||
/// that still fit within the work area.
|
||||
/// </summary>
|
||||
private List<Part> TilePattern(Pattern basePattern, NestDirection direction, OffsetPerimeterCache cache)
|
||||
private List<Part> TilePattern(
|
||||
Pattern basePattern,
|
||||
NestDirection direction,
|
||||
OffsetPerimeterCache cache,
|
||||
int maxParts = 0
|
||||
)
|
||||
{
|
||||
var copyDistance = FindPatternCopyDistance(basePattern, direction, cache);
|
||||
|
||||
@@ -211,7 +223,10 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
var count = 1;
|
||||
|
||||
while (true)
|
||||
// maxParts counts the base pattern too: stop once the tiling holds that many.
|
||||
bool Full() => maxParts > 0 && basePattern.Parts.Count + result.Count >= maxParts;
|
||||
|
||||
while (!Full())
|
||||
{
|
||||
var nextPos = start + copyDistance * count;
|
||||
|
||||
@@ -230,7 +245,7 @@ namespace OpenNest.Engine.Fill
|
||||
// next copy that didn't fit as a whole. This handles cases where
|
||||
// e.g. a 2-part pair only partially fits — one part may still be
|
||||
// within the work area even though the full pattern exceeds it.
|
||||
if (basePattern.Parts.Count > 1)
|
||||
if (basePattern.Parts.Count > 1 && !Full())
|
||||
{
|
||||
var offset = MakeOffset(direction, copyDistance * count);
|
||||
|
||||
@@ -383,7 +398,7 @@ namespace OpenNest.Engine.Fill
|
||||
// If primary tiling didn't produce copies, just tile along perpendicular
|
||||
if (row.Count <= pattern.Parts.Count)
|
||||
{
|
||||
row.AddRange(TilePattern(pattern, perpAxis, cache));
|
||||
row.AddRange(TilePattern(pattern, perpAxis, cache, MaxParts));
|
||||
|
||||
if (pattern.Parts.Count > 1 && HasOverlappingParts(row, out var a2, out var b2))
|
||||
{
|
||||
@@ -395,13 +410,19 @@ namespace OpenNest.Engine.Fill
|
||||
return row;
|
||||
}
|
||||
|
||||
// A capped fill whose first row already holds enough parts needs no more rows.
|
||||
// Step 2's check could only switch perpendicular tiling to the bbox fallback; it
|
||||
// never repairs the row itself, so returning the row loses no protection.
|
||||
if (MaxParts > 0 && row.Count >= MaxParts)
|
||||
return row;
|
||||
|
||||
// Step 2: Build row pattern and tile along perpendicular axis
|
||||
var rowPattern = new Pattern();
|
||||
rowPattern.Parts.AddRange(row);
|
||||
rowPattern.UpdateBounds();
|
||||
|
||||
var gridResult = new List<Part>(rowPattern.Parts);
|
||||
gridResult.AddRange(TilePattern(rowPattern, perpAxis, cache));
|
||||
gridResult.AddRange(TilePattern(rowPattern, perpAxis, cache, MaxParts));
|
||||
|
||||
// Only the unchanged row is covered by Step 1's clean verdict: skip Step 2
|
||||
// only when the perpendicular tiling appended zero parts, so gridResult
|
||||
|
||||
@@ -68,14 +68,16 @@ public class StripeFiller
|
||||
sheetSpan,
|
||||
spacing,
|
||||
axis,
|
||||
_context.Token
|
||||
_context.Token,
|
||||
_context.MaxQuantity
|
||||
);
|
||||
var shrinkResult = ConvergeStripeAngleShrink(
|
||||
pairParts,
|
||||
sheetSpan,
|
||||
spacing,
|
||||
axis,
|
||||
_context.Token
|
||||
_context.Token,
|
||||
_context.MaxQuantity
|
||||
);
|
||||
|
||||
foreach (var (angle, waste, count) in new[] { expandResult, shrinkResult })
|
||||
@@ -127,7 +129,11 @@ public class StripeFiller
|
||||
if (!_dedup.TryAdd(rotatedPattern.BoundingBox, workArea, primaryAxis))
|
||||
return null;
|
||||
|
||||
var stripeEngine = new FillLinear(stripeBox, spacing) { Label = "Stripe" };
|
||||
var stripeEngine = new FillLinear(stripeBox, spacing)
|
||||
{
|
||||
Label = "Stripe",
|
||||
MaxParts = _context.MaxQuantity,
|
||||
};
|
||||
var stripeParts = stripeEngine.Fill(rotatedPattern, primaryAxis);
|
||||
|
||||
if (stripeParts == null || stripeParts.Count == 0)
|
||||
@@ -144,7 +150,11 @@ public class StripeFiller
|
||||
stripePattern.Parts.AddRange(stripeParts);
|
||||
stripePattern.UpdateBounds();
|
||||
|
||||
var gridEngine = new FillLinear(workArea, spacing) { Label = "Stripe-Grid" };
|
||||
var gridEngine = new FillLinear(workArea, spacing)
|
||||
{
|
||||
Label = "Stripe-Grid",
|
||||
MaxParts = _context.MaxQuantity,
|
||||
};
|
||||
var gridParts = gridEngine.Fill(stripePattern, perpAxis);
|
||||
|
||||
if (gridParts == null || gridParts.Count == 0)
|
||||
@@ -179,6 +189,10 @@ public class StripeFiller
|
||||
|
||||
var allParts = new List<Part>(gridParts);
|
||||
|
||||
// A capped fill that already holds the quantity needs no remnant parts.
|
||||
if (_context.MaxQuantity > 0 && gridParts.Count >= _context.MaxQuantity)
|
||||
return allParts;
|
||||
|
||||
var remnantParts = FillRemnant(gridParts, primaryAxis);
|
||||
if (remnantParts != null)
|
||||
{
|
||||
@@ -351,18 +365,20 @@ public class StripeFiller
|
||||
/// <summary>
|
||||
/// Iteratively finds the rotation angle where N copies of the pattern
|
||||
/// span the given dimension with minimal waste by expanding pair width.
|
||||
/// Returns (angle, waste, pairCount).
|
||||
/// Returns (angle, waste, pairCount). A positive <paramref name="maxParts"/> caps
|
||||
/// the estimate rows like <see cref="FillLinear.MaxParts"/>.
|
||||
/// </summary>
|
||||
public static (double Angle, double Waste, int Count) ConvergeStripeAngle(
|
||||
List<Part> patternParts,
|
||||
double sheetSpan,
|
||||
double spacing,
|
||||
NestDirection axis,
|
||||
CancellationToken token = default
|
||||
CancellationToken token = default,
|
||||
int maxParts = 0
|
||||
)
|
||||
{
|
||||
var startAngle = OrientShortSideAlong(patternParts, axis);
|
||||
return ConvergeFromAngle(patternParts, startAngle, sheetSpan, spacing, axis, token);
|
||||
return ConvergeFromAngle(patternParts, startAngle, sheetSpan, spacing, axis, token, maxParts);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -374,7 +390,8 @@ public class StripeFiller
|
||||
double sheetSpan,
|
||||
double spacing,
|
||||
NestDirection axis,
|
||||
CancellationToken token = default
|
||||
CancellationToken token = default,
|
||||
int maxParts = 0
|
||||
)
|
||||
{
|
||||
var baseAngle = OrientShortSideAlong(patternParts, axis);
|
||||
@@ -392,7 +409,7 @@ public class StripeFiller
|
||||
return (0, double.MaxValue, 0);
|
||||
|
||||
var startAngle = FindAngleForTargetSpan(patternParts, targetSpan, axis);
|
||||
return ConvergeFromAngle(patternParts, startAngle, sheetSpan, spacing, axis, token);
|
||||
return ConvergeFromAngle(patternParts, startAngle, sheetSpan, spacing, axis, token, maxParts);
|
||||
}
|
||||
|
||||
private static (double Angle, double Waste, int Count) ConvergeFromAngle(
|
||||
@@ -401,7 +418,8 @@ public class StripeFiller
|
||||
double sheetSpan,
|
||||
double spacing,
|
||||
NestDirection axis,
|
||||
CancellationToken token
|
||||
CancellationToken token,
|
||||
int maxParts
|
||||
)
|
||||
{
|
||||
var bestWaste = double.MaxValue;
|
||||
@@ -428,7 +446,11 @@ public class StripeFiller
|
||||
axis == NestDirection.Horizontal
|
||||
? new Box(0, 0, sheetSpan, perpDim)
|
||||
: new Box(0, 0, perpDim, sheetSpan);
|
||||
var engine = new FillLinear(stripeBox, spacing) { Label = "Stripe-EstimateRow" };
|
||||
var engine = new FillLinear(stripeBox, spacing)
|
||||
{
|
||||
Label = "Stripe-EstimateRow",
|
||||
MaxParts = maxParts,
|
||||
};
|
||||
var filled = engine.Fill(rotated, axis);
|
||||
var n = filled?.Count ?? 0;
|
||||
|
||||
|
||||
@@ -28,6 +28,13 @@ internal class DefaultPlateFiller : PlateFillerBase
|
||||
set => angleBuilder.ForceFullSweep = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When true, a positive item quantity lets supporting strategies stop adding rows once a
|
||||
/// candidate holds the quantity. Other strategies still fill the work area; the winner is
|
||||
/// trimmed to the quantity. Off by default, so ordinary fills are unchanged.
|
||||
/// </summary>
|
||||
internal bool StopAtQuantity { get; init; }
|
||||
|
||||
public override List<double> BuildAngles(
|
||||
NestItem item,
|
||||
ClassificationResult classification,
|
||||
@@ -338,7 +345,7 @@ internal class DefaultPlateFiller : PlateFillerBase
|
||||
Token = token,
|
||||
Progress = progress,
|
||||
Policy = BuildPolicy(),
|
||||
MaxQuantity = item.Quantity,
|
||||
MaxQuantity = StopAtQuantity ? System.Math.Max(0, item.Quantity) : 0,
|
||||
};
|
||||
RunPipeline(context);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.Engine.BestFit;
|
||||
using OpenNest.Engine.Jobs.Adapters;
|
||||
using OpenNest.Engine.Jobs.Placement.Fillers;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.Jobs.Placement;
|
||||
@@ -29,7 +30,10 @@ internal static class PrivatePlateFill
|
||||
.ThenBy(f => f.Candidate.Part2Offset.Y).ThenBy(f => f.OptimalRotation).ToArray();
|
||||
fits.Clear();
|
||||
fits.AddRange(sorted);
|
||||
return PlateFillService.FillItem("Default", plate, new NestItem
|
||||
// A positive quantity lets supporting strategies avoid surplus rows. Zero still
|
||||
// fills the whole area (the cutout lattice caller requires an uncapped lattice).
|
||||
var filler = new DefaultPlateFiller(plate) { StopAtQuantity = true };
|
||||
return filler.Fill(new NestItem
|
||||
{
|
||||
Drawing = drawing,
|
||||
Quantity = quantity,
|
||||
|
||||
@@ -17,6 +17,10 @@ namespace OpenNest.Engine.Strategies
|
||||
public CancellationToken Token { get; init; }
|
||||
public IProgress<NestProgress> Progress { get; init; }
|
||||
public FillPolicy Policy { get; init; } = new FillPolicy(new DefaultFillComparer());
|
||||
/// <summary>
|
||||
/// When positive, strategies may stop placing parts once a candidate holds this
|
||||
/// many (see <see cref="FillLinear.MaxParts"/>). Zero fills the whole work area.
|
||||
/// </summary>
|
||||
public int MaxQuantity { get; init; }
|
||||
public PartType PartType { get; set; }
|
||||
|
||||
|
||||
@@ -70,6 +70,27 @@ public class EngineRefactorSmokeTests
|
||||
Assert.True(parts.Count > 0, "ForceFullAngleSweep should still produce results");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void DefaultPlateFiller_StopAtQuantity_StillDeliversExactlyTheQuantity(bool stopAtQuantity)
|
||||
{
|
||||
var plate = new Plate(60, 120) { PartSpacing = 0.5 };
|
||||
var filler = new DefaultPlateFiller(plate) { StopAtQuantity = stopAtQuantity };
|
||||
var item = new NestItem { Drawing = MakeRectDrawing(7, 3), Quantity = 23 };
|
||||
|
||||
var parts = filler.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None);
|
||||
|
||||
Assert.Equal(23, parts.Count);
|
||||
plate.Parts.AddRange(parts);
|
||||
Assert.False(plate.HasOverlappingParts(out _), "Fill overlaps");
|
||||
var area = plate.WorkArea();
|
||||
Assert.All(parts, p => Assert.True(
|
||||
p.BoundingBox.Left >= area.Left - 1e-6 && p.BoundingBox.Right <= area.Right + 1e-6
|
||||
&& p.BoundingBox.Bottom >= area.Bottom - 1e-6 && p.BoundingBox.Top <= area.Top + 1e-6,
|
||||
"Part outside the work area"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StripPlateFiller_Nest_ProducesResults()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Fill;
|
||||
|
||||
public class FillLinearMaxPartsTests
|
||||
{
|
||||
// 10 x 5 rectangles at 0.5 spacing in a 100 x 100 area: 9 per row, 18 rows.
|
||||
private static readonly Box Area = new(0, 0, 100, 100);
|
||||
private const double Spacing = 0.5;
|
||||
|
||||
private static Drawing Rectangle()
|
||||
{
|
||||
var pgm = new OpenNest.CNC.Program();
|
||||
pgm.Codes.Add(new OpenNest.CNC.RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(10, 0)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(10, 5)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, 5)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, 0)));
|
||||
return new Drawing("rect", pgm);
|
||||
}
|
||||
|
||||
private static List<(double X, double Y)> Poses(List<Part> parts) =>
|
||||
parts.Select(p => (p.Location.X, p.Location.Y)).OrderBy(p => p.Y).ThenBy(p => p.X).ToList();
|
||||
|
||||
[Fact]
|
||||
public void Uncapped_FillsTheWholeArea()
|
||||
{
|
||||
var parts = new FillLinear(Area, Spacing).Fill(Rectangle(), 0, NestDirection.Horizontal);
|
||||
|
||||
Assert.Equal(9 * 18, parts.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cap_StopsAddingRowsOnceReached_AndKeepsTheFullGridPrefix()
|
||||
{
|
||||
var drawing = Rectangle();
|
||||
var full = new FillLinear(Area, Spacing).Fill(drawing, 0, NestDirection.Horizontal);
|
||||
var capped = new FillLinear(Area, Spacing) { MaxParts = 20 }.Fill(drawing, 0, NestDirection.Horizontal);
|
||||
|
||||
// Whole rows only: 20 parts need three rows of nine.
|
||||
Assert.Equal(27, capped.Count);
|
||||
var rows = Poses(full).Select(p => p.Y).Distinct().OrderBy(y => y).Take(3).ToHashSet();
|
||||
Assert.Equal(Poses(full).Where(p => rows.Contains(p.Y)).ToList(), Poses(capped));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cap_SmallerThanTheFirstRow_ReturnsTheCompleteFirstRow()
|
||||
{
|
||||
var capped = new FillLinear(Area, Spacing) { MaxParts = 5 }.Fill(Rectangle(), 0, NestDirection.Horizontal);
|
||||
|
||||
Assert.Equal(9, capped.Count);
|
||||
Assert.Single(capped.Select(p => p.Location.Y).Distinct());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(-1)]
|
||||
public void NonpositiveCap_MatchesTheUncappedFill(int maxParts)
|
||||
{
|
||||
var drawing = Rectangle();
|
||||
var full = new FillLinear(Area, Spacing).Fill(drawing, 0, NestDirection.Horizontal);
|
||||
var capped = new FillLinear(Area, Spacing) { MaxParts = maxParts }.Fill(drawing, 0, NestDirection.Horizontal);
|
||||
|
||||
Assert.Equal(Poses(full), Poses(capped));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(NestDirection.Horizontal, 10, 100)]
|
||||
[InlineData(NestDirection.Vertical, 100, 5)]
|
||||
public void Cap_PerpendicularOnlyFill_StopsAtQuantity(NestDirection direction, double length, double width)
|
||||
{
|
||||
var area = new Box(0, 0, length, width);
|
||||
var parts = new FillLinear(area, Spacing) { MaxParts = 3 }.Fill(Rectangle(), 0, direction);
|
||||
|
||||
Assert.Equal(3, parts.Count);
|
||||
var plate = new Plate(width, length) { PartSpacing = Spacing };
|
||||
plate.Parts.AddRange(parts);
|
||||
Assert.False(plate.HasOverlappingParts(out _));
|
||||
Assert.All(parts, part => Assert.True(part.BoundingBox.Right <= area.Right
|
||||
&& part.BoundingBox.Top <= area.Top));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cap_DoesNotPlaceAPartThatCannotFit()
|
||||
{
|
||||
Assert.Empty(new FillLinear(new Box(0, 0, 4, 4), Spacing) { MaxParts = 1 }
|
||||
.Fill(Rectangle(), 0, NestDirection.Horizontal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cap_LargerThanTheAreaHolds_MatchesTheUncappedFill()
|
||||
{
|
||||
var drawing = Rectangle();
|
||||
var full = new FillLinear(Area, Spacing).Fill(drawing, 0, NestDirection.Horizontal);
|
||||
var capped = new FillLinear(Area, Spacing) { MaxParts = 1000 }.Fill(drawing, 0, NestDirection.Horizontal);
|
||||
|
||||
Assert.Equal(Poses(full), Poses(capped));
|
||||
}
|
||||
}
|
||||
@@ -227,6 +227,37 @@ public class StripeFillerTests
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Part> StripeFill(int maxQuantity, NestDirection axis)
|
||||
{
|
||||
var plate = new Plate(60, 120) { PartSpacing = 0.5 };
|
||||
var drawing = MakeRectDrawing(20, 10);
|
||||
var context = new OpenNest.Engine.Strategies.FillContext
|
||||
{
|
||||
Item = new NestItem { Drawing = drawing, Quantity = maxQuantity },
|
||||
WorkArea = new Box(0, 0, 120, 60),
|
||||
Plate = plate,
|
||||
Token = System.Threading.CancellationToken.None,
|
||||
MaxQuantity = maxQuantity,
|
||||
};
|
||||
context.SharedState["BestFits"] = MakeSideBySideBestFits(drawing, 0.5);
|
||||
return new StripeFiller(context, axis).Fill();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(NestDirection.Horizontal)]
|
||||
[InlineData(NestDirection.Vertical)]
|
||||
public void Fill_WithMaxQuantity_StopsOnceTheQuantityIsHeld(NestDirection axis)
|
||||
{
|
||||
var full = StripeFill(0, axis);
|
||||
var capped = StripeFill(6, axis);
|
||||
|
||||
Assert.True(full.Count > 6, $"Control fill should exceed the cap, got {full.Count}");
|
||||
Assert.InRange(capped.Count, 6, full.Count - 1);
|
||||
var plate = new Plate(60, 120) { PartSpacing = 0.5 };
|
||||
plate.Parts.AddRange(capped);
|
||||
Assert.False(plate.HasOverlappingParts(out _), "Capped stripe fill overlaps");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fill_ReturnsEmpty_WhenNoBestFits()
|
||||
{
|
||||
|
||||
@@ -11,6 +11,33 @@ Only the exact value `1` enables these tests; otherwise they skip. In PowerShell
|
||||
|
||||
The category covers comparer, group-pattern, rotated-pattern, extents-column, feature-extraction, no-model angle, and FillLinear offset-geometry workloads; individual filters match benchmark method names in `FillPerformanceTests.cs`. Overlap checks are measured separately by `OverlapCheck_ReportsPolygonPairsAndGridChecks` in `OpenNest.Tests/Fill/OverlapCheckPerformanceTests.cs` (same category). Keep harness, inputs, warmups and batches identical before/after; exclude setup/assertions from timing. Comparer/extents allocations are synchronous and current-thread only; parallel group fills omit allocation totals. No timing CI gates or whole-job speedup claims. Keep raw results and run-specific reports outside source control; this guide documents the reusable verification procedure.
|
||||
|
||||
### Quantity-limited private fills
|
||||
|
||||
Irregular's private block proposals opt into quantity-limited Row/Column stripe fills.
|
||||
`FillLinear.MaxParts` is a soft row limit: the first primary row stays complete, and
|
||||
perpendicular copies stop after reaching the requested count. The final fill still
|
||||
trims its winner to the exact quantity. Existing overlap fallbacks and block spacing
|
||||
certification remain active; fallback tiling can exceed the soft limit. Nonpositive
|
||||
limits remain uncapped. Ordinary Default fills and the zero-quantity cutout lattice
|
||||
keep their full-area behavior.
|
||||
|
||||
This can change which private block candidate wins; it is not a promise of identical
|
||||
Irregular layouts. Verify fulfillment, bounds, spacing and rotation policy through
|
||||
the full pipeline, rather than treating a faster cancelled run as acceptance.
|
||||
|
||||
```sh
|
||||
dotnet test OpenNest.Tests/OpenNest.Tests.csproj -c Release \
|
||||
--filter 'FullyQualifiedName~FillLinearMaxPartsTests|FullyQualifiedName~StripeFillerTests|FullyQualifiedName~EngineRefactorSmokeTests'
|
||||
dotnet test OpenNest.Engine.Tests/OpenNest.Engine.Tests.csproj -c Release \
|
||||
--filter 'FullyQualifiedName~IrregularBlockTests|FullyQualifiedName~CutoutLatticeFillTests'
|
||||
python3 scripts/check-synthetic-nests.py
|
||||
```
|
||||
|
||||
For a supplied slow job, retain a bounded before/after trace, rebuilt probe and input
|
||||
hashes, and a completion run with all demand placed and zero independent validation
|
||||
violations. Bound cancellation and the outer process watchdog separately. Do not
|
||||
extend a known unchanged timeout just to obtain a baseline duration.
|
||||
|
||||
### FillLinear unchanged-row validation baseline
|
||||
|
||||
```bash
|
||||
|
||||
Reference in new issue
Block a user