[Pather] add strategy arg

This commit is contained in:
Jan Petykiewicz 2026-07-10 14:20:04 -07:00
commit cf3f72b828
5 changed files with 188 additions and 29 deletions

View file

@ -6,6 +6,7 @@ import pytest
from numpy import pi
from masque import Library, Path, Port, Pather
from masque.builder.planner import RoutingPlanner
from masque.builder.tools import (
BendOffer,
PathTool,
@ -422,6 +423,103 @@ def test_pather_selects_lowest_cost_offer() -> None:
assert numpy.allclose(p.ports['A'].offset, (-7, 0))
class StrategyTieTool(PlanningOnlyTool):
def __init__(self) -> None:
self.seen_kwargs: list[dict[str, Any]] = []
def primitive_offers(
self,
kind: Literal['straight', 'bend', 's', 'u'],
*,
in_ptype: str | None = None,
out_ptype: str | None = None,
**kwargs: Any,
) -> tuple[PrimitiveOffer, ...]:
self.seen_kwargs.append(dict(kwargs))
endpoint_ptype = out_ptype or in_ptype
if kind == 'straight':
return (StraightOffer(
in_ptype=in_ptype,
out_ptype=endpoint_ptype,
**offer_callbacks(lambda length: (
Port((length, 0), rotation=pi, ptype=endpoint_ptype),
{'kind': 'straight', 'length': length},
)),
),)
if kind == 's':
return (SOffer(
in_ptype=in_ptype,
out_ptype=endpoint_ptype,
**offer_callbacks(lambda jog: (
Port((3, jog), rotation=pi, ptype=endpoint_ptype),
{'kind': 's', 'jog': jog},
)),
),)
return ()
def pather_with_strategy_tool(
planner: RoutingPlanner | None = None,
) -> tuple[Pather, StrategyTieTool]:
tool = StrategyTieTool()
pather = Pather(Library(), tools=tool, planner=planner, render='deferred')
pather.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
return pather, tool
def test_pather_route_strategy_defaults_to_straight_first() -> None:
pather, _tool = pather_with_strategy_tool()
pather.jog('A', 4, length=10)
assert [step.data['kind'] for step in pather._paths['A']] == ['straight', 's']
assert pather._paths['A'][0].data['length'] == 7
def test_pather_route_strategy_uses_planner_default() -> None:
pather, _tool = pather_with_strategy_tool(RoutingPlanner(strategy='turn_first'))
pather.jog('A', 4, length=10)
assert [step.data['kind'] for step in pather._paths['A']] == ['s', 'straight']
assert pather._paths['A'][1].data['length'] == 7
def test_pather_route_strategy_per_route_overrides_planner_default() -> None:
pather, _tool = pather_with_strategy_tool(RoutingPlanner(strategy='turn_first'))
pather.jog('A', 4, length=10, strategy='straight_first')
assert [step.data['kind'] for step in pather._paths['A']] == ['straight', 's']
def test_pather_route_strategy_per_route_can_request_turn_first() -> None:
pather, _tool = pather_with_strategy_tool()
pather.jog('A', 4, length=10, strategy='turn_first')
assert [step.data['kind'] for step in pather._paths['A']] == ['s', 'straight']
def test_pather_route_strategy_is_not_forwarded_to_tool() -> None:
pather, tool = pather_with_strategy_tool()
pather.jog('A', 4, length=10, strategy='turn_first', marker='sentinel')
assert tool.seen_kwargs
assert all('strategy' not in kwargs for kwargs in tool.seen_kwargs)
assert any(kwargs.get('marker') == 'sentinel' for kwargs in tool.seen_kwargs)
def test_pather_route_strategy_rejects_invalid_values() -> None:
with pytest.raises(BuildError, match='Invalid route strategy'):
RoutingPlanner(strategy='sideways')
pather, _tool = pather_with_strategy_tool()
with pytest.raises(BuildError, match='Invalid route strategy'):
pather.jog('A', 4, length=10, strategy='sideways')
def test_solver_rejects_rotation_impossible_candidates_before_parameter_solving() -> None:
invalid_parameters: list[float] = []

View file

@ -76,7 +76,7 @@ def test_deferred_render_jog_uses_lowest_cost_two_bend_route(deferred_render_set
rp.render()
path_shape = cast("Path", rp.pattern.shapes[(1, 0)][0])
assert_allclose(path_shape.vertices, [[0, 0], [0, -1], [1, -1], [3, -1], [4, -1], [4, -2], [4, -10]], atol=1e-10)
assert_allclose(path_shape.vertices, [[0, 0], [0, -8], [0, -9], [1, -9], [3, -9], [4, -9], [4, -10]], atol=1e-10)
assert_allclose(rp.ports["start"].offset, [4, -10], atol=1e-10)
def test_deferred_render_mirror_preserves_planned_bend_geometry(deferred_render_setup: tuple[Pather, PathTool, Library]) -> None: