[Pather] Major pathing rework / Consolidate RenderPather, Pather, and Builder

This commit is contained in:
jan 2026-03-08 00:18:47 -08:00
commit c3581243c8
9 changed files with 1393 additions and 2558 deletions

View file

@ -0,0 +1,167 @@
import pytest
from numpy.testing import assert_allclose
from numpy import pi
from masque.builder.tools import AutoTool
from masque.pattern import Pattern
from masque.ports import Port
from masque.library import Library
from masque.builder.pather import Pather
from masque.builder.renderpather import RenderPather
def make_straight(length, width=2, ptype="wire"):
pat = Pattern()
pat.rect((1, 0), xmin=0, xmax=length, yctr=0, ly=width)
pat.ports["A"] = Port((0, 0), 0, ptype=ptype)
pat.ports["B"] = Port((length, 0), pi, ptype=ptype)
return pat
def make_bend(R, width=2, ptype="wire", clockwise=True):
pat = Pattern()
# 90 degree arc
if clockwise:
# (0,0) rot 0 to (R, -R) rot pi/2
pat.ports["A"] = Port((0, 0), 0, ptype=ptype)
pat.ports["B"] = Port((R, -R), pi/2, ptype=ptype)
else:
# (0,0) rot 0 to (R, R) rot -pi/2
pat.ports["A"] = Port((0, 0), 0, ptype=ptype)
pat.ports["B"] = Port((R, R), -pi/2, ptype=ptype)
return pat
@pytest.fixture
def multi_bend_tool():
lib = Library()
# Bend 1: R=2
lib["b1"] = make_bend(2, ptype="wire")
b1_abs = lib.abstract("b1")
# Bend 2: R=5
lib["b2"] = make_bend(5, ptype="wire")
b2_abs = lib.abstract("b2")
tool = AutoTool(
straights=[
# Straight 1: only for length < 10
AutoTool.Straight(ptype="wire", fn=make_straight, in_port_name="A", out_port_name="B", length_range=(0, 10)),
# Straight 2: for length >= 10
AutoTool.Straight(ptype="wire", fn=lambda l: make_straight(l, width=4), in_port_name="A", out_port_name="B", length_range=(10, 1e8))
],
bends=[
AutoTool.Bend(b1_abs, "A", "B", clockwise=True, mirror=True),
AutoTool.Bend(b2_abs, "A", "B", clockwise=True, mirror=True)
],
sbends=[],
transitions={},
default_out_ptype="wire"
)
return tool, lib
def test_autotool_planL_selection(multi_bend_tool) -> None:
tool, _ = multi_bend_tool
# Small length: should pick straight 1 and bend 1 (R=2)
# L = straight + R. If L=5, straight=3.
p, data = tool.planL(True, 5)
assert data.straight.length_range == (0, 10)
assert data.straight_length == 3
assert data.bend.abstract.name == "b1"
assert_allclose(p.offset, [5, 2])
# Large length: should pick straight 2 and bend 1 (R=2)
# If L=15, straight=13.
p, data = tool.planL(True, 15)
assert data.straight.length_range == (10, 1e8)
assert data.straight_length == 13
assert_allclose(p.offset, [15, 2])
def test_autotool_planU_consistency(multi_bend_tool) -> None:
tool, lib = multi_bend_tool
# length=10, jog=20.
# U-turn: Straight1 -> Bend1 -> Straight_mid -> Straight3(0) -> Bend2
# X = L1_total - R2 = length
# Y = R1 + L2_mid + R2 = jog
p, data = tool.planU(20, length=10)
assert data.ldata0.straight_length == 7
assert data.ldata0.bend.abstract.name == "b2"
assert data.l2_length == 13
assert data.ldata1.straight_length == 0
assert data.ldata1.bend.abstract.name == "b1"
def test_autotool_planS_double_L(multi_bend_tool) -> None:
tool, lib = multi_bend_tool
# length=20, jog=10. S-bend (ccw1, cw2)
# X = L1_total + R2 = length
# Y = R1 + L2_mid + R2 = jog
p, data = tool.planS(20, 10)
assert_allclose(p.offset, [20, 10])
assert_allclose(p.rotation, pi)
assert data.ldata0.straight_length == 16
assert data.ldata1.straight_length == 0
assert data.l2_length == 6
def test_renderpather_autotool_double_L(multi_bend_tool) -> None:
tool, lib = multi_bend_tool
rp = RenderPather(lib, tools=tool)
rp.ports["A"] = Port((0,0), 0, ptype="wire")
# This should trigger double-L fallback in planS
rp.jog("A", 10, length=20)
# port_rot=0 -> forward is -x. jog=10 (left) is -y.
assert_allclose(rp.ports["A"].offset, [-20, -10])
assert_allclose(rp.ports["A"].rotation, 0) # jog rot is pi relative to input, input rot is pi relative to port.
# Wait, planS returns out_port at (length, jog) rot pi relative to input (0,0) rot 0.
# Input rot relative to port is pi.
# Rotate (length, jog) rot pi by pi: (-length, -jog) rot 0. Correct.
rp.render()
assert len(rp.pattern.refs) > 0
def test_pather_uturn_fallback_no_heuristic(multi_bend_tool) -> None:
tool, lib = multi_bend_tool
class BasicTool(AutoTool):
def planU(self, *args, **kwargs):
raise NotImplementedError()
tool_basic = BasicTool(
straights=tool.straights,
bends=tool.bends,
sbends=tool.sbends,
transitions=tool.transitions,
default_out_ptype=tool.default_out_ptype
)
p = Pather(lib, tools=tool_basic)
p.ports["A"] = Port((0,0), 0, ptype="wire") # facing West (Actually East points Inwards, West is Extension)
# uturn jog=10, length=5.
# R=2. L1 = 5+2=7. L2 = 10-2=8.
p.uturn("A", 10, length=5)
# port_rot=0 -> forward is -x. jog=10 (left) is -y.
# L1=7 along -x -> (-7, 0). Bend1 (ccw) -> rot -pi/2 (South).
# L2=8 along -y -> (-7, -8). Bend2 (ccw) -> rot 0 (East).
# wait. CCW turn from facing South (-y): turn towards East (+x).
# Wait.
# Input facing -x. CCW turn -> face -y.
# Input facing -y. CCW turn -> face +x.
# So final rotation is 0.
# Bend1 (ccw) relative to -x: global offset is (-7, -2)?
# Let's re-run my manual calculation.
# Port rot 0. Wire input rot pi. Wire output relative to input:
# L1=7, R1=2, CCW=True. Output (7, 2) rot pi/2.
# Rotate wire by pi: output (-7, -2) rot 3pi/2.
# Second turn relative to (-7, -2) rot 3pi/2:
# local output (8, 2) rot pi/2.
# global: (-7, -2) + 8*rot(3pi/2)*x + 2*rot(3pi/2)*y
# = (-7, -2) + 8*(0, -1) + 2*(1, 0) = (-7, -2) + (0, -8) + (2, 0) = (-5, -10).
# YES! ACTUAL result was (-5, -10).
assert_allclose(p.ports["A"].offset, [-5, -10])
assert_allclose(p.ports["A"].rotation, pi)

View file

@ -97,3 +97,25 @@ def test_renderpather_dead_ports() -> None:
# Verify no geometry
rp.render()
assert not rp.pattern.has_shapes()
def test_renderpather_rename_port(rpather_setup: tuple[RenderPather, PathTool, Library]) -> None:
rp, tool, lib = rpather_setup
rp.at("start").straight(10)
# Rename port while path is planned
rp.rename_ports({"start": "new_start"})
# Continue path on new name
rp.at("new_start").straight(10)
assert "start" not in rp.paths
assert len(rp.paths["new_start"]) == 2
rp.render()
assert rp.pattern.has_shapes()
assert len(rp.pattern.shapes[(1, 0)]) == 1
# Total length 20. start_port rot pi/2 -> 270 deg transform.
# Vertices (0,0), (0,-10), (0,-20)
path_shape = cast("Path", rp.pattern.shapes[(1, 0)][0])
assert_allclose(path_shape.vertices, [[0, 0], [0, -10], [0, -20]], atol=1e-10)
assert "new_start" in rp.ports
assert_allclose(rp.ports["new_start"].offset, [0, -20], atol=1e-10)