masque/masque/test/test_autotool_refactor.py

226 lines
7.7 KiB
Python

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, 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 approximation (just two rects for start and end)
if clockwise:
# (0,0) rot 0 to (R, -R) rot pi/2
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:
# (0,0) rot 0 to (R, R) rot -pi/2
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()
# 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_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
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)