masque/masque/test/test_autotool_planning.py

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)