From fe4e2f760bbfd4f4e132aea6c6e048ec9c5da6d3 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 10 Jul 2026 16:30:44 -0700 Subject: [PATCH] [AutoTool] infer port info automatically --- MIGRATION.md | 4 +- masque/builder/tools.py | 221 +++++++++++++++++++++-- masque/test/test_autotool_planning.py | 246 +++++++++++++++++++++++--- masque/test/test_pather_autotool.py | 6 +- 4 files changed, 434 insertions(+), 43 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index dad5969..2d94de9 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -158,7 +158,7 @@ from masque.builder import AutoTool tool = ( AutoTool() - .add_straight('m1wire', make_straight, 'input') + .add_straight(make_straight, 'm1wire', 'input') .add_bend(lib.abstract('bend'), 'input', 'output', clockwise=True) .add_transition(lib.abstract('via'), 'top', 'bottom') ) @@ -167,7 +167,7 @@ tool = ( The key differences are: - `BasicTool` -> `AutoTool` -- `straight=(fn, in_name, out_name)` -> `add_straight(ptype, fn, in_name)` +- `straight=(fn, in_name, out_name)` -> `add_straight(fn, ptype, in_name)` - `bend=(abstract, in_name, out_name)` -> `add_bend(abstract, in_name, out_name)` - transitions are registered with `add_transition(abstract, external_port, internal_port)` - transitions are bidirectional by default; pass `one_way=True` to inhibit the reverse adapter diff --git a/masque/builder/tools.py b/masque/builder/tools.py index 32a864e..40097a0 100644 --- a/masque/builder/tools.py +++ b/masque/builder/tools.py @@ -809,17 +809,150 @@ class AutoTool(Tool): repr = False, ) + @staticmethod + def _sample_positive_parameter(parameter_domain: tuple[float, float], route_name: str) -> float: + """Choose a finite positive value for generator metadata inference.""" + lower, upper = (float(parameter_domain[0]), float(parameter_domain[1])) + if lower > upper or (lower == upper and lower <= 0): + raise BuildError(f'{route_name} inference requires a positive in-domain sample') + if lower == upper: + return lower + sample_lower = max(lower, 0.0) + preferred = sample_lower if sample_lower > 0 else 1.0 + if numpy.isfinite(upper): + if upper <= sample_lower: + raise BuildError(f'{route_name} inference requires a positive in-domain sample') + return preferred if preferred < upper else (sample_lower + upper) / 2 + return preferred + + @staticmethod + def _generated_pattern(fn: GeneratedPrimitiveFn, parameter: float) -> Pattern: + generated = fn(parameter) + return generated if isinstance(generated, Pattern) else generated.top_pattern() + + @staticmethod + def _two_port_names(pattern: Pattern, route_name: str) -> tuple[str, str]: + port_names = tuple(pattern.ports.keys()) + if len(port_names) != 2: + raise BuildError(f'{route_name} inference requires a generated example with exactly two ports') + return port_names + + @staticmethod + def _resolve_equivalent_ptype( + in_port: Port, + out_port: Port, + ptype: str | None, + route_name: str, + ) -> str | None: + if not ptypes_compatible(in_port.ptype, out_port.ptype): + raise BuildError(f'{route_name} inference requires equivalent port ptypes') + if ptype is not None: + if not ptypes_compatible(in_port.ptype, ptype) or not ptypes_compatible(out_port.ptype, ptype): + raise BuildError(f'{route_name} ptype does not match generated example ports') + return ptype + return in_port.ptype if in_port.ptype not in (None, 'unk') else out_port.ptype + + @staticmethod + def _measure_opposite_ports( + in_port: Port, + out_port: Port, + route_name: str, + ) -> tuple[NDArray[numpy.float64], float]: + dxy, angle = in_port.measure_travel(out_port) + if angle is None: + raise BuildError(f'{route_name} inference requires generated ports with rotations') + normalized_angle = angle % (2 * pi) + if not (numpy.isclose(normalized_angle, pi)): + raise BuildError(f'{route_name} inference requires opposite generated port rotations') + return dxy, angle + + def _infer_straight_metadata( + self, + fn: GeneratedPrimitiveFn, + length_range: tuple[float, float], + ptype: str | None, + in_port_name: str | None, + ) -> tuple[str | None, str]: + if ptype is not None and in_port_name is not None: + return ptype, in_port_name + + sample = self._sample_positive_parameter(length_range, 'straight') + pattern = self._generated_pattern(fn, sample) + first_name, second_name = self._two_port_names(pattern, 'straight') + candidate_names = ( + (in_port_name, second_name if in_port_name == first_name else first_name), + ) if in_port_name is not None else ( + (first_name, second_name), + (second_name, first_name), + ) + for candidate_in, candidate_out in candidate_names: + if candidate_in not in pattern.ports or candidate_out not in pattern.ports: + continue + in_port = pattern.ports[candidate_in] + out_port = pattern.ports[candidate_out] + dxy, _angle = self._measure_opposite_ports(in_port, out_port, 'straight') + if dxy[0] > 0 and numpy.isclose(dxy[1], 0): + return ( + self._resolve_equivalent_ptype(in_port, out_port, ptype, 'straight'), + candidate_in, + ) + raise BuildError('straight inference requires an equivalent two-port straight example') + + def _infer_sbend_metadata( + self, + fn: GeneratedPrimitiveFn, + jog_range: tuple[float, float], + ptype: str | None, + in_port_name: str | None, + out_port_name: str | None, + ) -> tuple[str | None, str, str]: + if ptype is not None and in_port_name is not None and out_port_name is not None: + return ptype, in_port_name, out_port_name + + sample = self._sample_positive_parameter(jog_range, 'S-bend') + pattern = self._generated_pattern(fn, sample) + first_name, second_name = self._two_port_names(pattern, 'S-bend') + if in_port_name is not None and out_port_name is None: + out_port_name = second_name if in_port_name == first_name else first_name + elif in_port_name is None and out_port_name is not None: + in_port_name = second_name if out_port_name == first_name else first_name + + candidate_names = ( + (in_port_name, out_port_name), + ) if in_port_name is not None and out_port_name is not None else ( + (first_name, second_name), + (second_name, first_name), + ) + for candidate_in, candidate_out in candidate_names: + if candidate_in not in pattern.ports or candidate_out not in pattern.ports: + continue + in_port = pattern.ports[candidate_in] + out_port = pattern.ports[candidate_out] + dxy, _angle = self._measure_opposite_ports(in_port, out_port, 'S-bend') + if dxy[0] > 0 and numpy.isclose(dxy[1], sample): + return ( + self._resolve_equivalent_ptype(in_port, out_port, ptype, 'S-bend'), + candidate_in, + candidate_out, + ) + raise BuildError('S-bend inference requires an equivalent two-port S-bend example') + def add_straight( self, - ptype: str, fn: GeneratedPrimitiveFn, - in_port_name: str, + ptype: str | None = None, + in_port_name: str | None = None, *, length_range: tuple[float, float] = (0, numpy.inf), ) -> Self: """ Register a generated straight primitive. + + If `ptype` or `in_port_name` is omitted, one in-domain example is + generated and the missing metadata is inferred from an equivalent + two-port straight. """ + ptype, in_port_name = self._infer_straight_metadata(fn, length_range, ptype, in_port_name) priority_bias = len(self._straight_offers) * BUILTIN_PRIORITY_STEP def data_at(length: float) -> AutoTool.GeneratedData: @@ -837,26 +970,43 @@ class AutoTool(Tool): def add_bend( self, abstract: Abstract, - in_port_name: str, - out_port_name: str, + in_port_name: str | None = None, + out_port_name: str | None = None, *, - clockwise: bool = True, + clockwise: bool | None = None, mirror: bool = True, ) -> Self: """ Register a reusable L-bend primitive. + + If the bend has exactly two ports, port names may be omitted. The bend + direction is inferred from the selected port orientations; `clockwise`, + when provided, is checked against that inferred direction. """ + if (in_port_name is None) != (out_port_name is None): + raise BuildError('Bend port names must be provided together') + if in_port_name is None or out_port_name is None: + port_names = tuple(abstract.ports.keys()) + if len(port_names) != 2: + raise BuildError(f'Bend port names are required for {len(port_names)}-port abstracts') + in_port_name, out_port_name = port_names + priority_bias = len(self._bend_offers[0]) * BUILTIN_PRIORITY_STEP in_port = abstract.ports[in_port_name] out_port = abstract.ports[out_port_name] out_ptype = out_port.ptype + source_clockwise = self._bend_clockwise(in_port, out_port) + if clockwise is not None and bool(clockwise) != source_clockwise: + raise BuildError('Bend clockwise argument does not match port orientations') for ccw in (False, True): - bend_dxy, bend_angle = self._bend2dxy(in_port, out_port, ccw) + target_clockwise = not bool(ccw) + bend_dxy, bend_angle = self._bend2dxy(in_port, out_port, source_clockwise, target_clockwise) bend_dx = float(bend_dxy[0]) bend_dy = float(bend_dxy[1]) - mirrored = mirror and (ccw == clockwise) - port_name = in_port_name if (mirror or ccw != clockwise) else out_port_name + source_matches_target = source_clockwise == target_clockwise + mirrored = mirror and not source_matches_target + port_name = in_port_name if (mirror or source_matches_target) else out_port_name reusable_data = self.ReusableData(abstract, port_name, mirrored) endpoint = Port((bend_dx, bend_dy), rotation=bend_angle, ptype=out_ptype) @@ -873,10 +1023,10 @@ class AutoTool(Tool): def add_sbend( self, - ptype: str, fn: GeneratedPrimitiveFn, - in_port_name: str, - out_port_name: str, + ptype: str | None = None, + in_port_name: str | None = None, + out_port_name: str | None = None, *, jog_range: tuple[float, float] = (0, numpy.inf), endpoint: GeneratedEndpointFn | None = None, @@ -887,7 +1037,17 @@ class AutoTool(Tool): `endpoint`, when supplied, describes the generated S-bend output port directly during planning and avoids instantiating `fn()` inside `endpoint_at()`. + + If `ptype` or port names are omitted, one in-domain example is generated + and the missing metadata is inferred from an equivalent two-port S-bend. """ + ptype, in_port_name, out_port_name = self._infer_sbend_metadata( + fn, + jog_range, + ptype, + in_port_name, + out_port_name, + ) if endpoint is None: def endpoint_at(jog: float) -> Port: jog_magnitude = abs(jog) @@ -971,24 +1131,55 @@ class AutoTool(Tool): def add_transition( self, abstract: Abstract, - their_port_name: str, - our_port_name: str, + their_port_name: str | None = None, + our_port_name: str | None = None, *, one_way: bool = False, ) -> Self: """ Register a reusable port-type transition and expose it as router-visible adapter offers. + + If the transition has exactly two ports and is bidirectional, port names + may be omitted. """ + if (their_port_name is None) != (our_port_name is None): + raise BuildError('Transition port names must be provided together') + if their_port_name is None or our_port_name is None: + if one_way: + raise BuildError('one-way transitions require explicit port names') + port_names = tuple(abstract.ports.keys()) + if len(port_names) != 2: + raise BuildError(f'Transition port names are required for {len(port_names)}-port abstracts') + their_port_name, our_port_name = port_names + self._add_transition_direction(abstract, their_port_name, our_port_name) if not one_way: self._add_transition_direction(abstract, our_port_name, their_port_name) return self @staticmethod - def _bend2dxy(in_port: Port, out_port: Port, ccw: bool) -> tuple[NDArray[numpy.float64], float]: + def _bend_clockwise(in_port: Port, out_port: Port) -> bool: + """Return true when the selected reusable bend port order turns clockwise.""" + _bend_dxy, bend_angle = in_port.measure_travel(out_port) + if bend_angle is None: + raise BuildError('Bend primitive output port must have a 90-degree rotation from its input port') + normalized_angle = bend_angle % (2 * pi) + if numpy.isclose(normalized_angle, pi / 2): + return True + if numpy.isclose(normalized_angle, 3 * pi / 2): + return False + raise BuildError('Bend primitive output port must have a 90-degree rotation from its input port') + + @staticmethod + def _bend2dxy( + in_port: Port, + out_port: Port, + source_clockwise: bool, + target_clockwise: bool, + ) -> tuple[NDArray[numpy.float64], float]: bend_dxy, bend_angle = in_port.measure_travel(out_port) assert bend_angle is not None - if ccw: + if source_clockwise != target_clockwise: bend_dxy[1] *= -1 bend_angle *= -1 return bend_dxy, bend_angle diff --git a/masque/test/test_autotool_planning.py b/masque/test/test_autotool_planning.py index 8e101e6..f29b741 100644 --- a/masque/test/test_autotool_planning.py +++ b/masque/test/test_autotool_planning.py @@ -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") ) diff --git a/masque/test/test_pather_autotool.py b/masque/test/test_pather_autotool.py index e5928ab..fe8c0a6 100644 --- a/masque/test/test_pather_autotool.py +++ b/masque/test/test_pather_autotool.py @@ -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) )