[pather / planner] pass planner options via plan_options, not strategy/bend_policy
This commit is contained in:
parent
91a5d63b57
commit
ce7463e57c
8 changed files with 280 additions and 115 deletions
26
MIGRATION.md
26
MIGRATION.md
|
|
@ -349,7 +349,7 @@ class MyTool(Tool):
|
|||
```
|
||||
|
||||
Routing entry points now name every supported route argument explicitly.
|
||||
Custom per-route planning values must be placed under `tool_options`:
|
||||
Custom per-route Tool values must be placed under `tool_options`:
|
||||
|
||||
```python
|
||||
pather.jog('A', 4, length=10, tool_options={'process_corner': 'slow'})
|
||||
|
|
@ -453,14 +453,22 @@ 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 `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 route `strategy` affects
|
||||
only that final discovery-order tie-break.
|
||||
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'})
|
||||
```
|
||||
|
||||
Explicit-length `jog()` routes may also be satisfied by composing a straight
|
||||
primitive before or after an omitted-length native S primitive. `uturn()` routes
|
||||
|
|
|
|||
|
|
@ -8,9 +8,11 @@ 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. 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.
|
||||
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.
|
||||
|
||||
Routing is split into four ownership phases:
|
||||
- snapshot: `Pather` resolves the active Tool for each requested port and
|
||||
|
|
@ -78,11 +80,7 @@ from .planner.interface import (
|
|||
route_failure_policy,
|
||||
)
|
||||
from .error import RouteFailurePolicy, ToolContractError
|
||||
from .planner import (
|
||||
RouteTieBreakStrategy,
|
||||
TraceIntoBendPolicy,
|
||||
RoutingPlanner,
|
||||
)
|
||||
from .planner import RoutingPlanner
|
||||
from .planner.bounds import resolved_position_bound
|
||||
from .logging import PatherLogger
|
||||
from ._tolerances import angles_equal, array_close
|
||||
|
|
@ -129,6 +127,20 @@ 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
|
||||
|
|
@ -627,7 +639,7 @@ class Pather(PortList):
|
|||
length: float | None = None,
|
||||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
plan_options: Mapping[str, Any] | None = None,
|
||||
out_ptype: str | None = None,
|
||||
each: float | None = None,
|
||||
set_rotation: float | None = None,
|
||||
|
|
@ -653,9 +665,8 @@ 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. `strategy`
|
||||
controls straight-first vs turn-first ordering only after cost and
|
||||
structural tie-breakers.
|
||||
when provided, constrains only the final route endpoint. Planner-specific
|
||||
per-route settings belong in `plan_options`.
|
||||
|
||||
`spacing` and `set_rotation` are only valid when using a bundle bound.
|
||||
"""
|
||||
|
|
@ -673,18 +684,19 @@ class Pather(PortList):
|
|||
ymax=ymax,
|
||||
min_past_furthest=min_past_furthest,
|
||||
)
|
||||
options = _validated_tool_options(tool_options)
|
||||
plan_opts = _validated_plan_options(plan_options)
|
||||
tool_opts = _validated_tool_options(tool_options)
|
||||
with self._logger.log_operation(
|
||||
self, 'trace', portspec, ccw=ccw, length=length, spacing=spacing,
|
||||
strategy=strategy, tool_options=options, **bounds,
|
||||
plan_options=plan_opts, tool_options=tool_opts, **bounds,
|
||||
):
|
||||
if isinstance(portspec, str):
|
||||
portspec = [portspec]
|
||||
contexts = self._route_contexts(portspec)
|
||||
try:
|
||||
result = self.planner.plan_trace_route(
|
||||
contexts, ccw, length, spacing=spacing, strategy=strategy,
|
||||
tool_options=options, **bounds,
|
||||
contexts, ccw, length, spacing=spacing, plan_options=plan_opts,
|
||||
tool_options=tool_opts, **bounds,
|
||||
)
|
||||
except (BuildError, NotImplementedError) as err:
|
||||
if not self._dead or route_failure_policy(err) is RouteFailurePolicy.FATAL:
|
||||
|
|
@ -727,7 +739,7 @@ class Pather(PortList):
|
|||
*,
|
||||
length: float | None = None,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
plan_options: Mapping[str, Any] | None = None,
|
||||
out_ptype: str | None = None,
|
||||
each: float | None = None,
|
||||
set_rotation: float | None = None,
|
||||
|
|
@ -756,8 +768,7 @@ class Pather(PortList):
|
|||
|
||||
With no positional or bundle bound, single-port `trace_to()` uses the
|
||||
same omitted minimum-length primitive-offer behavior as `trace()`.
|
||||
`strategy` controls straight-first vs turn-first ordering only after
|
||||
cost and structural tie-breakers.
|
||||
Planner-specific per-route settings belong in `plan_options`.
|
||||
"""
|
||||
bounds = _present_route_args(
|
||||
length=length,
|
||||
|
|
@ -779,18 +790,19 @@ class Pather(PortList):
|
|||
ymax=ymax,
|
||||
min_past_furthest=min_past_furthest,
|
||||
)
|
||||
options = _validated_tool_options(tool_options)
|
||||
plan_opts = _validated_plan_options(plan_options)
|
||||
tool_opts = _validated_tool_options(tool_options)
|
||||
with self._logger.log_operation(
|
||||
self, 'trace_to', portspec, ccw=ccw, spacing=spacing,
|
||||
strategy=strategy, tool_options=options, **bounds,
|
||||
plan_options=plan_opts, tool_options=tool_opts, **bounds,
|
||||
):
|
||||
if isinstance(portspec, str):
|
||||
portspec = [portspec]
|
||||
contexts = self._route_contexts(portspec)
|
||||
try:
|
||||
result = self.planner.plan_trace_to_route(
|
||||
contexts, ccw, spacing=spacing, strategy=strategy,
|
||||
tool_options=options, **bounds,
|
||||
contexts, ccw, spacing=spacing, plan_options=plan_opts,
|
||||
tool_options=tool_opts, **bounds,
|
||||
)
|
||||
except (BuildError, NotImplementedError) as err:
|
||||
if (
|
||||
|
|
@ -830,7 +842,7 @@ class Pather(PortList):
|
|||
length: float | None = None,
|
||||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
plan_options: Mapping[str, Any] | None = None,
|
||||
out_ptype: str | None = None,
|
||||
each: float | None = None,
|
||||
set_rotation: float | None = None,
|
||||
|
|
@ -851,7 +863,7 @@ class Pather(PortList):
|
|||
tool_options: Mapping[str, Any] | None = None,
|
||||
) -> Self:
|
||||
return self.trace_to(
|
||||
portspec, None, length=length, spacing=spacing, strategy=strategy,
|
||||
portspec, None, length=length, spacing=spacing, plan_options=plan_options,
|
||||
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,
|
||||
|
|
@ -866,7 +878,7 @@ class Pather(PortList):
|
|||
length: float | None = None,
|
||||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
plan_options: Mapping[str, Any] | None = None,
|
||||
out_ptype: str | None = None,
|
||||
each: float | None = None,
|
||||
set_rotation: float | None = None,
|
||||
|
|
@ -887,7 +899,7 @@ class Pather(PortList):
|
|||
tool_options: Mapping[str, Any] | None = None,
|
||||
) -> Self:
|
||||
return self.trace_to(
|
||||
portspec, ccw, length=length, spacing=spacing, strategy=strategy,
|
||||
portspec, ccw, length=length, spacing=spacing, plan_options=plan_options,
|
||||
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,
|
||||
|
|
@ -901,7 +913,7 @@ class Pather(PortList):
|
|||
length: float | None = None,
|
||||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
plan_options: Mapping[str, Any] | None = None,
|
||||
out_ptype: str | None = None,
|
||||
each: float | None = None,
|
||||
set_rotation: float | None = None,
|
||||
|
|
@ -922,7 +934,7 @@ class Pather(PortList):
|
|||
tool_options: Mapping[str, Any] | None = None,
|
||||
) -> Self:
|
||||
return self.bend(
|
||||
portspec, True, length, spacing=spacing, strategy=strategy, out_ptype=out_ptype,
|
||||
portspec, True, length, spacing=spacing, plan_options=plan_options, 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,
|
||||
|
|
@ -934,7 +946,7 @@ class Pather(PortList):
|
|||
length: float | None = None,
|
||||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
plan_options: Mapping[str, Any] | None = None,
|
||||
out_ptype: str | None = None,
|
||||
each: float | None = None,
|
||||
set_rotation: float | None = None,
|
||||
|
|
@ -955,7 +967,7 @@ class Pather(PortList):
|
|||
tool_options: Mapping[str, Any] | None = None,
|
||||
) -> Self:
|
||||
return self.bend(
|
||||
portspec, False, length, spacing=spacing, strategy=strategy, out_ptype=out_ptype,
|
||||
portspec, False, length, spacing=spacing, plan_options=plan_options, 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,
|
||||
|
|
@ -968,7 +980,7 @@ class Pather(PortList):
|
|||
length: float | None = None,
|
||||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
plan_options: Mapping[str, Any] | None = None,
|
||||
out_ptype: str | None = None,
|
||||
p: float | None = None,
|
||||
pos: float | None = None,
|
||||
|
|
@ -990,23 +1002,23 @@ 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. `strategy`
|
||||
controls straight-first vs S-first ordering only after cost and
|
||||
structural tie-breakers.
|
||||
provided, constrains only each final route endpoint. Planner-specific
|
||||
per-route settings belong in `plan_options`.
|
||||
"""
|
||||
bounds = _present_route_args(out_ptype=out_ptype, p=p, pos=pos, position=position, x=x, y=y)
|
||||
options = _validated_tool_options(tool_options)
|
||||
plan_opts = _validated_plan_options(plan_options)
|
||||
tool_opts = _validated_tool_options(tool_options)
|
||||
with self._logger.log_operation(
|
||||
self, 'jog', portspec, offset=offset, length=length, spacing=spacing,
|
||||
strategy=strategy, tool_options=options, **bounds,
|
||||
plan_options=plan_opts, tool_options=tool_opts, **bounds,
|
||||
):
|
||||
if isinstance(portspec, str):
|
||||
portspec = [portspec]
|
||||
contexts = self._route_contexts(portspec)
|
||||
try:
|
||||
result = self.planner.plan_jog_route(
|
||||
contexts, offset, length, spacing=spacing, strategy=strategy,
|
||||
tool_options=options, **bounds,
|
||||
contexts, offset, length, spacing=spacing, plan_options=plan_opts,
|
||||
tool_options=tool_opts, **bounds,
|
||||
)
|
||||
except (BuildError, NotImplementedError) as err:
|
||||
if (
|
||||
|
|
@ -1052,7 +1064,7 @@ class Pather(PortList):
|
|||
length: float | None = None,
|
||||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
plan_options: Mapping[str, Any] | None = None,
|
||||
out_ptype: str | None = None,
|
||||
tool_options: Mapping[str, Any] | None = None,
|
||||
) -> Self:
|
||||
|
|
@ -1067,23 +1079,23 @@ 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. `strategy`
|
||||
controls straight-first vs U-first ordering only after cost and
|
||||
structural tie-breakers.
|
||||
when provided, constrains only each final route endpoint. Planner-specific
|
||||
per-route settings belong in `plan_options`.
|
||||
"""
|
||||
bounds = _present_route_args(out_ptype=out_ptype)
|
||||
options = _validated_tool_options(tool_options)
|
||||
plan_opts = _validated_plan_options(plan_options)
|
||||
tool_opts = _validated_tool_options(tool_options)
|
||||
with self._logger.log_operation(
|
||||
self, 'uturn', portspec, offset=offset, length=length, spacing=spacing,
|
||||
strategy=strategy, tool_options=options, **bounds,
|
||||
plan_options=plan_opts, tool_options=tool_opts, **bounds,
|
||||
):
|
||||
if isinstance(portspec, str):
|
||||
portspec = [portspec]
|
||||
contexts = self._route_contexts(portspec)
|
||||
try:
|
||||
result = self.planner.plan_uturn_route(
|
||||
contexts, offset, length, spacing=spacing, strategy=strategy,
|
||||
tool_options=options, **bounds,
|
||||
contexts, offset, length, spacing=spacing, plan_options=plan_opts,
|
||||
tool_options=tool_opts, **bounds,
|
||||
)
|
||||
except (BuildError, NotImplementedError) as err:
|
||||
if (
|
||||
|
|
@ -1116,8 +1128,7 @@ class Pather(PortList):
|
|||
out_ptype: str | None = None,
|
||||
plug_destination: bool = True,
|
||||
thru: str | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
bend_policy: TraceIntoBendPolicy | str | None = None,
|
||||
plan_options: Mapping[str, Any] | None = None,
|
||||
tool_options: Mapping[str, Any] | None = None,
|
||||
) -> Self:
|
||||
"""
|
||||
|
|
@ -1129,14 +1140,14 @@ class Pather(PortList):
|
|||
loop-like fallback routes without inspecting primitive geometry. Ptype
|
||||
adapters do not consume this bend budget.
|
||||
|
||||
Set `bend_policy='flexible'` to search bounded primitive-offer routes
|
||||
with up to four bend roles. Bend-family requests try one-bend routes
|
||||
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. `strategy` therefore affects only
|
||||
otherwise tied candidates.
|
||||
deterministic discovery order. The default planner's `strategy` option
|
||||
therefore affects only otherwise tied candidates.
|
||||
Custom planning options may be supplied through `tool_options`; they
|
||||
are forwarded only to primitive offer generation.
|
||||
|
||||
|
|
@ -1147,7 +1158,8 @@ class Pather(PortList):
|
|||
mutated; failures during selected-route execution, including primitive
|
||||
commit, plug/thru application, or render, may leave partial output.
|
||||
"""
|
||||
options = _validated_tool_options(tool_options)
|
||||
plan_opts = _validated_plan_options(plan_options)
|
||||
tool_opts = _validated_tool_options(tool_options)
|
||||
with self._logger.log_operation(
|
||||
self,
|
||||
'trace_into',
|
||||
|
|
@ -1155,9 +1167,8 @@ class Pather(PortList):
|
|||
out_ptype=out_ptype,
|
||||
plug_destination=plug_destination,
|
||||
thru=thru,
|
||||
strategy=strategy,
|
||||
bend_policy=bend_policy,
|
||||
tool_options=options,
|
||||
plan_options=plan_opts,
|
||||
tool_options=tool_opts,
|
||||
):
|
||||
result = self.planner.plan_trace_into(
|
||||
self._route_context(portspec_src),
|
||||
|
|
@ -1166,9 +1177,8 @@ class Pather(PortList):
|
|||
out_ptype = out_ptype,
|
||||
plug_destination = plug_destination,
|
||||
thru = thru,
|
||||
strategy = strategy,
|
||||
bend_policy = bend_policy,
|
||||
tool_options = options,
|
||||
plan_options = plan_opts,
|
||||
tool_options = tool_opts,
|
||||
)
|
||||
self._apply_route_result(result)
|
||||
return self
|
||||
|
|
@ -1385,7 +1395,7 @@ class PortPather:
|
|||
length: float | None = None,
|
||||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
plan_options: Mapping[str, Any] | None = None,
|
||||
out_ptype: str | None = None,
|
||||
each: float | None = None,
|
||||
set_rotation: float | None = None,
|
||||
|
|
@ -1403,7 +1413,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, strategy=strategy, out_ptype=out_ptype,
|
||||
self.ports, ccw, length, spacing=spacing, plan_options=plan_options, 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,
|
||||
|
|
@ -1416,7 +1426,7 @@ class PortPather:
|
|||
*,
|
||||
length: float | None = None,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
plan_options: Mapping[str, Any] | None = None,
|
||||
out_ptype: str | None = None,
|
||||
each: float | None = None,
|
||||
set_rotation: float | None = None,
|
||||
|
|
@ -1439,7 +1449,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, strategy=strategy, out_ptype=out_ptype,
|
||||
self.ports, ccw, length=length, spacing=spacing, plan_options=plan_options, 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,
|
||||
|
|
@ -1451,7 +1461,7 @@ class PortPather:
|
|||
length: float | None = None,
|
||||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
plan_options: Mapping[str, Any] | None = None,
|
||||
out_ptype: str | None = None,
|
||||
each: float | None = None,
|
||||
set_rotation: float | None = None,
|
||||
|
|
@ -1472,7 +1482,7 @@ class PortPather:
|
|||
tool_options: Mapping[str, Any] | None = None,
|
||||
) -> Self:
|
||||
return self.trace_to(
|
||||
None, length=length, spacing=spacing, strategy=strategy, out_ptype=out_ptype,
|
||||
None, length=length, spacing=spacing, plan_options=plan_options, 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,
|
||||
|
|
@ -1484,7 +1494,7 @@ class PortPather:
|
|||
length: float | None = None,
|
||||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
plan_options: Mapping[str, Any] | None = None,
|
||||
out_ptype: str | None = None,
|
||||
each: float | None = None,
|
||||
set_rotation: float | None = None,
|
||||
|
|
@ -1505,7 +1515,7 @@ class PortPather:
|
|||
tool_options: Mapping[str, Any] | None = None,
|
||||
) -> Self:
|
||||
return self.trace_to(
|
||||
ccw, length=length, spacing=spacing, strategy=strategy, out_ptype=out_ptype,
|
||||
ccw, length=length, spacing=spacing, plan_options=plan_options, 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,
|
||||
|
|
@ -1516,7 +1526,7 @@ class PortPather:
|
|||
length: float | None = None,
|
||||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
plan_options: Mapping[str, Any] | None = None,
|
||||
out_ptype: str | None = None,
|
||||
each: float | None = None,
|
||||
set_rotation: float | None = None,
|
||||
|
|
@ -1537,7 +1547,7 @@ class PortPather:
|
|||
tool_options: Mapping[str, Any] | None = None,
|
||||
) -> Self:
|
||||
return self.bend(
|
||||
True, length, spacing=spacing, strategy=strategy, out_ptype=out_ptype,
|
||||
True, length, spacing=spacing, plan_options=plan_options, 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,
|
||||
|
|
@ -1548,7 +1558,7 @@ class PortPather:
|
|||
length: float | None = None,
|
||||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
plan_options: Mapping[str, Any] | None = None,
|
||||
out_ptype: str | None = None,
|
||||
each: float | None = None,
|
||||
set_rotation: float | None = None,
|
||||
|
|
@ -1569,7 +1579,7 @@ class PortPather:
|
|||
tool_options: Mapping[str, Any] | None = None,
|
||||
) -> Self:
|
||||
return self.bend(
|
||||
False, length, spacing=spacing, strategy=strategy, out_ptype=out_ptype,
|
||||
False, length, spacing=spacing, plan_options=plan_options, 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,
|
||||
|
|
@ -1581,7 +1591,7 @@ class PortPather:
|
|||
length: float | None = None,
|
||||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
plan_options: Mapping[str, Any] | None = None,
|
||||
out_ptype: str | None = None,
|
||||
p: float | None = None,
|
||||
pos: float | None = None,
|
||||
|
|
@ -1593,7 +1603,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, strategy=strategy, out_ptype=out_ptype,
|
||||
self.ports, offset, length, spacing=spacing, plan_options=plan_options, out_ptype=out_ptype,
|
||||
p=p, pos=pos, position=position, x=x, y=y, tool_options=tool_options,
|
||||
)
|
||||
return self
|
||||
|
|
@ -1604,14 +1614,14 @@ class PortPather:
|
|||
length: float | None = None,
|
||||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
plan_options: Mapping[str, Any] | 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, strategy=strategy,
|
||||
self.ports, offset, length, spacing=spacing, plan_options=plan_options,
|
||||
out_ptype=out_ptype, tool_options=tool_options,
|
||||
)
|
||||
return self
|
||||
|
|
@ -1623,14 +1633,13 @@ class PortPather:
|
|||
out_ptype: str | None = None,
|
||||
plug_destination: bool = True,
|
||||
thru: str | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
bend_policy: TraceIntoBendPolicy | str | None = None,
|
||||
plan_options: Mapping[str, Any] | 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, strategy=strategy, bend_policy=bend_policy, tool_options=tool_options,
|
||||
thru=thru, plan_options=plan_options, tool_options=tool_options,
|
||||
)
|
||||
return self
|
||||
|
||||
|
|
|
|||
|
|
@ -1095,6 +1095,36 @@ class RoutingPlanner:
|
|||
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,
|
||||
|
|
@ -1322,11 +1352,13 @@ class RoutingPlanner:
|
|||
length: float | None = None,
|
||||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
plan_options: Mapping[str, Any] | 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
|
||||
|
|
@ -1335,8 +1367,8 @@ class RoutingPlanner:
|
|||
request_details['length'] = length
|
||||
if spacing is not None:
|
||||
request_details['spacing'] = spacing
|
||||
if strategy is not None:
|
||||
request_details['strategy'] = strategy
|
||||
if plan_opts:
|
||||
request_details['plan_options'] = dict(plan_opts)
|
||||
if tool_options:
|
||||
request_details['tool_options'] = dict(tool_options)
|
||||
operation: RouteOperation = 'trace'
|
||||
|
|
@ -1452,19 +1484,21 @@ class RoutingPlanner:
|
|||
ccw: SupportsBool | None,
|
||||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
plan_options: Mapping[str, Any] | 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 strategy is not None:
|
||||
request_details['strategy'] = strategy
|
||||
if plan_opts:
|
||||
request_details['plan_options'] = dict(plan_opts)
|
||||
if tool_options:
|
||||
request_details['tool_options'] = dict(tool_options)
|
||||
operation: RouteOperation = 'trace_to'
|
||||
|
|
@ -1540,11 +1574,13 @@ class RoutingPlanner:
|
|||
length: float | None = None,
|
||||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
plan_options: Mapping[str, Any] | 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
|
||||
|
|
@ -1553,8 +1589,8 @@ class RoutingPlanner:
|
|||
request_details['length'] = length
|
||||
if spacing is not None:
|
||||
request_details['spacing'] = spacing
|
||||
if strategy is not None:
|
||||
request_details['strategy'] = strategy
|
||||
if plan_opts:
|
||||
request_details['plan_options'] = dict(plan_opts)
|
||||
if tool_options:
|
||||
request_details['tool_options'] = dict(tool_options)
|
||||
operation: RouteOperation = 'jog'
|
||||
|
|
@ -1616,11 +1652,13 @@ class RoutingPlanner:
|
|||
length: float | None = None,
|
||||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
plan_options: Mapping[str, Any] | 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, **{
|
||||
|
|
@ -1630,8 +1668,8 @@ class RoutingPlanner:
|
|||
request_details['length'] = length
|
||||
if spacing is not None:
|
||||
request_details['spacing'] = spacing
|
||||
if strategy is not None:
|
||||
request_details['strategy'] = strategy
|
||||
if plan_opts:
|
||||
request_details['plan_options'] = dict(plan_opts)
|
||||
if tool_options:
|
||||
request_details['tool_options'] = dict(tool_options)
|
||||
operation: RouteOperation = 'uturn'
|
||||
|
|
@ -1758,11 +1796,13 @@ class RoutingPlanner:
|
|||
out_ptype: str | None,
|
||||
plug_destination: bool,
|
||||
thru: str | None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
bend_policy: TraceIntoBendPolicy | str | None = None,
|
||||
plan_options: Mapping[str, Any] | 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
|
||||
|
|
|
|||
|
|
@ -996,7 +996,13 @@ 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, strategy=strategy)
|
||||
pather.jog(
|
||||
'A',
|
||||
jog,
|
||||
length=route_length,
|
||||
out_ptype=primary,
|
||||
plan_options={'strategy': strategy},
|
||||
)
|
||||
selected_kinds[strategy] = [step.kind for step in pather._paths['A']]
|
||||
|
||||
assert selected_kinds['straight_first'] == ['straight', 'straight', 'straight', 's']
|
||||
|
|
|
|||
|
|
@ -986,6 +986,16 @@ 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')
|
||||
|
|
@ -1007,6 +1017,41 @@ 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',
|
||||
[
|
||||
|
|
|
|||
|
|
@ -93,6 +93,41 @@ 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')
|
||||
|
|
|
|||
|
|
@ -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, strategy='straight_first')
|
||||
pather.jog('A', 4, length=10, plan_options={'strategy': 'straight_first'})
|
||||
|
||||
assert [step.data['kind'] for step in pather._paths['A']] == ['straight', 's']
|
||||
|
||||
|
|
@ -740,15 +740,21 @@ 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, strategy='turn_first')
|
||||
pather.jog('A', 4, length=10, plan_options={'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:
|
||||
def test_pather_plan_options_are_not_forwarded_to_tool() -> None:
|
||||
pather, tool = pather_with_strategy_tool()
|
||||
|
||||
pather.jog('A', 4, length=10, strategy='turn_first', tool_options={'marker': 'sentinel'})
|
||||
pather.jog(
|
||||
'A',
|
||||
4,
|
||||
length=10,
|
||||
plan_options={'strategy': 'turn_first'},
|
||||
tool_options={'marker': 'sentinel'},
|
||||
)
|
||||
|
||||
assert tool.seen_kwargs
|
||||
assert all('strategy' not in kwargs for kwargs in tool.seen_kwargs)
|
||||
|
|
@ -762,7 +768,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, strategy='sideways')
|
||||
pather.jog('A', 4, length=10, plan_options={'strategy': 'sideways'})
|
||||
|
||||
|
||||
def test_solver_rejects_rotation_impossible_candidates_before_parameter_solving() -> None:
|
||||
|
|
|
|||
|
|
@ -131,7 +131,12 @@ def test_pather_trace_into_minimal_policy_accepts_required_topologies(dst: Port)
|
|||
pather.ports['src'] = Port((0, 0), rotation=0)
|
||||
pather.ports['dst'] = dst
|
||||
|
||||
pather.trace_into('src', 'dst', plug_destination=False, bend_policy='minimal')
|
||||
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
|
||||
|
|
@ -153,7 +158,7 @@ def test_pather_trace_into_bend_policy_changes_real_solver_fallback() -> None:
|
|||
flexible.at('src').trace_into(
|
||||
'dst',
|
||||
plug_destination=False,
|
||||
bend_policy='flexible',
|
||||
plan_options={'bend_policy': 'flexible'},
|
||||
)
|
||||
|
||||
assert_equal(flexible.ports['src'].offset, (2, 0))
|
||||
|
|
@ -415,7 +420,7 @@ def test_trace_into_reuses_solver_across_staged_bend_bands(
|
|||
out_ptype=None,
|
||||
plug_destination=True,
|
||||
thru=None,
|
||||
bend_policy='flexible',
|
||||
plan_options={'bend_policy': 'flexible'},
|
||||
)
|
||||
|
||||
assert planner.solver.attempts == attempts
|
||||
|
|
@ -434,7 +439,7 @@ def test_trace_into_staged_bend_budget_stops_on_fatal_error() -> None:
|
|||
out_ptype=None,
|
||||
plug_destination=True,
|
||||
thru=None,
|
||||
bend_policy='flexible',
|
||||
plan_options={'bend_policy': 'flexible'},
|
||||
)
|
||||
|
||||
assert planner.solver.attempts == [(0, 2)]
|
||||
|
|
@ -497,7 +502,7 @@ def test_trace_into_minimal_policy_uses_orientation_required_band(
|
|||
out_ptype=None,
|
||||
plug_destination=True,
|
||||
thru=None,
|
||||
bend_policy='minimal',
|
||||
plan_options={'bend_policy': 'minimal'},
|
||||
)
|
||||
|
||||
assert planner.solver.attempts == [required_band]
|
||||
|
|
@ -515,7 +520,7 @@ def test_trace_into_minimal_policy_rejects_fallback_without_mutation() -> None:
|
|||
pather.ports['dst'] = Port((-10, 0), rotation=pi, ptype='wire')
|
||||
|
||||
with pytest.raises(BuildError, match='try next budget'):
|
||||
pather.trace_into('src', 'dst', bend_policy='minimal')
|
||||
pather.trace_into('src', 'dst', plan_options={'bend_policy': 'minimal'})
|
||||
|
||||
assert planner.solver.attempts == [(0, 0)]
|
||||
assert set(pather.ports) == {'src', 'dst'}
|
||||
|
|
@ -547,7 +552,7 @@ def test_trace_into_bend_policy_planner_default_and_route_override() -> None:
|
|||
out_ptype=None,
|
||||
plug_destination=True,
|
||||
thru=None,
|
||||
bend_policy='flexible',
|
||||
plan_options={'bend_policy': 'flexible'},
|
||||
)
|
||||
assert flexible_planner.solver.attempts == [(0, 2), (4, 4)]
|
||||
|
||||
|
|
@ -555,3 +560,14 @@ def test_trace_into_bend_policy_planner_default_and_route_override() -> None:
|
|||
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'})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue