1454 lines
52 KiB
Python
1454 lines
52 KiB
Python
from contextlib import suppress
|
|
from typing import Any
|
|
|
|
import pytest
|
|
import numpy
|
|
from numpy import pi
|
|
from numpy.testing import assert_allclose
|
|
|
|
from masque.builder.tools import AutoTool, PrimitiveOffer, RenderStep, UOffer, circular_arc_sbend_endpoint
|
|
from masque.builder.pather import Pather
|
|
from masque.library import Library
|
|
from masque.pattern import Pattern
|
|
from masque.ports import Port
|
|
from masque.error import BuildError
|
|
|
|
|
|
def commit_offer(offer: PrimitiveOffer, parameter: float) -> tuple[Port, Any]:
|
|
return offer.endpoint_at(parameter), offer.commit(parameter)
|
|
|
|
|
|
def selected_offer(tool: AutoTool, kind: str, parameter: float, **kwargs: Any) -> tuple[PrimitiveOffer, Port, Any]:
|
|
valid = []
|
|
for offer in tool.primitive_offers(kind, **kwargs):
|
|
try:
|
|
out_port, data = commit_offer(offer, parameter)
|
|
except BuildError:
|
|
continue
|
|
valid.append((offer, out_port, data))
|
|
|
|
assert valid
|
|
return min(valid, key=lambda item: item[0].cost_at(parameter))
|
|
|
|
|
|
def selected_matching_offer(
|
|
tool: AutoTool,
|
|
kind: str,
|
|
parameter: float,
|
|
predicate: Any,
|
|
**kwargs: Any,
|
|
) -> tuple[PrimitiveOffer, Port, Any]:
|
|
valid = []
|
|
for offer in tool.primitive_offers(kind, **kwargs):
|
|
try:
|
|
out_port, data = commit_offer(offer, parameter)
|
|
except BuildError:
|
|
continue
|
|
if predicate(offer, out_port, data):
|
|
valid.append((offer, out_port, data))
|
|
|
|
assert valid
|
|
return min(valid, key=lambda item: item[0].cost_at(parameter))
|
|
|
|
|
|
def rendered_offer_tree(
|
|
tool: AutoTool,
|
|
offer: PrimitiveOffer,
|
|
parameter: float,
|
|
source_ptype: str | None = None,
|
|
) -> Library:
|
|
start = Port((0, 0), rotation=0, ptype=source_ptype or offer.in_ptype or "unk")
|
|
end, data = commit_offer(offer, parameter)
|
|
return tool.render((RenderStep(offer.kind, tool, start, end, data),))
|
|
|
|
|
|
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(bbox_library=lib)
|
|
.add_straight(
|
|
lambda length: _make_transition_straight(length, ptype="wire_m1"),
|
|
"wire_m1",
|
|
"in",
|
|
)
|
|
.add_transition(via_abs, "m2", "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 test_autotool_route_level_methods_removed(autotool_setup: tuple[Pather, AutoTool, Library]) -> None:
|
|
_p, tool, _lib = autotool_setup
|
|
|
|
for name in ("planL", "planS", "planU", "traceL", "traceS", "traceU"):
|
|
assert not hasattr(tool, name)
|
|
|
|
|
|
def test_autotool_straight_offer_supports_requested_output_transition(
|
|
autotool_setup: tuple[Pather, AutoTool, Library],
|
|
) -> None:
|
|
_p, tool, lib = autotool_setup
|
|
|
|
p = Pather(lib, tools=tool, render='deferred')
|
|
p.ports["start"] = Port((0, 0), pi, ptype="wire_m1")
|
|
p.straight("start", 10, out_ptype="wire_m2")
|
|
|
|
assert_allclose(p.ports["start"].offset, [10, 0], atol=1e-10)
|
|
assert p.ports["start"].ptype == "wire_m2"
|
|
assert [type(step.data) for step in p._paths["start"]] == [AutoTool.GeneratedData, AutoTool.ReusableData]
|
|
assert p._paths["start"][0].data.parameter == 9
|
|
assert p._paths["start"][1].data.port_name == "m1"
|
|
|
|
|
|
def test_pather_straight_topology_allows_transition_offset_cancellation() -> None:
|
|
lib = Library()
|
|
|
|
trans_in = Pattern()
|
|
trans_in.ports["EXT"] = Port((0, 0), 0, ptype="ext_in")
|
|
trans_in.ports["CORE"] = Port((1, 1), pi, ptype="core")
|
|
lib["trans_in"] = trans_in
|
|
|
|
trans_out = Pattern()
|
|
trans_out.ports["EXT"] = Port((0, 0), 0, ptype="ext_out")
|
|
trans_out.ports["CORE"] = Port((1, -1), pi, ptype="core")
|
|
lib["trans_out"] = trans_out
|
|
|
|
tool = (
|
|
AutoTool(bbox_library=lib)
|
|
.add_straight(
|
|
lambda length: _make_transition_straight(length, ptype="core"),
|
|
"core",
|
|
"in",
|
|
length_range=(0, 1e8),
|
|
)
|
|
.add_transition(lib.abstract("trans_in"), "EXT", "CORE")
|
|
.add_transition(lib.abstract("trans_out"), "EXT", "CORE")
|
|
)
|
|
|
|
p = Pather(lib, tools=tool, render='deferred')
|
|
p.ports["A"] = Port((0, 0), pi, ptype="ext_in")
|
|
p.trace("A", None, length=10, out_ptype="ext_out")
|
|
|
|
assert_allclose(p.ports["A"].offset, [10, 0], atol=1e-10)
|
|
assert p.ports["A"].ptype == "ext_out"
|
|
assert [type(step.data) for step in p._paths["A"]] == [
|
|
AutoTool.ReusableData,
|
|
AutoTool.GeneratedData,
|
|
AutoTool.ReusableData,
|
|
]
|
|
assert p._paths["A"][1].data.parameter == 8
|
|
|
|
|
|
def test_autotool_add_transition_dedupes_bidirectional_adapter_offers() -> None:
|
|
lib = Library()
|
|
|
|
trans_pat = Pattern()
|
|
trans_pat.ports["EXT"] = Port((0, 0), 0, ptype="ext")
|
|
trans_pat.ports["CORE"] = Port((2, 0), pi, ptype="core")
|
|
lib["trans"] = trans_pat
|
|
trans_abs = lib.abstract("trans")
|
|
|
|
tool = AutoTool(bbox_library=lib)
|
|
tool.add_transition(trans_abs, "EXT", "CORE")
|
|
tool.add_transition(trans_abs, "EXT", "CORE")
|
|
|
|
ext_offers = tool.primitive_offers("straight", in_ptype="ext")
|
|
core_offers = tool.primitive_offers("straight", in_ptype="core")
|
|
|
|
assert len(ext_offers) == 1
|
|
assert len(core_offers) == 1
|
|
|
|
_ext_port, ext_data = commit_offer(ext_offers[0], 2)
|
|
_core_port, core_data = commit_offer(core_offers[0], 2)
|
|
assert isinstance(ext_data, AutoTool.ReusableData)
|
|
assert isinstance(core_data, AutoTool.ReusableData)
|
|
assert ext_data.port_name == "EXT"
|
|
assert core_data.port_name == "CORE"
|
|
|
|
|
|
def test_autotool_add_transition_infers_two_port_bidirectional_transition() -> None:
|
|
lib = Library()
|
|
|
|
trans_pat = Pattern()
|
|
trans_pat.ports["EXT"] = Port((0, 0), 0, ptype="ext")
|
|
trans_pat.ports["CORE"] = Port((2, 0), pi, ptype="core")
|
|
lib["trans"] = trans_pat
|
|
|
|
tool = AutoTool(bbox_library=lib)
|
|
tool.add_transition(lib.abstract("trans"))
|
|
|
|
ext_offers = tool.primitive_offers("straight", in_ptype="ext")
|
|
core_offers = tool.primitive_offers("straight", in_ptype="core")
|
|
|
|
assert len(ext_offers) == 1
|
|
assert len(core_offers) == 1
|
|
|
|
ext_port, ext_data = commit_offer(ext_offers[0], 2)
|
|
core_port, core_data = commit_offer(core_offers[0], 2)
|
|
assert_allclose(ext_port.offset, [2, 0])
|
|
assert_allclose(core_port.offset, [2, 0])
|
|
assert isinstance(ext_data, AutoTool.ReusableData)
|
|
assert isinstance(core_data, AutoTool.ReusableData)
|
|
assert ext_data.port_name == "EXT"
|
|
assert core_data.port_name == "CORE"
|
|
|
|
|
|
def test_autotool_add_transition_one_way_inhibits_reverse_adapter_offer() -> None:
|
|
lib = Library()
|
|
|
|
trans_pat = Pattern()
|
|
trans_pat.ports["EXT"] = Port((0, 0), 0, ptype="ext")
|
|
trans_pat.ports["CORE"] = Port((2, 0), pi, ptype="core")
|
|
lib["trans"] = trans_pat
|
|
trans_abs = lib.abstract("trans")
|
|
|
|
tool = AutoTool(bbox_library=lib)
|
|
tool.add_transition(trans_abs, "EXT", "CORE", one_way=True)
|
|
|
|
ext_offers = tool.primitive_offers("straight", in_ptype="ext")
|
|
core_offers = tool.primitive_offers("straight", in_ptype="core")
|
|
|
|
assert len(ext_offers) == 1
|
|
assert core_offers == ()
|
|
|
|
_ext_port, ext_data = commit_offer(ext_offers[0], 2)
|
|
assert isinstance(ext_data, AutoTool.ReusableData)
|
|
assert ext_data.port_name == "EXT"
|
|
|
|
|
|
def test_autotool_add_transition_requires_explicit_names_for_one_way() -> None:
|
|
lib = Library()
|
|
|
|
trans_pat = Pattern()
|
|
trans_pat.ports["EXT"] = Port((0, 0), 0, ptype="ext")
|
|
trans_pat.ports["CORE"] = Port((2, 0), pi, ptype="core")
|
|
lib["trans"] = trans_pat
|
|
|
|
with pytest.raises(BuildError, match='one-way transitions require explicit port names'):
|
|
AutoTool().add_transition(lib.abstract("trans"), one_way=True)
|
|
|
|
|
|
def test_autotool_add_transition_rejects_partial_port_names() -> None:
|
|
lib = Library()
|
|
|
|
trans_pat = Pattern()
|
|
trans_pat.ports["EXT"] = Port((0, 0), 0, ptype="ext")
|
|
trans_pat.ports["CORE"] = Port((2, 0), pi, ptype="core")
|
|
lib["trans"] = trans_pat
|
|
|
|
with pytest.raises(BuildError, match='Transition port names must be provided together'):
|
|
AutoTool().add_transition(lib.abstract("trans"), "EXT")
|
|
|
|
|
|
def test_autotool_add_transition_requires_explicit_names_for_non_two_port_abstract() -> None:
|
|
lib = Library()
|
|
|
|
trans_pat = Pattern()
|
|
trans_pat.ports["EXT"] = Port((0, 0), 0, ptype="ext")
|
|
trans_pat.ports["CORE"] = Port((2, 0), pi, ptype="core")
|
|
trans_pat.ports["TAP"] = Port((1, 1), pi, ptype="tap")
|
|
lib["trans"] = trans_pat
|
|
|
|
with pytest.raises(BuildError, match='Transition port names are required for 3-port abstracts'):
|
|
AutoTool().add_transition(lib.abstract("trans"))
|
|
|
|
|
|
def make_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["A"] = Port((0, 0), 0, ptype=ptype)
|
|
pat.ports["B"] = Port((length, 0), pi, ptype=ptype)
|
|
return pat
|
|
|
|
|
|
def make_bend(R: float, width: float = 2, ptype: str = "wire", clockwise: bool = True) -> Pattern:
|
|
pat = Pattern()
|
|
# Rectangular approximation of a 90 degree bend.
|
|
if clockwise:
|
|
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:
|
|
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
|
|
|
|
|
|
def make_sbend(jog: float, ptype: str = "wire") -> Pattern:
|
|
pat = Pattern()
|
|
pat.ports["A"] = Port((0, 0), 0, ptype=ptype)
|
|
pat.ports["B"] = Port((10, jog), pi, ptype=ptype)
|
|
return pat
|
|
|
|
|
|
@pytest.fixture
|
|
def multi_bend_tool() -> tuple[AutoTool, Library]:
|
|
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(bbox_library=lib)
|
|
.add_straight(make_straight, "wire", "A", length_range=(0, 10))
|
|
.add_straight(lambda length: make_straight(length, width=4), "wire", "A", length_range=(10, 1e8))
|
|
.add_bend(b1_abs, "A", "B", clockwise=True, mirror=True)
|
|
.add_bend(b2_abs, "A", "B", clockwise=True, mirror=True)
|
|
)
|
|
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(bbox_library=lib)
|
|
.add_straight(lambda length: make_straight(length, ptype="core"), "core", "A", length_range=(0, 3))
|
|
.add_straight(lambda length: make_straight(length, ptype="mid"), "mid", "A", length_range=(0, 1e8))
|
|
.add_bend(lib.abstract("core_bend"), "in", "out", clockwise=True, mirror=True)
|
|
.add_transition(lib.abstract("core_mid"), "MID", "CORE")
|
|
)
|
|
|
|
@pytest.fixture
|
|
def wildcard_transition_tool() -> tuple[AutoTool, Library]:
|
|
lib = Library()
|
|
|
|
def make_core_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["WILD"] = Port((0, 0), 0, ptype="unk")
|
|
trans_pat.ports["CORE"] = Port((2, 0), pi, ptype="core")
|
|
lib["wild_core"] = trans_pat
|
|
|
|
tool = (
|
|
AutoTool(bbox_library=lib)
|
|
.add_straight(lambda length: make_straight(length, ptype="core"), "core", "A", length_range=(0, 1e8))
|
|
.add_sbend(make_core_sbend, "core", "A", "B", jog_range=(0, 1e8))
|
|
.add_transition(lib.abstract("wild_core"), "WILD", "CORE")
|
|
)
|
|
return tool, lib
|
|
|
|
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 assert_rendered_offer_endpoint_matches_plan(
|
|
tool: AutoTool,
|
|
offer: PrimitiveOffer,
|
|
parameter: float,
|
|
source_ptype: str | None = None,
|
|
) -> None:
|
|
out_port = offer.endpoint_at(parameter)
|
|
tree = rendered_offer_tree(tool, offer, parameter, source_ptype)
|
|
assert_trace_matches_plan(out_port, tree)
|
|
|
|
|
|
def assert_offer_bbox_matches_trace(offer: PrimitiveOffer, parameter: float, tree: Library, source_lib: Library) -> None:
|
|
expected = tree.top_pattern().get_bounds(library=source_lib)
|
|
assert expected is not None
|
|
assert_allclose(offer.bbox_at(parameter), expected)
|
|
|
|
|
|
def assert_offer_bbox_matches_rendered_offer(
|
|
tool: AutoTool,
|
|
offer: PrimitiveOffer,
|
|
parameter: float,
|
|
source_lib: Library,
|
|
) -> None:
|
|
tree = rendered_offer_tree(tool, offer, parameter)
|
|
assert_offer_bbox_matches_trace(offer, parameter, tree, source_lib)
|
|
|
|
|
|
def make_sbend_tool(jog_range: tuple[float, float]) -> AutoTool:
|
|
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
|
|
|
|
return (
|
|
AutoTool()
|
|
.add_straight(make_straight, "core", "A", length_range=(0, 1e8))
|
|
.add_sbend(make_sbend, "core", "A", "B", jog_range=jog_range)
|
|
)
|
|
|
|
|
|
def test_autotool_add_straight_infers_metadata_from_generated_example() -> None:
|
|
calls: list[float] = []
|
|
|
|
def make_counted_straight(length: float) -> Pattern:
|
|
calls.append(length)
|
|
return make_straight(length, ptype="core")
|
|
|
|
tool = AutoTool().add_straight(make_counted_straight, length_range=(0, 10))
|
|
|
|
assert calls == [1]
|
|
offer = tool.primitive_offers("straight", in_ptype="core")[0]
|
|
out_port, data = commit_offer(offer, 7)
|
|
assert out_port.ptype == "core"
|
|
assert isinstance(data, AutoTool.GeneratedData)
|
|
assert data.port_name == "A"
|
|
assert data.parameter == 7
|
|
assert calls == [1]
|
|
|
|
|
|
def test_autotool_add_straight_explicit_metadata_does_not_sample_generator() -> None:
|
|
calls: list[float] = []
|
|
|
|
def make_counted_straight(length: float) -> Pattern:
|
|
calls.append(length)
|
|
return make_straight(length, ptype="core")
|
|
|
|
tool = AutoTool().add_straight(make_counted_straight, "core", "A", length_range=(0, 10))
|
|
|
|
assert calls == []
|
|
offer = tool.primitive_offers("straight", in_ptype="core")[0]
|
|
_out_port, data = commit_offer(offer, 7)
|
|
assert isinstance(data, AutoTool.GeneratedData)
|
|
assert data.port_name == "A"
|
|
assert calls == []
|
|
|
|
|
|
def test_autotool_add_sbend_infers_metadata_from_generated_example() -> None:
|
|
calls: list[float] = []
|
|
|
|
def make_counted_sbend(jog: float) -> Pattern:
|
|
calls.append(jog)
|
|
return make_sbend(jog, ptype="core")
|
|
|
|
tool = AutoTool().add_sbend(make_counted_sbend, jog_range=(0, 10))
|
|
|
|
assert calls == [1]
|
|
offer = tool.primitive_offers("s", in_ptype="core")[0]
|
|
data = offer.commit(4)
|
|
assert offer.in_ptype == "core"
|
|
assert offer.out_ptype == "core"
|
|
assert isinstance(data, AutoTool.GeneratedData)
|
|
assert data.port_name == "A"
|
|
assert data.parameter == 4
|
|
assert calls == [1]
|
|
|
|
|
|
def test_autotool_sbend_custom_endpoint_avoids_generator_during_planning() -> None:
|
|
calls = 0
|
|
|
|
def make_counted_sbend(jog: float) -> Pattern:
|
|
nonlocal calls
|
|
calls += 1
|
|
return make_sbend(jog, ptype="core")
|
|
|
|
def endpoint(jog: float) -> Port:
|
|
return Port((20, jog), rotation=pi, ptype="core")
|
|
|
|
tool = AutoTool().add_sbend(
|
|
make_counted_sbend,
|
|
"core",
|
|
"A",
|
|
"B",
|
|
jog_range=(0, 1e8),
|
|
endpoint=endpoint,
|
|
)
|
|
|
|
offer = tool.primitive_offers("s", in_ptype="core")[0]
|
|
out_port = offer.endpoint_at(4)
|
|
|
|
assert calls == 0
|
|
assert_allclose(out_port.offset, [20, 4])
|
|
assert out_port.rotation == pi
|
|
assert out_port.ptype == "core"
|
|
|
|
data = offer.commit(4)
|
|
assert data.parameter == 4
|
|
assert data.mirrored is False
|
|
|
|
|
|
def test_circular_arc_sbend_endpoint() -> None:
|
|
endpoint = circular_arc_sbend_endpoint(radius=5, ptype="core")
|
|
|
|
out_port = endpoint(4)
|
|
assert_allclose(out_port.offset, [8, 4])
|
|
assert out_port.rotation == pi
|
|
assert out_port.ptype == "core"
|
|
|
|
mirrored_port = endpoint(-4)
|
|
assert_allclose(mirrored_port.offset, [8, -4])
|
|
assert mirrored_port.rotation == pi
|
|
assert mirrored_port.ptype == "core"
|
|
|
|
zero_port = endpoint(0)
|
|
assert_allclose(zero_port.offset, [0, 0])
|
|
assert zero_port.rotation == pi
|
|
|
|
with pytest.raises(BuildError, match="exceeds diameter"):
|
|
endpoint(11)
|
|
|
|
|
|
def make_uturn_pattern(length: float = 10, jog: float = 4, ptype: str = "core") -> Pattern:
|
|
pat = Pattern()
|
|
y0, y1 = sorted((0, jog))
|
|
pat.rect((2, 0), xmin=0, xmax=length, ymin=y0 - 1, ymax=y1 + 1)
|
|
pat.ports["A"] = Port((0, 0), 0, ptype=ptype)
|
|
pat.ports["B"] = Port((length, jog), 0, ptype=ptype)
|
|
return pat
|
|
|
|
|
|
def make_uturn_tool() -> tuple[AutoTool, Library]:
|
|
lib = Library()
|
|
lib["u"] = make_uturn_pattern()
|
|
tool = AutoTool(bbox_library=lib).add_uturn(lib.abstract("u"), "A", "B")
|
|
return tool, lib
|
|
|
|
|
|
@pytest.mark.parametrize("in_ptype", [None, "unk"])
|
|
def test_autotool_transition_offer_wildcard_input_key_treats_none_and_unk_equivalently(
|
|
wildcard_transition_tool: tuple[AutoTool, Library],
|
|
in_ptype: str | None,
|
|
) -> None:
|
|
tool, _lib = wildcard_transition_tool
|
|
|
|
valid = []
|
|
for offer in tool.primitive_offers("straight", in_ptype=in_ptype, out_ptype="core"):
|
|
try:
|
|
out_port, data = commit_offer(offer, 2)
|
|
except BuildError:
|
|
continue
|
|
valid.append((offer, out_port, data))
|
|
|
|
assert valid
|
|
_offer, out_port, data = min(valid, key=lambda item: item[0].cost_at(2))
|
|
assert isinstance(data, AutoTool.ReusableData)
|
|
assert_allclose(out_port.offset, [2, 0])
|
|
assert out_port.ptype == "core"
|
|
|
|
|
|
@pytest.mark.parametrize("out_ptype", [None, "unk"])
|
|
def test_autotool_transition_offer_wildcard_output_key_treats_none_and_unk_equivalently(
|
|
wildcard_transition_tool: tuple[AutoTool, Library],
|
|
out_ptype: str | None,
|
|
) -> None:
|
|
tool, _lib = wildcard_transition_tool
|
|
|
|
_offer, out_port, data = selected_matching_offer(
|
|
tool,
|
|
"straight",
|
|
2,
|
|
lambda _offer, out_port, data: out_port.ptype == "unk" and isinstance(data, AutoTool.ReusableData),
|
|
in_ptype="core",
|
|
out_ptype=out_ptype,
|
|
)
|
|
|
|
assert_allclose(out_port.offset, [2, 0])
|
|
assert out_port.ptype == "unk"
|
|
|
|
|
|
def test_autotool_l_offer_selection_uses_primitive_cost_and_domains(
|
|
multi_bend_tool: tuple[AutoTool, Library],
|
|
) -> None:
|
|
tool, _lib = multi_bend_tool
|
|
|
|
small_straight_offer, small_straight_port, small_straight_data = selected_offer(tool, "straight", 5)
|
|
assert small_straight_offer.parameter_domain == (0, 10)
|
|
assert small_straight_data.parameter == 5
|
|
assert_allclose(small_straight_port.offset, [5, 0])
|
|
|
|
large_straight_offer, large_straight_port, large_straight_data = selected_offer(tool, "straight", 15)
|
|
assert large_straight_offer.parameter_domain == (10, 1e8)
|
|
assert large_straight_data.parameter == 15
|
|
assert_allclose(large_straight_port.offset, [15, 0])
|
|
|
|
_small_bend_offer, small_bend_port, small_bend_data = selected_offer(tool, "bend", 2, ccw=True)
|
|
assert small_bend_data.abstract.name == "b1"
|
|
assert_allclose(small_bend_port.offset, [2, 2])
|
|
|
|
large_bend_offer, large_bend_port, large_bend_data = selected_offer(tool, "bend", 5, ccw=True)
|
|
assert large_bend_data.abstract.name == "b2"
|
|
assert_allclose(large_bend_port.offset, [5, 5])
|
|
valid_costs = []
|
|
for offer in tool.primitive_offers("straight"):
|
|
with suppress(BuildError):
|
|
valid_costs.append(offer.cost_at(15))
|
|
assert large_straight_offer.cost_at(15) == min(valid_costs)
|
|
assert large_bend_offer.parameter_domain == (5, 5)
|
|
|
|
|
|
def test_autotool_add_bend_infers_two_port_clockwise_bend() -> None:
|
|
lib = Library()
|
|
lib["bend"] = make_bend(2, ptype="wire", clockwise=True)
|
|
|
|
tool = AutoTool(bbox_library=lib).add_bend(lib.abstract("bend"))
|
|
|
|
_cw_offer, cw_port, cw_data = selected_offer(tool, "bend", 2, in_ptype="wire", ccw=False)
|
|
_ccw_offer, ccw_port, ccw_data = selected_offer(tool, "bend", 2, in_ptype="wire", ccw=True)
|
|
assert_allclose(cw_port.offset, [2, -2])
|
|
assert_allclose(ccw_port.offset, [2, 2])
|
|
assert isinstance(cw_data, AutoTool.ReusableData)
|
|
assert isinstance(ccw_data, AutoTool.ReusableData)
|
|
assert cw_data.port_name == "A"
|
|
assert not cw_data.mirrored
|
|
assert ccw_data.port_name == "A"
|
|
assert ccw_data.mirrored
|
|
|
|
|
|
def test_autotool_add_bend_infers_two_port_counterclockwise_bend() -> None:
|
|
lib = Library()
|
|
lib["bend"] = make_bend(2, ptype="wire", clockwise=False)
|
|
|
|
tool = AutoTool(bbox_library=lib).add_bend(lib.abstract("bend"))
|
|
|
|
_cw_offer, cw_port, cw_data = selected_offer(tool, "bend", 2, in_ptype="wire", ccw=False)
|
|
_ccw_offer, ccw_port, ccw_data = selected_offer(tool, "bend", 2, in_ptype="wire", ccw=True)
|
|
assert_allclose(cw_port.offset, [2, -2])
|
|
assert_allclose(ccw_port.offset, [2, 2])
|
|
assert isinstance(cw_data, AutoTool.ReusableData)
|
|
assert isinstance(ccw_data, AutoTool.ReusableData)
|
|
assert cw_data.port_name == "A"
|
|
assert cw_data.mirrored
|
|
assert ccw_data.port_name == "A"
|
|
assert not ccw_data.mirrored
|
|
|
|
|
|
def test_autotool_add_bend_inferred_names_allow_rotational_reuse_without_mirror() -> None:
|
|
lib = Library()
|
|
lib["bend"] = make_bend(2, ptype="wire", clockwise=True)
|
|
|
|
tool = AutoTool(bbox_library=lib).add_bend(lib.abstract("bend"), mirror=False)
|
|
|
|
cw_offer, _cw_port, cw_data = selected_offer(tool, "bend", 2, in_ptype="wire", ccw=False)
|
|
ccw_offer, _ccw_port, ccw_data = selected_offer(tool, "bend", 2, in_ptype="wire", ccw=True)
|
|
assert isinstance(cw_data, AutoTool.ReusableData)
|
|
assert isinstance(ccw_data, AutoTool.ReusableData)
|
|
assert cw_data.port_name == "A"
|
|
assert not cw_data.mirrored
|
|
assert ccw_data.port_name == "B"
|
|
assert not ccw_data.mirrored
|
|
assert_rendered_offer_endpoint_matches_plan(tool, cw_offer, 2, "wire")
|
|
assert_rendered_offer_endpoint_matches_plan(tool, ccw_offer, 2, "wire")
|
|
|
|
|
|
def test_autotool_add_bend_reverse_reuse_swaps_cross_ptypes() -> None:
|
|
lib = Library()
|
|
bend = make_bend(2, ptype="core", clockwise=True)
|
|
bend.ports["B"].ptype = "external"
|
|
lib["bend"] = bend
|
|
|
|
tool = AutoTool(bbox_library=lib).add_bend(lib.abstract("bend"), mirror=False)
|
|
|
|
cw_offer = tool.primitive_offers("bend", in_ptype="core", ccw=False)[0]
|
|
ccw_offer = tool.primitive_offers("bend", in_ptype="external", ccw=True)[0]
|
|
assert (cw_offer.in_ptype, cw_offer.out_ptype) == ("core", "external")
|
|
assert (ccw_offer.in_ptype, ccw_offer.out_ptype) == ("external", "core")
|
|
assert cw_offer.commit(2).port_name == "A"
|
|
assert ccw_offer.commit(2).port_name == "B"
|
|
assert_rendered_offer_endpoint_matches_plan(tool, cw_offer, 2, "core")
|
|
assert_rendered_offer_endpoint_matches_plan(tool, ccw_offer, 2, "external")
|
|
|
|
|
|
def test_autotool_add_bend_rejects_clockwise_mismatch() -> None:
|
|
lib = Library()
|
|
lib["bend"] = make_bend(2, ptype="wire", clockwise=True)
|
|
|
|
with pytest.raises(BuildError, match='Bend clockwise argument does not match port orientations'):
|
|
AutoTool().add_bend(lib.abstract("bend"), "A", "B", clockwise=False)
|
|
|
|
|
|
def test_autotool_add_bend_rejects_partial_port_names() -> None:
|
|
lib = Library()
|
|
lib["bend"] = make_bend(2, ptype="wire", clockwise=True)
|
|
|
|
with pytest.raises(BuildError, match='Bend port names must be provided together'):
|
|
AutoTool().add_bend(lib.abstract("bend"), "A")
|
|
|
|
|
|
def test_autotool_add_bend_requires_explicit_names_for_non_two_port_abstract() -> None:
|
|
lib = Library()
|
|
bend = make_bend(2, ptype="wire", clockwise=True)
|
|
bend.ports["TAP"] = Port((1, 1), pi, ptype="wire")
|
|
lib["bend"] = bend
|
|
|
|
with pytest.raises(BuildError, match='Bend port names are required for 3-port abstracts'):
|
|
AutoTool().add_bend(lib.abstract("bend"))
|
|
|
|
|
|
def test_autotool_l_offer_bbox_matches_rendered_primitive(multi_bend_tool: tuple[AutoTool, Library]) -> None:
|
|
tool, lib = multi_bend_tool
|
|
offer, _out_port, _data = selected_offer(tool, "bend", 2, ccw=True)
|
|
|
|
assert_offer_bbox_matches_rendered_offer(tool, offer, 2, lib)
|
|
|
|
|
|
def test_autotool_generated_straight_endpoint_matches_rendered_offer(
|
|
multi_bend_tool: tuple[AutoTool, Library],
|
|
) -> None:
|
|
tool, _lib = multi_bend_tool
|
|
offer, _out_port, data = selected_offer(tool, "straight", 7, in_ptype="wire")
|
|
|
|
assert isinstance(data, AutoTool.GeneratedData)
|
|
assert_rendered_offer_endpoint_matches_plan(tool, offer, 7, "wire")
|
|
|
|
|
|
@pytest.mark.parametrize("ccw", [False, True])
|
|
def test_autotool_reusable_bend_endpoint_matches_rendered_offer(
|
|
multi_bend_tool: tuple[AutoTool, Library],
|
|
ccw: bool,
|
|
) -> None:
|
|
tool, _lib = multi_bend_tool
|
|
offer, _out_port, data = selected_offer(tool, "bend", 2, in_ptype="wire", ccw=ccw)
|
|
|
|
assert isinstance(data, AutoTool.ReusableData)
|
|
assert_rendered_offer_endpoint_matches_plan(tool, offer, 2, "wire")
|
|
|
|
|
|
def test_autotool_transition_offer_bbox_matches_rendered_primitive() -> None:
|
|
lib = Library()
|
|
|
|
lib["core_bend"] = make_bend(2, ptype="core")
|
|
trans_pat = Pattern()
|
|
trans_pat.rect((2, 0), xmin=0, xmax=3, yctr=2, ly=2)
|
|
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(bbox_library=lib)
|
|
.add_straight(lambda length: make_straight(length, ptype="core"), "core", "A", length_range=(0, 1e8))
|
|
.add_bend(lib.abstract("core_bend"), "A", "B", clockwise=True, mirror=True)
|
|
.add_transition(lib.abstract("out_trans"), "EXT", "CORE")
|
|
)
|
|
offer, _out_port, data = selected_offer(tool, "s", 1, in_ptype="core", out_ptype="ext")
|
|
|
|
assert isinstance(data, AutoTool.ReusableData)
|
|
assert_offer_bbox_matches_rendered_offer(tool, offer, 1, lib)
|
|
|
|
|
|
def test_autotool_transition_endpoint_matches_rendered_offer(
|
|
autotool_setup: tuple[Pather, AutoTool, Library],
|
|
) -> None:
|
|
_pather, tool, _lib = autotool_setup
|
|
offer, _out_port, data = selected_matching_offer(
|
|
tool,
|
|
"straight",
|
|
1,
|
|
lambda _offer, _out_port, data: isinstance(data, AutoTool.ReusableData),
|
|
in_ptype="wire_m1",
|
|
out_ptype="wire_m2",
|
|
)
|
|
|
|
assert isinstance(data, AutoTool.ReusableData)
|
|
assert_rendered_offer_endpoint_matches_plan(tool, offer, 1, "wire_m1")
|
|
|
|
|
|
def test_autotool_s_offer_bbox_matches_rendered_sbend() -> None:
|
|
lib = Library()
|
|
|
|
def make_wide_sbend(jog: float) -> Pattern:
|
|
pat = Pattern()
|
|
pat.rect((2, 0), xmin=0, xmax=10, yctr=jog / 2, ly=abs(jog) + 2)
|
|
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.rect((2, 0), xmin=0, xmax=5, yctr=0, ly=2)
|
|
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(bbox_library=lib)
|
|
.add_straight(make_straight, "core", "A", length_range=(1, 1e8))
|
|
.add_sbend(make_wide_sbend, "core", "A", "B", jog_range=(0, 1e8))
|
|
.add_transition(lib.abstract("xin"), "EXT", "CORE")
|
|
)
|
|
offer, _out_port, _data = selected_offer(tool, "s", 4, in_ptype="core")
|
|
|
|
assert_offer_bbox_matches_rendered_offer(tool, offer, 4, lib)
|
|
|
|
pather = Pather(lib, tools=tool, render='deferred')
|
|
pather.ports["A"] = Port((0, 0), 0, ptype="ext")
|
|
pather.jog("A", 4)
|
|
|
|
assert_allclose(pather.ports["A"].offset, [-15, -4])
|
|
assert [step.opcode for step in pather._paths["A"]] == ['L', 'S']
|
|
assert isinstance(pather._paths["A"][0].data, AutoTool.ReusableData)
|
|
assert isinstance(pather._paths["A"][1].data, AutoTool.GeneratedData)
|
|
|
|
|
|
def test_autotool_s_offer_uses_absolute_jog_range_for_both_signs() -> None:
|
|
tool = make_sbend_tool((0, 1e8))
|
|
offers = tool.primitive_offers('s', in_ptype="core")
|
|
|
|
valid_positive = []
|
|
valid_negative = []
|
|
for offer in offers:
|
|
try:
|
|
if offer.endpoint_at(4).y == 4:
|
|
valid_positive.append(offer)
|
|
except BuildError:
|
|
pass
|
|
try:
|
|
if offer.endpoint_at(-4).y == -4:
|
|
valid_negative.append(offer)
|
|
except BuildError:
|
|
pass
|
|
|
|
assert valid_positive
|
|
assert valid_negative
|
|
|
|
pather = Pather(Library(), tools=tool, render='deferred')
|
|
pather.ports["A"] = Port((0, 0), 0, ptype="core")
|
|
pather.jog("A", -4)
|
|
assert_allclose(pather.ports["A"].offset, [-10, 4])
|
|
assert isinstance(pather._paths["A"][0].data, AutoTool.GeneratedData)
|
|
|
|
|
|
@pytest.mark.parametrize('reverse', [False, True])
|
|
def test_autotool_sbend_cost_is_independent_of_registration_order(reverse: bool) -> None:
|
|
def first_sbend(jog: float) -> Pattern:
|
|
pat = Pattern()
|
|
pat.ports["A"] = Port((0, 0), 0, ptype="core")
|
|
pat.ports["B"] = Port((20, jog), pi, ptype="core")
|
|
return pat
|
|
|
|
def second_sbend(jog: float) -> Pattern:
|
|
pat = Pattern()
|
|
pat.ports["A"] = Port((0, 0), 0, ptype="core")
|
|
pat.ports["B"] = Port((5, jog), pi, ptype="core")
|
|
return pat
|
|
|
|
tool = AutoTool()
|
|
generators = (second_sbend, first_sbend) if reverse else (first_sbend, second_sbend)
|
|
for generator in generators:
|
|
tool.add_sbend(generator, "core", "A", "B", jog_range=(0, 1e8))
|
|
|
|
_offer, out_port, data = selected_offer(tool, "s", 4, in_ptype="core")
|
|
|
|
assert isinstance(data, AutoTool.GeneratedData)
|
|
assert data.fn is second_sbend
|
|
assert_allclose(out_port.offset, [5, 4])
|
|
|
|
|
|
def test_autotool_sbend_explicit_cost_can_override_geometric_cost() -> None:
|
|
def long_sbend(jog: float) -> Pattern:
|
|
pat = Pattern()
|
|
pat.ports["A"] = Port((0, 0), 0, ptype="core")
|
|
pat.ports["B"] = Port((20, jog), pi, ptype="core")
|
|
return pat
|
|
|
|
def short_sbend(jog: float) -> Pattern:
|
|
pat = Pattern()
|
|
pat.ports["A"] = Port((0, 0), 0, ptype="core")
|
|
pat.ports["B"] = Port((5, jog), pi, ptype="core")
|
|
return pat
|
|
|
|
tool = (
|
|
AutoTool()
|
|
.add_sbend(short_sbend, "core", "A", "B", jog_range=(0, 1e8), cost=10)
|
|
.add_sbend(long_sbend, "core", "A", "B", jog_range=(0, 1e8), cost=1)
|
|
)
|
|
|
|
_offer, out_port, data = selected_offer(tool, "s", 4, in_ptype="core")
|
|
|
|
assert isinstance(data, AutoTool.GeneratedData)
|
|
assert data.fn is long_sbend
|
|
assert_allclose(out_port.offset, [20, 4])
|
|
|
|
|
|
def test_autotool_add_methods_propagate_callable_cost_to_all_created_offers() -> None:
|
|
def cost(parameter: float, endpoint: Port) -> float:
|
|
return abs(parameter) + abs(endpoint.x) + abs(endpoint.y)
|
|
|
|
def make_straight(length: float) -> Pattern:
|
|
return _make_transition_straight(length, ptype="core")
|
|
|
|
def make_sbend(jog: float) -> Pattern:
|
|
pat = Pattern()
|
|
pat.ports["A"] = Port((0, 0), 0, ptype="core")
|
|
pat.ports["B"] = Port((5, jog), pi, ptype="core")
|
|
return pat
|
|
|
|
lib = Library()
|
|
|
|
bend = Pattern()
|
|
bend.ports["A"] = Port((0, 0), 0, ptype="core")
|
|
bend.ports["B"] = Port((2, -2), pi / 2, ptype="core")
|
|
lib["bend"] = bend
|
|
|
|
uturn = Pattern()
|
|
uturn.ports["A"] = Port((0, 0), 0, ptype="core")
|
|
uturn.ports["B"] = Port((3, 4), 0, ptype="core")
|
|
lib["uturn"] = uturn
|
|
|
|
transition = Pattern()
|
|
transition.ports["EXT"] = Port((0, 0), 0, ptype="external")
|
|
transition.ports["CORE"] = Port((1, 0), pi, ptype="core")
|
|
lib["transition"] = transition
|
|
|
|
tool = (
|
|
AutoTool()
|
|
.add_straight(make_straight, "core", "in", cost=cost)
|
|
.add_bend(lib.abstract("bend"), "A", "B", clockwise=True, cost=cost)
|
|
.add_sbend(
|
|
make_sbend,
|
|
"core",
|
|
"A",
|
|
"B",
|
|
endpoint=lambda jog: Port((5, jog), pi, ptype="core"),
|
|
cost=cost,
|
|
)
|
|
.add_uturn(lib.abstract("uturn"), "A", "B", cost=cost)
|
|
.add_transition(lib.abstract("transition"), "EXT", "CORE", cost=cost)
|
|
)
|
|
|
|
offers = [
|
|
*(offer for offer in tool.primitive_offers("straight", in_ptype="core")
|
|
if offer.in_ptype == offer.out_ptype == "core"),
|
|
*tool.primitive_offers("bend", in_ptype="core", ccw=False),
|
|
*tool.primitive_offers("bend", in_ptype="core", ccw=True),
|
|
*(offer for offer in tool.primitive_offers("s", in_ptype="core")
|
|
if offer.in_ptype == offer.out_ptype == "core"),
|
|
*tool.primitive_offers("u", in_ptype="core"),
|
|
*(offer for offer in tool.primitive_offers("straight", in_ptype="external")
|
|
if offer.in_ptype == "external" and offer.out_ptype == "core"),
|
|
*(offer for offer in tool.primitive_offers("straight", in_ptype="core")
|
|
if offer.in_ptype == "core" and offer.out_ptype == "external"),
|
|
]
|
|
|
|
assert len(offers) == 9
|
|
assert all(offer.cost is cost for offer in offers)
|
|
|
|
|
|
def test_autotool_validates_cost_before_registering_any_offers() -> None:
|
|
def unused_sbend(_jog: float) -> Pattern:
|
|
raise AssertionError('invalid cost should be rejected before metadata inference')
|
|
|
|
with pytest.raises(BuildError, match='cost factor'):
|
|
AutoTool().add_sbend(unused_sbend, jog_range=(-1, 1), cost=-1)
|
|
|
|
|
|
@pytest.mark.parametrize('length_range', [(-1, 2), (2, 1), (numpy.nan, 2), (numpy.inf, numpy.inf)])
|
|
def test_autotool_rejects_invalid_straight_range_before_sampling(
|
|
length_range: tuple[float, float],
|
|
) -> None:
|
|
def unused_straight(_length: float) -> Pattern:
|
|
raise AssertionError('invalid range should be rejected before metadata inference')
|
|
|
|
with pytest.raises(BuildError, match='Straight length_range'):
|
|
AutoTool().add_straight(unused_straight, length_range=length_range)
|
|
|
|
|
|
@pytest.mark.parametrize('jog_range', [(-1, 2), (2, 1), (numpy.nan, 2), (numpy.inf, numpy.inf)])
|
|
def test_autotool_rejects_invalid_sbend_range_before_sampling(
|
|
jog_range: tuple[float, float],
|
|
) -> None:
|
|
def unused_sbend(_jog: float) -> Pattern:
|
|
raise AssertionError('invalid range should be rejected before metadata inference')
|
|
|
|
with pytest.raises(BuildError, match='S-bend jog_range'):
|
|
AutoTool().add_sbend(unused_sbend, jog_range=jog_range)
|
|
|
|
|
|
def test_autotool_s_offer_singleton_jog_range_includes_both_signs() -> None:
|
|
tool = make_sbend_tool((4, 4))
|
|
offers = tool.primitive_offers('s', in_ptype="core")
|
|
|
|
assert sorted(offer.jog_domain for offer in offers) == [(-4.0, -4.0), (4.0, 4.0)]
|
|
assert [offer.endpoint_at(4).y for offer in offers if offer.jog_domain == (4.0, 4.0)] == [4]
|
|
assert [offer.endpoint_at(-4).y for offer in offers if offer.jog_domain == (-4.0, -4.0)] == [-4]
|
|
|
|
for jog, expected_y in ((4, -4), (-4, 4)):
|
|
pather = Pather(Library(), tools=tool)
|
|
pather.ports["A"] = Port((0, 0), 0, ptype="core")
|
|
pather.jog("A", jog)
|
|
assert_allclose(pather.ports["A"].offset, [-10, expected_y])
|
|
|
|
|
|
def test_autotool_s_offer_rejects_negative_minimum_jog_range() -> None:
|
|
with pytest.raises(BuildError, match='finite, nonnegative minimum'):
|
|
make_sbend_tool((-4, 4))
|
|
|
|
|
|
def test_autotool_uturn_offer_endpoint_matches_rendered_offer() -> None:
|
|
tool, lib = make_uturn_tool()
|
|
offer, out_port, data = selected_offer(tool, "u", 4, in_ptype="core")
|
|
|
|
assert isinstance(offer, UOffer)
|
|
assert isinstance(data, AutoTool.ReusableData)
|
|
assert data.abstract.name == "u"
|
|
assert not data.mirrored
|
|
assert_allclose(out_port.offset, [10, 4])
|
|
assert out_port.rotation is not None
|
|
assert_allclose(out_port.rotation, 0)
|
|
assert_rendered_offer_endpoint_matches_plan(tool, offer, 4, "core")
|
|
assert_offer_bbox_matches_rendered_offer(tool, offer, 4, lib)
|
|
|
|
|
|
def test_autotool_uturn_offer_mirror_exposes_both_signs() -> None:
|
|
tool, _lib = make_uturn_tool()
|
|
offers = tool.primitive_offers('u', in_ptype="core")
|
|
|
|
assert sorted(offer.jog_domain for offer in offers if isinstance(offer, UOffer)) == [
|
|
(-4.0, -4.0),
|
|
(4.0, 4.0),
|
|
]
|
|
for offer in offers:
|
|
assert isinstance(offer, UOffer)
|
|
jog = offer.jog_domain[0]
|
|
data = offer.commit(jog)
|
|
assert isinstance(data, AutoTool.ReusableData)
|
|
assert data.mirrored == (jog < 0)
|
|
assert_allclose(offer.endpoint_at(jog).offset, [10, jog])
|
|
|
|
|
|
def test_pather_autotool_uses_prebaked_uturn_offer() -> None:
|
|
tool, lib = make_uturn_tool()
|
|
p = Pather(lib, tools=tool, render='deferred')
|
|
p.ports["A"] = Port((0, 0), 0, ptype="core")
|
|
|
|
p.uturn("A", 4, length=10)
|
|
|
|
assert_allclose(p.ports["A"].offset, [-10, -4])
|
|
assert p.ports["A"].rotation is not None
|
|
assert_allclose(p.ports["A"].rotation, pi)
|
|
assert [step.opcode for step in p._paths["A"]] == ['U']
|
|
assert isinstance(p._paths["A"][0].data, AutoTool.ReusableData)
|
|
|
|
|
|
def test_autotool_uturn_rejects_non_uturn_orientation() -> None:
|
|
lib = Library()
|
|
pat = make_uturn_pattern()
|
|
pat.ports["B"].rotation = pi
|
|
lib["bad_u"] = pat
|
|
|
|
with pytest.raises(BuildError, match="U-turn primitive output port"):
|
|
AutoTool().add_uturn(lib.abstract("bad_u"), "A", "B")
|
|
|
|
|
|
def test_pather_autotool_omitted_uturn_composes_l_offers(
|
|
multi_bend_tool: tuple[AutoTool, Library],
|
|
) -> None:
|
|
tool, lib = multi_bend_tool
|
|
p = Pather(lib, tools=tool, render='deferred')
|
|
p.ports["A"] = Port((0, 0), 0, ptype="wire")
|
|
|
|
p.uturn("A", 20)
|
|
|
|
assert_allclose(p.ports["A"].offset, [0, -20])
|
|
assert p.ports["A"].rotation is not None
|
|
assert_allclose(p.ports["A"].rotation, pi)
|
|
assert [type(step.data) for step in p._paths["A"]] == [
|
|
AutoTool.ReusableData,
|
|
AutoTool.GeneratedData,
|
|
AutoTool.ReusableData,
|
|
]
|
|
|
|
|
|
def test_pather_autotool_explicit_uturn_composes_l_offers(
|
|
multi_bend_tool: tuple[AutoTool, Library],
|
|
) -> None:
|
|
tool, lib = multi_bend_tool
|
|
p = Pather(lib, tools=tool, render='deferred')
|
|
p.ports["A"] = Port((0, 0), 0, ptype="wire")
|
|
|
|
p.uturn("A", 20, length=10)
|
|
|
|
assert_allclose(p.ports["A"].offset, [-10, -20])
|
|
assert p.ports["A"].rotation is not None
|
|
assert_allclose(p.ports["A"].rotation, pi)
|
|
assert [type(step.data) for step in p._paths["A"]] == [
|
|
AutoTool.GeneratedData,
|
|
AutoTool.ReusableData,
|
|
AutoTool.GeneratedData,
|
|
AutoTool.ReusableData,
|
|
]
|
|
|
|
|
|
def test_autotool_offer_bbox_rejects_invalid_parameter(multi_bend_tool: tuple[AutoTool, Library]) -> None:
|
|
tool, _lib = multi_bend_tool
|
|
offer = tool.primitive_offers('bend', ccw=True)[0]
|
|
|
|
with pytest.raises(BuildError, match='outside singleton domain'):
|
|
offer.bbox_at(offer.parameter_domain[0] - 1)
|
|
|
|
def test_pather_autotool_uses_l_offer_domains(multi_bend_tool: tuple[AutoTool, Library]) -> None:
|
|
tool, lib = multi_bend_tool
|
|
p = Pather(lib, tools=tool, render='deferred')
|
|
p.ports["A"] = Port((0, 0), 0, ptype="wire")
|
|
|
|
p.trace("A", True, length=15)
|
|
|
|
assert_allclose(p.ports["A"].offset, [-15, -2])
|
|
straight_step, bend_step = p._paths["A"]
|
|
assert isinstance(straight_step.data, AutoTool.GeneratedData)
|
|
assert isinstance(bend_step.data, AutoTool.ReusableData)
|
|
assert straight_step.data.parameter == 13
|
|
assert bend_step.data.abstract.name == "b1"
|
|
|
|
|
|
def test_autotool_generated_primitives_snapshot_route_options() -> None:
|
|
markers: list[str | None] = []
|
|
|
|
def make_marked_straight(
|
|
length: float,
|
|
marker: str | None = None,
|
|
nested: dict[str, list[int]] | None = None,
|
|
) -> Pattern:
|
|
_ = nested
|
|
markers.append(marker)
|
|
return make_straight(length, ptype="wire")
|
|
|
|
tool = AutoTool().add_straight(make_marked_straight, "wire", "A", length_range=(0, 1e8))
|
|
p = Pather(Library(), tools=tool, render='deferred')
|
|
p.ports["A"] = Port((0, 0), 0, ptype="wire")
|
|
|
|
first_options = {'marker': 'first', 'nested': {'values': [1]}}
|
|
p.straight("A", 5, tool_options=first_options)
|
|
first_options['marker'] = 'mutated'
|
|
first_options['nested']['values'].append(2)
|
|
p.straight("A", 6, tool_options={'marker': 'second'})
|
|
|
|
first_data, second_data = (step.data for step in p._paths['A'])
|
|
assert isinstance(first_data, AutoTool.GeneratedData)
|
|
assert isinstance(second_data, AutoTool.GeneratedData)
|
|
assert dict(first_data.tool_options) == {'marker': 'first', 'nested': {'values': [1]}}
|
|
assert dict(second_data.tool_options) == {'marker': 'second'}
|
|
|
|
p.render()
|
|
|
|
assert markers == ['first', 'second']
|
|
|
|
|
|
def test_autotool_route_options_must_be_deepcopyable() -> None:
|
|
class NotCopyable:
|
|
def __deepcopy__(self, memo: dict[int, Any]) -> None:
|
|
_ = memo
|
|
raise TypeError('no copy')
|
|
|
|
tool = AutoTool().add_straight(make_straight, 'wire', 'A')
|
|
with pytest.raises(BuildError, match='must be deep-copyable'):
|
|
tool.primitive_offers('straight', marker=NotCopyable())
|
|
|
|
|
|
def test_autotool_route_options_do_not_attach_to_reusable_bends(
|
|
multi_bend_tool: tuple[AutoTool, Library],
|
|
) -> None:
|
|
tool, _lib = multi_bend_tool
|
|
offers = tool.primitive_offers('bend', in_ptype='wire', ccw=True, marker='ignored')
|
|
|
|
assert offers
|
|
assert all(isinstance(offer.commit(offer.parameter_domain[0]), AutoTool.ReusableData) for offer in offers)
|
|
|
|
|
|
def test_autotool_sbend_generator_receives_route_options() -> None:
|
|
markers: list[str] = []
|
|
|
|
def make_marked_sbend(jog: float, *, marker: str) -> Pattern:
|
|
markers.append(marker)
|
|
pat = Pattern()
|
|
pat.ports['A'] = Port((0, 0), 0, ptype='wire')
|
|
pat.ports['B'] = Port((3, jog), pi, ptype='wire')
|
|
return pat
|
|
|
|
tool = AutoTool().add_sbend(
|
|
make_marked_sbend,
|
|
'wire',
|
|
'A',
|
|
'B',
|
|
endpoint=lambda jog: Port((3, jog), pi, ptype='wire'),
|
|
)
|
|
p = Pather(Library(), tools=tool, render='deferred')
|
|
p.ports['A'] = Port((0, 0), 0, ptype='wire')
|
|
|
|
p.jog('A', 4, length=3, tool_options={'marker': 'sbend'})
|
|
p.render()
|
|
|
|
assert markers == ['sbend']
|
|
|
|
|
|
def test_autotool_generated_bbox_uses_route_options() -> None:
|
|
tool = AutoTool().add_straight(make_straight, 'wire', 'A')
|
|
offer = tool.primitive_offers('straight', width=6)[0]
|
|
|
|
assert_allclose(offer.bbox_at(10), [[0, -3], [10, 3]])
|
|
|
|
|
|
def test_autotool_unsupported_generator_option_fails_during_render() -> None:
|
|
tool = AutoTool().add_straight(make_straight, 'wire', 'A')
|
|
p = Pather(Library(), tools=tool, render='deferred')
|
|
p.ports['A'] = Port((0, 0), 0, ptype='wire')
|
|
|
|
p.straight('A', 5, tool_options={'unknown_generator_option': True})
|
|
|
|
with pytest.raises(TypeError, match='unknown_generator_option'):
|
|
p.render()
|
|
|
|
|
|
def test_autotool_generator_options_must_preserve_planned_endpoint() -> None:
|
|
def shifted_straight(length: float, endpoint_shift: float = 0) -> Pattern:
|
|
pat = make_straight(length)
|
|
pat.ports['B'].x += endpoint_shift
|
|
return pat
|
|
|
|
tool = AutoTool().add_straight(shifted_straight, 'wire', 'A')
|
|
p = Pather(Library(), tools=tool, render='deferred')
|
|
p.ports['A'] = Port((0, 0), 0, ptype='wire')
|
|
|
|
p.straight('A', 5, tool_options={'endpoint_shift': 1})
|
|
|
|
with pytest.raises(BuildError, match='does not match planned endpoint'):
|
|
p.render()
|
|
|
|
|
|
@pytest.mark.parametrize("ccw", [False, True])
|
|
def test_autotool_bend_offer_supports_requested_output_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(bbox_library=lib)
|
|
.add_straight(lambda length: make_straight(length, ptype="core"), "core", "A", length_range=(0, 1e8))
|
|
.add_bend(lib.abstract("core_bend"), "A", "B", clockwise=True, mirror=True)
|
|
.add_transition(lib.abstract("out_trans"), "EXT", "CORE")
|
|
)
|
|
|
|
p = Pather(lib, tools=tool, render='deferred')
|
|
p.ports["A"] = Port((0, 0), 0, ptype="core")
|
|
p.trace("A", ccw, length=10, out_ptype="ext")
|
|
|
|
assert p.ports["A"].ptype == "ext"
|
|
assert [type(step.data) for step in p._paths["A"]] == [
|
|
AutoTool.GeneratedData,
|
|
AutoTool.ReusableData,
|
|
AutoTool.ReusableData,
|
|
]
|
|
assert p._paths["A"][2].data.port_name == "CORE"
|
|
|
|
|
|
@pytest.mark.parametrize("ccw", [False, True])
|
|
def test_autotool_bend_offer_supports_bend_input_transition(ccw: bool) -> None:
|
|
lib = Library()
|
|
|
|
bend_pat = Pattern()
|
|
bend_pat.ports["A"] = Port((0, 0), 0, ptype="mid")
|
|
bend_pat.ports["B"] = Port((2, -2), pi / 2, ptype="mid")
|
|
lib["mid_bend"] = bend_pat
|
|
|
|
trans_pat = Pattern()
|
|
trans_pat.ports["MID"] = Port((0, 0), 0, ptype="mid")
|
|
trans_pat.ports["CORE"] = Port((1, 0), pi, ptype="core")
|
|
lib["bend_trans"] = trans_pat
|
|
|
|
tool = (
|
|
AutoTool(bbox_library=lib)
|
|
.add_straight(lambda length: make_straight(length, ptype="core"), "core", "A", length_range=(0, 1e8))
|
|
.add_bend(lib.abstract("mid_bend"), "A", "B", clockwise=True, mirror=True)
|
|
.add_transition(lib.abstract("bend_trans"), "MID", "CORE")
|
|
)
|
|
|
|
p = Pather(lib, tools=tool, render='deferred')
|
|
p.ports["A"] = Port((0, 0), 0, ptype="core")
|
|
p.trace("A", ccw, length=10, out_ptype="mid")
|
|
|
|
assert p.ports["A"].ptype == "mid"
|
|
assert [type(step.data) for step in p._paths["A"]] == [
|
|
AutoTool.GeneratedData,
|
|
AutoTool.ReusableData,
|
|
AutoTool.ReusableData,
|
|
]
|
|
assert p._paths["A"][1].data.port_name == "CORE"
|
|
|
|
|
|
def test_pather_accepts_bend_offer_with_zero_lateral_endpoint() -> None:
|
|
lib = Library()
|
|
|
|
bend_pat = Pattern()
|
|
bend_pat.ports["A"] = Port((0, 0), 0, ptype="mid")
|
|
bend_pat.ports["B"] = Port((2, -2), pi / 2, ptype="mid")
|
|
lib["mid_bend"] = bend_pat
|
|
|
|
trans_pat = Pattern()
|
|
trans_pat.ports["MID"] = Port((0, 0), 0, ptype="mid")
|
|
trans_pat.ports["CORE"] = Port((1, -2), pi, ptype="core")
|
|
lib["bend_trans"] = trans_pat
|
|
|
|
tool = (
|
|
AutoTool(bbox_library=lib)
|
|
.add_straight(lambda length: make_straight(length, ptype="core"), "core", "A", length_range=(0, 1e8))
|
|
.add_bend(lib.abstract("mid_bend"), "A", "B", clockwise=True, mirror=True)
|
|
.add_transition(lib.abstract("bend_trans"), "MID", "CORE")
|
|
)
|
|
|
|
p = Pather(lib, tools=tool, render='deferred')
|
|
p.ports["A"] = Port((0, 0), 0, ptype="core")
|
|
p.trace("A", True, length=10, out_ptype="mid")
|
|
|
|
assert_allclose(p.ports["A"].offset, [-10, 0], atol=1e-10)
|
|
assert p.ports["A"].ptype == "mid"
|
|
assert [type(step.data) for step in p._paths["A"]] == [
|
|
AutoTool.GeneratedData,
|
|
AutoTool.ReusableData,
|
|
AutoTool.ReusableData,
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize("ccw", [False, True])
|
|
def test_autotool_bend_offer_supports_bend_and_output_transitions(ccw: bool) -> None:
|
|
lib = Library()
|
|
|
|
bend_pat = Pattern()
|
|
bend_pat.ports["A"] = Port((0, 0), 0, ptype="mid")
|
|
bend_pat.ports["B"] = Port((2, -2), pi / 2, ptype="mid")
|
|
lib["mid_bend"] = bend_pat
|
|
|
|
bend_trans_pat = Pattern()
|
|
bend_trans_pat.ports["MID"] = Port((0, 0), 0, ptype="mid")
|
|
bend_trans_pat.ports["CORE"] = Port((1, 0), pi, ptype="core")
|
|
lib["bend_trans"] = bend_trans_pat
|
|
|
|
out_trans_pat = Pattern()
|
|
out_trans_pat.ports["MID"] = Port((0, 0), 0, ptype="mid")
|
|
out_trans_pat.ports["EXT"] = Port((3, 1), pi, ptype="ext")
|
|
lib["out_trans"] = out_trans_pat
|
|
|
|
tool = (
|
|
AutoTool(bbox_library=lib)
|
|
.add_straight(lambda length: make_straight(length, ptype="core"), "core", "A", length_range=(0, 1e8))
|
|
.add_bend(lib.abstract("mid_bend"), "A", "B", clockwise=True, mirror=True)
|
|
.add_transition(lib.abstract("bend_trans"), "MID", "CORE")
|
|
.add_transition(lib.abstract("out_trans"), "EXT", "MID")
|
|
)
|
|
|
|
p = Pather(lib, tools=tool, render='deferred')
|
|
p.ports["A"] = Port((0, 0), 0, ptype="core")
|
|
p.trace("A", ccw, length=12, out_ptype="ext")
|
|
|
|
assert p.ports["A"].ptype == "ext"
|
|
assert [type(step.data) for step in p._paths["A"]] == [
|
|
AutoTool.GeneratedData,
|
|
AutoTool.ReusableData,
|
|
AutoTool.ReusableData,
|
|
AutoTool.ReusableData,
|
|
]
|
|
assert p._paths["A"][1].data.port_name == "CORE"
|
|
assert p._paths["A"][3].data.port_name == "MID"
|
|
|
|
|
|
def test_pather_autotool_pure_sbend_with_transition_dx() -> None:
|
|
lib = Library()
|
|
|
|
def make_core_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_core_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()
|
|
.add_straight(make_core_straight, "core", "A", length_range=(1, 1e8))
|
|
.add_sbend(make_core_sbend, "core", "A", "B", jog_range=(0, 1e8))
|
|
.add_transition(lib.abstract("xin"), "EXT", "CORE")
|
|
)
|
|
|
|
transition_offer, trans_port, trans_data = selected_offer(
|
|
tool,
|
|
"straight",
|
|
5,
|
|
in_ptype="ext",
|
|
out_ptype="core",
|
|
)
|
|
assert transition_offer.out_ptype == "core"
|
|
assert_allclose(trans_port.offset, [5, 0])
|
|
assert isinstance(trans_data, AutoTool.ReusableData)
|
|
|
|
s_offer, s_port, s_data = selected_offer(tool, "s", 4, in_ptype="core")
|
|
assert_allclose(s_port.offset, [10, 4])
|
|
assert isinstance(s_data, AutoTool.GeneratedData)
|
|
assert s_offer.out_ptype == "core"
|
|
|
|
p = Pather(lib, tools=tool, render='deferred')
|
|
p.ports["A"] = Port((0, 0), 0, ptype="ext")
|
|
p.jog("A", 4)
|
|
|
|
assert_allclose(p.ports["A"].offset, [-15, -4])
|
|
assert [step.opcode for step in p._paths["A"]] == ['L', 'S']
|
|
assert isinstance(p._paths["A"][0].data, AutoTool.ReusableData)
|
|
assert isinstance(p._paths["A"][1].data, AutoTool.GeneratedData)
|