import numpy import pytest from numpy.testing import assert_equal, assert_allclose from numpy import pi from ..builder import MinimumStatus, Pather, RouteFailureDetails from ..builder.utils import ell from ..error import BuildError from ..library import Library from ..pattern import Pattern from ..ports import Port def test_builder_public_imports() -> None: from masque import PortPather as TopPortPather from masque import RenderStep as TopRenderStep from masque import RouteError as TopRouteError from masque import RouteFailureDetails as TopRouteFailureDetails from masque import RouteFailurePolicy as TopRouteFailurePolicy from masque import MinimumStatus as TopMinimumStatus from masque.builder import PortPather as BuilderPortPather from masque.builder import RenderStep as BuilderRenderStep from masque.builder import RouteError as BuilderRouteError from masque.builder import RouteFailureDetails as BuilderRouteFailureDetails from masque.builder import RouteFailurePolicy as BuilderRouteFailurePolicy from masque.builder import MinimumStatus as BuilderMinimumStatus assert TopPortPather is BuilderPortPather assert TopRenderStep is BuilderRenderStep assert TopRouteError is BuilderRouteError assert TopRouteFailureDetails is BuilderRouteFailureDetails assert TopRouteFailurePolicy is BuilderRouteFailurePolicy assert TopMinimumStatus is BuilderMinimumStatus def test_route_failure_details_enforces_minimum_status_invariants() -> None: common = { 'operation': 'trace', 'portspec': 'A', 'in_ptype': 'wire', 'out_ptype': 'wide', 'request': {}, 'resolved_length': 1, 'resolved_jog': None, 'cause': 'no route', } with pytest.raises(BuildError, match='FOUND requires minimum_length'): RouteFailureDetails( **common, minimum_length=None, minimum_status=MinimumStatus.FOUND, ) with pytest.raises(BuildError, match='requires minimum_length=None'): RouteFailureDetails( **common, minimum_length=2, minimum_status=MinimumStatus.NO_ROUTE, ) def test_plain_build_error_does_not_include_stacktrace() -> None: assert str(BuildError('plain builder failure')) == 'plain builder failure' def test_builder_init() -> None: lib = Library() b = Pather(lib, name="mypat") assert b.pattern is lib["mypat"] assert b.library is lib def test_builder_place() -> None: lib = Library() child = Pattern() child.ports["A"] = Port((0, 0), 0) lib["child"] = child b = Pather(lib) b.place("child", offset=(10, 20), port_map={"A": "child_A"}) assert "child_A" in b.ports assert_equal(b.ports["child_A"].offset, [10, 20]) assert "child" in b.pattern.refs def test_builder_plug() -> None: lib = Library() wire = Pattern() wire.ports["in"] = Port((0, 0), 0) wire.ports["out"] = Port((10, 0), pi) lib["wire"] = wire b = Pather(lib) b.ports["start"] = Port((100, 100), 0) # Plug wire's "in" port into builder's "start" port # Wire's "out" port should be renamed to "start" because thru=True (default) and wire has 2 ports # builder start: (100, 100) rotation 0 # wire in: (0, 0) rotation 0 # wire out: (10, 0) rotation pi # Plugging wire in (rot 0) to builder start (rot 0) means wire is rotated by pi (180 deg) # so wire in is at (100, 100), wire out is at (100 - 10, 100) = (90, 100) b.plug("wire", map_in={"start": "in"}) assert "start" in b.ports assert_equal(b.ports["start"].offset, [90, 100]) assert b.ports["start"].rotation is not None assert_allclose(b.ports["start"].rotation, 0, atol=1e-10) def test_builder_interface() -> None: lib = Library() source = Pattern() source.ports["P1"] = Port((0, 0), 0) lib["source"] = source b = Pather.interface("source", library=lib, name="iface") assert "in_P1" in b.ports assert "P1" in b.ports assert b.pattern is lib["iface"] def test_builder_set_dead() -> None: lib = Library() lib["sub"] = Pattern() b = Pather(lib) b.set_dead() b.place("sub") assert not b.pattern.has_refs() def test_builder_dead_ports() -> None: lib = Library() pat = Pattern() pat.ports['A'] = Port((0, 0), 0) b = Pather(lib, pattern=pat) b.set_dead() # Attempt to plug a device where ports don't line up # A has rotation 0, C has rotation 0. plug() expects opposing rotations (pi difference). other = Pattern(ports={'C': Port((10, 10), 0), 'D': Port((20, 20), 0)}) # This should NOT raise PortError because b is dead b.plug(other, map_in={'A': 'C'}, map_out={'D': 'B'}) # Port A should be removed, and Port B (renamed from D) should be added assert 'A' not in b.ports assert 'B' in b.ports # Verify geometry was not added assert not b.pattern.has_refs() assert not b.pattern.has_shapes() def test_dead_plug_best_effort() -> None: lib = Library() pat = Pattern() pat.ports['A'] = Port((0, 0), 0) b = Pather(lib, pattern=pat) b.set_dead() # Device with multiple ports, none of which line up correctly other = Pattern(ports={ 'P1': Port((10, 10), 0), # Wrong rotation (0 instead of pi) 'P2': Port((20, 20), pi) # Correct rotation but wrong offset }) # Try to plug. find_transform will fail. # It should fall back to aligning the first pair ('A' and 'P1'). b.plug(other, map_in={'A': 'P1'}, map_out={'P2': 'B'}) assert 'A' not in b.ports assert 'B' in b.ports # Dummy transform aligns A (0,0) with P1 (10,10) # A rotation 0, P1 rotation 0 -> rotation = (0 - 0 - pi) = -pi # P2 (20,20) rotation pi: # 1. Translate P2 so P1 is at origin: (20,20) - (10,10) = (10,10) # 2. Rotate (10,10) by -pi: (-10,-10) # 3. Translate by s_port.offset (0,0): (-10,-10) assert_allclose(b.ports['B'].offset, [-10, -10], atol=1e-10) # P2 rot pi + transform rot -pi = 0 assert b.ports['B'].rotation is not None assert_allclose(b.ports['B'].rotation, 0, atol=1e-10) def test_ell_validates_spacing_length() -> None: ports = { 'A': Port((0, 0), 0), 'B': Port((0, 1), 0), 'C': Port((0, 2), 0), } with pytest.raises(BuildError, match='spacing must be scalar or have length 2'): ell(ports, True, 'min_extension', 5, spacing=[1, 2, 3]) with pytest.raises(BuildError, match='spacing must be scalar or have length 2'): ell(ports, True, 'min_extension', 5, spacing=[]) def test_ell_handles_array_spacing_when_ccw_none() -> None: ports = { 'A': Port((0, 0), 0), 'B': Port((0, 1), 0), } scalar = ell(ports, None, 'min_extension', 5, spacing=0) array_zero = ell(ports, None, 'min_extension', 5, spacing=numpy.array([0, 0])) assert scalar == array_zero with pytest.raises(BuildError, match='Spacing must be 0 or None'): ell(ports, None, 'min_extension', 5, spacing=numpy.array([1, 0])) @pytest.mark.parametrize('bound_type', ['emin', 'emax', 'min_past_furthest']) @pytest.mark.parametrize( ('rotation', 'expected'), [ (0, 5), (pi / 2, 7), (pi / 6, 5), (pi / 3, 7), (pi / 4, 5), ], ) def test_ell_extension_vector_selects_dominant_route_axis( bound_type: str, rotation: float, expected: float, ) -> None: result = ell({'A': Port((0, 0), rotation)}, None, bound_type, (5, 7), spacing=0) assert_allclose(result['A'], expected) @pytest.mark.parametrize('bound', [(-1, 2), (1, -2), (numpy.nan, 2), (1, numpy.inf), (1, 2, 3)]) def test_ell_rejects_invalid_extension_vector(bound: tuple[float, ...]) -> None: with pytest.raises(BuildError, match='bound|negative'): ell({'A': Port((0, 0), 0)}, None, 'emin', bound, spacing=0) def test_ell_position_vector_still_projects_onto_route_direction() -> None: result = ell({'A': Port((0, 0), pi)}, None, 'pmax', (5, 7), spacing=0) assert_allclose(result['A'], 5) def test_ell_rejects_invalid_bound_type() -> None: with pytest.raises(BuildError, match='Invalid bound type'): ell({'A': Port((0, 0), 0)}, None, 'nearest', 5, spacing=0)