masque/masque/test/test_autotool_planning.py

310 lines
10 KiB
Python
Raw Normal View History

import pytest
from numpy import pi
2026-06-15 19:59:21 -07:00
from numpy.testing import assert_allclose
from masque.builder.tools import AutoTool
2026-06-15 19:59:21 -07:00
from masque.builder.pather import Pather
from masque.library import Library
from masque.pattern import Pattern
from masque.ports import Port
2026-06-15 19:59:21 -07:00
def _make_transition_straight(length: float, width: float = 2, ptype: str = "wire") -> Pattern:
pat = Pattern()
pat.rect((1, 0), xmin=0, xmax=length, yctr=0, ly=width)
pat.ports["in"] = Port((0, 0), 0, ptype=ptype)
pat.ports["out"] = Port((length, 0), pi, ptype=ptype)
return pat
@pytest.fixture
def autotool_setup() -> tuple[Pather, AutoTool, Library]:
lib = Library()
bend_pat = Pattern()
bend_pat.ports["in"] = Port((0, 0), 0, ptype="wire")
bend_pat.ports["out"] = Port((2, -2), pi / 2, ptype="wire")
lib["bend"] = bend_pat
lib.abstract("bend")
via_pat = Pattern()
via_pat.ports["m1"] = Port((0, 0), 0, ptype="wire_m1")
via_pat.ports["m2"] = Port((1, 0), pi, ptype="wire_m2")
lib["via"] = via_pat
via_abs = lib.abstract("via")
tool_m1 = AutoTool(
straights=[
AutoTool.Straight(ptype="wire_m1", fn=lambda length: _make_transition_straight(length, ptype="wire_m1"), in_port_name="in", out_port_name="out")
],
bends=[],
sbends=[],
transitions={("wire_m2", "wire_m1"): AutoTool.Transition(via_abs, "m2", "m1")},
default_out_ptype="wire_m1",
)
p = Pather(lib, tools=tool_m1)
p.ports["start"] = Port((0, 0), pi, ptype="wire_m2")
return p, tool_m1, lib
def test_autotool_transition(autotool_setup: tuple[Pather, AutoTool, Library]) -> None:
p, _tool, _lib = autotool_setup
p.straight("start", 10)
# Via length is 1, so the remaining wire_m1 straight length is 9.
assert_allclose(p.ports["start"].offset, [10, 0], atol=1e-10)
assert p.ports["start"].ptype == "wire_m1"
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()
2026-06-15 19:59:21 -07:00
# Rectangular approximation of a 90 degree bend.
if clockwise:
2026-03-08 10:15:09 -07:00
pat.rect((1, 0), xmin=0, xmax=R, yctr=0, ly=width)
pat.rect((1, 0), xctr=R, lx=width, ymin=-R, ymax=0)
pat.ports["A"] = Port((0, 0), 0, ptype=ptype)
pat.ports["B"] = Port((R, -R), pi/2, ptype=ptype)
else:
2026-03-08 10:15:09 -07:00
pat.rect((1, 0), xmin=0, xmax=R, yctr=0, ly=width)
pat.rect((1, 0), xctr=R, lx=width, ymin=0, ymax=R)
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()
lib["b1"] = make_bend(2, ptype="wire")
b1_abs = lib.abstract("b1")
lib["b2"] = make_bend(5, ptype="wire")
b2_abs = lib.abstract("b2")
tool = AutoTool(
straights=[
AutoTool.Straight(ptype="wire", fn=make_straight, in_port_name="A", out_port_name="B", length_range=(0, 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
@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
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])
p, data = tool.planL(True, 15)
assert data.straight.length_range == (10, 1e8)
assert data.straight_length == 13
assert_allclose(p.offset, [15, 2])
@pytest.mark.parametrize("ccw", [False, True])
def test_autotool_traceL_matches_plan_with_post_bend_transition(ccw: bool) -> None:
lib = Library()
bend_pat = Pattern()
bend_pat.ports["A"] = Port((0, 0), 0, ptype="core")
bend_pat.ports["B"] = 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["EXT"] = Port((3, 1), pi, ptype="ext")
lib["out_trans"] = trans_pat
tool = 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, 1e8),
),
],
bends=[
AutoTool.Bend(lib.abstract("core_bend"), "A", "B", clockwise=True, mirror=True),
],
sbends=[],
transitions={
("ext", "core"): AutoTool.Transition(lib.abstract("out_trans"), "EXT", "CORE"),
},
default_out_ptype="core",
)
plan_port, data = tool.planL(ccw, 10, out_ptype="ext")
assert data.out_transition is not None
tree = tool.traceL(ccw, 10, out_ptype="ext")
assert_trace_matches_plan(plan_port, tree)
def test_autotool_planU_consistency(multi_bend_tool) -> None:
tool, lib = multi_bend_tool
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_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
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_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)
2026-03-31 00:24:52 -07:00
def test_autotool_planS_pure_sbend_with_transition_dx() -> None:
lib = Library()
def make_straight(length: float) -> Pattern:
pat = Pattern()
pat.ports["A"] = Port((0, 0), 0, ptype="core")
pat.ports["B"] = Port((length, 0), pi, ptype="core")
return pat
def make_sbend(jog: float) -> Pattern:
pat = Pattern()
pat.ports["A"] = Port((0, 0), 0, ptype="core")
pat.ports["B"] = Port((10, jog), pi, ptype="core")
return pat
trans_pat = Pattern()
trans_pat.ports["EXT"] = Port((0, 0), 0, ptype="ext")
trans_pat.ports["CORE"] = Port((5, 0), pi, ptype="core")
lib["xin"] = trans_pat
tool = AutoTool(
straights=[
AutoTool.Straight(
ptype="core",
fn=make_straight,
in_port_name="A",
out_port_name="B",
length_range=(1, 1e8),
)
],
bends=[],
sbends=[
AutoTool.SBend(
ptype="core",
fn=make_sbend,
in_port_name="A",
out_port_name="B",
jog_range=(0, 1e8),
)
],
transitions={
("ext", "core"): AutoTool.Transition(lib.abstract("xin"), "EXT", "CORE"),
},
default_out_ptype="core",
)
p, data = tool.planS(15, 4, in_ptype="ext")
assert_allclose(p.offset, [15, 4])
assert_allclose(p.rotation, pi)
assert data.straight_length == 0
assert data.jog_remaining == 4
assert data.in_transition is not None