[planner] make solver carry state across increasing bend counts

This commit is contained in:
Jan Petykiewicz 2026-07-09 11:09:52 -07:00
commit f864ebbeab
2 changed files with 186 additions and 101 deletions

View file

@ -7,7 +7,7 @@ from numpy.testing import assert_equal
from masque import Library, PathTool, Port, Pather
from masque.builder.planner import PreparedRouteResult, RoutePlanningError, RoutePortContext, RoutingPlanner
from masque.builder.planner.planner import Candidate, RouteLeg
from masque.builder.planner.planner import Candidate, RouteRequest
from masque.builder.tools import BendOffer, PrimitiveOffer, StraightOffer, Tool
from masque.error import BuildError, PortError
@ -264,38 +264,37 @@ def test_pather_trace_into_rejects_reserved_route_kwargs(
assert len(p._paths['A']) == 0
class TraceIntoBudgetPlanner(RoutingPlanner):
def __init__(self, successes: set[int], fatal_at: set[int] | None = None) -> None:
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[int | None] = []
self.attempts: list[tuple[int, int]] = []
def plan_leg(
def solve(
self,
family: Any,
context: RoutePortContext,
*,
length: float | None = None,
jog: float | None = None,
ccw: Any = None,
plug_into: str | None = None,
constrain_jog: bool = False,
min_bends: int = 0,
max_bends: int | None = None,
**kwargs: Any,
) -> RouteLeg:
_ = family, length, jog, ccw, constrain_jog, kwargs
self.attempts.append(max_bends)
if max_bends in self.fatal_at:
) -> 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', fatal=True)
if max_bends not in self.successes:
if band not in self.successes:
raise BuildError('try next budget')
return RouteLeg(
portspec=context.portspec,
start_port=context.port.copy(),
tool=context.tool,
candidate=Candidate((), context.port.copy(), 0.0, 0, 0.0),
plug_into=plug_into,
)
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:
self.solver = TraceIntoBudgetSolver(successes, fatal_at=fatal_at)
self.solver_requests = 0
def solver_for_request(self, request: RouteRequest) -> Any:
_ = request
self.solver_requests += 1
return self.solver
def prepared_result_from_legs(
self,
@ -310,30 +309,43 @@ class TraceIntoBudgetPlanner(RoutingPlanner):
@pytest.mark.parametrize(
('dst', 'successes', 'attempts'),
[
(Port((-10, 0), rotation=pi, ptype='wire'), {2}, [2]),
(Port((-10, 0), rotation=pi, ptype='wire'), {4}, [2, 4]),
(Port((-10, -10), rotation=3 * pi / 2, ptype='wire'), {1}, [1]),
(Port((-10, -10), rotation=3 * pi / 2, ptype='wire'), {3}, [1, 3]),
(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_uses_staged_bend_budgets(
def test_trace_into_reuses_solver_across_staged_bend_bands(
dst: Port,
successes: set[int],
attempts: list[int],
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)
assert planner.attempts == attempts
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}, fatal_at={2})
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)
assert planner.attempts == [2]
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()
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),)