mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-14 18:21:55 -04:00
feat(cutoffs): shift strip lines to shorten cuts
This commit is contained in:
1 parent
15055aef4d
commit
5857e7fc69
4 files changed
+450
-6
No files matched your search
@@ -21,6 +21,12 @@ public sealed class AutomaticCutOffOptions
|
||||
/// The desktop default is 12 inches (304.8 mm).
|
||||
/// </summary>
|
||||
public double MinimumTailLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Minimum strip length in model units. Null or equal to Spacing preserves the fixed grid.
|
||||
/// Otherwise must be finite, greater than MinimumSpacing and no greater than Spacing.
|
||||
/// </summary>
|
||||
public double? MinimumStripLength { get; set; }
|
||||
}
|
||||
|
||||
public enum AutomaticCutOffDiagnosticCode
|
||||
@@ -32,6 +38,7 @@ public enum AutomaticCutOffDiagnosticCode
|
||||
ClampBlockedCut,
|
||||
NoSafeTailSeparator,
|
||||
TailBelowMinimum,
|
||||
ShiftedCut,
|
||||
}
|
||||
|
||||
public sealed record AutomaticCutOffDiagnostic(
|
||||
@@ -70,6 +77,12 @@ public sealed class AutomaticCutOffPlan
|
||||
/// <summary>True only for a verified full-width separator, never just a nominal boundary.</summary>
|
||||
public bool HasSeparatedTail => TailSeparatorX.HasValue;
|
||||
|
||||
/// <summary>Total cut length of proposed new strip lines (excludes the tail separator).</summary>
|
||||
public double StripCutLength { get; internal set; }
|
||||
|
||||
/// <summary>Length of nominal strip lines in the same gaps. Zero on the legacy path.</summary>
|
||||
public double FixedGridCutLength { get; internal set; }
|
||||
|
||||
public bool HasBlockingDiagnostics => Diagnostics.Any(d => d.IsBlocking);
|
||||
}
|
||||
|
||||
@@ -79,6 +92,7 @@ public static class AutomaticCutOffPlanner
|
||||
public const double GeometryTolerance = Tolerance.Epsilon;
|
||||
public const double MinimumSpacing = 2 * GeometryTolerance;
|
||||
public const int MaximumCandidateCount = 10000;
|
||||
public const int MaximumShiftCandidateCount = 20000;
|
||||
|
||||
/// <summary>
|
||||
/// Plans without changing the plate, its parts, or existing cut-off definitions/programs.
|
||||
@@ -133,14 +147,25 @@ public static class AutomaticCutOffPlanner
|
||||
var diagnostics = new List<AutomaticCutOffDiagnostic>();
|
||||
var separated = false;
|
||||
var separatorX = (double?)null;
|
||||
var shifted = options.MinimumStripLength.HasValue && options.MinimumStripLength.Value < options.Spacing
|
||||
&& !plate.CutOffs.Any(c => c.Axis == CutOffAxis.Vertical);
|
||||
var stripCutLength = 0.0;
|
||||
var fixedGridCutLength = 0.0;
|
||||
var measurements = new Dictionary<double, (double Length, int SegmentCount)>();
|
||||
var candidates = new HashSet<double>();
|
||||
|
||||
// Multiplication by an integer avoids drift from repeated floating-point addition.
|
||||
for (var index = 1; index <= (int)candidateCount; index++)
|
||||
if (shifted)
|
||||
AddShiftedGap(0, usedSpan);
|
||||
else
|
||||
{
|
||||
var distance = index * options.Spacing;
|
||||
if (distance >= usedSpan - GeometryTolerance)
|
||||
break;
|
||||
AddCandidate(sign * distance, false);
|
||||
// Keep the legacy loop and certification unchanged when shifting is disabled.
|
||||
for (var index = 1; index <= (int)candidateCount; index++)
|
||||
{
|
||||
var distance = index * options.Spacing;
|
||||
if (distance >= usedSpan - GeometryTolerance)
|
||||
break;
|
||||
AddCandidate(sign * distance, false);
|
||||
}
|
||||
}
|
||||
if (hasTailCandidate)
|
||||
{
|
||||
@@ -158,6 +183,7 @@ public static class AutomaticCutOffPlanner
|
||||
{
|
||||
// A partial proposal must not accidentally be accepted around a manual conflict.
|
||||
definitions.Clear();
|
||||
stripCutLength = 0;
|
||||
separated = false;
|
||||
separatorX = null;
|
||||
}
|
||||
@@ -171,8 +197,64 @@ public static class AutomaticCutOffPlanner
|
||||
UsedSpan = separated ? System.Math.Abs(separatorX.Value) : length,
|
||||
TailLength = separated ? length - System.Math.Abs(separatorX.Value) : 0,
|
||||
TailSeparatorX = separatorX,
|
||||
StripCutLength = stripCutLength,
|
||||
FixedGridCutLength = fixedGridCutLength,
|
||||
};
|
||||
|
||||
void AddShiftedGap(double start, double end)
|
||||
{
|
||||
var nominal = new List<double>();
|
||||
for (var index = 1; index <= (int)candidateCount; index++)
|
||||
{
|
||||
var distance = start + index * options.Spacing;
|
||||
if (distance >= end - GeometryTolerance)
|
||||
break;
|
||||
nominal.Add(distance);
|
||||
fixedGridCutLength += Measure(distance).Length;
|
||||
}
|
||||
if (nominal.Count == 0)
|
||||
return;
|
||||
var step = System.Math.Max(options.Spacing / 700, usedSpan / MaximumShiftCandidateCount);
|
||||
var positions = AutomaticCutOffStripSearch.FindPositions(start, end, nominal.Count,
|
||||
options.Spacing, options.MinimumStripLength.Value, step, Allowed, Measure) ?? nominal.ToArray();
|
||||
for (var index = 0; index < positions.Length; index++)
|
||||
{
|
||||
var distance = positions[index];
|
||||
AddCandidate(sign * distance, false);
|
||||
if (!Near(distance, nominal[index]))
|
||||
diagnostics.Add(new AutomaticCutOffDiagnostic(AutomaticCutOffDiagnosticCode.ShiftedCut,
|
||||
$"Moved from {nominal[index]:G8} to {distance:G8} model units; this line cuts "
|
||||
+ $"{Measure(nominal[index]).Length - Measure(distance).Length:G8} model units less.",
|
||||
X: sign * distance));
|
||||
}
|
||||
}
|
||||
|
||||
bool Allowed(double distance)
|
||||
{
|
||||
RegisterCandidate(distance);
|
||||
return true;
|
||||
}
|
||||
|
||||
void RegisterCandidate(double distance)
|
||||
{
|
||||
if (candidates.Add(distance))
|
||||
Require(candidates.Count <= MaximumShiftCandidateCount,
|
||||
$"Shift search would generate more than {MaximumShiftCandidateCount} distinct positions. "
|
||||
+ "Increase spacing or the minimum strip length, or use the fixed grid.", nameof(options));
|
||||
}
|
||||
|
||||
(double Length, int SegmentCount) Measure(double distance)
|
||||
{
|
||||
RegisterCandidate(distance);
|
||||
if (!measurements.TryGetValue(distance, out var measured))
|
||||
{
|
||||
measured = new CutOff(new Vector(sign * distance, 0), CutOffAxis.Vertical)
|
||||
.MeasureSegments(plate, settings, cache);
|
||||
measurements.Add(distance, measured);
|
||||
}
|
||||
return measured;
|
||||
}
|
||||
|
||||
void AddCandidate(double x, bool isSeparator)
|
||||
{
|
||||
var matches = plate.CutOffs.Where(c => c.Axis == CutOffAxis.Vertical &&
|
||||
@@ -246,7 +328,11 @@ public static class AutomaticCutOffPlanner
|
||||
X: x));
|
||||
|
||||
if (usable && existing == null)
|
||||
{
|
||||
definitions.Add(candidate);
|
||||
if (shifted && !isSeparator)
|
||||
stripCutLength += Measure(System.Math.Abs(x)).Length;
|
||||
}
|
||||
}
|
||||
|
||||
void WarnNoSeparator(double x) => diagnostics.Add(new AutomaticCutOffDiagnostic(
|
||||
@@ -259,6 +345,10 @@ public static class AutomaticCutOffPlanner
|
||||
{
|
||||
Require(double.IsFinite(options.Spacing) && options.Spacing > MinimumSpacing,
|
||||
$"Spacing must be finite and greater than {MinimumSpacing} model units.", nameof(options));
|
||||
Require(!options.MinimumStripLength.HasValue ||
|
||||
(double.IsFinite(options.MinimumStripLength.Value) && options.MinimumStripLength.Value > MinimumSpacing &&
|
||||
options.MinimumStripLength.Value <= options.Spacing),
|
||||
$"Minimum strip length must be finite, greater than {MinimumSpacing} and no greater than spacing.", nameof(options));
|
||||
Require(Nonnegative(options.MinimumTailLength),
|
||||
"Minimum tail length must be finite and nonnegative.", nameof(options));
|
||||
Require(double.IsFinite(plate.Size.Length) && plate.Size.Length > 0 &&
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest;
|
||||
|
||||
/// <summary>Exact layered search over sorted positions; each transition uses a sliding minimum.</summary>
|
||||
internal static class AutomaticCutOffStripSearch
|
||||
{
|
||||
internal static double[] FindPositions(double start, double end, int count, double spacing,
|
||||
double minimum, double step, Func<double, bool> allowed,
|
||||
Func<double, (double Length, int SegmentCount)> measure)
|
||||
{
|
||||
var rows = new List<double[]>(count);
|
||||
var nominal = new double[count];
|
||||
for (var line = 1; line <= count; line++)
|
||||
{
|
||||
var today = start + line * spacing;
|
||||
nominal[line - 1] = today;
|
||||
var low = System.Math.Max(start + line * minimum, end - (count - line + 1) * spacing);
|
||||
var high = System.Math.Min(today, end - AutomaticCutOffPlanner.GeometryTolerance);
|
||||
var positions = new List<double>();
|
||||
// Integer indices keep candidates on the origin-based lattice without cumulative drift.
|
||||
var first = (long)System.Math.Ceiling(low / step);
|
||||
var last = (long)System.Math.Floor(high / step);
|
||||
for (var index = first; index <= last; index++)
|
||||
{
|
||||
var position = index * step;
|
||||
if (position >= low && position <= high && allowed(position))
|
||||
positions.Add(position);
|
||||
}
|
||||
if (allowed(today))
|
||||
positions.Add(today);
|
||||
positions.Sort();
|
||||
for (var index = positions.Count - 1; index > 0; index--)
|
||||
if (positions[index] == positions[index - 1])
|
||||
positions.RemoveAt(index);
|
||||
if (positions.Count == 0)
|
||||
return null;
|
||||
rows.Add(positions.ToArray());
|
||||
}
|
||||
return Solve(start, rows, nominal, spacing, minimum, measure);
|
||||
}
|
||||
|
||||
// Kept independent of geometry so tests can compare against exhaustive, non-greedy oracles.
|
||||
internal static double[] Solve(double start, IReadOnlyList<double[]> rows,
|
||||
IReadOnlyList<double> nominal, double spacing, double minimum,
|
||||
Func<double, (double Length, int SegmentCount)> measure)
|
||||
{
|
||||
var previous = new List<State> { new() { Position = start } };
|
||||
for (var line = 0; line < rows.Count; line++)
|
||||
{
|
||||
var current = new List<State>(rows[line].Length);
|
||||
var deque = new int[previous.Count];
|
||||
var head = 0;
|
||||
var tail = 0;
|
||||
var added = 0;
|
||||
foreach (var position in rows[line])
|
||||
{
|
||||
var upper = position - minimum + AutomaticCutOffPlanner.GeometryTolerance;
|
||||
var lower = position - spacing - AutomaticCutOffPlanner.GeometryTolerance;
|
||||
while (added < previous.Count && previous[added].Position <= upper)
|
||||
{
|
||||
while (tail > head && Compare(previous[added], previous[deque[tail - 1]]) < 0)
|
||||
tail--;
|
||||
deque[tail++] = added++;
|
||||
}
|
||||
while (head < tail && previous[deque[head]].Position < lower)
|
||||
head++;
|
||||
if (head == tail)
|
||||
continue;
|
||||
var parent = previous[deque[head]];
|
||||
var cost = measure(position);
|
||||
current.Add(new State
|
||||
{
|
||||
Position = position,
|
||||
Length = parent.Length + System.Math.Round(cost.Length, 6),
|
||||
Pierces = parent.Pierces + cost.SegmentCount,
|
||||
Deviation = parent.Deviation + System.Math.Abs(nominal[line] - position),
|
||||
Parent = parent,
|
||||
});
|
||||
}
|
||||
if (current.Count == 0)
|
||||
return null;
|
||||
AssignLexicographicRanks(current, previous.Count);
|
||||
previous = current;
|
||||
}
|
||||
State best = null;
|
||||
foreach (var state in previous)
|
||||
if (best == null || Compare(state, best) < 0)
|
||||
best = state;
|
||||
var result = new double[rows.Count];
|
||||
for (var line = result.Length - 1; line >= 0; line--)
|
||||
{
|
||||
result[line] = best.Position;
|
||||
best = best.Parent;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int Compare(State left, State right)
|
||||
{
|
||||
var comparison = left.Length.CompareTo(right.Length);
|
||||
if (comparison == 0)
|
||||
comparison = left.Pierces.CompareTo(right.Pierces);
|
||||
if (comparison == 0)
|
||||
comparison = left.Deviation.CompareTo(right.Deviation);
|
||||
return comparison != 0 ? comparison : left.LexicographicRank.CompareTo(right.LexicographicRank);
|
||||
}
|
||||
|
||||
private static void AssignLexicographicRanks(List<State> states, int parentCount)
|
||||
{
|
||||
// Stable counting sort by prefix rank. Positions are already sorted, so within a
|
||||
// prefix their order is lexicographic. Ranking and transitions are linear per row.
|
||||
var offsets = new int[parentCount];
|
||||
foreach (var state in states)
|
||||
offsets[state.Parent.LexicographicRank]++;
|
||||
var total = 0;
|
||||
for (var index = 0; index < offsets.Length; index++)
|
||||
{
|
||||
var count = offsets[index];
|
||||
offsets[index] = total;
|
||||
total += count;
|
||||
}
|
||||
foreach (var state in states)
|
||||
state.LexicographicRank = offsets[state.Parent.LexicographicRank]++;
|
||||
}
|
||||
|
||||
private sealed class State
|
||||
{
|
||||
public double Position;
|
||||
public double Length;
|
||||
public int Pierces;
|
||||
public double Deviation;
|
||||
public State Parent;
|
||||
public int LexicographicRank;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.CutOffs;
|
||||
|
||||
public class AutomaticCutOffShiftTests
|
||||
{
|
||||
internal static AutomaticCutOffOptions Options(double scale = 1)
|
||||
{
|
||||
return new AutomaticCutOffOptions { Spacing = 35 * scale, MinimumStripLength = 17.5 * scale };
|
||||
}
|
||||
|
||||
internal static Part Rectangle(double length, double width, double x, double y)
|
||||
{
|
||||
var program = new Program();
|
||||
program.Codes.Add(new RapidMove(0, 0));
|
||||
program.Codes.Add(new LinearMove(length, 0));
|
||||
program.Codes.Add(new LinearMove(length, width));
|
||||
program.Codes.Add(new LinearMove(0, width));
|
||||
program.Codes.Add(new LinearMove(0, 0));
|
||||
return new Part(new Drawing("shift rectangle", program), new Vector(x, y));
|
||||
}
|
||||
|
||||
internal static Plate UserCase(int quadrant = 1, double scale = 1)
|
||||
{
|
||||
var plate = new Plate(60 * scale, 120 * scale) { Quadrant = quadrant, PartSpacing = 0.3 * scale };
|
||||
foreach (var right in new[] { 34.75, 69.75, 80.0 })
|
||||
plate.Parts.Add(Rectangle((right == 80 ? 5 : 10) * scale, (right == 80 ? 5 : 20) * scale,
|
||||
(quadrant is 2 or 3 ? -right : right - (right == 80 ? 5 : 10)) * scale,
|
||||
(quadrant is 3 or 4 ? -30 : 10) * scale));
|
||||
return plate;
|
||||
}
|
||||
|
||||
internal static double CutLength(Program program) => Enumerable.Range(0, program.Codes.Count / 2)
|
||||
.Sum(i => System.Math.Abs(((LinearMove)program.Codes[2 * i + 1]).EndPoint.Y -
|
||||
((RapidMove)program.Codes[2 * i]).EndPoint.Y));
|
||||
|
||||
[Theory]
|
||||
[InlineData(1, 1)]
|
||||
[InlineData(2, 1)]
|
||||
[InlineData(3, 1)]
|
||||
[InlineData(4, 1)]
|
||||
[InlineData(1, 25.4)]
|
||||
[InlineData(2, 25.4)]
|
||||
[InlineData(3, 25.4)]
|
||||
[InlineData(4, 25.4)]
|
||||
public void ParallelEdgeCase_SavesFortyInchesWithSameCountAndSeparator(int quadrant, double scale)
|
||||
{
|
||||
var plate = UserCase(quadrant, scale);
|
||||
var settings = new CutOffSettings { PartClearance = 0.02 * scale };
|
||||
var legacy = AutomaticCutOffPlanner.Create(plate, new AutomaticCutOffOptions { Spacing = 35 * scale }, settings);
|
||||
var shifted = AutomaticCutOffPlanner.Create(plate, Options(scale), settings);
|
||||
Assert.Equal(legacy.TailSeparatorX, shifted.TailSeparatorX);
|
||||
Assert.Equal(3, shifted.Definitions.Count);
|
||||
Assert.True(legacy.PreviewParts.Sum(p => CutLength(p.Program)) -
|
||||
shifted.PreviewParts.Sum(p => CutLength(p.Program)) >= 39.9 * scale);
|
||||
AssertLimits(shifted, 35 * scale, 17.5 * scale);
|
||||
Assert.Empty(plate.CutOffs);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(-1)]
|
||||
[InlineData(0.00002)]
|
||||
[InlineData(36)]
|
||||
[InlineData(double.NaN)]
|
||||
[InlineData(double.PositiveInfinity)]
|
||||
public void MinimumStrip_RejectsInvalidValues(double minimum)
|
||||
{
|
||||
var options = Options();
|
||||
options.MinimumStripLength = minimum;
|
||||
Assert.Throws<ArgumentException>(() => AutomaticCutOffPlanner.Create(UserCase(), options, new CutOffSettings()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EqualCosts_SelectNominalPositions_AndLastStripMayBeShort()
|
||||
{
|
||||
var plate = new Plate(60, 120) { PartSpacing = 0.3 };
|
||||
plate.Parts.Add(Rectangle(40, 20, 0, 10));
|
||||
var plan = AutomaticCutOffPlanner.Create(plate, Options(), new CutOffSettings());
|
||||
Assert.Equal(new[] { 35.0, 40.3 + AutomaticCutOffPlanner.GeometryTolerance }, plan.Definitions.Select(c => c.Position.X));
|
||||
Assert.DoesNotContain(plan.Diagnostics, d => d.Code == AutomaticCutOffDiagnosticCode.ShiftedCut);
|
||||
Assert.Equal(CutLength(plan.PreviewParts[0].Program), plan.StripCutLength, 8);
|
||||
Assert.Equal(plan.StripCutLength, plan.FixedGridCutLength);
|
||||
AssertLimits(plan, 35, 17.5);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
[InlineData(3)]
|
||||
[InlineData(4)]
|
||||
public void NullAndEqualSpacing_AreIdenticalIncludingDiagnosticsAndTotals(int quadrant)
|
||||
{
|
||||
var plate = UserCase(quadrant);
|
||||
plate.CutOffs.Add(new CutOff(new Vector(quadrant is 2 or 3 ? -35 : 35, 0), CutOffAxis.Vertical));
|
||||
var options = new AutomaticCutOffOptions { Spacing = 35 };
|
||||
var legacy = AutomaticCutOffPlanner.Create(plate, options, new CutOffSettings());
|
||||
options.MinimumStripLength = 35;
|
||||
var fixedGrid = AutomaticCutOffPlanner.Create(plate, options, new CutOffSettings());
|
||||
Assert.Equal(legacy.Definitions.Select(c => (c.Position, c.Axis, c.StartLimit, c.EndLimit)),
|
||||
fixedGrid.Definitions.Select(c => (c.Position, c.Axis, c.StartLimit, c.EndLimit)));
|
||||
Assert.Equal(legacy.PreviewParts.Select(p => p.Program.ToString()), fixedGrid.PreviewParts.Select(p => p.Program.ToString()));
|
||||
Assert.Equal(legacy.Diagnostics, fixedGrid.Diagnostics);
|
||||
Assert.Equal(legacy.TailSeparatorX, fixedGrid.TailSeparatorX);
|
||||
Assert.Equal(legacy.UsedSpan, fixedGrid.UsedSpan);
|
||||
Assert.Equal(legacy.StripCutLength, fixedGrid.StripCutLength);
|
||||
Assert.Equal(legacy.FixedGridCutLength, fixedGrid.FixedGridCutLength);
|
||||
Assert.Equal(0, legacy.StripCutLength);
|
||||
Assert.Equal(0, legacy.FixedGridCutLength);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SeededTwoHundredPlates_NeverLonger_RespectLimitsAndAreDeterministic()
|
||||
{
|
||||
var random = new Random(42131);
|
||||
for (var sample = 0; sample < 200; sample++)
|
||||
{
|
||||
var quadrant = sample % 4 + 1;
|
||||
var plate = new Plate(60, 120) { Quadrant = quadrant, PartSpacing = 0.3 };
|
||||
for (var index = 0; index < 4; index++)
|
||||
{
|
||||
var right = 20 + random.NextDouble() * 90;
|
||||
var width = 2 + random.NextDouble() * 8;
|
||||
var height = 2 + random.NextDouble() * 12;
|
||||
var top = 20 + random.NextDouble() * 20;
|
||||
plate.Parts.Add(Rectangle(width, height, quadrant is 2 or 3 ? -right : right - width,
|
||||
quadrant is 3 or 4 ? -top : top - height));
|
||||
}
|
||||
var options = Options();
|
||||
var settings = new CutOffSettings();
|
||||
var first = AutomaticCutOffPlanner.Create(plate, options, settings);
|
||||
var second = AutomaticCutOffPlanner.Create(plate, options, settings);
|
||||
var legacy = AutomaticCutOffPlanner.Create(plate, new AutomaticCutOffOptions { Spacing = 35 }, settings);
|
||||
Assert.Equal(legacy.Definitions.Count, first.Definitions.Count);
|
||||
Assert.Equal(legacy.TailSeparatorX, first.TailSeparatorX);
|
||||
Assert.True(first.StripCutLength <= first.FixedGridCutLength + 0.000001);
|
||||
Assert.Equal(first.Definitions.Select(c => c.Position.X), second.Definitions.Select(c => c.Position.X));
|
||||
Assert.Equal(first.Diagnostics, second.Diagnostics);
|
||||
Assert.Equal(first.StripCutLength, second.StripCutLength);
|
||||
Assert.Equal(first.PreviewParts.Take(first.PreviewParts.Count - 1).Sum(p => CutLength(p.Program)), first.StripCutLength, 8);
|
||||
AssertLimits(first, 35, 17.5);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void AssertLimits(AutomaticCutOffPlan plan, double spacing, double minimum)
|
||||
{
|
||||
var positions = plan.Definitions.Where(c => c.Position.X != plan.TailSeparatorX)
|
||||
.Select(c => System.Math.Abs(c.Position.X)).ToArray();
|
||||
var previous = 0.0;
|
||||
foreach (var position in positions)
|
||||
{
|
||||
Assert.InRange(position - previous, minimum - AutomaticCutOffPlanner.GeometryTolerance,
|
||||
spacing + AutomaticCutOffPlanner.GeometryTolerance);
|
||||
previous = position;
|
||||
}
|
||||
Assert.InRange(plan.UsedSpan - previous, 0, spacing + AutomaticCutOffPlanner.GeometryTolerance);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
namespace OpenNest.Tests.CutOffs;
|
||||
|
||||
public class AutomaticCutOffStripSearchTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void EqualLength_PiercesThenDeviationBreakTies(bool pierceTieBreak)
|
||||
{
|
||||
var result = AutomaticCutOffStripSearch.Solve(0, new[] { new[] { 18.0, 35.0 } },
|
||||
new[] { 35.0 }, 35, 17.5, x => (10, pierceTieBreak && x == 35 ? 2 : 1));
|
||||
Assert.Equal(new[] { pierceTieBreak ? 18.0 : 35.0 }, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GlobalOptimum_IsBetterThanGreedyFirstLine()
|
||||
{
|
||||
var result = AutomaticCutOffStripSearch.Solve(0,
|
||||
new[] { new[] { 20.0, 35.0 }, new[] { 40.0, 55.0, 70.0 } },
|
||||
new[] { 35.0, 70.0 }, 35, 17.5, x => (x == 20 || x == 70 ? 0 : x == 35 ? 1 : 100, 1));
|
||||
Assert.Equal(new[] { 35.0, 70.0 }, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LastTieBreak_IsLexicographicOverTheWholePrefix_NotTheLastPosition()
|
||||
{
|
||||
// Both paths are equally far from the nominal list, length and pierces are equal.
|
||||
var result = AutomaticCutOffStripSearch.Solve(0,
|
||||
new[] { new[] { 20.0, 25.0 }, new[] { 45.0, 50.0 } },
|
||||
new[] { 22.5, 47.5 }, 35, 17.5, _ => (10, 1));
|
||||
Assert.Equal(new[] { 20.0, 45.0 }, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SeededFiniteCandidateSearch_EqualsIndependentExhaustiveOracle()
|
||||
{
|
||||
var random = new Random(93512);
|
||||
for (var sample = 0; sample < 200; sample++)
|
||||
{
|
||||
var rows = new[] { new[] { 18.0, 23, 29, 35 }, new[] { 38.0, 47, 56, 62, 70 }, new[] { 60.0, 74, 81, 91, 105 } };
|
||||
var costs = rows.SelectMany(row => row).Distinct().ToDictionary(x => x,
|
||||
_ => (Length: random.Next(0, 4) + random.Next(0, 2) * 0.0000001, SegmentCount: random.Next(0, 3)));
|
||||
var nominal = new[] { 35.0, 70, 105 };
|
||||
var paths = new List<double[]>();
|
||||
foreach (var first in rows[0])
|
||||
foreach (var second in rows[1])
|
||||
foreach (var third in rows[2])
|
||||
if (second - first >= 17.5 && second - first <= 35 && third - second >= 17.5 && third - second <= 35)
|
||||
paths.Add(new[] { first, second, third });
|
||||
var expected = paths.OrderBy(path => path.Sum(x => System.Math.Round(costs[x].Length, 6)))
|
||||
.ThenBy(path => path.Sum(x => costs[x].SegmentCount))
|
||||
.ThenBy(path => Enumerable.Range(0, 3).Sum(i => System.Math.Abs(nominal[i] - path[i])))
|
||||
.ThenBy(path => path[0]).ThenBy(path => path[1]).ThenBy(path => path[2]).First();
|
||||
var actual = AutomaticCutOffStripSearch.Solve(0, rows, nominal, 35, 17.5, x => costs[x]);
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user