[Pather / AutoTool] rework route kwargs into tool_options, and actualyl pass them through AutoTool
This commit is contained in:
parent
49713896c3
commit
67afee7704
11 changed files with 865 additions and 130 deletions
|
|
@ -800,7 +800,7 @@ def test_autotool_s_offer_bbox_matches_rendered_sbend() -> None:
|
|||
.add_sbend(make_wide_sbend, "core", "A", "B", jog_range=(0, 1e8))
|
||||
.add_transition(lib.abstract("xin"), "EXT", "CORE")
|
||||
)
|
||||
offer, _out_port, _data = selected_offer(tool, "s", 4, length=15, in_ptype="core")
|
||||
offer, _out_port, _data = selected_offer(tool, "s", 4, in_ptype="core")
|
||||
|
||||
assert_offer_bbox_matches_rendered_offer(tool, offer, 4, lib)
|
||||
|
||||
|
|
@ -1006,7 +1006,7 @@ def test_pather_autotool_uses_l_offer_domains(multi_bend_tool: tuple[AutoTool, L
|
|||
assert bend_step.data.abstract.name == "b1"
|
||||
|
||||
|
||||
def test_autotool_generated_primitives_do_not_capture_route_kwargs() -> None:
|
||||
def test_autotool_generated_primitives_snapshot_route_options() -> None:
|
||||
markers: list[str | None] = []
|
||||
|
||||
def make_marked_straight(length: float, marker: str | None = None) -> Pattern:
|
||||
|
|
@ -1017,10 +1017,90 @@ def test_autotool_generated_primitives_do_not_capture_route_kwargs() -> None:
|
|||
p = Pather(Library(), tools=tool, render='deferred')
|
||||
p.ports["A"] = Port((0, 0), 0, ptype="wire")
|
||||
|
||||
p.straight("A", 5, marker="route")
|
||||
first_options = {'marker': 'first'}
|
||||
p.straight("A", 5, tool_options=first_options)
|
||||
first_options['marker'] = 'mutated'
|
||||
p.straight("A", 6, tool_options={'marker': 'second'})
|
||||
|
||||
first_data, second_data = (step.data for step in p._paths['A'])
|
||||
assert isinstance(first_data, AutoTool.GeneratedData)
|
||||
assert isinstance(second_data, AutoTool.GeneratedData)
|
||||
assert dict(first_data.tool_options) == {'marker': 'first'}
|
||||
assert dict(second_data.tool_options) == {'marker': 'second'}
|
||||
|
||||
p.render()
|
||||
|
||||
assert markers == [None]
|
||||
assert markers == ['first', 'second']
|
||||
|
||||
|
||||
def test_autotool_route_options_do_not_attach_to_reusable_bends(
|
||||
multi_bend_tool: tuple[AutoTool, Library],
|
||||
) -> None:
|
||||
tool, _lib = multi_bend_tool
|
||||
offers = tool.primitive_offers('bend', in_ptype='wire', ccw=True, marker='ignored')
|
||||
|
||||
assert offers
|
||||
assert all(isinstance(offer.commit(offer.parameter_domain[0]), AutoTool.ReusableData) for offer in offers)
|
||||
|
||||
|
||||
def test_autotool_sbend_generator_receives_route_options() -> None:
|
||||
markers: list[str] = []
|
||||
|
||||
def make_marked_sbend(jog: float, *, marker: str) -> Pattern:
|
||||
markers.append(marker)
|
||||
pat = Pattern()
|
||||
pat.ports['A'] = Port((0, 0), 0, ptype='wire')
|
||||
pat.ports['B'] = Port((3, jog), pi, ptype='wire')
|
||||
return pat
|
||||
|
||||
tool = AutoTool().add_sbend(
|
||||
make_marked_sbend,
|
||||
'wire',
|
||||
'A',
|
||||
'B',
|
||||
endpoint=lambda jog: Port((3, jog), pi, ptype='wire'),
|
||||
)
|
||||
p = Pather(Library(), tools=tool, render='deferred')
|
||||
p.ports['A'] = Port((0, 0), 0, ptype='wire')
|
||||
|
||||
p.jog('A', 4, length=3, tool_options={'marker': 'sbend'})
|
||||
p.render()
|
||||
|
||||
assert markers == ['sbend']
|
||||
|
||||
|
||||
def test_autotool_generated_bbox_uses_route_options() -> None:
|
||||
tool = AutoTool().add_straight(make_straight, 'wire', 'A')
|
||||
offer = tool.primitive_offers('straight', width=6)[0]
|
||||
|
||||
assert_allclose(offer.bbox_at(10), [[0, -3], [10, 3]])
|
||||
|
||||
|
||||
def test_autotool_unsupported_generator_option_fails_during_render() -> None:
|
||||
tool = AutoTool().add_straight(make_straight, 'wire', 'A')
|
||||
p = Pather(Library(), tools=tool, render='deferred')
|
||||
p.ports['A'] = Port((0, 0), 0, ptype='wire')
|
||||
|
||||
p.straight('A', 5, tool_options={'unknown_generator_option': True})
|
||||
|
||||
with pytest.raises(TypeError, match='unknown_generator_option'):
|
||||
p.render()
|
||||
|
||||
|
||||
def test_autotool_generator_options_must_preserve_planned_endpoint() -> None:
|
||||
def shifted_straight(length: float, endpoint_shift: float = 0) -> Pattern:
|
||||
pat = make_straight(length)
|
||||
pat.ports['B'].x += endpoint_shift
|
||||
return pat
|
||||
|
||||
tool = AutoTool().add_straight(shifted_straight, 'wire', 'A')
|
||||
p = Pather(Library(), tools=tool, render='deferred')
|
||||
p.ports['A'] = Port((0, 0), 0, ptype='wire')
|
||||
|
||||
p.straight('A', 5, tool_options={'endpoint_shift': 1})
|
||||
|
||||
with pytest.raises(BuildError, match='does not match planned endpoint'):
|
||||
p.render()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ccw", [False, True])
|
||||
|
|
@ -1204,7 +1284,7 @@ def test_pather_autotool_pure_sbend_with_transition_dx() -> None:
|
|||
assert_allclose(trans_port.offset, [5, 0])
|
||||
assert isinstance(trans_data, AutoTool.ReusableData)
|
||||
|
||||
s_offer, s_port, s_data = selected_offer(tool, "s", 4, length=15, in_ptype="core")
|
||||
s_offer, s_port, s_data = selected_offer(tool, "s", 4, in_ptype="core")
|
||||
assert_allclose(s_port.offset, [10, 4])
|
||||
assert isinstance(s_data, AutoTool.GeneratedData)
|
||||
assert s_offer.out_ptype == "core"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
from collections.abc import Sequence
|
||||
from typing import Any, Literal, Never
|
||||
import inspect
|
||||
|
||||
import pytest
|
||||
import numpy
|
||||
from numpy import pi
|
||||
|
||||
from masque import MinimumStatus, Pather, Library, Port, RouteError, RouteFailurePolicy
|
||||
from masque import MinimumStatus, Pather, PortPather, Library, Port, RouteError, RouteFailurePolicy
|
||||
from masque.builder.planner import RoutePortContext, RoutingPlanner
|
||||
from masque.builder.planner.planner import NoLegalRouteError
|
||||
from masque.builder.tools import BendOffer, PathTool, RenderStep, StraightOffer, Tool, UOffer
|
||||
|
|
@ -199,7 +200,7 @@ def test_route_error_reports_preferred_minimum_and_request_details() -> None:
|
|||
)
|
||||
|
||||
with pytest.raises(RouteError) as exc_info:
|
||||
p.ccw('A', 1, out_ptype='wide', diagnostic_context='tool-value')
|
||||
p.ccw('A', 1, out_ptype='wide', tool_options={'diagnostic_context': 'tool-value'})
|
||||
|
||||
details = exc_info.value.details
|
||||
assert isinstance(exc_info.value, BuildError)
|
||||
|
|
@ -211,7 +212,7 @@ def test_route_error_reports_preferred_minimum_and_request_details() -> None:
|
|||
'ccw': True,
|
||||
'length': 1,
|
||||
'out_ptype': 'wide',
|
||||
'diagnostic_context': 'tool-value',
|
||||
'tool_options': {'diagnostic_context': 'tool-value'},
|
||||
}
|
||||
assert details.resolved_length == 1
|
||||
assert details.resolved_jog is None
|
||||
|
|
@ -705,7 +706,7 @@ def test_pather_uturn_rejects_routing_bounds(kwargs: dict[str, int]) -> None:
|
|||
p = Pather(Library(), tools=PathTool(layer='M1', width=1, ptype='wire'))
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
with pytest.raises(BuildError, match='Unsupported routing bounds for uturn'):
|
||||
with pytest.raises(TypeError, match='unexpected keyword argument'):
|
||||
p.uturn('A', 4, **kwargs)
|
||||
|
||||
def test_pather_uturn_omitted_length_uses_minimum_length_route() -> None:
|
||||
|
|
@ -935,3 +936,74 @@ def test_pather_uturn_failed_two_bend_route_is_atomic() -> None:
|
|||
assert numpy.allclose(p.pattern.ports['A'].offset, (0, 0))
|
||||
assert p.pattern.ports['A'].rotation == 0
|
||||
assert len(p._paths['A']) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize('route_type', [Pather, PortPather])
|
||||
@pytest.mark.parametrize('name', ['trace', 'trace_to', 'straight', 'bend', 'ccw', 'cw', 'jog', 'uturn', 'trace_into'])
|
||||
def test_routing_entry_points_have_no_catch_all_kwargs(route_type: type, name: str) -> None:
|
||||
parameters = inspect.signature(getattr(route_type, name)).parameters.values()
|
||||
assert all(parameter.kind is not inspect.Parameter.VAR_KEYWORD for parameter in parameters)
|
||||
|
||||
|
||||
def test_routing_typo_fails_before_tool_lookup() -> None:
|
||||
tool = RequestCountingTool()
|
||||
p = Pather(Library(), tools=tool, render='deferred')
|
||||
|
||||
with pytest.raises(TypeError, match='unexpected keyword argument'):
|
||||
p.straight('A', lenght=10) # type: ignore[call-arg]
|
||||
|
||||
assert tool.offer_calls == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize('tool_options', [{1: 'bad'}, {'ccw': False}, {'out_ptype': 'bad'}])
|
||||
def test_tool_options_reject_invalid_or_reserved_keys(tool_options: dict[Any, Any]) -> None:
|
||||
tool = RequestCountingTool()
|
||||
p = Pather(Library(), tools=tool, render='deferred')
|
||||
|
||||
with pytest.raises(BuildError, match='tool_options'):
|
||||
p.trace('A', None, tool_options=tool_options)
|
||||
|
||||
assert tool.offer_calls == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'operation',
|
||||
[
|
||||
lambda p: p.jog('A', numpy.nan, length=1),
|
||||
lambda p: p.uturn('A', numpy.inf, length=1),
|
||||
lambda p: p.straight('A', x=numpy.nan),
|
||||
],
|
||||
ids=['jog-offset', 'uturn-offset', 'position-bound'],
|
||||
)
|
||||
def test_nonfinite_route_geometry_fails_before_offer_query(operation: Any) -> None:
|
||||
tool = RequestCountingTool()
|
||||
p = Pather(
|
||||
Library(),
|
||||
ports={'A': Port((0, 0), rotation=0, ptype='wire')},
|
||||
tools=tool,
|
||||
render='deferred',
|
||||
)
|
||||
|
||||
with pytest.raises(BuildError, match='finite'):
|
||||
operation(p)
|
||||
|
||||
assert tool.offer_calls == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize('spacing', [-1, numpy.nan, numpy.inf])
|
||||
def test_invalid_bundle_spacing_fails_before_offer_query(spacing: float) -> None:
|
||||
tool = RequestCountingTool()
|
||||
p = Pather(
|
||||
Library(),
|
||||
ports={
|
||||
'A': Port((0, 0), rotation=0, ptype='wire'),
|
||||
'B': Port((0, 4), rotation=0, ptype='wire'),
|
||||
},
|
||||
tools=tool,
|
||||
render='deferred',
|
||||
)
|
||||
|
||||
with pytest.raises(BuildError, match='spacing'):
|
||||
p.trace(['A', 'B'], True, xmin=-10, spacing=spacing)
|
||||
|
||||
assert tool.offer_calls == 0
|
||||
|
|
|
|||
|
|
@ -313,7 +313,7 @@ def test_pathtool_bend_offer_bbox_matches_path_bounds() -> None:
|
|||
|
||||
def test_pathtool_s_offer_bbox_uses_intrinsic_minimum_length() -> None:
|
||||
tool = PathTool(layer=(1, 0), width=2, ptype='wire')
|
||||
offer = tool.primitive_offers('s', in_ptype='wire', length=6)[0]
|
||||
offer = tool.primitive_offers('s', in_ptype='wire')[0]
|
||||
|
||||
bounds = offer.bbox_at(3)
|
||||
expected = Path(vertices=[(0, 0), (1, 0), (1, 3), (2, 3)], width=2).get_bounds_single()
|
||||
|
|
@ -333,7 +333,7 @@ def test_pathtool_u_offers_remain_unsupported() -> None:
|
|||
[
|
||||
('straight', {}),
|
||||
('bend', {'ccw': True}),
|
||||
('s', {'length': 6}),
|
||||
('s', {}),
|
||||
],
|
||||
)
|
||||
def test_pathtool_out_ptype_unk_is_wildcard(
|
||||
|
|
@ -348,6 +348,12 @@ def test_pathtool_out_ptype_unk_is_wildcard(
|
|||
assert offers[0].out_ptype == 'wire'
|
||||
|
||||
|
||||
def test_pathtool_rejects_unsupported_planning_options() -> None:
|
||||
tool = PathTool(layer='M1', width=1)
|
||||
with pytest.raises(BuildError, match='does not support tool options: marker'):
|
||||
tool.primitive_offers('straight', marker='sentinel')
|
||||
|
||||
|
||||
def test_pather_treats_notimplemented_offer_query_as_no_offers() -> None:
|
||||
class NoOfferTool(PlanningOnlyTool):
|
||||
def primitive_offers(
|
||||
|
|
@ -437,13 +443,14 @@ class StrategyTieTool(PlanningOnlyTool):
|
|||
) -> tuple[PrimitiveOffer, ...]:
|
||||
self.seen_kwargs.append(dict(kwargs))
|
||||
endpoint_ptype = out_ptype or in_ptype
|
||||
marker = kwargs.get('marker')
|
||||
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},
|
||||
{'kind': 'straight', 'length': length, 'marker': marker},
|
||||
)),
|
||||
),)
|
||||
if kind == 's':
|
||||
|
|
@ -452,7 +459,7 @@ class StrategyTieTool(PlanningOnlyTool):
|
|||
out_ptype=endpoint_ptype,
|
||||
**offer_callbacks(lambda jog: (
|
||||
Port((3, jog), rotation=pi, ptype=endpoint_ptype),
|
||||
{'kind': 's', 'jog': jog},
|
||||
{'kind': 's', 'jog': jog, 'marker': marker},
|
||||
)),
|
||||
),)
|
||||
return ()
|
||||
|
|
@ -504,11 +511,12 @@ def test_pather_route_strategy_per_route_can_request_turn_first() -> None:
|
|||
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')
|
||||
pather.jog('A', 4, length=10, strategy='turn_first', tool_options={'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)
|
||||
assert all(step.data['marker'] == 'sentinel' for step in pather._paths['A'])
|
||||
|
||||
|
||||
def test_pather_route_strategy_rejects_invalid_values() -> None:
|
||||
|
|
|
|||
|
|
@ -252,7 +252,8 @@ def test_pather_trace_into_rejects_reserved_route_kwargs(
|
|||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
p.pattern.ports['B'] = dst
|
||||
|
||||
with pytest.raises(BuildError, match=match):
|
||||
_ = 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))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue