perf(planning): skip lead checks against material no native query can reach

A lead check scanned every other part's material with native contact
queries. On the 5,742-washer plate that was about 80% of planning and
it never finished inside 120 s.

The earlier extent filter (reverted in ea1270e1) used a fixed 0.001 gap,
which native line/circle rounding can exceed for long leads beside small
circles. This skip adds 1e-6 times the span of both extents. The
discriminant's rounding is bounded by about 1e-7 times that span, and
about 2e-9 is seen. So a skipped material has no native contact, and the
900000-long lead beside a small circle is still checked. Each skipped
scan is charged to the native query budget as if it ran. Ray parity is
still asked natively unless the lead's midpoint is outside the
material's Y range. The pierce zone skips only material farther than its
radius by the same gap.

A randomized test checks that skipped material never has a native
contact. It fails with the span allowance at zero and passes with it as
low as 1e-8. Plate 1 of the washer nest now plans Ready in about 54 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-19 20:16:03 -04:00
1 parent 40e4e559dd
commit 94898225e8
5 files changed
+143 -16

No files matched your search

@@ -17,12 +17,24 @@ public sealed class LeadMaterialSnapshot
Rings = rings;
Tabs = tabs;
Reason = reason;
var extent = PostVerificationGeometry.Extent.None;
foreach (var ring in rings)
foreach (var curve in ring)
extent = extent.Union(curve.Extent);
Extent = extent;
BoundaryCount = rings.Sum(ring => ring.Length);
}
public bool IsComplete => Reason == null;
public string Reason { get; }
internal IReadOnlyList<PostVerificationGeometry.Curve[]> Rings { get; }
/// <summary>A conservative extent of every boundary's native contact reach; material lies within it.</summary>
internal PostVerificationGeometry.Extent Extent { get; }
/// <summary>The boundary curves of all rings: the native queries one full scan charges.</summary>
internal int BoundaryCount { get; }
/// <summary>
/// Per ring curve, whether it is a tabbed cutout's tab: material boundary that the
/// program never cuts. Aligned with <see cref="Rings"/>.
@@ -158,13 +158,14 @@ public static class LeadPathValidator
return (new(true, false, breakLead
? $"Break cut lead motion {i} contacts its part's boundary ({failure}). Try a shorter lead-in or move the tabs."
: $"Lead motion {i} contacts or enters target material outside its adjacent contour joint ({failure})."), null);
var reach = move.Curve.Extent;
for (var other = 0; other < otherMaterials.Count; other++)
{
token.ThrowIfCancellationRequested();
var material = otherMaterials[other];
if (ReferenceEquals(material, target))
continue;
if (OtherContact(material))
if (reach.IsClearOfQueriesFrom(material.Extent) ? FarInside(material) : OtherContact(material))
return (new(true, false, "Lead contacts or enters another placed material. "
+ "Try spacing the parts farther apart or reducing the lead-in/lead-out length, then replan. "
+ "For locked parts, edit the leads or unlock the part before replanning."), other);
@@ -183,8 +184,17 @@ public static class LeadPathValidator
for (var other = 0; other < otherMaterials.Count; other++)
{
token.ThrowIfCancellationRequested();
if (!ReferenceEquals(otherMaterials[other], target)
&& Clearance(otherMaterials[other]) is { } distance)
var material = otherMaterials[other];
if (ReferenceEquals(material, target))
continue;
// Every boundary of material this far away is farther than the zone.
if (new PostVerificationGeometry.Extent(start.X, start.Y, start.X, start.Y)
.IsClearOfQueriesFrom(material.Extent, pierceRadius))
{
Charge(material.BoundaryCount);
continue;
}
if (Clearance(material) is { } distance)
return (Short(distance, "another placed part"), other);
}
return (null, null);
@@ -212,9 +222,7 @@ public static class LeadPathValidator
return CheckMaterial(material, null) != null;
if (memo.TryGet(move.Curve, material, out var scan))
{
if (budget - scan.Queries < 0)
throw new NotSupportedException("Lead validation exceeds the native query limit.");
budget -= scan.Queries;
Charge(scan.Queries);
return scan.Contact;
}
var before = budget;
@@ -222,6 +230,23 @@ public static class LeadPathValidator
memo.Add(move.Curve, material, new(contact, before - budget));
return contact;
}
// Material too far from the lead for any native boundary query to report a contact
// (see IsClearOfQueriesFrom): the result the full scan gives, charged as if it ran.
// Ray parity is still asked natively unless the lead lies beside the material's rows.
bool FarInside(LeadMaterialSnapshot material)
{
Charge(material.BoundaryCount);
var midpoint = move.Curve.Midpoint;
return !material.Extent.IsClearOfRow(midpoint.Y) && material.ContainsMaterial(midpoint, token);
}
void Charge(int queries)
{
if (budget - queries < 0)
throw new NotSupportedException("Lead validation exceeds the native query limit.");
budget -= queries;
}
}
}
catch (Exception ex) when (ex is ArgumentException or NotSupportedException)
@@ -44,9 +44,37 @@ internal static class PostVerificationGeometry
/// are more than <see cref="ClearMargin"/> apart on some axis, so no native query can count
/// anything in one as touching or entering the other. Everything else must be checked.
/// </summary>
internal bool IsClearOf(Extent other) => IsWellConditioned && other.IsWellConditioned
&& (MaxX + ClearMargin < other.MinX || other.MaxX + ClearMargin < MinX
|| MaxY + ClearMargin < other.MinY || other.MaxY + ClearMargin < MinY);
internal bool IsClearOf(Extent other) => IsClearOf(other, ClearMargin);
/// <summary>
/// The relative rounding allowance of native line/circle intersections. Their discriminant
/// b^2 - 4ac cancels: its absolute error stays below about 30 units of roundoff times
/// |d1|^2 (|d2|^2 + r^2) (d1 the line, d2 from the circle's centre to the line's start), so a
/// spurious root can lie at most about 1e-7 times the span of both extents beyond the true
/// distance between line and circle (about 2e-9 is seen in practice). A long lead beside a
/// small circle needs the full check; a short lead beside a nearby part does not.
/// </summary>
internal const double SpanRounding = 1e-6;
/// <summary>
/// As <see cref="IsClearOf(Extent)"/>, with a gap that also covers the rounding of native
/// line/circle queries over the span of both extents (<see cref="SpanRounding"/>), plus
/// <paramref name="reach"/>. Lead checks skip a material only on this test.
/// </summary>
internal bool IsClearOfQueriesFrom(Extent other, double reach = 0)
{
var union = Union(other);
var span = System.Math.Max(union.MaxX - union.MinX, union.MaxY - union.MinY);
return IsClearOf(other, ClearMargin + reach + SpanRounding * span);
}
/// <summary>True when <paramref name="y"/> lies more than <see cref="ClearMargin"/> outside the Y range.</summary>
internal bool IsClearOfRow(double y) => IsWellConditioned && System.Math.Abs(y) <= WellConditionedLimit
&& (y > MaxY + ClearMargin || y < MinY - ClearMargin);
private bool IsClearOf(Extent other, double margin) => IsWellConditioned && other.IsWellConditioned
&& (MaxX + margin < other.MinX || other.MaxX + margin < MinX
|| MaxY + margin < other.MinY || other.MaxY + margin < MinY);
// Math.Max propagates NaN and no comparison with NaN holds, so NaN and infinite bounds
// (including the empty extent's) fail this test too.
@@ -6,9 +6,10 @@ using OpenNest.Geometry;
namespace OpenNest.Tests.CuttingPlanning;
/// <summary>
/// Rapid checks skip completed contours whose extents are well clear of the motion; lead checks
/// examine every other part's material. These cases sit just inside the skip, or show that lead
/// checks are never filtered, and must keep the native result.
/// Rapid checks skip completed contours whose extents are well clear of the motion. Lead checks
/// skip other material only when the gap also covers native line/circle rounding over the span
/// of both extents, so a long lead beside a small circle is still checked. These cases sit just
/// inside the skips and must keep the native result.
/// </summary>
public class NearbyMaterialCheckTests
{
@@ -135,6 +136,61 @@ public class NearbyMaterialCheckTests
Assert.True(memo.Hits > 0);
}
[Fact]
public void LeadCheck_SkipsOnlyMaterialWithNoNativeContact()
{
// Leads from 0.01 to 900000 long, lines and arcs, beside circles and rectangles placed just
// beyond the skip gap, often near the lead's own line where the native line/circle
// discriminant cancels. Whatever is skipped must be clear by the full native scan too.
var random = new Random(11);
var skipped = 0;
for (var i = 0; i < 4000; i++)
{
var length = System.Math.Pow(10, random.NextDouble() * 7.95 - 2);
// Only a nearly axis-aligned lead has an extent thin enough to pass beside material.
var angle = random.Next(4) * System.Math.PI / 2
+ (random.Next(2) == 0 ? 0 : (random.NextDouble() - 0.5) * 1e-6);
var direction = new Vector(System.Math.Cos(angle), System.Math.Sin(angle));
var start = new Vector(random.NextDouble() * 200 - 100, random.NextDouble() * 200 - 100);
var end = start + direction * length;
var lead = random.Next(3) == 0
? PostVerificationGeometry.Curve.Create(start, end, (start + end) * 0.5
+ new Vector(-direction.Y, direction.X) * (length * random.NextDouble()), random.Next(2) == 0)
: PostVerificationGeometry.Curve.Create(start, end, null, false);
var size = System.Math.Pow(10, random.NextDouble() * 5 - 4);
var normal = new Vector(-direction.Y, direction.X) * (random.Next(2) == 0 ? 1 : -1);
// Beside the lead, just past either end, or anywhere along its line.
var along = random.Next(3) switch
{
0 => -random.NextDouble() * length * 0.1,
1 => length * (1 + random.NextDouble() * 0.1),
_ => random.NextDouble() * length,
};
var span = length + 2 * size;
var gap = (PostVerificationGeometry.Extent.ClearMargin + PostVerificationGeometry.Extent.SpanRounding * span)
* (1 + random.NextDouble()) + size;
var centre = start + direction * along + normal * (random.Next(4) == 0 ? 0 : gap);
var program = random.Next(2) == 0
? Circle(centre, size)
: LeadPathValidationTests.Rectangle(centre.X - size, centre.Y - size, centre.X + size, centre.Y + size);
var material = LeadMaterialSnapshot.Capture(program, Vector.Zero);
if (!material.IsComplete || !lead.Extent.IsClearOfQueriesFrom(material.Extent))
continue;
skipped++;
var budget = int.MaxValue;
Assert.Null(LeadPathValidator.MaterialContact(lead, material, null, ref budget, default));
}
Assert.True(skipped > 1000, $"Only {skipped} cases were skipped.");
static Program Circle(Vector centre, double radius)
{
var p = new Program();
p.MoveTo(centre.X + radius, centre.Y);
p.Codes.Add(new ArcMove(new Vector(centre.X + radius, centre.Y), centre, RotationType.CCW));
return p;
}
}
[Fact]
public void RapidJustOutsideASmallCompletedCircle_IsStillACrossing()
{
+10 -4
View File
@@ -225,10 +225,16 @@ Every candidate rapid is checked against contours already completed, including
earlier holes in the same part. Future contours are not yet obstacles. Rapid
checks skip completed contours whose extents are more than 0.001 clear of the
rapid, ten times the widest band any native contact query allows beyond an
extent; anything closer, touching included, gets the full native check. Lead
checks examine every other part's material: native line/circle queries can report
rounding contacts for long leads far from small circles, and leads keep those
results. Every prefix of a part repeats its earlier contours' leads, so one search
extent; anything closer, touching included, gets the full native check. Native
line/circle queries can report rounding contacts for long leads far from small
circles, and leads keep those results: a lead check skips another part's material
only when their extents are clear by that 0.001 plus 1e-6 times the span of both
extents. The discriminant's rounding error is bounded by about 1e-7 times that span
(about 2e-9 is seen), so a skipped material has no native contact; a 900000-long lead
beside a small circle is still checked. A skipped scan is charged to the native query
budget as if it ran, and ray parity is still asked natively unless the lead's midpoint
lies outside the material's Y range. A pierce zone skips material farther than its
radius by the same gap. Every prefix of a part repeats its earlier contours' leads, so one search
remembers each other-material verdict by the lead curve's exact bits and the material's
reference, and charges the native query budget the scan ran; replay never uses it. An arc's
extent is its whole supporting circle widened to the distance at which the native