[AutoTool] rework two-L routing to avoid some bugs with incorrect transitions

This commit is contained in:
Jan Petykiewicz 2026-04-17 20:41:37 -07:00
commit 950d144ead
2 changed files with 119 additions and 48 deletions

View file

@ -791,55 +791,46 @@ class AutoTool(Tool, metaclass=ABCMeta):
Solve for a path consisting of two L-bends connected by a straight segment.
Used for both U-turns (ccw1 == ccw2) and S-bends (ccw1 != ccw2).
"""
is_u = bool(ccw1) == bool(ccw2)
out_rot = 0 if is_u else pi
for plan1 in self._iter_l_plans(ccw1, in_ptype, None):
for plan2 in self._iter_l_plans(ccw2, plan1.out_ptype, out_ptype):
# Solving for:
# X = L1_total +/- R2_actual = length
# Y = R1_actual + L2_straight + overhead_mid + overhead_b2 + L3_total = jog
rot_mid = rotation_matrix_2d(pi + plan1.bend_angle)
mid_axis = rot_mid @ numpy.array((1.0, 0.0))
if not numpy.isclose(mid_axis[0], 0) or numpy.isclose(mid_axis[1], 0):
continue
# Sign for overhead_y2 depends on whether it's a U-turn or S-bend
is_u = bool(ccw1) == bool(ccw2)
# U-turn: X = L1_total - R2 = length => L1_total = length + R2
# S-bend: X = L1_total + R2 = length => L1_total = length - R2
l1_total = length + (abs(plan2.overhead_y) if is_u else -abs(plan2.overhead_y))
l1_straight = l1_total - plan1.overhead_x
for straight_mid in self.straights:
mid_ptype_pair = (plan1.out_ptype, straight_mid.ptype)
mid_trans = self.transitions.get(mid_ptype_pair, None)
mid_trans_dxy = self._itransition2dxy(mid_trans)
for plan2 in self._iter_l_plans(ccw2, straight_mid.ptype, out_ptype):
fixed_dxy = numpy.array((plan1.overhead_x, plan1.overhead_y))
fixed_dxy += rot_mid @ (
mid_trans_dxy
+ numpy.array((plan2.overhead_x, plan2.overhead_y))
)
if plan1.straight.length_range[0] <= l1_straight < plan1.straight.length_range[1]:
for straight_mid in self.straights:
# overhead_mid accounts for the transition from bend1 to straight_mid
mid_ptype_pair = (plan1.out_ptype, straight_mid.ptype)
mid_trans = self.transitions.get(mid_ptype_pair, None)
mid_trans_dxy = self._itransition2dxy(mid_trans)
l1_straight = length - fixed_dxy[0]
l2_straight = (jog - fixed_dxy[1]) / mid_axis[1]
# b_trans2 accounts for the transition from straight_mid to bend2
b2_trans = None
if plan2.bend is not None and plan2.bend.in_port.ptype != straight_mid.ptype:
b2_trans = self.transitions.get((plan2.bend.in_port.ptype, straight_mid.ptype), None)
b2_trans_dxy = self._itransition2dxy(b2_trans)
if plan1.straight.length_range[0] <= l1_straight < plan1.straight.length_range[1] \
and straight_mid.length_range[0] <= l2_straight < straight_mid.length_range[1]:
l3_straight = 0
if plan2.straight.length_range[0] <= l3_straight < plan2.straight.length_range[1]:
ldata0 = self.LData(
l1_straight, plan1.straight, kwargs, ccw1, plan1.bend,
plan1.in_trans, plan1.b_trans, plan1.out_trans,
)
ldata1 = self.LData(
l3_straight, plan2.straight, kwargs, ccw2, plan2.bend,
plan2.in_trans, plan2.b_trans, plan2.out_trans,
)
l2_straight = abs(jog) - abs(plan1.overhead_y) - plan2.overhead_x - mid_trans_dxy[0] - b2_trans_dxy[0]
if straight_mid.length_range[0] <= l2_straight < straight_mid.length_range[1]:
# Found a solution!
# For plan2, we assume l3_straight = 0.
# We need to verify if l3=0 is valid for plan2.straight.
l3_straight = 0
if plan2.straight.length_range[0] <= l3_straight < plan2.straight.length_range[1]:
ldata0 = self.LData(
l1_straight, plan1.straight, kwargs, ccw1, plan1.bend,
plan1.in_trans, plan1.b_trans, plan1.out_trans,
)
ldata1 = self.LData(
l3_straight, plan2.straight, kwargs, ccw2, plan2.bend,
b2_trans, None, plan2.out_trans,
)
data = self.UData(ldata0, ldata1, straight_mid, l2_straight, mid_trans)
# out_port is at (length, jog) rot pi (for S-bend) or 0 (for U-turn) relative to input
out_rot = 0 if is_u else pi
out_port = Port((length, jog), rotation=out_rot, ptype=plan2.out_ptype)
return out_port, data
data = self.UData(ldata0, ldata1, straight_mid, l2_straight, mid_trans)
out_port = Port((length, jog), rotation=out_rot, ptype=plan2.out_ptype)
return out_port, data
raise BuildError(f"Failed to find a valid double-L configuration for {length=}, {jog=}")
straights: list[Straight]

View file

@ -60,6 +60,60 @@ def multi_bend_tool():
)
return tool, lib
@pytest.fixture
def asymmetric_transition_tool() -> AutoTool:
lib = Library()
bend_pat = Pattern()
bend_pat.ports["in"] = Port((0, 0), 0, ptype="core")
bend_pat.ports["out"] = Port((2, -2), pi / 2, ptype="core")
lib["core_bend"] = bend_pat
trans_pat = Pattern()
trans_pat.ports["CORE"] = Port((0, 0), 0, ptype="core")
trans_pat.ports["MID"] = Port((3, 1), pi, ptype="mid")
lib["core_mid"] = trans_pat
return AutoTool(
straights=[
AutoTool.Straight(
ptype="core",
fn=lambda length: make_straight(length, ptype="core"),
in_port_name="A",
out_port_name="B",
length_range=(0, 3),
),
AutoTool.Straight(
ptype="mid",
fn=lambda length: make_straight(length, ptype="mid"),
in_port_name="A",
out_port_name="B",
length_range=(0, 1e8),
),
],
bends=[
AutoTool.Bend(lib.abstract("core_bend"), "in", "out", clockwise=True, mirror=True),
],
sbends=[],
transitions={
("mid", "core"): AutoTool.Transition(lib.abstract("core_mid"), "MID", "CORE"),
},
default_out_ptype="core",
).add_complementary_transitions()
def assert_trace_matches_plan(plan_port: Port, tree: Library, port_names: tuple[str, str] = ("A", "B")) -> None:
pat = tree.top_pattern()
out_port = pat[port_names[1]]
dxy, rot = pat[port_names[0]].measure_travel(out_port)
assert_allclose(dxy, plan_port.offset)
assert rot is not None
assert plan_port.rotation is not None
assert_allclose(rot, plan_port.rotation)
assert out_port.ptype == plan_port.ptype
def test_autotool_planL_selection(multi_bend_tool) -> None:
tool, _ = multi_bend_tool
@ -93,6 +147,19 @@ def test_autotool_planU_consistency(multi_bend_tool) -> None:
assert data.ldata1.straight_length == 0
assert data.ldata1.bend.abstract.name == "b1"
def test_autotool_traceU_matches_plan_with_asymmetric_transition(asymmetric_transition_tool: AutoTool) -> None:
tool = asymmetric_transition_tool
plan_port, data = tool.planU(12, length=0, in_ptype="core")
assert data.ldata1.in_transition is not None
assert data.ldata1.b_transition is not None
tree = tool.traceU(12, length=0, in_ptype="core")
assert_trace_matches_plan(plan_port, tree)
def test_autotool_planS_double_L(multi_bend_tool) -> None:
tool, lib = multi_bend_tool
@ -109,6 +176,19 @@ def test_autotool_planS_double_L(multi_bend_tool) -> None:
assert data.l2_length == 6
def test_autotool_traceS_double_l_matches_plan_with_asymmetric_transition(asymmetric_transition_tool: AutoTool) -> None:
tool = asymmetric_transition_tool
plan_port, data = tool.planS(4, 10, in_ptype="core")
assert isinstance(data, AutoTool.UData)
assert data.ldata1.in_transition is not None
assert data.ldata1.b_transition is not None
tree = tool.traceS(4, 10, in_ptype="core")
assert_trace_matches_plan(plan_port, tree)
def test_autotool_planS_pure_sbend_with_transition_dx() -> None:
lib = Library()