557 lines
20 KiB
Python
557 lines
20 KiB
Python
from typing import Any
|
|
|
|
import numpy
|
|
import pytest
|
|
from numpy import pi
|
|
from numpy.testing import assert_equal
|
|
|
|
from masque import Library, PathTool, Port, Pather, RouteFailurePolicy
|
|
from masque.builder.planner import PreparedRouteResult, RoutePlanningError, RoutePortContext, RoutingPlanner
|
|
from masque.builder.planner.planner import Candidate, SolverRequest
|
|
from masque.builder.tools import BendOffer, PrimitiveOffer, StraightOffer, Tool
|
|
from masque.error import BuildError, PortError
|
|
|
|
|
|
@pytest.fixture
|
|
def trace_into_setup() -> tuple[Pather, PathTool, Library]:
|
|
lib = Library()
|
|
tool = PathTool(layer=(1, 0), width=2, ptype="wire")
|
|
p = Pather(lib, tools=tool, render='immediate', render_append=False)
|
|
return p, tool, lib
|
|
|
|
|
|
def test_path_into_straight(trace_into_setup: tuple[Pather, PathTool, Library]) -> None:
|
|
p, _tool, _lib = trace_into_setup
|
|
p.ports["src"] = Port((0, 0), 0, ptype="wire")
|
|
p.ports["dst"] = Port((-20, 0), pi, ptype="wire")
|
|
|
|
p.trace_into("src", "dst")
|
|
|
|
assert "src" not in p.ports
|
|
assert "dst" not in p.ports
|
|
assert len(p.pattern.refs) == 1
|
|
|
|
|
|
def test_path_into_bend(trace_into_setup: tuple[Pather, PathTool, Library]) -> None:
|
|
p, _tool, _lib = trace_into_setup
|
|
p.ports["src"] = Port((0, 0), 0, ptype="wire")
|
|
p.ports["dst"] = Port((-20, -20), 3 * pi / 2, ptype="wire")
|
|
|
|
p.trace_into("src", "dst")
|
|
|
|
assert "src" not in p.ports
|
|
assert "dst" not in p.ports
|
|
assert len(p.pattern.refs) == 1
|
|
|
|
|
|
def test_path_into_sbend(trace_into_setup: tuple[Pather, PathTool, Library]) -> None:
|
|
p, _tool, _lib = trace_into_setup
|
|
p.ports["src"] = Port((0, 0), 0, ptype="wire")
|
|
p.ports["dst"] = Port((-20, -10), pi, ptype="wire")
|
|
|
|
p.trace_into("src", "dst")
|
|
|
|
assert "src" not in p.ports
|
|
assert "dst" not in p.ports
|
|
|
|
|
|
def test_path_into_thru(trace_into_setup: tuple[Pather, PathTool, Library]) -> None:
|
|
p, _tool, _lib = trace_into_setup
|
|
p.ports["src"] = Port((0, 0), 0, ptype="wire")
|
|
p.ports["dst"] = Port((-20, 0), pi, ptype="wire")
|
|
p.ports["other"] = Port((10, 10), 0)
|
|
|
|
p.trace_into("src", "dst", thru="other")
|
|
|
|
assert "src" in p.ports
|
|
assert_equal(p.ports["src"].offset, [10, 10])
|
|
assert "other" not in p.ports
|
|
|
|
|
|
def test_pather_trace_into_shapes() -> 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((-10000, 0), rotation=pi)
|
|
p.at('A').trace_into('B', plug_destination=False)
|
|
assert 'B' in p.pattern.ports
|
|
assert 'A' in p.pattern.ports
|
|
assert numpy.allclose(p.pattern.ports['A'].offset, (-10000, 0))
|
|
|
|
p.pattern.ports['C'] = Port((0, 0), rotation=0)
|
|
p.pattern.ports['D'] = Port((-5000, 5000), rotation=pi / 2)
|
|
p.at('C').trace_into('D', plug_destination=False)
|
|
assert 'D' in p.pattern.ports
|
|
assert 'C' in p.pattern.ports
|
|
assert numpy.allclose(p.pattern.ports['C'].offset, (-5000, 5000))
|
|
|
|
p.pattern.ports['E'] = Port((0, 0), rotation=0)
|
|
p.pattern.ports['F'] = Port((-10000, 2000), rotation=pi)
|
|
p.at('E').trace_into('F', plug_destination=False)
|
|
assert 'F' in p.pattern.ports
|
|
assert 'E' in p.pattern.ports
|
|
assert numpy.allclose(p.pattern.ports['E'].offset, (-10000, 2000))
|
|
|
|
p.pattern.ports['G'] = Port((0, 0), rotation=0)
|
|
p.pattern.ports['H'] = Port((-10000, 2000), rotation=0)
|
|
p.at('G').trace_into('H', plug_destination=False)
|
|
assert 'H' in p.pattern.ports
|
|
assert 'G' in p.pattern.ports
|
|
assert numpy.allclose(p.pattern.ports['G'].offset, (-10000, 2000))
|
|
assert p.pattern.ports['G'].rotation is not None
|
|
assert numpy.isclose(p.pattern.ports['G'].rotation, pi)
|
|
|
|
p.pattern.ports['I'] = Port((0, 0), rotation=pi / 2)
|
|
p.pattern.ports['J'] = Port((0, -10000), rotation=3 * pi / 2)
|
|
p.at('I').trace_into('J', plug_destination=False)
|
|
assert 'J' in p.pattern.ports
|
|
assert 'I' in p.pattern.ports
|
|
assert numpy.allclose(p.pattern.ports['I'].offset, (0, -10000))
|
|
assert p.pattern.ports['I'].rotation is not None
|
|
assert numpy.isclose(p.pattern.ports['I'].rotation, pi / 2)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'dst',
|
|
[
|
|
Port((-10_000, 0), rotation=pi),
|
|
Port((-10_000, 2_000), rotation=pi),
|
|
Port((-5_000, 5_000), rotation=pi / 2),
|
|
Port((-10_000, 2_000), rotation=0),
|
|
],
|
|
)
|
|
def test_pather_trace_into_minimal_policy_accepts_required_topologies(dst: Port) -> None:
|
|
pather = Pather(
|
|
Library(),
|
|
tools=PathTool(layer='M1', width=1_000),
|
|
render='deferred',
|
|
)
|
|
pather.ports['src'] = Port((0, 0), rotation=0)
|
|
pather.ports['dst'] = dst
|
|
|
|
pather.trace_into('src', 'dst', plug_destination=False, bend_policy='minimal')
|
|
|
|
assert numpy.allclose(pather.ports['src'].offset, dst.offset)
|
|
assert pather.ports['src'].rotation is not None
|
|
assert numpy.isclose((pather.ports['src'].rotation - dst.rotation) % (2 * pi), pi)
|
|
|
|
|
|
def test_pather_trace_into_bend_policy_changes_real_solver_fallback() -> None:
|
|
def make_pather() -> Pather:
|
|
pather = Pather(
|
|
Library(),
|
|
tools=PathTool(layer='M1', width=2, ptype='wire'),
|
|
render='deferred',
|
|
)
|
|
pather.ports['src'] = Port((0, 0), rotation=0, ptype='wire')
|
|
pather.ports['dst'] = Port((2, 0), rotation=pi, ptype='wire')
|
|
return pather
|
|
|
|
flexible = make_pather()
|
|
flexible.at('src').trace_into(
|
|
'dst',
|
|
plug_destination=False,
|
|
bend_policy='flexible',
|
|
)
|
|
|
|
assert_equal(flexible.ports['src'].offset, (2, 0))
|
|
assert flexible.ports['src'].rotation is not None
|
|
assert numpy.isclose(flexible.ports['src'].rotation, 0)
|
|
bend_roles = sum(
|
|
1 if step.kind == 'bend' else 2 if step.kind in ('s', 'u') else 0
|
|
for step in flexible._paths['src']
|
|
)
|
|
assert bend_roles == 4
|
|
|
|
minimal = make_pather()
|
|
with pytest.raises(BuildError):
|
|
minimal.at('src').trace_into(
|
|
'dst',
|
|
plug_destination=False,
|
|
)
|
|
|
|
assert set(minimal.ports) == {'src', 'dst'}
|
|
assert_equal(minimal.ports['src'].offset, (0, 0))
|
|
assert numpy.isclose(minimal.ports['src'].rotation, 0)
|
|
assert_equal(minimal.ports['dst'].offset, (2, 0))
|
|
assert numpy.isclose(minimal.ports['dst'].rotation, pi)
|
|
assert not minimal._paths
|
|
|
|
|
|
def test_pather_trace_into_large_composed_manhattan_route_plugs() -> None:
|
|
p = Pather(
|
|
Library(),
|
|
tools=PathTool(layer='M1', width=1000, ptype='wire'),
|
|
render='deferred',
|
|
)
|
|
p.ports['src'] = Port((123.25, 456.75), rotation=0, ptype='wire')
|
|
p.ports['dst'] = Port((-99_999_876.75, 20_000_456.75), rotation=0, ptype='wire')
|
|
|
|
p.trace_into('src', 'dst')
|
|
|
|
assert 'src' not in p.ports
|
|
assert 'dst' not in p.ports
|
|
assert [step.kind for step in p._paths['src']] == ['straight', 'bend', 'straight', 'bend', 'plug']
|
|
|
|
|
|
def test_pather_trace_into_refines_output_adapter_route_before_plug() -> None:
|
|
class TransitionTool(Tool):
|
|
def primitive_offers(
|
|
self,
|
|
kind, # noqa: ANN001
|
|
*,
|
|
in_ptype=None, # noqa: ANN001
|
|
out_ptype=None, # noqa: ANN001
|
|
**kwargs, # noqa: ANN003
|
|
) -> tuple[PrimitiveOffer, ...]:
|
|
_ = in_ptype
|
|
if kind == 'straight':
|
|
def native_endpoint(length: float) -> Port:
|
|
return Port((length, 0), rotation=pi, ptype='m1wire')
|
|
|
|
native = StraightOffer(
|
|
in_ptype='m1wire',
|
|
out_ptype='m1wire',
|
|
endpoint_planner=native_endpoint,
|
|
commit_planner=lambda length: {'kind': 'straight', 'length': length},
|
|
)
|
|
if out_ptype in ('unk', 'm1wire'):
|
|
return (native,)
|
|
|
|
def transition_endpoint(length: float) -> Port:
|
|
return Port((length, 0), rotation=pi, ptype='m2wire')
|
|
|
|
transition = StraightOffer(
|
|
in_ptype='m1wire',
|
|
out_ptype='m2wire',
|
|
length_domain=(2500, numpy.inf),
|
|
endpoint_planner=transition_endpoint,
|
|
commit_planner=lambda length: {'kind': 'transition', 'length': length},
|
|
)
|
|
return native, transition
|
|
|
|
if kind == 'bend':
|
|
ccw = bool(kwargs['ccw'])
|
|
|
|
def endpoint(length: float) -> Port:
|
|
return Port(
|
|
(length, 500 if ccw else -500),
|
|
rotation=-pi / 2 if ccw else pi / 2,
|
|
ptype='m1wire',
|
|
)
|
|
|
|
return (BendOffer(
|
|
in_ptype='m1wire',
|
|
out_ptype='m1wire',
|
|
ccw=ccw,
|
|
length_domain=(500, numpy.inf),
|
|
endpoint_planner=endpoint,
|
|
commit_planner=lambda length: {'kind': 'bend', 'length': length},
|
|
),)
|
|
|
|
return ()
|
|
|
|
def render(self, batch, *, port_names=('A', 'B'), **kwargs) -> Library: # noqa: ANN001,ANN202,ARG002
|
|
tree, pat = Library.mktree('transition_tool')
|
|
pat.add_port_pair(names=port_names, ptype=batch[-1].end_port.ptype if batch else 'm1wire')
|
|
return tree
|
|
|
|
p = Pather(Library(), tools=TransitionTool(), render='deferred')
|
|
p.pattern.ports['src'] = Port((-65000, -11500), rotation=pi / 2, ptype='m1wire')
|
|
p.pattern.ports['dst'] = Port((-100000, -100000), rotation=pi, ptype='m2wire')
|
|
|
|
p.trace_into('src', 'dst')
|
|
|
|
assert 'src' not in p.pattern.ports
|
|
assert 'dst' not in p.pattern.ports
|
|
|
|
|
|
def test_pather_trace_into_dead_updates_ports_without_geometry() -> 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.pattern.ports['B'] = Port((-10000, 0), rotation=pi, ptype='wire')
|
|
p.set_dead()
|
|
|
|
p.trace_into('A', 'B', plug_destination=False)
|
|
|
|
assert set(p.pattern.ports) == {'A', 'B'}
|
|
assert numpy.allclose(p.pattern.ports['A'].offset, (-10000, 0))
|
|
assert p.pattern.ports['A'].rotation is not None
|
|
assert numpy.isclose(p.pattern.ports['A'].rotation, 0)
|
|
assert len(p._paths['A']) == 0
|
|
assert not p.pattern.has_shapes()
|
|
assert not p.pattern.has_refs()
|
|
|
|
|
|
def test_pather_trace_into_planning_failure_leaves_state_unchanged() -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer='M1', width=1, ptype='wire')
|
|
p = Pather(lib, tools=tool)
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
p.pattern.ports['B'] = Port((-5, 5), rotation=pi / 2, ptype='wire')
|
|
|
|
with pytest.raises(BuildError):
|
|
p.trace_into('A', 'B', plug_destination=False, out_ptype='other')
|
|
|
|
assert numpy.allclose(p.pattern.ports['A'].offset, (0, 0))
|
|
assert numpy.isclose(p.pattern.ports['A'].rotation, 0)
|
|
assert numpy.allclose(p.pattern.ports['B'].offset, (-5, 5))
|
|
assert numpy.isclose(p.pattern.ports['B'].rotation, pi / 2)
|
|
assert len(p._paths['A']) == 0
|
|
|
|
|
|
def test_pather_trace_into_rename_failure_propagates() -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer='M1', width=1, ptype='wire')
|
|
p = Pather(lib, tools=tool)
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
p.pattern.ports['B'] = Port((-10, 0), rotation=pi, ptype='wire')
|
|
p.pattern.ports['other'] = Port((3, 4), rotation=0, ptype='wire')
|
|
|
|
with pytest.raises(PortError, match='overwritten'):
|
|
p.trace_into('A', 'B', plug_destination=False, thru='other')
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
('dst', 'kwargs', 'match'),
|
|
[
|
|
(Port((-5, 5), rotation=pi / 2, ptype='wire'), {'x': -99}, r'route arguments: x'),
|
|
(Port((-10, 2), rotation=pi, ptype='wire'), {'length': 1}, r'route arguments: length'),
|
|
(Port((-10, 2), rotation=0, ptype='wire'), {'length': 1}, r'route arguments: length'),
|
|
],
|
|
)
|
|
def test_pather_trace_into_rejects_reserved_route_kwargs(
|
|
dst: Port,
|
|
kwargs: dict[str, Any],
|
|
match: str,
|
|
) -> None:
|
|
lib = Library()
|
|
tool = PathTool(layer='M1', width=1, ptype='wire')
|
|
p = Pather(lib, tools=tool)
|
|
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
|
p.pattern.ports['B'] = dst
|
|
|
|
_ = match
|
|
with pytest.raises(TypeError, match='unexpected keyword argument'):
|
|
p.trace_into('A', 'B', plug_destination=False, **kwargs)
|
|
|
|
assert numpy.allclose(p.pattern.ports['A'].offset, (0, 0))
|
|
assert numpy.isclose(p.pattern.ports['A'].rotation, 0)
|
|
assert numpy.allclose(p.pattern.ports['B'].offset, dst.offset)
|
|
assert dst.rotation is not None
|
|
assert p.pattern.ports['B'].rotation is not None
|
|
assert numpy.isclose(p.pattern.ports['B'].rotation, dst.rotation)
|
|
assert len(p._paths['A']) == 0
|
|
|
|
|
|
class TraceIntoBudgetSolver:
|
|
def __init__(self, successes: set[tuple[int, int]], fatal_at: set[tuple[int, int]] | None = None) -> None:
|
|
self.successes = successes
|
|
self.fatal_at = set() if fatal_at is None else fatal_at
|
|
self.attempts: list[tuple[int, int]] = []
|
|
|
|
def solve(
|
|
self,
|
|
*,
|
|
min_bends: int = 0,
|
|
max_bends: int | None = None,
|
|
) -> Candidate:
|
|
assert max_bends is not None
|
|
band = (min_bends, max_bends)
|
|
self.attempts.append(band)
|
|
if band in self.fatal_at:
|
|
raise RoutePlanningError('fatal', policy=RouteFailurePolicy.FATAL)
|
|
if band not in self.successes:
|
|
raise BuildError('try next budget')
|
|
return Candidate((), Port((0, 0), rotation=0, ptype='wire'), 0.0, 0, 0.0)
|
|
|
|
|
|
class TraceIntoBudgetPlanner(RoutingPlanner):
|
|
def __init__(self, successes: set[tuple[int, int]], fatal_at: set[tuple[int, int]] | None = None) -> None:
|
|
super().__init__()
|
|
self.solver = TraceIntoBudgetSolver(successes, fatal_at=fatal_at)
|
|
self.solver_requests = 0
|
|
|
|
def solver_for_request(self, request: SolverRequest) -> Any:
|
|
_ = request
|
|
self.solver_requests += 1
|
|
return self.solver
|
|
|
|
def prepared_result_from_legs(
|
|
self,
|
|
legs: Any,
|
|
*,
|
|
renames: tuple[tuple[str, str], ...] = (),
|
|
) -> PreparedRouteResult:
|
|
_ = legs, renames
|
|
return PreparedRouteResult(())
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
('dst', 'successes', 'attempts'),
|
|
[
|
|
(Port((-10, 0), rotation=pi, ptype='wire'), {(0, 2)}, [(0, 2)]),
|
|
(Port((-10, 0), rotation=pi, ptype='wire'), {(4, 4)}, [(0, 2), (4, 4)]),
|
|
(Port((-10, -10), rotation=3 * pi / 2, ptype='wire'), {(1, 1)}, [(1, 1)]),
|
|
(Port((-10, -10), rotation=3 * pi / 2, ptype='wire'), {(3, 3)}, [(1, 1), (3, 3)]),
|
|
],
|
|
)
|
|
def test_trace_into_reuses_solver_across_staged_bend_bands(
|
|
dst: Port,
|
|
successes: set[tuple[int, int]],
|
|
attempts: list[tuple[int, int]],
|
|
) -> None:
|
|
planner = TraceIntoBudgetPlanner(successes)
|
|
context = RoutePortContext('src', Port((0, 0), rotation=0, ptype='wire'), PathTool(layer='M1', width=1, ptype='wire'))
|
|
|
|
planner.plan_trace_into(
|
|
context,
|
|
'dst',
|
|
dst,
|
|
out_ptype=None,
|
|
plug_destination=True,
|
|
thru=None,
|
|
bend_policy='flexible',
|
|
)
|
|
|
|
assert planner.solver.attempts == attempts
|
|
assert planner.solver_requests == 1
|
|
|
|
|
|
def test_trace_into_staged_bend_budget_stops_on_fatal_error() -> None:
|
|
planner = TraceIntoBudgetPlanner({(4, 4)}, fatal_at={(0, 2)})
|
|
context = RoutePortContext('src', Port((0, 0), rotation=0, ptype='wire'), PathTool(layer='M1', width=1, ptype='wire'))
|
|
|
|
with pytest.raises(RoutePlanningError, match='fatal'):
|
|
planner.plan_trace_into(
|
|
context,
|
|
'dst',
|
|
Port((-10, 0), rotation=pi, ptype='wire'),
|
|
out_ptype=None,
|
|
plug_destination=True,
|
|
thru=None,
|
|
bend_policy='flexible',
|
|
)
|
|
|
|
assert planner.solver.attempts == [(0, 2)]
|
|
assert planner.solver_requests == 1
|
|
|
|
|
|
def test_trace_into_bend_bands_respect_max_bends() -> None:
|
|
class OneBendPlanner(RoutingPlanner):
|
|
TRACE_INTO_MAX_BENDS = 1
|
|
|
|
planner = OneBendPlanner(bend_policy='flexible')
|
|
|
|
assert planner.trace_into_bend_bands('straight') == ((0, 0),)
|
|
assert planner.trace_into_bend_bands('s') == ((0, 0),)
|
|
assert planner.trace_into_bend_bands('bend') == ((1, 1),)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
('family', 'expected'),
|
|
[
|
|
('straight', ((0, 0),)),
|
|
('bend', ((1, 1),)),
|
|
('s', ((2, 2),)),
|
|
('u', ((2, 2),)),
|
|
],
|
|
)
|
|
def test_trace_into_default_minimal_bend_bands(family: str, expected: tuple[tuple[int, int], ...]) -> None:
|
|
planner = RoutingPlanner()
|
|
|
|
assert planner.trace_into_bend_bands(family) == expected
|
|
assert planner.trace_into_bend_bands(family, bend_policy='flexible') == (
|
|
((1, 1), (3, 3)) if family == 'bend' else ((0, 2), (4, 4))
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
('dst', 'required_band'),
|
|
[
|
|
(Port((-10, 0), rotation=pi, ptype='wire'), (0, 0)),
|
|
(Port((-10, -5), rotation=pi, ptype='wire'), (2, 2)),
|
|
(Port((-10, -10), rotation=3 * pi / 2, ptype='wire'), (1, 1)),
|
|
(Port((-10, -5), rotation=0, ptype='wire'), (2, 2)),
|
|
],
|
|
)
|
|
def test_trace_into_minimal_policy_uses_orientation_required_band(
|
|
dst: Port,
|
|
required_band: tuple[int, int],
|
|
) -> None:
|
|
planner = TraceIntoBudgetPlanner({required_band})
|
|
context = RoutePortContext(
|
|
'src',
|
|
Port((0, 0), rotation=0, ptype='wire'),
|
|
PathTool(layer='M1', width=1, ptype='wire'),
|
|
)
|
|
|
|
planner.plan_trace_into(
|
|
context,
|
|
'dst',
|
|
dst,
|
|
out_ptype=None,
|
|
plug_destination=True,
|
|
thru=None,
|
|
bend_policy='minimal',
|
|
)
|
|
|
|
assert planner.solver.attempts == [required_band]
|
|
|
|
|
|
def test_trace_into_minimal_policy_rejects_fallback_without_mutation() -> None:
|
|
planner = TraceIntoBudgetPlanner({(4, 4)})
|
|
pather = Pather(
|
|
Library(),
|
|
tools=PathTool(layer='M1', width=1, ptype='wire'),
|
|
planner=planner,
|
|
render='deferred',
|
|
)
|
|
pather.ports['src'] = Port((0, 0), rotation=0, ptype='wire')
|
|
pather.ports['dst'] = Port((-10, 0), rotation=pi, ptype='wire')
|
|
|
|
with pytest.raises(BuildError, match='try next budget'):
|
|
pather.trace_into('src', 'dst', bend_policy='minimal')
|
|
|
|
assert planner.solver.attempts == [(0, 0)]
|
|
assert set(pather.ports) == {'src', 'dst'}
|
|
assert_equal(pather.ports['src'].offset, (0, 0))
|
|
assert_equal(pather.ports['dst'].offset, (-10, 0))
|
|
assert not pather._paths
|
|
|
|
|
|
def test_trace_into_bend_policy_planner_default_and_route_override() -> None:
|
|
context = RoutePortContext(
|
|
'src',
|
|
Port((0, 0), rotation=0, ptype='wire'),
|
|
PathTool(layer='M1', width=1, ptype='wire'),
|
|
)
|
|
dst = Port((-10, 0), rotation=pi, ptype='wire')
|
|
minimal_planner = TraceIntoBudgetPlanner({(4, 4)})
|
|
|
|
with pytest.raises(BuildError, match='try next budget'):
|
|
minimal_planner.plan_trace_into(
|
|
context, 'dst', dst, out_ptype=None, plug_destination=True, thru=None,
|
|
)
|
|
assert minimal_planner.solver.attempts == [(0, 0)]
|
|
|
|
flexible_planner = TraceIntoBudgetPlanner({(4, 4)})
|
|
flexible_planner.plan_trace_into(
|
|
context,
|
|
'dst',
|
|
dst,
|
|
out_ptype=None,
|
|
plug_destination=True,
|
|
thru=None,
|
|
bend_policy='flexible',
|
|
)
|
|
assert flexible_planner.solver.attempts == [(0, 2), (4, 4)]
|
|
|
|
|
|
def test_trace_into_rejects_invalid_bend_policy() -> None:
|
|
with pytest.raises(BuildError, match='Invalid trace_into bend policy'):
|
|
RoutingPlanner(bend_policy='sideways') # type: ignore[arg-type]
|