579 lines
20 KiB
Python
579 lines
20 KiB
Python
from typing import Any
|
|
|
|
import pytest
|
|
import numpy
|
|
from numpy import pi
|
|
from numpy.testing import assert_allclose, assert_equal
|
|
|
|
from masque import Pather, Library, Pattern, Port, RouteError
|
|
from masque.builder import PathTool, PrimitiveOffer, StraightOffer
|
|
from masque.builder.planner import RoutingPlanner
|
|
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_builder_tool_symbol_exports() -> None:
|
|
import masque
|
|
import masque.builder
|
|
import masque.builder.tools as builder_tools
|
|
|
|
package_root_exports = (
|
|
'RenderStep',
|
|
'PortPather',
|
|
)
|
|
builder_tool_names = (
|
|
'PrimitiveOffer',
|
|
'StraightOffer',
|
|
'BendOffer',
|
|
'SOffer',
|
|
'UOffer',
|
|
)
|
|
internal_names = (
|
|
'PTypeMatch',
|
|
'canonicalize_domain_value',
|
|
'ptype_match',
|
|
'ptypes_compatible',
|
|
)
|
|
|
|
for name in (*package_root_exports, *builder_tool_names):
|
|
assert hasattr(masque.builder, name)
|
|
|
|
for name in package_root_exports:
|
|
assert hasattr(masque, name)
|
|
|
|
for name in (*builder_tool_names, *internal_names):
|
|
assert not hasattr(masque, name)
|
|
|
|
for name in internal_names:
|
|
assert not hasattr(masque.builder, name)
|
|
|
|
for name in builder_tool_names:
|
|
assert hasattr(masque.builder, name)
|
|
assert hasattr(builder_tools, name)
|
|
|
|
|
|
def test_pather_pending_render_steps_are_private() -> None:
|
|
p = Pather(Library(), tools=PathTool(layer=(1, 0), width=1))
|
|
|
|
assert not hasattr(p, 'paths')
|
|
assert hasattr(p, '_paths')
|
|
|
|
|
|
def test_pather_accepts_and_reuses_planner_instance() -> None:
|
|
class CountingPlanner(RoutingPlanner):
|
|
def __init__(self) -> None:
|
|
self.trace_to_calls = 0
|
|
|
|
def plan_trace_to_route(self, *args: Any, **kwargs: Any) -> Any:
|
|
self.trace_to_calls += 1
|
|
return super().plan_trace_to_route(*args, **kwargs)
|
|
|
|
planner = CountingPlanner()
|
|
p = Pather(
|
|
Library(),
|
|
tools=PathTool(layer=(1, 0), width=1),
|
|
render='deferred',
|
|
planner=planner,
|
|
)
|
|
p.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
|
|
p.straight('A', 1)
|
|
p.straight('A', 2)
|
|
|
|
assert p.planner is planner
|
|
assert planner.trace_to_calls == 2
|
|
|
|
|
|
def test_port_tool_policy_and_portpather_selection_follow_names() -> None:
|
|
default_tool = PathTool(layer=(1, 0), width=1, ptype='wire')
|
|
named_tool = PathTool(layer=(2, 0), width=1, ptype='wire')
|
|
p = Pather(
|
|
Library(),
|
|
ports={'A': Port((0, 0), rotation=0, ptype='wire')},
|
|
tools={None: default_tool, 'A': named_tool},
|
|
render='deferred',
|
|
)
|
|
selected = p.at('A')
|
|
|
|
p.rename_ports({'A': 'B'})
|
|
|
|
assert selected.ports == ['A']
|
|
assert p.tools['A'] is named_tool
|
|
assert 'B' not in p.tools
|
|
p.straight('B', 1)
|
|
assert p._paths['B'][0].tool is default_tool
|
|
|
|
p.mkport('A', Port((10, 0), rotation=0, ptype='wire'))
|
|
p.straight('A', 1)
|
|
assert p._paths['A'][0].tool is named_tool
|
|
|
|
|
|
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()
|
|
|
|
with pytest.raises(RouteError, match='finite and nonnegative'):
|
|
p.straight("in", -10)
|
|
|
|
assert_allclose(p.ports["in"].offset, [0, 0], atol=1e-10)
|
|
|
|
p.straight("in", 20)
|
|
assert_allclose(p.ports["in"].offset, [-20, 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)
|
|
|
|
# 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)
|
|
|
|
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)
|
|
|
|
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)
|
|
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)
|
|
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))
|
|
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))
|
|
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))
|
|
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))
|
|
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)
|
|
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)
|
|
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)
|
|
|
|
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 == []
|
|
|
|
|
|
@pytest.mark.parametrize('action', ['plug', 'plugged', 'rename', 'mark', 'fork'])
|
|
def test_empty_selection_exact_one_operations_raise_build_error(action: str) -> None:
|
|
p = Pather(Library())
|
|
pp = p.at([])
|
|
operations = {
|
|
'plug': lambda: pp.plug('unused', 'A'),
|
|
'plugged': lambda: pp.plugged('A'),
|
|
'rename': lambda: pp.rename('new'),
|
|
'mark': lambda: pp.mark('new'),
|
|
'fork': lambda: pp.fork('new'),
|
|
}
|
|
|
|
with pytest.raises(BuildError, match='expected exactly one'):
|
|
operations[action]()
|
|
|
|
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)
|
|
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_fallback_does_not_hide_fatal_route_errors() -> None:
|
|
class MismatchedEndpointTool(PathTool):
|
|
def primitive_offers(
|
|
self,
|
|
kind, # noqa: ANN001
|
|
*,
|
|
in_ptype=None, # noqa: ANN001
|
|
out_ptype=None, # noqa: ANN001,ARG002
|
|
**kwargs, # noqa: ANN003
|
|
) -> tuple[PrimitiveOffer, ...]:
|
|
_ = kwargs
|
|
if kind != 'straight':
|
|
return ()
|
|
|
|
def endpoint(length: float) -> Port:
|
|
return Port((length, 0), rotation=pi, ptype='wire')
|
|
|
|
return (StraightOffer(
|
|
in_ptype=in_ptype,
|
|
out_ptype='other',
|
|
endpoint_planner=endpoint,
|
|
commit_planner=lambda length: {'length': length},
|
|
),)
|
|
|
|
p = Pather(Library(), tools=MismatchedEndpointTool(layer='M1', width=1000, ptype='wire'))
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
p.set_dead()
|
|
|
|
with pytest.raises(BuildError, match='does not match declared offer out_ptype'):
|
|
p.straight('A', 1000)
|
|
|
|
assert numpy.allclose(p.pattern.ports['A'].offset, (0, 0))
|
|
assert p.pattern.ports['A'].ptype == 'wire'
|
|
assert len(p._paths['A']) == 0
|
|
|
|
|
|
def test_pather_valid_candidate_does_not_hide_fatal_route_errors() -> None:
|
|
class PartlyMismatchedEndpointTool(PathTool):
|
|
def primitive_offers(
|
|
self,
|
|
kind, # noqa: ANN001
|
|
*,
|
|
in_ptype=None, # noqa: ANN001
|
|
out_ptype=None, # noqa: ANN001,ARG002
|
|
**kwargs, # noqa: ANN003
|
|
) -> tuple[PrimitiveOffer, ...]:
|
|
_ = kwargs
|
|
if kind != 'straight':
|
|
return ()
|
|
|
|
def mismatched_endpoint(length: float) -> Port:
|
|
ptype = 'wire' if numpy.isclose(length, 0) else 'other'
|
|
return Port((length, 0), rotation=pi, ptype=ptype)
|
|
|
|
def valid_endpoint(length: float) -> Port:
|
|
return Port((length, 0), rotation=pi, ptype='wire')
|
|
|
|
return (
|
|
StraightOffer(
|
|
in_ptype=in_ptype,
|
|
out_ptype='wire',
|
|
endpoint_planner=mismatched_endpoint,
|
|
commit_planner=lambda length: {'kind': 'mismatched', 'length': length},
|
|
),
|
|
StraightOffer(
|
|
in_ptype=in_ptype,
|
|
out_ptype='wire',
|
|
endpoint_planner=valid_endpoint,
|
|
commit_planner=lambda length: {'kind': 'valid', 'length': length},
|
|
),
|
|
)
|
|
|
|
p = Pather(Library(), tools=PartlyMismatchedEndpointTool(layer='M1', width=1000, ptype='wire'))
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
|
|
with pytest.raises(BuildError, match='does not match declared offer out_ptype'):
|
|
p.straight('A', 1000)
|
|
|
|
assert numpy.allclose(p.pattern.ports['A'].offset, (0, 0))
|
|
assert p.pattern.ports['A'].ptype == 'wire'
|
|
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))
|