[planner] try bends=2 before bends=(2, 4)

This commit is contained in:
Jan Petykiewicz 2026-07-09 11:00:05 -07:00
commit 54c4cd9a4a
2 changed files with 108 additions and 11 deletions

View file

@ -50,6 +50,7 @@ from .interface import (
PreparedRouteResult, PreparedRouteResult,
RoutePlanningError, RoutePlanningError,
RoutePortContext, RoutePortContext,
route_error_is_fatal,
) )
@ -908,6 +909,12 @@ class RoutingPlanner:
TRACE_INTO_MAX_BENDS: int = 4 TRACE_INTO_MAX_BENDS: int = 4
def trace_into_bend_budgets(self, family: PrimitiveKind) -> tuple[int, ...]:
"""Return staged trace_into bend budgets for the route rotation parity."""
if family == 'bend':
return tuple(budget for budget in (1, 3) if budget <= self.TRACE_INTO_MAX_BENDS)
return tuple(budget for budget in (2, 4) if budget <= self.TRACE_INTO_MAX_BENDS)
def plan_leg( def plan_leg(
self, self,
family: PrimitiveKind, family: PrimitiveKind,
@ -1197,17 +1204,30 @@ class RoutingPlanner:
desired.rotation = port_dst.rotation - pi desired.rotation = port_dst.rotation - pi
desired.ptype = out_ptype desired.ptype = out_ptype
family, length, jog, ccw = self.trace_into_spec(context_src.port, desired) family, length, jog, ccw = self.trace_into_spec(context_src.port, desired)
leg = self.plan_leg( leg = None
family, last_error: Exception | None = None
context_src, for max_bends in self.trace_into_bend_budgets(family):
length=length, try:
jog=jog, leg = self.plan_leg(
ccw=ccw, family,
plug_into=portspec_dst if plug_destination else None, context_src,
constrain_jog=family == 'bend', length=length,
max_bends=self.TRACE_INTO_MAX_BENDS, jog=jog,
**(dict(kwargs) | {'out_ptype': out_ptype}), ccw=ccw,
) plug_into=portspec_dst if plug_destination else None,
constrain_jog=family == 'bend',
max_bends=max_bends,
**(dict(kwargs) | {'out_ptype': out_ptype}),
)
break
except (BuildError, NotImplementedError) as err:
if route_error_is_fatal(err):
raise
last_error = err
if leg is None:
if last_error is not None:
raise last_error
raise BuildError('No legal primitive offer for trace_into route')
renames = ((thru, context_src.portspec),) if thru is not None else () renames = ((thru, context_src.portspec),) if thru is not None else ()
return self.prepared_result_from_legs( return self.prepared_result_from_legs(
(leg,), (leg,),

View file

@ -6,6 +6,8 @@ from numpy import pi
from numpy.testing import assert_equal from numpy.testing import assert_equal
from masque import Library, PathTool, Port, Pather 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.tools import BendOffer, PrimitiveOffer, StraightOffer, Tool from masque.builder.tools import BendOffer, PrimitiveOffer, StraightOffer, Tool
from masque.error import BuildError, PortError from masque.error import BuildError, PortError
@ -260,3 +262,78 @@ def test_pather_trace_into_rejects_reserved_route_kwargs(
assert p.pattern.ports['B'].rotation is not None assert p.pattern.ports['B'].rotation is not None
assert numpy.isclose(p.pattern.ports['B'].rotation, dst.rotation) assert numpy.isclose(p.pattern.ports['B'].rotation, dst.rotation)
assert len(p._paths['A']) == 0 assert len(p._paths['A']) == 0
class TraceIntoBudgetPlanner(RoutingPlanner):
def __init__(self, successes: set[int], fatal_at: set[int] | None = None) -> None:
self.successes = successes
self.fatal_at = set() if fatal_at is None else fatal_at
self.attempts: list[int | None] = []
def plan_leg(
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,
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:
raise RoutePlanningError('fatal', fatal=True)
if max_bends 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,
)
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'), {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]),
],
)
def test_trace_into_uses_staged_bend_budgets(
dst: Port,
successes: set[int],
attempts: list[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
def test_trace_into_staged_bend_budget_stops_on_fatal_error() -> None:
planner = TraceIntoBudgetPlanner({4}, fatal_at={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]