import pytest import numpy from numpy import pi from numpy.testing import assert_allclose, assert_equal from masque import Pather, Library, Pattern, Port from masque.builder.tools import PathTool from masque.error import BuildError, PortError @pytest.fixture def pather_setup() -> tuple[Pather, PathTool, Library]: lib = Library() tool = PathTool(layer=(1, 0), width=2, ptype="wire") p = Pather(lib, tools=tool) # Port rotation points into the device, so path extension moves in the opposite direction. p.ports["start"] = Port((0, 0), pi / 2, ptype="wire") return p, tool, lib def test_pather_straight(pather_setup: tuple[Pather, PathTool, Library]) -> None: p, tool, lib = pather_setup p.straight("start", 10) assert_allclose(p.ports["start"].offset, [0, -10], atol=1e-10) assert p.ports["start"].rotation is not None assert_allclose(p.ports["start"].rotation, pi / 2, atol=1e-10) def test_pather_bend(pather_setup: tuple[Pather, PathTool, Library]) -> None: p, tool, lib = pather_setup p.cw("start", 10) assert_allclose(p.ports["start"].offset, [-1, -10], atol=1e-10) assert p.ports["start"].rotation is not None assert_allclose(p.ports["start"].rotation, 0, atol=1e-10) def test_pather_path_to(pather_setup: tuple[Pather, PathTool, Library]) -> None: p, tool, lib = pather_setup p.straight("start", y=-50) assert_equal(p.ports["start"].offset, [0, -50]) def test_pather_mpath(pather_setup: tuple[Pather, PathTool, Library]) -> None: p, tool, lib = pather_setup p.ports["A"] = Port((0, 0), pi / 2, ptype="wire") p.ports["B"] = Port((10, 0), pi / 2, ptype="wire") p.straight(["A", "B"], ymin=-20) assert_equal(p.ports["A"].offset, [0, -20]) assert_equal(p.ports["B"].offset, [10, -20]) def test_pather_at_chaining(pather_setup: tuple[Pather, PathTool, Library]) -> None: p, tool, lib = pather_setup p.at("start").straight(10).ccw(10) assert_allclose(p.ports["start"].offset, [1, -20], atol=1e-10) assert p.ports["start"].rotation is not None assert_allclose(p.ports["start"].rotation, pi, atol=1e-10) def test_pather_dead_ports() -> None: lib = Library() tool = PathTool(layer=(1, 0), width=1) p = Pather(lib, ports={"in": Port((0, 0), 0)}, tools=tool) p.set_dead() p.straight("in", -10) assert_allclose(p.ports["in"].offset, [10, 0], atol=1e-10) p.straight("in", 20) assert_allclose(p.ports["in"].offset, [-10, 0], atol=1e-10) assert not p.pattern.has_shapes() def test_pather_trace_basic() -> None: lib = Library() tool = PathTool(layer='M1', width=1000) p = Pather(lib, tools=tool, auto_render=False) # Routing extends opposite the port's inward-facing rotation. p.pattern.ports['A'] = Port((0, 0), rotation=0) p.at('A').trace(None, 5000) assert numpy.allclose(p.pattern.ports['A'].offset, (-5000, 0)) p.at('A').trace(True, 5000) # CCW bend assert numpy.allclose(p.pattern.ports['A'].offset, (-10000, -500)) assert p.pattern.ports['A'].rotation is not None assert numpy.isclose(p.pattern.ports['A'].rotation, pi/2) def test_pather_trace_to() -> None: lib = Library() tool = PathTool(layer='M1', width=1000) p = Pather(lib, tools=tool, auto_render=False) p.pattern.ports['A'] = Port((0, 0), rotation=0) p.at('A').trace_to(None, x=-10000) assert numpy.allclose(p.pattern.ports['A'].offset, (-10000, 0)) p.at('A').trace_to(None, p=-20000) assert numpy.allclose(p.pattern.ports['A'].offset, (-20000, 0)) def test_pather_bundle_trace() -> None: lib = Library() tool = PathTool(layer='M1', width=1000) p = Pather(lib, tools=tool, auto_render=False) p.pattern.ports['A'] = Port((0, 0), rotation=0) p.pattern.ports['B'] = Port((0, 2000), rotation=0) p.at(['A', 'B']).straight(xmin=-10000) assert numpy.isclose(p.pattern.ports['A'].offset[0], -10000) assert numpy.isclose(p.pattern.ports['B'].offset[0], -10000) p.at(['A', 'B']).ccw(xmin=-20000, spacing=2000) # The lower port is on the inner bend, so `xmin` applies to that route. assert numpy.isclose(p.pattern.ports['A'].offset[0], -20000) assert numpy.isclose(p.pattern.ports['B'].offset[0], -22000) def test_portpather_default_spacing_matches_explicit_spacing() -> None: lib_default = Library() tool_default = PathTool(layer='M1', width=1000) p_default = Pather(lib_default, tools=tool_default, auto_render=False) p_default.pattern.ports['A'] = Port((0, 0), rotation=0) p_default.pattern.ports['B'] = Port((0, 2000), rotation=0) lib_explicit = Library() tool_explicit = PathTool(layer='M1', width=1000) p_explicit = Pather(lib_explicit, tools=tool_explicit, auto_render=False) p_explicit.pattern.ports['A'] = Port((0, 0), rotation=0) p_explicit.pattern.ports['B'] = Port((0, 2000), rotation=0) p_default.at(['A', 'B'], spacing=2000).ccw(xmin=-20000) p_explicit.at(['A', 'B']).ccw(xmin=-20000, spacing=2000) assert_allclose(p_default.pattern.ports['A'].offset, p_explicit.pattern.ports['A'].offset) assert_allclose(p_default.pattern.ports['B'].offset, p_explicit.pattern.ports['B'].offset) def test_portpather_default_spacing_reused_and_overridden() -> None: p_default = Pather(Library(), tools=PathTool(layer='M1', width=1000), auto_render=False) p_default.pattern.ports['A'] = Port((0, 0), rotation=0) p_default.pattern.ports['B'] = Port((0, 2000), rotation=0) p_explicit = Pather(Library(), tools=PathTool(layer='M1', width=1000), auto_render=False) p_explicit.pattern.ports['A'] = Port((0, 0), rotation=0) p_explicit.pattern.ports['B'] = Port((0, 2000), rotation=0) pp_default = p_default.at(['A', 'B'], spacing=2000) pp_default.ccw(xmin=-20000) pp_default.cw(emin=1000) p_explicit.at(['A', 'B']).ccw(xmin=-20000, spacing=2000).cw(emin=1000, spacing=2000) assert_allclose(p_default.pattern.ports['A'].offset, p_explicit.pattern.ports['A'].offset) assert_allclose(p_default.pattern.ports['B'].offset, p_explicit.pattern.ports['B'].offset) p_override = Pather(Library(), tools=PathTool(layer='M1', width=1000), auto_render=False) p_override.pattern.ports['A'] = Port((0, 0), rotation=0) p_override.pattern.ports['B'] = Port((0, 2000), rotation=0) p_override_explicit = Pather(Library(), tools=PathTool(layer='M1', width=1000), auto_render=False) p_override_explicit.pattern.ports['A'] = Port((0, 0), rotation=0) p_override_explicit.pattern.ports['B'] = Port((0, 2000), rotation=0) p_override.at(['A', 'B'], spacing=2000).ccw(xmin=-20000, spacing=3000) p_override_explicit.at(['A', 'B']).ccw(xmin=-20000, spacing=3000) assert_allclose(p_override.pattern.ports['A'].offset, p_override_explicit.pattern.ports['A'].offset) assert_allclose(p_override.pattern.ports['B'].offset, p_override_explicit.pattern.ports['B'].offset) def test_portpather_default_spacing_not_injected_for_straight_bundle() -> None: lib = Library() tool = PathTool(layer='M1', width=1000) p = Pather(lib, tools=tool, auto_render=False) p.pattern.ports['A'] = Port((0, 0), rotation=0) p.pattern.ports['B'] = Port((0, 2000), rotation=0) p.at(['A', 'B'], spacing=2000).straight(xmin=-10000) assert numpy.isclose(p.pattern.ports['A'].offset[0], -10000) assert numpy.isclose(p.pattern.ports['B'].offset[0], -10000) def test_portpather_default_spacing_vector_revalidated_after_selection_change() -> None: lib = Library() tool = PathTool(layer='M1', width=1000) p = Pather(lib, tools=tool, auto_render=False) p.pattern.ports['A'] = Port((0, 0), rotation=0) p.pattern.ports['B'] = Port((0, 2000), rotation=0) p.pattern.ports['C'] = Port((0, 4000), rotation=0) pp = p.at(['A', 'B', 'C'], spacing=[2000, 3000]) pp.deselect('C') with pytest.raises(BuildError, match='spacing must be scalar or have length 1'): pp.ccw(xmin=-20000) def test_pather_each_bound() -> None: lib = Library() tool = PathTool(layer='M1', width=1000) p = Pather(lib, tools=tool, auto_render=False) p.pattern.ports['A'] = Port((0, 0), rotation=0) p.pattern.ports['B'] = Port((-1000, 2000), rotation=0) p.at(['A', 'B']).trace(None, each=5000) assert numpy.allclose(p.pattern.ports['A'].offset, (-5000, 0)) assert numpy.allclose(p.pattern.ports['B'].offset, (-6000, 2000)) def test_selection_management() -> None: lib = Library() p = Pather(lib) p.pattern.ports['A'] = Port((0, 0), rotation=0) p.pattern.ports['B'] = Port((0, 0), rotation=0) pp = p.at('A') assert pp.ports == ['A'] pp.select('B') assert pp.ports == ['A', 'B'] pp.deselect('A') assert pp.ports == ['B'] pp.select(['A']) assert pp.ports == ['B', 'A'] pp.drop() assert 'A' not in p.pattern.ports assert 'B' not in p.pattern.ports assert pp.ports == [] def test_mark_fork() -> None: lib = Library() p = Pather(lib) p.pattern.ports['A'] = Port((100, 200), rotation=1) pp = p.at('A') pp.mark('B') assert 'B' in p.pattern.ports assert numpy.allclose(p.pattern.ports['B'].offset, (100, 200)) assert p.pattern.ports['B'].rotation == 1 assert pp.ports == ['A'] pp.fork('C') assert 'C' in p.pattern.ports assert pp.ports == ['C'] def test_mark_fork_reject_overwrite_and_duplicate_targets() -> None: lib = Library() p_mark = Pather(lib, pattern=Pattern(ports={ 'A': Port((0, 0), rotation=0), 'C': Port((2, 0), rotation=0), })) with pytest.raises(PortError, match='overwrite existing ports'): p_mark.at('A').mark('C') assert numpy.allclose(p_mark.pattern.ports['C'].offset, (2, 0)) p_fork = Pather(lib, pattern=Pattern(ports={ 'A': Port((0, 0), rotation=0), 'B': Port((1, 0), rotation=0), })) pp = p_fork.at(['A', 'B']) with pytest.raises(PortError, match='targets would collide'): pp.fork({'A': 'X', 'B': 'X'}) assert set(p_fork.pattern.ports) == {'A', 'B'} assert pp.ports == ['A', 'B'] def test_mark_fork_dead_overwrite_and_duplicate_targets() -> None: lib = Library() p = Pather(lib, pattern=Pattern(ports={ 'A': Port((0, 0), rotation=0), 'B': Port((1, 0), rotation=0), 'C': Port((2, 0), rotation=0), })) p.set_dead() p.at('A').mark('C') assert numpy.allclose(p.pattern.ports['C'].offset, (0, 0)) pp = p.at(['A', 'B']) pp.fork({'A': 'X', 'B': 'X'}) assert numpy.allclose(p.pattern.ports['X'].offset, (1, 0)) assert pp.ports == ['X'] def test_mark_fork_reject_missing_sources() -> None: lib = Library() p = Pather(lib, pattern=Pattern(ports={ 'A': Port((0, 0), rotation=0), 'B': Port((1, 0), rotation=0), })) with pytest.raises(PortError, match='selected ports'): p.at(['A', 'B']).mark({'Z': 'C'}) with pytest.raises(PortError, match='selected ports'): p.at(['A', 'B']).fork({'Z': 'C'}) def test_rename() -> None: lib = Library() p = Pather(lib) p.pattern.ports['A'] = Port((0, 0), rotation=0) p.at('A').rename('B') assert 'A' not in p.pattern.ports assert 'B' in p.pattern.ports p.pattern.ports['C'] = Port((0, 0), rotation=0) pp = p.at(['B', 'C']) pp.rename({'B': 'D', 'C': 'E'}) assert 'B' not in p.pattern.ports assert 'C' not in p.pattern.ports assert 'D' in p.pattern.ports assert 'E' in p.pattern.ports assert set(pp.ports) == {'D', 'E'} def test_pather_dead_fallback_preserves_out_ptype() -> None: lib = Library() tool = PathTool(layer='M1', width=1000, ptype='wire') p = Pather(lib, tools=tool, auto_render=False) p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire') p.set_dead() p.straight('A', -1000, out_ptype='other') assert numpy.allclose(p.pattern.ports['A'].offset, (1000, 0)) assert p.pattern.ports['A'].ptype == 'other' assert len(p.paths['A']) == 0 def test_pather_dead_place_overwrites_colliding_ports_last_wins() -> None: lib = Library() p = Pather(lib, pattern=Pattern(ports={ 'A': Port((5, 5), rotation=0), 'keep': Port((9, 9), rotation=0), })) p.set_dead() other = Pattern() other.ports['X'] = Port((1, 0), rotation=0) other.ports['Y'] = Port((2, 0), rotation=pi / 2) p.place(other, port_map={'X': 'A', 'Y': 'A'}) assert set(p.pattern.ports) == {'A', 'keep'} assert numpy.allclose(p.pattern.ports['A'].offset, (2, 0)) assert p.pattern.ports['A'].rotation is not None assert numpy.isclose(p.pattern.ports['A'].rotation, pi / 2) def test_pather_dead_plug_overwrites_colliding_outputs_last_wins() -> None: lib = Library() tool = PathTool(layer='M1', width=1000, ptype='wire') p = Pather(lib, tools=tool, pattern=Pattern(ports={ 'A': Port((0, 0), rotation=0, ptype='wire'), 'B': Port((99, 99), rotation=0, ptype='wire'), })) p.set_dead() other = Pattern() other.ports['in'] = Port((0, 0), rotation=pi, ptype='wire') other.ports['X'] = Port((10, 0), rotation=0, ptype='wire') other.ports['Y'] = Port((20, 0), rotation=0, ptype='wire') p.plug(other, map_in={'A': 'in'}, map_out={'X': 'B', 'Y': 'B'}) assert 'A' not in p.pattern.ports assert 'B' in p.pattern.ports assert numpy.allclose(p.pattern.ports['B'].offset, (20, 0)) assert p.pattern.ports['B'].rotation is not None assert numpy.isclose(p.pattern.ports['B'].rotation, 0) def test_pather_dead_rename_overwrites_colliding_ports_last_wins() -> None: p = Pather(Library(), pattern=Pattern(ports={ 'A': Port((0, 0), rotation=0), 'B': Port((1, 0), rotation=0), 'C': Port((2, 0), rotation=0), })) p.set_dead() p.rename_ports({'A': 'C', 'B': 'C'}) assert set(p.pattern.ports) == {'C'} assert numpy.allclose(p.pattern.ports['C'].offset, (1, 0))