[Pather] Major pathing rework / Consolidate RenderPather, Pather, and Builder
This commit is contained in:
parent
338c123fb1
commit
c3581243c8
9 changed files with 1393 additions and 2558 deletions
167
masque/test/test_autotool_refactor.py
Normal file
167
masque/test/test_autotool_refactor.py
Normal 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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue