[AutoTool] infer port info automatically

This commit is contained in:
Jan Petykiewicz 2026-07-10 16:30:44 -07:00
commit fe4e2f760b
4 changed files with 434 additions and 43 deletions

View file

@ -88,8 +88,8 @@ def autotool_setup() -> tuple[Pather, AutoTool, Library]:
tool_m1 = (
AutoTool(bbox_library=lib)
.add_straight(
"wire_m1",
lambda length: _make_transition_straight(length, ptype="wire_m1"),
"wire_m1",
"in",
)
.add_transition(via_abs, "m2", "m1")
@ -149,8 +149,8 @@ def test_pather_straight_topology_allows_transition_offset_cancellation() -> Non
tool = (
AutoTool(bbox_library=lib)
.add_straight(
"core",
lambda length: _make_transition_straight(length, ptype="core"),
"core",
"in",
length_range=(0, 1e8),
)
@ -199,6 +199,33 @@ def test_autotool_add_transition_dedupes_bidirectional_adapter_offers() -> None:
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()
@ -222,6 +249,43 @@ def test_autotool_add_transition_one_way_inhibits_reverse_adapter_offer() -> Non
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)
@ -264,8 +328,8 @@ def multi_bend_tool() -> tuple[AutoTool, Library]:
tool = (
AutoTool(bbox_library=lib)
.add_straight("wire", make_straight, "A", length_range=(0, 10))
.add_straight("wire", lambda length: make_straight(length, width=4), "A", length_range=(10, 1e8))
.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)
)
@ -287,8 +351,8 @@ def asymmetric_transition_tool() -> AutoTool:
return (
AutoTool(bbox_library=lib)
.add_straight("core", lambda length: make_straight(length, ptype="core"), "A", length_range=(0, 3))
.add_straight("mid", lambda length: make_straight(length, ptype="mid"), "A", length_range=(0, 1e8))
.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")
)
@ -310,8 +374,8 @@ def wildcard_transition_tool() -> tuple[AutoTool, Library]:
tool = (
AutoTool(bbox_library=lib)
.add_straight("core", lambda length: make_straight(length, ptype="core"), "A", length_range=(0, 1e8))
.add_sbend("core", make_core_sbend, "A", "B", jog_range=(-1e8, 1e8))
.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=(-1e8, 1e8))
.add_transition(lib.abstract("wild_core"), "WILD", "CORE")
)
return tool, lib
@ -363,11 +427,67 @@ def make_sbend_tool(jog_range: tuple[float, float]) -> AutoTool:
return (
AutoTool()
.add_straight("core", make_straight, "A", length_range=(0, 1e8))
.add_sbend("core", make_sbend, "A", "B", jog_range=jog_range)
.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
@ -380,8 +500,8 @@ def test_autotool_sbend_custom_endpoint_avoids_generator_during_planning() -> No
return Port((20, jog), rotation=pi, ptype="core")
tool = AutoTool().add_sbend(
"core",
make_counted_sbend,
"core",
"A",
"B",
jog_range=(0, 1e8),
@ -510,6 +630,86 @@ def test_autotool_l_offer_selection_uses_primitive_cost_and_domains(
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_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)
@ -551,7 +751,7 @@ def test_autotool_transition_offer_bbox_matches_rendered_primitive() -> None:
tool = (
AutoTool(bbox_library=lib)
.add_straight("core", lambda length: make_straight(length, ptype="core"), "A", length_range=(0, 1e8))
.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")
)
@ -596,8 +796,8 @@ def test_autotool_s_offer_bbox_matches_rendered_sbend() -> None:
tool = (
AutoTool(bbox_library=lib)
.add_straight("core", make_straight, "A", length_range=(1, 1e8))
.add_sbend("core", make_wide_sbend, "A", "B", jog_range=(0, 1e8))
.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, length=15, in_ptype="core")
@ -657,8 +857,8 @@ def test_autotool_sbend_registration_order_sets_priority() -> None:
tool = (
AutoTool()
.add_sbend("core", first_sbend, "A", "B", jog_range=(0, 1e8))
.add_sbend("core", second_sbend, "A", "B", jog_range=(0, 1e8))
.add_sbend(first_sbend, "core", "A", "B", jog_range=(0, 1e8))
.add_sbend(second_sbend, "core", "A", "B", jog_range=(0, 1e8))
)
_offer, out_port, data = selected_offer(tool, "s", 4, in_ptype="core")
@ -813,7 +1013,7 @@ def test_autotool_generated_primitives_do_not_capture_route_kwargs() -> None:
markers.append(marker)
return make_straight(length, ptype="wire")
tool = AutoTool().add_straight("wire", make_marked_straight, "A", length_range=(0, 1e8))
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")
@ -839,7 +1039,7 @@ def test_autotool_bend_offer_supports_requested_output_transition(ccw: bool) ->
tool = (
AutoTool(bbox_library=lib)
.add_straight("core", lambda length: make_straight(length, ptype="core"), "A", length_range=(0, 1e8))
.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")
)
@ -873,7 +1073,7 @@ def test_autotool_bend_offer_supports_bend_input_transition(ccw: bool) -> None:
tool = (
AutoTool(bbox_library=lib)
.add_straight("core", lambda length: make_straight(length, ptype="core"), "A", length_range=(0, 1e8))
.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")
)
@ -906,7 +1106,7 @@ def test_pather_accepts_bend_offer_with_zero_lateral_endpoint() -> None:
tool = (
AutoTool(bbox_library=lib)
.add_straight("core", lambda length: make_straight(length, ptype="core"), "A", length_range=(0, 1e8))
.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")
)
@ -945,7 +1145,7 @@ def test_autotool_bend_offer_supports_bend_and_output_transitions(ccw: bool) ->
tool = (
AutoTool(bbox_library=lib)
.add_straight("core", lambda length: make_straight(length, ptype="core"), "A", length_range=(0, 1e8))
.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")
@ -988,8 +1188,8 @@ def test_pather_autotool_pure_sbend_with_transition_dx() -> None:
tool = (
AutoTool()
.add_straight("core", make_core_straight, "A", length_range=(1, 1e8))
.add_sbend("core", make_core_sbend, "A", "B", jog_range=(0, 1e8))
.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")
)

View file

@ -40,8 +40,8 @@ def multi_bend_tool() -> tuple[AutoTool, Library]:
tool = (
AutoTool()
.add_straight("wire", make_straight, "A", length_range=(0, 10))
.add_straight("wire", lambda length: make_straight(length, width=4), "A", length_range=(10, 1e8))
.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)
)
@ -66,7 +66,7 @@ def test_autotool_uturn() -> None:
tool = (
AutoTool()
.add_straight('wire', make_straight, 'in')
.add_straight(make_straight, 'wire', 'in')
.add_bend(lib.abstract('bend'), 'in', 'out', clockwise=True)
)