diff --git a/MIGRATION.md b/MIGRATION.md index e723821..2f4765d 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -349,7 +349,7 @@ class MyTool(Tool): ``` Routing entry points now name every supported route argument explicitly. -Custom per-route Tool values must be placed under `tool_options`: +Custom per-route planning values must be placed under `tool_options`: ```python pather.jog('A', 4, length=10, tool_options={'process_corner': 'slow'}) @@ -450,25 +450,15 @@ Stable imports for custom tool authors live in `masque.builder`. The `masque.builder.planner` module is an internal planner implementation; do not import it from user code. -`trace_into()` uses the same primitive-offer route selection and defaults to -the minimal main-route bend count required by the endpoint relationship: zero -for a straight, one for a quarter-turn, and two for S- and U-like connections. -Ptype adapters do not consume this bend budget. Set -`plan_options={'bend_policy': 'flexible'}` to search bounded route topologies -with up to four bend roles, including dogleg and loop-like fallbacks. -Bend-family requests then search one-bend routes before three-bend routes; -other families search zero-to-two-bend routes before four-bend routes. The -first band with a legal route wins. Within that band, candidates are ordered -by total primitive-offer cost, adapter count, step count, and deterministic -discovery order. The default planner's `strategy` option affects only that -final discovery-order tie-break and is also supplied through `plan_options`. - -`plan_options` is reserved for planner-specific per-route policy, while -`tool_options` is forwarded only to `Tool.primitive_offers()`. For example: - -```python -pather.jog('A', 4, length=10, plan_options={'strategy': 'turn_first'}) -``` +`trace_into()` uses the same primitive-offer route selection and now searches +bounded route topologies with up to four bend roles. This preserves the common +straight, bend, S-like, U-like, and dogleg cases while allowing routes that +need an additional bounded bend pair. Bend-family requests search one-bend +routes before three-bend routes; other families search zero-to-two-bend routes +before four-bend routes. The first band with a legal route wins. Within that +band, candidates are ordered by total primitive-offer cost, adapter count, step +count, and deterministic discovery order. The route `strategy` affects only +that final discovery-order tie-break. Explicit-length `jog()` routes may also be satisfied by composing a straight primitive before or after an omitted-length native S primitive. `uturn()` routes diff --git a/masque/builder/error.py b/masque/builder/error.py index eb2b205..927c03c 100644 --- a/masque/builder/error.py +++ b/masque/builder/error.py @@ -5,7 +5,6 @@ from dataclasses import dataclass from enum import Enum, auto from pprint import pformat from types import MappingProxyType -import traceback from ..error import BuildError @@ -69,11 +68,10 @@ class RouteFailureDetails: class RouteError(BuildError): - """A route-selection failure with structured request and saved call-stack diagnostics.""" + """A route-selection failure with structured request diagnostics.""" details: RouteFailureDetails policy: RouteFailurePolicy - _call_stack: tuple[traceback.FrameSummary, ...] def __init__( self, @@ -105,5 +103,4 @@ class RouteError(BuildError): ] if details.minimum_cause is not None: lines.append(f' minimum_failure: {details.minimum_cause}') - self._call_stack = tuple(traceback.extract_stack()[:-1]) super().__init__('\n'.join(lines)) diff --git a/masque/builder/pather.py b/masque/builder/pather.py index c9bd1d4..2968868 100644 --- a/masque/builder/pather.py +++ b/masque/builder/pather.py @@ -8,11 +8,9 @@ planner package is intentionally internal: custom route generators should extend `Tool.primitive_offers()` and `Tool.render()` rather than depending on planner classes or search details. -Public routing arguments are explicit. Planner-specific per-route settings -belong in `plan_options`, while custom Tool offer values belong in -`tool_options`. Pather keeps those namespaces separate and forwards Tool -options only to offer discovery. A Tool must capture any selected render-time -value in its offer's committed data. +Public routing arguments are explicit. Custom per-route planning values belong +in `tool_options`; Pather forwards those values only to offer discovery. A Tool +must capture any selected render-time value in its offer's committed data. Routing is split into four ownership phases: - snapshot: `Pather` resolves the active Tool for each requested port and @@ -79,8 +77,11 @@ from .planner.interface import ( RoutePortContext, route_failure_policy, ) -from .error import RouteError, RouteFailurePolicy, ToolContractError -from .planner import RoutingPlanner +from .error import RouteFailurePolicy, ToolContractError +from .planner import ( + RouteTieBreakStrategy, + RoutingPlanner, + ) from .planner.bounds import resolved_position_bound from .logging import PatherLogger from ._tolerances import angles_equal, array_close @@ -127,20 +128,6 @@ def _validated_tool_options(tool_options: Mapping[str, Any] | None) -> dict[str, return options -def _validated_plan_options(plan_options: Mapping[str, Any] | None) -> dict[str, Any]: - """Copy generic per-route planner options without interpreting their keys.""" - if plan_options is None: - return {} - try: - options = dict(plan_options) - except (TypeError, ValueError) as err: - raise BuildError('plan_options must be a mapping with string keys') from err - nonstring = [key for key in options if not isinstance(key, str)] - if nonstring: - raise BuildError(f'plan_options keys must be strings; got {nonstring!r}') - return options - - class Pather(PortList): """ A `Pather` is a helper object used for snapping together multiple @@ -639,7 +626,7 @@ class Pather(PortList): length: float | None = None, *, spacing: float | ArrayLike | None = None, - plan_options: Mapping[str, Any] | None = None, + strategy: RouteTieBreakStrategy | str | None = None, out_ptype: str | None = None, each: float | None = None, set_rotation: float | None = None, @@ -665,8 +652,9 @@ class Pather(PortList): For a single port with no length or bound, legal primitive-offer candidates are evaluated at their minimum legal length-like parameters, then cost selects among those minimum-length candidates. `out_ptype`, - when provided, constrains only the final route endpoint. Planner-specific - per-route settings belong in `plan_options`. + when provided, constrains only the final route endpoint. `strategy` + controls straight-first vs turn-first ordering only after cost and + structural tie-breakers. `spacing` and `set_rotation` are only valid when using a bundle bound. """ @@ -684,23 +672,20 @@ class Pather(PortList): ymax=ymax, min_past_furthest=min_past_furthest, ) - plan_opts = _validated_plan_options(plan_options) - tool_opts = _validated_tool_options(tool_options) + options = _validated_tool_options(tool_options) with self._logger.log_operation( self, 'trace', portspec, ccw=ccw, length=length, spacing=spacing, - plan_options=plan_opts, tool_options=tool_opts, **bounds, + strategy=strategy, tool_options=options, **bounds, ): if isinstance(portspec, str): portspec = [portspec] contexts = self._route_contexts(portspec) try: result = self.planner.plan_trace_route( - contexts, ccw, length, spacing=spacing, plan_options=plan_opts, - tool_options=tool_opts, **bounds, + contexts, ccw, length, spacing=spacing, strategy=strategy, + tool_options=options, **bounds, ) except (BuildError, NotImplementedError) as err: - if isinstance(err, RouteError): - err.__traceback__ = None if not self._dead or route_failure_policy(err) is RouteFailurePolicy.FATAL: raise if length is not None and len(contexts) == 1: @@ -741,7 +726,7 @@ class Pather(PortList): *, length: float | None = None, spacing: float | ArrayLike | None = None, - plan_options: Mapping[str, Any] | None = None, + strategy: RouteTieBreakStrategy | str | None = None, out_ptype: str | None = None, each: float | None = None, set_rotation: float | None = None, @@ -770,7 +755,8 @@ class Pather(PortList): With no positional or bundle bound, single-port `trace_to()` uses the same omitted minimum-length primitive-offer behavior as `trace()`. - Planner-specific per-route settings belong in `plan_options`. + `strategy` controls straight-first vs turn-first ordering only after + cost and structural tie-breakers. """ bounds = _present_route_args( length=length, @@ -792,23 +778,20 @@ class Pather(PortList): ymax=ymax, min_past_furthest=min_past_furthest, ) - plan_opts = _validated_plan_options(plan_options) - tool_opts = _validated_tool_options(tool_options) + options = _validated_tool_options(tool_options) with self._logger.log_operation( self, 'trace_to', portspec, ccw=ccw, spacing=spacing, - plan_options=plan_opts, tool_options=tool_opts, **bounds, + strategy=strategy, tool_options=options, **bounds, ): if isinstance(portspec, str): portspec = [portspec] contexts = self._route_contexts(portspec) try: result = self.planner.plan_trace_to_route( - contexts, ccw, spacing=spacing, plan_options=plan_opts, - tool_options=tool_opts, **bounds, + contexts, ccw, spacing=spacing, strategy=strategy, + tool_options=options, **bounds, ) except (BuildError, NotImplementedError) as err: - if isinstance(err, RouteError): - err.__traceback__ = None if ( not self._dead or len(contexts) != 1 @@ -846,7 +829,7 @@ class Pather(PortList): length: float | None = None, *, spacing: float | ArrayLike | None = None, - plan_options: Mapping[str, Any] | None = None, + strategy: RouteTieBreakStrategy | str | None = None, out_ptype: str | None = None, each: float | None = None, set_rotation: float | None = None, @@ -867,7 +850,7 @@ class Pather(PortList): tool_options: Mapping[str, Any] | None = None, ) -> Self: return self.trace_to( - portspec, None, length=length, spacing=spacing, plan_options=plan_options, + portspec, None, length=length, spacing=spacing, strategy=strategy, out_ptype=out_ptype, each=each, set_rotation=set_rotation, p=p, pos=pos, position=position, x=x, y=y, emin=emin, emax=emax, pmin=pmin, pmax=pmax, @@ -882,7 +865,7 @@ class Pather(PortList): length: float | None = None, *, spacing: float | ArrayLike | None = None, - plan_options: Mapping[str, Any] | None = None, + strategy: RouteTieBreakStrategy | str | None = None, out_ptype: str | None = None, each: float | None = None, set_rotation: float | None = None, @@ -903,7 +886,7 @@ class Pather(PortList): tool_options: Mapping[str, Any] | None = None, ) -> Self: return self.trace_to( - portspec, ccw, length=length, spacing=spacing, plan_options=plan_options, + portspec, ccw, length=length, spacing=spacing, strategy=strategy, out_ptype=out_ptype, each=each, set_rotation=set_rotation, p=p, pos=pos, position=position, x=x, y=y, emin=emin, emax=emax, pmin=pmin, pmax=pmax, @@ -917,7 +900,7 @@ class Pather(PortList): length: float | None = None, *, spacing: float | ArrayLike | None = None, - plan_options: Mapping[str, Any] | None = None, + strategy: RouteTieBreakStrategy | str | None = None, out_ptype: str | None = None, each: float | None = None, set_rotation: float | None = None, @@ -938,7 +921,7 @@ class Pather(PortList): tool_options: Mapping[str, Any] | None = None, ) -> Self: return self.bend( - portspec, True, length, spacing=spacing, plan_options=plan_options, out_ptype=out_ptype, + portspec, True, length, spacing=spacing, strategy=strategy, out_ptype=out_ptype, each=each, set_rotation=set_rotation, p=p, pos=pos, position=position, x=x, y=y, emin=emin, emax=emax, pmin=pmin, pmax=pmax, xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax, min_past_furthest=min_past_furthest, tool_options=tool_options, @@ -950,7 +933,7 @@ class Pather(PortList): length: float | None = None, *, spacing: float | ArrayLike | None = None, - plan_options: Mapping[str, Any] | None = None, + strategy: RouteTieBreakStrategy | str | None = None, out_ptype: str | None = None, each: float | None = None, set_rotation: float | None = None, @@ -971,7 +954,7 @@ class Pather(PortList): tool_options: Mapping[str, Any] | None = None, ) -> Self: return self.bend( - portspec, False, length, spacing=spacing, plan_options=plan_options, out_ptype=out_ptype, + portspec, False, length, spacing=spacing, strategy=strategy, out_ptype=out_ptype, each=each, set_rotation=set_rotation, p=p, pos=pos, position=position, x=x, y=y, emin=emin, emax=emax, pmin=pmin, pmax=pmax, xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax, min_past_furthest=min_past_furthest, tool_options=tool_options, @@ -984,7 +967,7 @@ class Pather(PortList): length: float | None = None, *, spacing: float | ArrayLike | None = None, - plan_options: Mapping[str, Any] | None = None, + strategy: RouteTieBreakStrategy | str | None = None, out_ptype: str | None = None, p: float | None = None, pos: float | None = None, @@ -1006,27 +989,25 @@ class Pather(PortList): Multi-port jogs require `spacing`; the innermost first-bend port uses the base `length` or omitted-length solve, and other ports derive exact route lengths and offsets from that base route. `out_ptype`, when - provided, constrains only each final route endpoint. Planner-specific - per-route settings belong in `plan_options`. + provided, constrains only each final route endpoint. `strategy` + controls straight-first vs S-first ordering only after cost and + structural tie-breakers. """ bounds = _present_route_args(out_ptype=out_ptype, p=p, pos=pos, position=position, x=x, y=y) - plan_opts = _validated_plan_options(plan_options) - tool_opts = _validated_tool_options(tool_options) + options = _validated_tool_options(tool_options) with self._logger.log_operation( self, 'jog', portspec, offset=offset, length=length, spacing=spacing, - plan_options=plan_opts, tool_options=tool_opts, **bounds, + strategy=strategy, tool_options=options, **bounds, ): if isinstance(portspec, str): portspec = [portspec] contexts = self._route_contexts(portspec) try: result = self.planner.plan_jog_route( - contexts, offset, length, spacing=spacing, plan_options=plan_opts, - tool_options=tool_opts, **bounds, + contexts, offset, length, spacing=spacing, strategy=strategy, + tool_options=options, **bounds, ) except (BuildError, NotImplementedError) as err: - if isinstance(err, RouteError): - err.__traceback__ = None if ( not self._dead or len(contexts) != 1 @@ -1070,7 +1051,7 @@ class Pather(PortList): length: float | None = None, *, spacing: float | ArrayLike | None = None, - plan_options: Mapping[str, Any] | None = None, + strategy: RouteTieBreakStrategy | str | None = None, out_ptype: str | None = None, tool_options: Mapping[str, Any] | None = None, ) -> Self: @@ -1085,27 +1066,25 @@ class Pather(PortList): other ports derive exact lengths and offsets from it. Use `length=0` to request the old zero-public-length U-turn shape. Positional and bundle-bound keywords are not supported for this operation. `out_ptype`, - when provided, constrains only each final route endpoint. Planner-specific - per-route settings belong in `plan_options`. + when provided, constrains only each final route endpoint. `strategy` + controls straight-first vs U-first ordering only after cost and + structural tie-breakers. """ bounds = _present_route_args(out_ptype=out_ptype) - plan_opts = _validated_plan_options(plan_options) - tool_opts = _validated_tool_options(tool_options) + options = _validated_tool_options(tool_options) with self._logger.log_operation( self, 'uturn', portspec, offset=offset, length=length, spacing=spacing, - plan_options=plan_opts, tool_options=tool_opts, **bounds, + strategy=strategy, tool_options=options, **bounds, ): if isinstance(portspec, str): portspec = [portspec] contexts = self._route_contexts(portspec) try: result = self.planner.plan_uturn_route( - contexts, offset, length, spacing=spacing, plan_options=plan_opts, - tool_options=tool_opts, **bounds, + contexts, offset, length, spacing=spacing, strategy=strategy, + tool_options=options, **bounds, ) except (BuildError, NotImplementedError) as err: - if isinstance(err, RouteError): - err.__traceback__ = None if ( not self._dead or len(contexts) != 1 @@ -1136,26 +1115,21 @@ class Pather(PortList): out_ptype: str | None = None, plug_destination: bool = True, thru: str | None = None, - plan_options: Mapping[str, Any] | None = None, + strategy: RouteTieBreakStrategy | str | None = None, tool_options: Mapping[str, Any] | None = None, ) -> Self: """ Route one port into another using a bounded primitive-offer selection. - By default, searches only the exact main-route bend count required by - the endpoint relationship: zero for a straight, one for a quarter-turn, - and two for S- and U-like connections. This rejects extra dogleg and - loop-like fallback routes without inspecting primitive geometry. Ptype - adapters do not consume this bend budget. - - Set `plan_options={'bend_policy': 'flexible'}` to search bounded - primitive-offer routes with up to four bend roles. Bend-family requests try one-bend routes - before three-bend routes; other families try zero-to-two-bend routes - before four-bend routes. The first band with a legal candidate wins. - Within a band, candidates are ordered by total cost, adapter count, - step count, the requested straight-vs-turn topology preference, and - deterministic discovery order. The default planner's `strategy` option - therefore affects only otherwise tied candidates. + The current baseline searches bounded primitive-offer routes with up to + four bend roles, including straight, single-bend, S-like, U-like, and + dogleg topologies. Bend-family requests try one-bend routes before + three-bend routes; other families try zero-to-two-bend routes before + four-bend routes. The first band with a legal candidate wins. Within a + band, candidates are ordered by total cost, adapter count, step count, + the requested straight-vs-turn topology preference, and deterministic + discovery order. `strategy` therefore affects only otherwise tied + candidates. Custom planning options may be supplied through `tool_options`; they are forwarded only to primitive offer generation. @@ -1166,8 +1140,7 @@ class Pather(PortList): mutated; failures during selected-route execution, including primitive commit, plug/thru application, or render, may leave partial output. """ - plan_opts = _validated_plan_options(plan_options) - tool_opts = _validated_tool_options(tool_options) + options = _validated_tool_options(tool_options) with self._logger.log_operation( self, 'trace_into', @@ -1175,23 +1148,19 @@ class Pather(PortList): out_ptype=out_ptype, plug_destination=plug_destination, thru=thru, - plan_options=plan_opts, - tool_options=tool_opts, + strategy=strategy, + tool_options=options, ): - try: - result = self.planner.plan_trace_into( - self._route_context(portspec_src), - portspec_dst, - self.pattern[portspec_dst].copy(), - out_ptype = out_ptype, - plug_destination = plug_destination, - thru = thru, - plan_options = plan_opts, - tool_options = tool_opts, - ) - except RouteError as err: - err.__traceback__ = None - raise + result = self.planner.plan_trace_into( + self._route_context(portspec_src), + portspec_dst, + self.pattern[portspec_dst].copy(), + out_ptype = out_ptype, + plug_destination = plug_destination, + thru = thru, + strategy = strategy, + tool_options = options, + ) self._apply_route_result(result) return self @@ -1407,7 +1376,7 @@ class PortPather: length: float | None = None, *, spacing: float | ArrayLike | None = None, - plan_options: Mapping[str, Any] | None = None, + strategy: RouteTieBreakStrategy | str | None = None, out_ptype: str | None = None, each: float | None = None, set_rotation: float | None = None, @@ -1425,7 +1394,7 @@ class PortPather: if spacing is None and self.default_spacing is not None and len(self.ports) > 1 and ccw is not None: spacing = self.default_spacing self.pather.trace( - self.ports, ccw, length, spacing=spacing, plan_options=plan_options, out_ptype=out_ptype, + self.ports, ccw, length, spacing=spacing, strategy=strategy, out_ptype=out_ptype, each=each, set_rotation=set_rotation, emin=emin, emax=emax, pmin=pmin, pmax=pmax, xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax, min_past_furthest=min_past_furthest, tool_options=tool_options, @@ -1438,7 +1407,7 @@ class PortPather: *, length: float | None = None, spacing: float | ArrayLike | None = None, - plan_options: Mapping[str, Any] | None = None, + strategy: RouteTieBreakStrategy | str | None = None, out_ptype: str | None = None, each: float | None = None, set_rotation: float | None = None, @@ -1461,7 +1430,7 @@ class PortPather: if spacing is None and self.default_spacing is not None and len(self.ports) > 1 and ccw is not None: spacing = self.default_spacing self.pather.trace_to( - self.ports, ccw, length=length, spacing=spacing, plan_options=plan_options, out_ptype=out_ptype, + self.ports, ccw, length=length, spacing=spacing, strategy=strategy, out_ptype=out_ptype, each=each, set_rotation=set_rotation, p=p, pos=pos, position=position, x=x, y=y, emin=emin, emax=emax, pmin=pmin, pmax=pmax, xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax, min_past_furthest=min_past_furthest, tool_options=tool_options, @@ -1473,7 +1442,7 @@ class PortPather: length: float | None = None, *, spacing: float | ArrayLike | None = None, - plan_options: Mapping[str, Any] | None = None, + strategy: RouteTieBreakStrategy | str | None = None, out_ptype: str | None = None, each: float | None = None, set_rotation: float | None = None, @@ -1494,7 +1463,7 @@ class PortPather: tool_options: Mapping[str, Any] | None = None, ) -> Self: return self.trace_to( - None, length=length, spacing=spacing, plan_options=plan_options, out_ptype=out_ptype, + None, length=length, spacing=spacing, strategy=strategy, out_ptype=out_ptype, each=each, set_rotation=set_rotation, p=p, pos=pos, position=position, x=x, y=y, emin=emin, emax=emax, pmin=pmin, pmax=pmax, xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax, min_past_furthest=min_past_furthest, tool_options=tool_options, @@ -1506,7 +1475,7 @@ class PortPather: length: float | None = None, *, spacing: float | ArrayLike | None = None, - plan_options: Mapping[str, Any] | None = None, + strategy: RouteTieBreakStrategy | str | None = None, out_ptype: str | None = None, each: float | None = None, set_rotation: float | None = None, @@ -1527,7 +1496,7 @@ class PortPather: tool_options: Mapping[str, Any] | None = None, ) -> Self: return self.trace_to( - ccw, length=length, spacing=spacing, plan_options=plan_options, out_ptype=out_ptype, + ccw, length=length, spacing=spacing, strategy=strategy, out_ptype=out_ptype, each=each, set_rotation=set_rotation, p=p, pos=pos, position=position, x=x, y=y, emin=emin, emax=emax, pmin=pmin, pmax=pmax, xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax, min_past_furthest=min_past_furthest, tool_options=tool_options, @@ -1538,7 +1507,7 @@ class PortPather: length: float | None = None, *, spacing: float | ArrayLike | None = None, - plan_options: Mapping[str, Any] | None = None, + strategy: RouteTieBreakStrategy | str | None = None, out_ptype: str | None = None, each: float | None = None, set_rotation: float | None = None, @@ -1559,7 +1528,7 @@ class PortPather: tool_options: Mapping[str, Any] | None = None, ) -> Self: return self.bend( - True, length, spacing=spacing, plan_options=plan_options, out_ptype=out_ptype, + True, length, spacing=spacing, strategy=strategy, out_ptype=out_ptype, each=each, set_rotation=set_rotation, p=p, pos=pos, position=position, x=x, y=y, emin=emin, emax=emax, pmin=pmin, pmax=pmax, xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax, min_past_furthest=min_past_furthest, tool_options=tool_options, @@ -1570,7 +1539,7 @@ class PortPather: length: float | None = None, *, spacing: float | ArrayLike | None = None, - plan_options: Mapping[str, Any] | None = None, + strategy: RouteTieBreakStrategy | str | None = None, out_ptype: str | None = None, each: float | None = None, set_rotation: float | None = None, @@ -1591,7 +1560,7 @@ class PortPather: tool_options: Mapping[str, Any] | None = None, ) -> Self: return self.bend( - False, length, spacing=spacing, plan_options=plan_options, out_ptype=out_ptype, + False, length, spacing=spacing, strategy=strategy, out_ptype=out_ptype, each=each, set_rotation=set_rotation, p=p, pos=pos, position=position, x=x, y=y, emin=emin, emax=emax, pmin=pmin, pmax=pmax, xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax, min_past_furthest=min_past_furthest, tool_options=tool_options, @@ -1603,7 +1572,7 @@ class PortPather: length: float | None = None, *, spacing: float | ArrayLike | None = None, - plan_options: Mapping[str, Any] | None = None, + strategy: RouteTieBreakStrategy | str | None = None, out_ptype: str | None = None, p: float | None = None, pos: float | None = None, @@ -1615,7 +1584,7 @@ class PortPather: if spacing is None and self.default_spacing is not None and len(self.ports) > 1 and not numpy.isclose(offset, 0): spacing = self.default_spacing self.pather.jog( - self.ports, offset, length, spacing=spacing, plan_options=plan_options, out_ptype=out_ptype, + self.ports, offset, length, spacing=spacing, strategy=strategy, out_ptype=out_ptype, p=p, pos=pos, position=position, x=x, y=y, tool_options=tool_options, ) return self @@ -1626,14 +1595,14 @@ class PortPather: length: float | None = None, *, spacing: float | ArrayLike | None = None, - plan_options: Mapping[str, Any] | None = None, + strategy: RouteTieBreakStrategy | str | None = None, out_ptype: str | None = None, tool_options: Mapping[str, Any] | None = None, ) -> Self: if spacing is None and self.default_spacing is not None and len(self.ports) > 1: spacing = self.default_spacing self.pather.uturn( - self.ports, offset, length, spacing=spacing, plan_options=plan_options, + self.ports, offset, length, spacing=spacing, strategy=strategy, out_ptype=out_ptype, tool_options=tool_options, ) return self @@ -1645,13 +1614,13 @@ class PortPather: out_ptype: str | None = None, plug_destination: bool = True, thru: str | None = None, - plan_options: Mapping[str, Any] | None = None, + strategy: RouteTieBreakStrategy | str | None = None, tool_options: Mapping[str, Any] | None = None, ) -> Self: port = self._single_port('trace_into') self.pather.trace_into( port, target_port, out_ptype=out_ptype, plug_destination=plug_destination, - thru=thru, plan_options=plan_options, tool_options=tool_options, + thru=thru, strategy=strategy, tool_options=tool_options, ) return self diff --git a/masque/builder/planner/__init__.py b/masque/builder/planner/__init__.py index 752ac8a..41c53d6 100644 --- a/masque/builder/planner/__init__.py +++ b/masque/builder/planner/__init__.py @@ -13,5 +13,4 @@ from .interface import ( route_failure_policy as route_failure_policy, ) from .planner import RouteTieBreakStrategy as RouteTieBreakStrategy -from .planner import TraceIntoBendPolicy as TraceIntoBendPolicy from .planner import RoutingPlanner as RoutingPlanner diff --git a/masque/builder/planner/planner.py b/masque/builder/planner/planner.py index 60c6e93..9fd5a87 100644 --- a/masque/builder/planner/planner.py +++ b/masque/builder/planner/planner.py @@ -72,7 +72,6 @@ from .interface import ( ) RouteTieBreakStrategy = Literal['straight_first', 'turn_first'] -TraceIntoBendPolicy = Literal['flexible', 'minimal'] COST_RTOL = 1e-10 COST_ATOL = 1e-8 @@ -88,17 +87,6 @@ def validate_strategy(strategy: RouteTieBreakStrategy | str) -> RouteTieBreakStr raise BuildError(f'Invalid route strategy {strategy!r}; expected straight_first or turn_first') -def validate_trace_into_bend_policy( - bend_policy: TraceIntoBendPolicy | str, - ) -> TraceIntoBendPolicy: - """Return a supported trace-into bend policy or raise a routing error.""" - if bend_policy in ('flexible', 'minimal'): - return bend_policy - raise BuildError( - f'Invalid trace_into bend policy {bend_policy!r}; expected flexible or minimal' - ) - - def is_close(a: float, b: float) -> bool: """Compare route-solver scalars with the planner tolerance.""" return scalar_close(a, b) @@ -1069,16 +1057,9 @@ class RoutingPlanner: TRACE_INTO_MAX_BENDS: int = 4 DEFAULT_STRATEGY: RouteTieBreakStrategy = 'straight_first' - DEFAULT_TRACE_INTO_BEND_POLICY: TraceIntoBendPolicy = 'minimal' - def __init__( - self, - strategy: RouteTieBreakStrategy = DEFAULT_STRATEGY, - *, - bend_policy: TraceIntoBendPolicy = DEFAULT_TRACE_INTO_BEND_POLICY, - ) -> None: + def __init__(self, strategy: RouteTieBreakStrategy = DEFAULT_STRATEGY) -> None: self.strategy = validate_strategy(strategy) - self.bend_policy = validate_trace_into_bend_policy(bend_policy) def resolve_strategy(self, strategy: RouteTieBreakStrategy | str | None) -> RouteTieBreakStrategy: """Return the per-route strategy or the planner default.""" @@ -1086,56 +1067,9 @@ class RoutingPlanner: return getattr(self, 'strategy', self.DEFAULT_STRATEGY) return validate_strategy(strategy) - def resolve_trace_into_bend_policy( - self, - bend_policy: TraceIntoBendPolicy | str | None, - ) -> TraceIntoBendPolicy: - """Return the per-route trace-into bend policy or the planner default.""" - if bend_policy is None: - return getattr(self, 'bend_policy', self.DEFAULT_TRACE_INTO_BEND_POLICY) - return validate_trace_into_bend_policy(bend_policy) - - def validate_plan_options( - self, - plan_options: Mapping[str, Any] | None, - *, - allow_bend_policy: bool = False, - ) -> dict[str, Any]: - """Copy and validate the default planner's per-route option mapping.""" - if plan_options is None: - return {} - try: - options = dict(plan_options) - except (TypeError, ValueError) as err: - raise BuildError('plan_options must be a mapping with string keys') from err - nonstring = [key for key in options if not isinstance(key, str)] - if nonstring: - raise BuildError(f'plan_options keys must be strings; got {nonstring!r}') - supported = {'strategy'} - if allow_bend_policy: - supported.add('bend_policy') - unsupported = sorted(options.keys() - supported) - if unsupported: - raise BuildError( - f'RoutingPlanner plan_options contains unsupported keys: {", ".join(unsupported)}' - ) - if options.get('strategy') is not None: - options['strategy'] = validate_strategy(options['strategy']) - if options.get('bend_policy') is not None: - options['bend_policy'] = validate_trace_into_bend_policy(options['bend_policy']) - return options - - def trace_into_bend_bands( - self, - family: PrimitiveKind, - *, - bend_policy: TraceIntoBendPolicy | str | None = None, - ) -> tuple[tuple[int, int], ...]: - """Return trace_into bend-budget bands for the requested detour policy.""" + def trace_into_bend_bands(self, family: PrimitiveKind) -> tuple[tuple[int, int], ...]: + """Return non-overlapping trace_into bend-budget bands for staged solving.""" max_bends = self.TRACE_INTO_MAX_BENDS - if self.resolve_trace_into_bend_policy(bend_policy) == 'minimal': - required_bends = 0 if family == 'straight' else 1 if family == 'bend' else 2 - return ((required_bends, required_bends),) if required_bends <= max_bends else () if family == 'bend': return tuple(band for band in ((1, 1), (3, 3)) if band[1] <= max_bends) bands: list[tuple[int, int]] = [] @@ -1352,13 +1286,11 @@ class RoutingPlanner: length: float | None = None, *, spacing: float | ArrayLike | None = None, - plan_options: Mapping[str, Any] | None = None, + strategy: RouteTieBreakStrategy | str | None = None, tool_options: Mapping[str, Any] | None = None, **bounds: Any, ) -> PreparedRouteResult: """Plan straight or single-bend traces, including `each` and bundle-bound modes.""" - plan_opts = self.validate_plan_options(plan_options) - strategy = plan_opts.get('strategy') route_bounds = dict(bounds) request_details = {'ccw': ccw, **{ key: value for key, value in route_bounds.items() if value is not None @@ -1367,8 +1299,8 @@ class RoutingPlanner: request_details['length'] = length if spacing is not None: request_details['spacing'] = spacing - if plan_opts: - request_details['plan_options'] = dict(plan_opts) + if strategy is not None: + request_details['strategy'] = strategy if tool_options: request_details['tool_options'] = dict(tool_options) operation: RouteOperation = 'trace' @@ -1484,21 +1416,19 @@ class RoutingPlanner: ccw: SupportsBool | None, *, spacing: float | ArrayLike | None = None, - plan_options: Mapping[str, Any] | None = None, + strategy: RouteTieBreakStrategy | str | None = None, tool_options: Mapping[str, Any] | None = None, **bounds: Any, ) -> PreparedRouteResult: """Plan `trace_to()` by resolving positional targets or delegating to `trace()` modes.""" - plan_opts = self.validate_plan_options(plan_options) - strategy = plan_opts.get('strategy') route_bounds = dict(bounds) request_details = {'ccw': ccw, **{ key: value for key, value in route_bounds.items() if value is not None }} if spacing is not None: request_details['spacing'] = spacing - if plan_opts: - request_details['plan_options'] = dict(plan_opts) + if strategy is not None: + request_details['strategy'] = strategy if tool_options: request_details['tool_options'] = dict(tool_options) operation: RouteOperation = 'trace_to' @@ -1574,13 +1504,11 @@ class RoutingPlanner: length: float | None = None, *, spacing: float | ArrayLike | None = None, - plan_options: Mapping[str, Any] | None = None, + strategy: RouteTieBreakStrategy | str | None = None, tool_options: Mapping[str, Any] | None = None, **bounds: Any, ) -> PreparedRouteResult: """Plan S-bend routes for single ports or spaced bundles.""" - plan_opts = self.validate_plan_options(plan_options) - strategy = plan_opts.get('strategy') offset = planner_bounds.finite_scalar(offset, 'offset') request_details = {'offset': offset, **{ key: value for key, value in bounds.items() if value is not None @@ -1589,8 +1517,8 @@ class RoutingPlanner: request_details['length'] = length if spacing is not None: request_details['spacing'] = spacing - if plan_opts: - request_details['plan_options'] = dict(plan_opts) + if strategy is not None: + request_details['strategy'] = strategy if tool_options: request_details['tool_options'] = dict(tool_options) operation: RouteOperation = 'jog' @@ -1652,13 +1580,11 @@ class RoutingPlanner: length: float | None = None, *, spacing: float | ArrayLike | None = None, - plan_options: Mapping[str, Any] | None = None, + strategy: RouteTieBreakStrategy | str | None = None, tool_options: Mapping[str, Any] | None = None, **bounds: Any, ) -> PreparedRouteResult: """Plan U-turn routes for single ports or spaced bundles.""" - plan_opts = self.validate_plan_options(plan_options) - strategy = plan_opts.get('strategy') offset = planner_bounds.finite_scalar(offset, 'offset') route_bounds = dict(bounds) request_details = {'offset': offset, **{ @@ -1668,8 +1594,8 @@ class RoutingPlanner: request_details['length'] = length if spacing is not None: request_details['spacing'] = spacing - if plan_opts: - request_details['plan_options'] = dict(plan_opts) + if strategy is not None: + request_details['strategy'] = strategy if tool_options: request_details['tool_options'] = dict(tool_options) operation: RouteOperation = 'uturn' @@ -1796,14 +1722,10 @@ class RoutingPlanner: out_ptype: str | None, plug_destination: bool, thru: str | None, - plan_options: Mapping[str, Any] | None = None, + strategy: RouteTieBreakStrategy | str | None = None, tool_options: Mapping[str, Any] | None = None, ) -> PreparedRouteResult: """Plan a bounded route from one source port into a destination port.""" - plan_opts = self.validate_plan_options(plan_options, allow_bend_policy=True) - strategy = plan_opts.get('strategy') - bend_policy = plan_opts.get('bend_policy') - resolved_bend_policy = self.resolve_trace_into_bend_policy(bend_policy) if out_ptype is None: out_ptype = port_dst.ptype if context_src.port.rotation is None or port_dst.rotation is None: @@ -1812,8 +1734,6 @@ class RoutingPlanner: desired.rotation = port_dst.rotation - pi desired.ptype = out_ptype family, length, jog, ccw = self.trace_into_spec(context_src.port, desired) - bend_bands = self.trace_into_bend_bands(family, bend_policy=resolved_bend_policy) - max_bends = max((band[1] for band in bend_bands), default=0) request = self.solver_request( family, context_src, @@ -1821,7 +1741,7 @@ class RoutingPlanner: jog=jog, ccw=ccw, constrain_jog=family == 'bend', - max_bends=max_bends, + max_bends=self.TRACE_INTO_MAX_BENDS, strategy=strategy, tool_options=tool_options, out_ptype=out_ptype, @@ -1829,7 +1749,7 @@ class RoutingPlanner: solver = self.solver_for_request(request) candidate = None last_error: Exception | None = None - for min_bends, max_bends in bend_bands: + for min_bends, max_bends in self.trace_into_bend_bands(family): try: candidate = solver.solve(min_bends=min_bends, max_bends=max_bends) break diff --git a/masque/test/test_autotool_planning.py b/masque/test/test_autotool_planning.py index 969674b..33cbee7 100644 --- a/masque/test/test_autotool_planning.py +++ b/masque/test/test_autotool_planning.py @@ -996,13 +996,7 @@ def test_autotool_strategy_orders_main_steps_across_adapters(jog: float) -> None for strategy in ('straight_first', 'turn_first'): pather = Pather(library, tools=tool, render='deferred') pather.ports['A'] = Port((0, 0), 0, ptype=primary) - pather.jog( - 'A', - jog, - length=route_length, - out_ptype=primary, - plan_options={'strategy': strategy}, - ) + pather.jog('A', jog, length=route_length, out_ptype=primary, strategy=strategy) selected_kinds[strategy] = [step.kind for step in pather._paths['A']] assert selected_kinds['straight_first'] == ['straight', 'straight', 'straight', 's'] diff --git a/masque/test/test_build_library.py b/masque/test/test_build_library.py index 27393af..e020912 100644 --- a/masque/test/test_build_library.py +++ b/masque/test/test_build_library.py @@ -1,10 +1,8 @@ from collections.abc import Iterator -from types import FunctionType -import traceback import pytest -from ..builder import Pather, PathTool, RouteError +from ..builder import Pather from ..error import BuildError, LibraryError from ..library import ( INameView, @@ -30,17 +28,6 @@ def _owned_by(report: BuildReport, owner: str) -> set[str]: } -def _external_failing_route_recipe(lib: ILibrary) -> Pattern: - pather = Pather( - lib, - ports={'A': Port((0, 0), rotation=0, ptype='wire')}, - tools=PathTool(layer='M1', width=2, ptype='wire'), - render='deferred', - ) - pather.ccw('A', 10, out_ptype='optical') - return pather.pattern - - class _MetadataSource(ILibraryView): def __init__(self, mapping: dict[str, Pattern], child_graph: dict[str, set[str]]) -> None: self.mapping = mapping @@ -276,34 +263,6 @@ def test_build_library_validate_is_retryable_after_failure() -> None: assert report.dependency_graph["parent"] == frozenset({"child"}) -def test_build_library_error_preserves_route_call_site() -> None: - builder = LibraryBuilder() - external_recipe = FunctionType( - _external_failing_route_recipe.__code__.replace(co_filename='/project/user_recipe.py'), - globals(), - ) - builder.cells.top = cell(external_recipe)(builder.library) - - with pytest.raises(BuildError) as exc_info: - builder.validate() - - message = str(exc_info.value) - assert 'Cause: Unable to plan trace_to route' in message - assert 'Stack trace:' not in message - - route_error = exc_info.value.__cause__ - assert isinstance(route_error, RouteError) - assert route_error.__traceback__ is not None - route_frames = traceback.extract_tb(route_error.__traceback__) - assert sum(frame.filename == '/project/user_recipe.py' for frame in route_frames) == 1 - assert not any('/builder/planner/' in frame.filename for frame in route_frames) - assert route_error.__cause__ is not None - assert any('/builder/planner/' in frame.filename for frame in route_error._call_stack) - - native = ''.join(traceback.format_exception(exc_info.value)) - assert native.count('/project/user_recipe.py') == 1 - - def test_build_library_depends_on_supports_hidden_dependencies_for_partial_validation() -> None: builder = LibraryBuilder() builder["child"] = Pattern() diff --git a/masque/test/test_builder.py b/masque/test/test_builder.py index 55e1b21..4bf5c53 100644 --- a/masque/test/test_builder.py +++ b/masque/test/test_builder.py @@ -60,10 +60,6 @@ def test_route_failure_details_enforces_minimum_status_invariants() -> None: ) -def test_plain_build_error_does_not_include_stacktrace() -> None: - assert str(BuildError('plain builder failure')) == 'plain builder failure' - - def test_builder_init() -> None: lib = Library() b = Pather(lib, name="mypat") diff --git a/masque/test/test_pather_constraints.py b/masque/test/test_pather_constraints.py index 58ee5f6..30e3762 100644 --- a/masque/test/test_pather_constraints.py +++ b/masque/test/test_pather_constraints.py @@ -1,8 +1,6 @@ from collections.abc import Sequence from typing import Any, Literal, Never -from types import FunctionType import inspect -import traceback import pytest import numpy @@ -36,18 +34,6 @@ class PlanningOnlyTool(Tool): return tree -def _external_failing_ccw(pather: Pather) -> None: - pather.ccw('A', 10, out_ptype='optical') - - -def _external_failing_trace_to(pather: Pather) -> None: - pather.trace_to('A', True, length=10, out_ptype='optical') - - -def _external_failing_portpather_ccw(pather: Pather) -> None: - pather.at('A').ccw(10, out_ptype='optical') - - class FirstPortOnlyTraceTool(PlanningOnlyTool): def __init__(self) -> None: self.render_calls = 0 @@ -284,90 +270,6 @@ def test_route_error_reports_no_route_at_any_length() -> None: assert 'no legal route exists at any length' in str(exc_info.value) -def test_route_error_reports_external_pather_call_site() -> None: - p = Pather( - Library(), - ports={'A': Port((0, 0), rotation=0, ptype='wire')}, - tools=PathTool(layer='M1', width=2, ptype='wire'), - render='deferred', - ) - external_call = FunctionType( - _external_failing_ccw.__code__.replace(co_filename='/project/user_layout.py'), - globals(), - ) - - with pytest.raises(RouteError) as exc_info: - external_call(p) - - message = str(exc_info.value) - assert 'Stack trace:' not in message - - route_error = exc_info.value - native_frames = traceback.extract_tb(route_error.__traceback__) - assert native_frames - assert [frame.name for frame in native_frames if frame.filename.endswith('/builder/pather.py')] == [ - 'ccw', - 'bend', - ] - assert not any('/builder/planner/' in frame.filename for frame in native_frames) - - native = ''.join(traceback.format_exception(route_error)) - assert native.count('/project/user_layout.py') == 1 - assert '\nStack trace:\n' not in native - assert route_error.__cause__ is not None - - assert isinstance(route_error._call_stack, tuple) - assert sum(frame.filename == '/project/user_layout.py' for frame in route_error._call_stack) == 1 - assert any(frame.filename.endswith('/builder/pather.py') for frame in route_error._call_stack) - assert any('/builder/planner/' in frame.filename for frame in route_error._call_stack) - - -def test_route_error_direct_trace_to_starts_native_traceback_at_caller() -> None: - p = Pather( - Library(), - ports={'A': Port((0, 0), rotation=0, ptype='wire')}, - tools=PathTool(layer='M1', width=2, ptype='wire'), - render='deferred', - ) - external_call = FunctionType( - _external_failing_trace_to.__code__.replace(co_filename='/project/direct_route.py'), - globals(), - ) - - with pytest.raises(RouteError) as exc_info: - external_call(p) - - frames = traceback.extract_tb(exc_info.value.__traceback__) - assert sum(frame.filename == '/project/direct_route.py' for frame in frames) == 1 - assert not any(frame.filename.endswith('/builder/pather.py') for frame in frames) - assert not any('/builder/planner/' in frame.filename for frame in frames) - - -def test_route_error_portpather_keeps_only_forwarding_frames() -> None: - p = Pather( - Library(), - ports={'A': Port((0, 0), rotation=0, ptype='wire')}, - tools=PathTool(layer='M1', width=2, ptype='wire'), - render='deferred', - ) - external_call = FunctionType( - _external_failing_portpather_ccw.__code__.replace(co_filename='/project/selected_route.py'), - globals(), - ) - - with pytest.raises(RouteError) as exc_info: - external_call(p) - - frames = traceback.extract_tb(exc_info.value.__traceback__) - assert sum(frame.filename == '/project/selected_route.py' for frame in frames) == 1 - assert [frame.name for frame in frames if frame.filename.endswith('/builder/pather.py')] == [ - 'ccw', - 'bend', - 'trace_to', - ] - assert not any('/builder/planner/' in frame.filename for frame in frames) - - def test_route_error_reports_failed_minimum_diagnosis(monkeypatch: pytest.MonkeyPatch) -> None: planner = RoutingPlanner() context = RoutePortContext( @@ -1084,16 +986,6 @@ def test_routing_entry_points_have_no_catch_all_kwargs(route_type: type, name: s assert all(parameter.kind is not inspect.Parameter.VAR_KEYWORD for parameter in parameters) -@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_use_generic_plan_options(route_type: type, name: str) -> None: - parameters = inspect.signature(getattr(route_type, name)).parameters - - assert 'plan_options' in parameters - assert 'strategy' not in parameters - assert 'bend_policy' not in parameters - - def test_routing_typo_fails_before_tool_lookup() -> None: tool = RequestCountingTool() p = Pather(Library(), tools=tool, render='deferred') @@ -1115,41 +1007,6 @@ def test_tool_options_reject_invalid_or_reserved_keys(tool_options: dict[Any, An assert tool.offer_calls == 0 -@pytest.mark.parametrize('plan_options', [7, {1: 'bad'}]) -def test_plan_options_reject_invalid_mapping_shape(plan_options: Any) -> None: - tool = RequestCountingTool() - p = Pather(Library(), tools=tool, render='deferred') - - with pytest.raises(BuildError, match='plan_options'): - p.trace('A', None, plan_options=plan_options) - - assert tool.offer_calls == 0 - - -@pytest.mark.parametrize( - 'plan_options', - [ - {'bend_policy': 'flexible'}, - {'unknown': True}, - ], -) -def test_default_planner_rejects_unsupported_plan_options_before_tool_lookup( - plan_options: dict[str, 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='unsupported keys'): - p.trace('A', None, length=1, plan_options=plan_options) - - assert tool.offer_calls == 0 - - @pytest.mark.parametrize( 'operation', [ diff --git a/masque/test/test_pather_core.py b/masque/test/test_pather_core.py index af068b8..93f874c 100644 --- a/masque/test/test_pather_core.py +++ b/masque/test/test_pather_core.py @@ -93,41 +93,6 @@ def test_pather_accepts_and_reuses_planner_instance() -> None: assert planner.trace_to_calls == 2 -def test_pather_copies_and_forwards_plan_options_to_planner() -> None: - class RecordingPlanner(RoutingPlanner): - def __init__(self) -> None: - super().__init__() - self.received_plan_options: Any = None - - def plan_trace_to_route( - self, - *args: Any, - plan_options: Any = None, - **kwargs: Any, - ) -> Any: - self.received_plan_options = plan_options - return super().plan_trace_to_route( - *args, - plan_options=plan_options, - **kwargs, - ) - - planner = RecordingPlanner() - p = Pather( - Library(), - tools=PathTool(layer=(1, 0), width=1), - render='deferred', - planner=planner, - ) - p.ports['A'] = Port((0, 0), rotation=0) - supplied = {'strategy': 'turn_first'} - - p.straight('A', 1, plan_options=supplied) - - assert planner.received_plan_options == supplied - assert planner.received_plan_options is not supplied - - def test_port_tool_policy_and_portpather_selection_follow_names() -> None: default_tool = PathTool(layer=(1, 0), width=1, ptype='wire') named_tool = PathTool(layer=(2, 0), width=1, ptype='wire') diff --git a/masque/test/test_pather_primitive_offers.py b/masque/test/test_pather_primitive_offers.py index 0a6abe3..626779c 100644 --- a/masque/test/test_pather_primitive_offers.py +++ b/masque/test/test_pather_primitive_offers.py @@ -732,7 +732,7 @@ def test_pather_route_strategy_uses_planner_default() -> None: 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, plan_options={'strategy': 'straight_first'}) + pather.jog('A', 4, length=10, strategy='straight_first') assert [step.data['kind'] for step in pather._paths['A']] == ['straight', 's'] @@ -740,21 +740,15 @@ def test_pather_route_strategy_per_route_overrides_planner_default() -> None: def test_pather_route_strategy_per_route_can_request_turn_first() -> None: pather, _tool = pather_with_strategy_tool() - pather.jog('A', 4, length=10, plan_options={'strategy': 'turn_first'}) + pather.jog('A', 4, length=10, strategy='turn_first') assert [step.data['kind'] for step in pather._paths['A']] == ['s', 'straight'] -def test_pather_plan_options_are_not_forwarded_to_tool() -> None: +def test_pather_route_strategy_is_not_forwarded_to_tool() -> None: pather, tool = pather_with_strategy_tool() - pather.jog( - 'A', - 4, - length=10, - plan_options={'strategy': 'turn_first'}, - tool_options={'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) @@ -768,7 +762,7 @@ def test_pather_route_strategy_rejects_invalid_values() -> None: pather, _tool = pather_with_strategy_tool() with pytest.raises(BuildError, match='Invalid route strategy'): - pather.jog('A', 4, length=10, plan_options={'strategy': 'sideways'}) + pather.jog('A', 4, length=10, strategy='sideways') def test_solver_rejects_rotation_impossible_candidates_before_parameter_solving() -> None: diff --git a/masque/test/test_pather_trace_into.py b/masque/test/test_pather_trace_into.py index 1e86ded..48ab125 100644 --- a/masque/test/test_pather_trace_into.py +++ b/masque/test/test_pather_trace_into.py @@ -113,78 +113,6 @@ 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, - plan_options={'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, - plan_options={'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(), @@ -377,7 +305,6 @@ 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 @@ -413,15 +340,7 @@ 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, - plan_options={'bend_policy': 'flexible'}, - ) + planner.plan_trace_into(context, 'dst', dst, out_ptype=None, plug_destination=True, thru=None) assert planner.solver.attempts == attempts assert planner.solver_requests == 1 @@ -432,15 +351,7 @@ 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, - plan_options={'bend_policy': 'flexible'}, - ) + planner.plan_trace_into(context, 'dst', Port((-10, 0), rotation=pi, ptype='wire'), out_ptype=None, plug_destination=True, thru=None) assert planner.solver.attempts == [(0, 2)] assert planner.solver_requests == 1 @@ -450,124 +361,8 @@ def test_trace_into_bend_bands_respect_max_bends() -> None: class OneBendPlanner(RoutingPlanner): TRACE_INTO_MAX_BENDS = 1 - planner = OneBendPlanner(bend_policy='flexible') + 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),) - - -@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, - plan_options={'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', plan_options={'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, - plan_options={'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] - - pather = Pather( - Library(), - tools=PathTool(layer='M1', width=1, ptype='wire'), - 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='Invalid trace_into bend policy'): - pather.trace_into('src', 'dst', plan_options={'bend_policy': 'sideways'})