[pather / planner] add bend_policy, default to error on non-minimal number of bends
This commit is contained in:
parent
1da5ac550a
commit
91a5d63b57
5 changed files with 267 additions and 25 deletions
|
|
@ -113,6 +113,73 @@ def test_pather_trace_into_shapes() -> 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(),
|
||||
|
|
@ -305,6 +372,7 @@ class TraceIntoBudgetSolver:
|
|||
|
||||
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
|
||||
|
||||
|
|
@ -340,7 +408,15 @@ def test_trace_into_reuses_solver_across_staged_bend_bands(
|
|||
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)
|
||||
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
|
||||
|
|
@ -351,7 +427,15 @@ def test_trace_into_staged_bend_budget_stops_on_fatal_error() -> None:
|
|||
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)
|
||||
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
|
||||
|
|
@ -361,8 +445,113 @@ def test_trace_into_bend_bands_respect_max_bends() -> None:
|
|||
class OneBendPlanner(RoutingPlanner):
|
||||
TRACE_INTO_MAX_BENDS = 1
|
||||
|
||||
planner = OneBendPlanner()
|
||||
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]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue