Compare commits
4 commits
1da5ac550a
...
4d9aaf2fd9
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d9aaf2fd9 | |||
| 1b84c87d9b | |||
| ce7463e57c | |||
| 91a5d63b57 |
12 changed files with 692 additions and 127 deletions
30
MIGRATION.md
30
MIGRATION.md
|
|
@ -349,7 +349,7 @@ class MyTool(Tool):
|
||||||
```
|
```
|
||||||
|
|
||||||
Routing entry points now name every supported route argument explicitly.
|
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
|
```python
|
||||||
pather.jog('A', 4, length=10, tool_options={'process_corner': 'slow'})
|
pather.jog('A', 4, length=10, tool_options={'process_corner': 'slow'})
|
||||||
|
|
@ -450,15 +450,25 @@ Stable imports for custom tool authors live in `masque.builder`. The
|
||||||
`masque.builder.planner` module is an internal planner implementation; do not
|
`masque.builder.planner` module is an internal planner implementation; do not
|
||||||
import it from user code.
|
import it from user code.
|
||||||
|
|
||||||
`trace_into()` uses the same primitive-offer route selection and now searches
|
`trace_into()` uses the same primitive-offer route selection and defaults to
|
||||||
bounded route topologies with up to four bend roles. This preserves the common
|
the minimal main-route bend count required by the endpoint relationship: zero
|
||||||
straight, bend, S-like, U-like, and dogleg cases while allowing routes that
|
for a straight, one for a quarter-turn, and two for S- and U-like connections.
|
||||||
need an additional bounded bend pair. Bend-family requests search one-bend
|
Ptype adapters do not consume this bend budget. Set
|
||||||
routes before three-bend routes; other families search zero-to-two-bend routes
|
`plan_options={'bend_policy': 'flexible'}` to search bounded route topologies
|
||||||
before four-bend routes. The first band with a legal route wins. Within that
|
with up to four bend roles, including dogleg and loop-like fallbacks.
|
||||||
band, candidates are ordered by total primitive-offer cost, adapter count, step
|
Bend-family requests then search one-bend routes before three-bend routes;
|
||||||
count, and deterministic discovery order. The route `strategy` affects only
|
other families search zero-to-two-bend routes before four-bend routes. The
|
||||||
that final discovery-order tie-break.
|
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
|
Explicit-length `jog()` routes may also be satisfied by composing a straight
|
||||||
primitive before or after an omitted-length native S primitive. `uturn()` routes
|
primitive before or after an omitted-length native S primitive. `uturn()` routes
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ from dataclasses import dataclass
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
from types import MappingProxyType
|
from types import MappingProxyType
|
||||||
|
import traceback
|
||||||
|
|
||||||
from ..error import BuildError
|
from ..error import BuildError
|
||||||
|
|
||||||
|
|
@ -68,10 +69,11 @@ class RouteFailureDetails:
|
||||||
|
|
||||||
|
|
||||||
class RouteError(BuildError):
|
class RouteError(BuildError):
|
||||||
"""A route-selection failure with structured request diagnostics."""
|
"""A route-selection failure with structured request and saved call-stack diagnostics."""
|
||||||
|
|
||||||
details: RouteFailureDetails
|
details: RouteFailureDetails
|
||||||
policy: RouteFailurePolicy
|
policy: RouteFailurePolicy
|
||||||
|
_call_stack: tuple[traceback.FrameSummary, ...]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
|
@ -103,4 +105,5 @@ class RouteError(BuildError):
|
||||||
]
|
]
|
||||||
if details.minimum_cause is not None:
|
if details.minimum_cause is not None:
|
||||||
lines.append(f' minimum_failure: {details.minimum_cause}')
|
lines.append(f' minimum_failure: {details.minimum_cause}')
|
||||||
|
self._call_stack = tuple(traceback.extract_stack()[:-1])
|
||||||
super().__init__('\n'.join(lines))
|
super().__init__('\n'.join(lines))
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,11 @@ planner package is intentionally internal: custom route generators should
|
||||||
extend `Tool.primitive_offers()` and `Tool.render()` rather than depending on
|
extend `Tool.primitive_offers()` and `Tool.render()` rather than depending on
|
||||||
planner classes or search details.
|
planner classes or search details.
|
||||||
|
|
||||||
Public routing arguments are explicit. Custom per-route planning values belong
|
Public routing arguments are explicit. Planner-specific per-route settings
|
||||||
in `tool_options`; Pather forwards those values only to offer discovery. A Tool
|
belong in `plan_options`, while custom Tool offer values belong in
|
||||||
must capture any selected render-time value in its offer's committed data.
|
`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:
|
Routing is split into four ownership phases:
|
||||||
- snapshot: `Pather` resolves the active Tool for each requested port and
|
- snapshot: `Pather` resolves the active Tool for each requested port and
|
||||||
|
|
@ -77,11 +79,8 @@ from .planner.interface import (
|
||||||
RoutePortContext,
|
RoutePortContext,
|
||||||
route_failure_policy,
|
route_failure_policy,
|
||||||
)
|
)
|
||||||
from .error import RouteFailurePolicy, ToolContractError
|
from .error import RouteError, RouteFailurePolicy, ToolContractError
|
||||||
from .planner import (
|
from .planner import RoutingPlanner
|
||||||
RouteTieBreakStrategy,
|
|
||||||
RoutingPlanner,
|
|
||||||
)
|
|
||||||
from .planner.bounds import resolved_position_bound
|
from .planner.bounds import resolved_position_bound
|
||||||
from .logging import PatherLogger
|
from .logging import PatherLogger
|
||||||
from ._tolerances import angles_equal, array_close
|
from ._tolerances import angles_equal, array_close
|
||||||
|
|
@ -128,6 +127,20 @@ def _validated_tool_options(tool_options: Mapping[str, Any] | None) -> dict[str,
|
||||||
return options
|
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):
|
class Pather(PortList):
|
||||||
"""
|
"""
|
||||||
A `Pather` is a helper object used for snapping together multiple
|
A `Pather` is a helper object used for snapping together multiple
|
||||||
|
|
@ -626,7 +639,7 @@ class Pather(PortList):
|
||||||
length: float | None = None,
|
length: float | None = None,
|
||||||
*,
|
*,
|
||||||
spacing: float | ArrayLike | None = None,
|
spacing: float | ArrayLike | None = None,
|
||||||
strategy: RouteTieBreakStrategy | str | None = None,
|
plan_options: Mapping[str, Any] | None = None,
|
||||||
out_ptype: str | None = None,
|
out_ptype: str | None = None,
|
||||||
each: float | None = None,
|
each: float | None = None,
|
||||||
set_rotation: float | None = None,
|
set_rotation: float | None = None,
|
||||||
|
|
@ -652,9 +665,8 @@ class Pather(PortList):
|
||||||
For a single port with no length or bound, legal primitive-offer
|
For a single port with no length or bound, legal primitive-offer
|
||||||
candidates are evaluated at their minimum legal length-like parameters,
|
candidates are evaluated at their minimum legal length-like parameters,
|
||||||
then cost selects among those minimum-length candidates. `out_ptype`,
|
then cost selects among those minimum-length candidates. `out_ptype`,
|
||||||
when provided, constrains only the final route endpoint. `strategy`
|
when provided, constrains only the final route endpoint. Planner-specific
|
||||||
controls straight-first vs turn-first ordering only after cost and
|
per-route settings belong in `plan_options`.
|
||||||
structural tie-breakers.
|
|
||||||
|
|
||||||
`spacing` and `set_rotation` are only valid when using a bundle bound.
|
`spacing` and `set_rotation` are only valid when using a bundle bound.
|
||||||
"""
|
"""
|
||||||
|
|
@ -672,20 +684,23 @@ class Pather(PortList):
|
||||||
ymax=ymax,
|
ymax=ymax,
|
||||||
min_past_furthest=min_past_furthest,
|
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(
|
with self._logger.log_operation(
|
||||||
self, 'trace', portspec, ccw=ccw, length=length, spacing=spacing,
|
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):
|
if isinstance(portspec, str):
|
||||||
portspec = [portspec]
|
portspec = [portspec]
|
||||||
contexts = self._route_contexts(portspec)
|
contexts = self._route_contexts(portspec)
|
||||||
try:
|
try:
|
||||||
result = self.planner.plan_trace_route(
|
result = self.planner.plan_trace_route(
|
||||||
contexts, ccw, length, spacing=spacing, strategy=strategy,
|
contexts, ccw, length, spacing=spacing, plan_options=plan_opts,
|
||||||
tool_options=options, **bounds,
|
tool_options=tool_opts, **bounds,
|
||||||
)
|
)
|
||||||
except (BuildError, NotImplementedError) as err:
|
except (BuildError, NotImplementedError) as err:
|
||||||
|
if isinstance(err, RouteError):
|
||||||
|
err.__traceback__ = None
|
||||||
if not self._dead or route_failure_policy(err) is RouteFailurePolicy.FATAL:
|
if not self._dead or route_failure_policy(err) is RouteFailurePolicy.FATAL:
|
||||||
raise
|
raise
|
||||||
if length is not None and len(contexts) == 1:
|
if length is not None and len(contexts) == 1:
|
||||||
|
|
@ -726,7 +741,7 @@ class Pather(PortList):
|
||||||
*,
|
*,
|
||||||
length: float | None = None,
|
length: float | None = None,
|
||||||
spacing: float | ArrayLike | None = None,
|
spacing: float | ArrayLike | None = None,
|
||||||
strategy: RouteTieBreakStrategy | str | None = None,
|
plan_options: Mapping[str, Any] | None = None,
|
||||||
out_ptype: str | None = None,
|
out_ptype: str | None = None,
|
||||||
each: float | None = None,
|
each: float | None = None,
|
||||||
set_rotation: float | None = None,
|
set_rotation: float | None = None,
|
||||||
|
|
@ -755,8 +770,7 @@ class Pather(PortList):
|
||||||
|
|
||||||
With no positional or bundle bound, single-port `trace_to()` uses the
|
With no positional or bundle bound, single-port `trace_to()` uses the
|
||||||
same omitted minimum-length primitive-offer behavior as `trace()`.
|
same omitted minimum-length primitive-offer behavior as `trace()`.
|
||||||
`strategy` controls straight-first vs turn-first ordering only after
|
Planner-specific per-route settings belong in `plan_options`.
|
||||||
cost and structural tie-breakers.
|
|
||||||
"""
|
"""
|
||||||
bounds = _present_route_args(
|
bounds = _present_route_args(
|
||||||
length=length,
|
length=length,
|
||||||
|
|
@ -778,20 +792,23 @@ class Pather(PortList):
|
||||||
ymax=ymax,
|
ymax=ymax,
|
||||||
min_past_furthest=min_past_furthest,
|
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(
|
with self._logger.log_operation(
|
||||||
self, 'trace_to', portspec, ccw=ccw, spacing=spacing,
|
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):
|
if isinstance(portspec, str):
|
||||||
portspec = [portspec]
|
portspec = [portspec]
|
||||||
contexts = self._route_contexts(portspec)
|
contexts = self._route_contexts(portspec)
|
||||||
try:
|
try:
|
||||||
result = self.planner.plan_trace_to_route(
|
result = self.planner.plan_trace_to_route(
|
||||||
contexts, ccw, spacing=spacing, strategy=strategy,
|
contexts, ccw, spacing=spacing, plan_options=plan_opts,
|
||||||
tool_options=options, **bounds,
|
tool_options=tool_opts, **bounds,
|
||||||
)
|
)
|
||||||
except (BuildError, NotImplementedError) as err:
|
except (BuildError, NotImplementedError) as err:
|
||||||
|
if isinstance(err, RouteError):
|
||||||
|
err.__traceback__ = None
|
||||||
if (
|
if (
|
||||||
not self._dead
|
not self._dead
|
||||||
or len(contexts) != 1
|
or len(contexts) != 1
|
||||||
|
|
@ -829,7 +846,7 @@ class Pather(PortList):
|
||||||
length: float | None = None,
|
length: float | None = None,
|
||||||
*,
|
*,
|
||||||
spacing: float | ArrayLike | None = None,
|
spacing: float | ArrayLike | None = None,
|
||||||
strategy: RouteTieBreakStrategy | str | None = None,
|
plan_options: Mapping[str, Any] | None = None,
|
||||||
out_ptype: str | None = None,
|
out_ptype: str | None = None,
|
||||||
each: float | None = None,
|
each: float | None = None,
|
||||||
set_rotation: float | None = None,
|
set_rotation: float | None = None,
|
||||||
|
|
@ -850,7 +867,7 @@ class Pather(PortList):
|
||||||
tool_options: Mapping[str, Any] | None = None,
|
tool_options: Mapping[str, Any] | None = None,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
return self.trace_to(
|
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,
|
out_ptype=out_ptype, each=each, set_rotation=set_rotation,
|
||||||
p=p, pos=pos, position=position, x=x, y=y,
|
p=p, pos=pos, position=position, x=x, y=y,
|
||||||
emin=emin, emax=emax, pmin=pmin, pmax=pmax,
|
emin=emin, emax=emax, pmin=pmin, pmax=pmax,
|
||||||
|
|
@ -865,7 +882,7 @@ class Pather(PortList):
|
||||||
length: float | None = None,
|
length: float | None = None,
|
||||||
*,
|
*,
|
||||||
spacing: float | ArrayLike | None = None,
|
spacing: float | ArrayLike | None = None,
|
||||||
strategy: RouteTieBreakStrategy | str | None = None,
|
plan_options: Mapping[str, Any] | None = None,
|
||||||
out_ptype: str | None = None,
|
out_ptype: str | None = None,
|
||||||
each: float | None = None,
|
each: float | None = None,
|
||||||
set_rotation: float | None = None,
|
set_rotation: float | None = None,
|
||||||
|
|
@ -886,7 +903,7 @@ class Pather(PortList):
|
||||||
tool_options: Mapping[str, Any] | None = None,
|
tool_options: Mapping[str, Any] | None = None,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
return self.trace_to(
|
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,
|
out_ptype=out_ptype, each=each, set_rotation=set_rotation,
|
||||||
p=p, pos=pos, position=position, x=x, y=y,
|
p=p, pos=pos, position=position, x=x, y=y,
|
||||||
emin=emin, emax=emax, pmin=pmin, pmax=pmax,
|
emin=emin, emax=emax, pmin=pmin, pmax=pmax,
|
||||||
|
|
@ -900,7 +917,7 @@ class Pather(PortList):
|
||||||
length: float | None = None,
|
length: float | None = None,
|
||||||
*,
|
*,
|
||||||
spacing: float | ArrayLike | None = None,
|
spacing: float | ArrayLike | None = None,
|
||||||
strategy: RouteTieBreakStrategy | str | None = None,
|
plan_options: Mapping[str, Any] | None = None,
|
||||||
out_ptype: str | None = None,
|
out_ptype: str | None = None,
|
||||||
each: float | None = None,
|
each: float | None = None,
|
||||||
set_rotation: float | None = None,
|
set_rotation: float | None = None,
|
||||||
|
|
@ -921,7 +938,7 @@ class Pather(PortList):
|
||||||
tool_options: Mapping[str, Any] | None = None,
|
tool_options: Mapping[str, Any] | None = None,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
return self.bend(
|
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,
|
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,
|
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,
|
min_past_furthest=min_past_furthest, tool_options=tool_options,
|
||||||
|
|
@ -933,7 +950,7 @@ class Pather(PortList):
|
||||||
length: float | None = None,
|
length: float | None = None,
|
||||||
*,
|
*,
|
||||||
spacing: float | ArrayLike | None = None,
|
spacing: float | ArrayLike | None = None,
|
||||||
strategy: RouteTieBreakStrategy | str | None = None,
|
plan_options: Mapping[str, Any] | None = None,
|
||||||
out_ptype: str | None = None,
|
out_ptype: str | None = None,
|
||||||
each: float | None = None,
|
each: float | None = None,
|
||||||
set_rotation: float | None = None,
|
set_rotation: float | None = None,
|
||||||
|
|
@ -954,7 +971,7 @@ class Pather(PortList):
|
||||||
tool_options: Mapping[str, Any] | None = None,
|
tool_options: Mapping[str, Any] | None = None,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
return self.bend(
|
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,
|
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,
|
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,
|
min_past_furthest=min_past_furthest, tool_options=tool_options,
|
||||||
|
|
@ -967,7 +984,7 @@ class Pather(PortList):
|
||||||
length: float | None = None,
|
length: float | None = None,
|
||||||
*,
|
*,
|
||||||
spacing: float | ArrayLike | None = None,
|
spacing: float | ArrayLike | None = None,
|
||||||
strategy: RouteTieBreakStrategy | str | None = None,
|
plan_options: Mapping[str, Any] | None = None,
|
||||||
out_ptype: str | None = None,
|
out_ptype: str | None = None,
|
||||||
p: float | None = None,
|
p: float | None = None,
|
||||||
pos: float | None = None,
|
pos: float | None = None,
|
||||||
|
|
@ -989,25 +1006,27 @@ class Pather(PortList):
|
||||||
Multi-port jogs require `spacing`; the innermost first-bend port uses
|
Multi-port jogs require `spacing`; the innermost first-bend port uses
|
||||||
the base `length` or omitted-length solve, and other ports derive exact
|
the base `length` or omitted-length solve, and other ports derive exact
|
||||||
route lengths and offsets from that base route. `out_ptype`, when
|
route lengths and offsets from that base route. `out_ptype`, when
|
||||||
provided, constrains only each final route endpoint. `strategy`
|
provided, constrains only each final route endpoint. Planner-specific
|
||||||
controls straight-first vs S-first ordering only after cost and
|
per-route settings belong in `plan_options`.
|
||||||
structural tie-breakers.
|
|
||||||
"""
|
"""
|
||||||
bounds = _present_route_args(out_ptype=out_ptype, p=p, pos=pos, position=position, x=x, y=y)
|
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(
|
with self._logger.log_operation(
|
||||||
self, 'jog', portspec, offset=offset, length=length, spacing=spacing,
|
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):
|
if isinstance(portspec, str):
|
||||||
portspec = [portspec]
|
portspec = [portspec]
|
||||||
contexts = self._route_contexts(portspec)
|
contexts = self._route_contexts(portspec)
|
||||||
try:
|
try:
|
||||||
result = self.planner.plan_jog_route(
|
result = self.planner.plan_jog_route(
|
||||||
contexts, offset, length, spacing=spacing, strategy=strategy,
|
contexts, offset, length, spacing=spacing, plan_options=plan_opts,
|
||||||
tool_options=options, **bounds,
|
tool_options=tool_opts, **bounds,
|
||||||
)
|
)
|
||||||
except (BuildError, NotImplementedError) as err:
|
except (BuildError, NotImplementedError) as err:
|
||||||
|
if isinstance(err, RouteError):
|
||||||
|
err.__traceback__ = None
|
||||||
if (
|
if (
|
||||||
not self._dead
|
not self._dead
|
||||||
or len(contexts) != 1
|
or len(contexts) != 1
|
||||||
|
|
@ -1051,7 +1070,7 @@ class Pather(PortList):
|
||||||
length: float | None = None,
|
length: float | None = None,
|
||||||
*,
|
*,
|
||||||
spacing: float | ArrayLike | None = None,
|
spacing: float | ArrayLike | None = None,
|
||||||
strategy: RouteTieBreakStrategy | str | None = None,
|
plan_options: Mapping[str, Any] | None = None,
|
||||||
out_ptype: str | None = None,
|
out_ptype: str | None = None,
|
||||||
tool_options: Mapping[str, Any] | None = None,
|
tool_options: Mapping[str, Any] | None = None,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
|
|
@ -1066,25 +1085,27 @@ class Pather(PortList):
|
||||||
other ports derive exact lengths and offsets from it. Use `length=0` to
|
other ports derive exact lengths and offsets from it. Use `length=0` to
|
||||||
request the old zero-public-length U-turn shape. Positional and
|
request the old zero-public-length U-turn shape. Positional and
|
||||||
bundle-bound keywords are not supported for this operation. `out_ptype`,
|
bundle-bound keywords are not supported for this operation. `out_ptype`,
|
||||||
when provided, constrains only each final route endpoint. `strategy`
|
when provided, constrains only each final route endpoint. Planner-specific
|
||||||
controls straight-first vs U-first ordering only after cost and
|
per-route settings belong in `plan_options`.
|
||||||
structural tie-breakers.
|
|
||||||
"""
|
"""
|
||||||
bounds = _present_route_args(out_ptype=out_ptype)
|
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(
|
with self._logger.log_operation(
|
||||||
self, 'uturn', portspec, offset=offset, length=length, spacing=spacing,
|
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):
|
if isinstance(portspec, str):
|
||||||
portspec = [portspec]
|
portspec = [portspec]
|
||||||
contexts = self._route_contexts(portspec)
|
contexts = self._route_contexts(portspec)
|
||||||
try:
|
try:
|
||||||
result = self.planner.plan_uturn_route(
|
result = self.planner.plan_uturn_route(
|
||||||
contexts, offset, length, spacing=spacing, strategy=strategy,
|
contexts, offset, length, spacing=spacing, plan_options=plan_opts,
|
||||||
tool_options=options, **bounds,
|
tool_options=tool_opts, **bounds,
|
||||||
)
|
)
|
||||||
except (BuildError, NotImplementedError) as err:
|
except (BuildError, NotImplementedError) as err:
|
||||||
|
if isinstance(err, RouteError):
|
||||||
|
err.__traceback__ = None
|
||||||
if (
|
if (
|
||||||
not self._dead
|
not self._dead
|
||||||
or len(contexts) != 1
|
or len(contexts) != 1
|
||||||
|
|
@ -1115,21 +1136,26 @@ class Pather(PortList):
|
||||||
out_ptype: str | None = None,
|
out_ptype: str | None = None,
|
||||||
plug_destination: bool = True,
|
plug_destination: bool = True,
|
||||||
thru: str | None = None,
|
thru: str | None = None,
|
||||||
strategy: RouteTieBreakStrategy | str | None = None,
|
plan_options: Mapping[str, Any] | None = None,
|
||||||
tool_options: Mapping[str, Any] | None = None,
|
tool_options: Mapping[str, Any] | None = None,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
"""
|
"""
|
||||||
Route one port into another using a bounded primitive-offer selection.
|
Route one port into another using a bounded primitive-offer selection.
|
||||||
|
|
||||||
The current baseline searches bounded primitive-offer routes with up to
|
By default, searches only the exact main-route bend count required by
|
||||||
four bend roles, including straight, single-bend, S-like, U-like, and
|
the endpoint relationship: zero for a straight, one for a quarter-turn,
|
||||||
dogleg topologies. Bend-family requests try one-bend routes before
|
and two for S- and U-like connections. This rejects extra dogleg and
|
||||||
three-bend routes; other families try zero-to-two-bend routes before
|
loop-like fallback routes without inspecting primitive geometry. Ptype
|
||||||
four-bend routes. The first band with a legal candidate wins. Within a
|
adapters do not consume this bend budget.
|
||||||
band, candidates are ordered by total cost, adapter count, step count,
|
|
||||||
the requested straight-vs-turn topology preference, and deterministic
|
Set `plan_options={'bend_policy': 'flexible'}` to search bounded
|
||||||
discovery order. `strategy` therefore affects only otherwise tied
|
primitive-offer routes with up to four bend roles. Bend-family requests try one-bend routes
|
||||||
candidates.
|
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.
|
||||||
Custom planning options may be supplied through `tool_options`; they
|
Custom planning options may be supplied through `tool_options`; they
|
||||||
are forwarded only to primitive offer generation.
|
are forwarded only to primitive offer generation.
|
||||||
|
|
||||||
|
|
@ -1140,7 +1166,8 @@ class Pather(PortList):
|
||||||
mutated; failures during selected-route execution, including primitive
|
mutated; failures during selected-route execution, including primitive
|
||||||
commit, plug/thru application, or render, may leave partial output.
|
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(
|
with self._logger.log_operation(
|
||||||
self,
|
self,
|
||||||
'trace_into',
|
'trace_into',
|
||||||
|
|
@ -1148,9 +1175,10 @@ class Pather(PortList):
|
||||||
out_ptype=out_ptype,
|
out_ptype=out_ptype,
|
||||||
plug_destination=plug_destination,
|
plug_destination=plug_destination,
|
||||||
thru=thru,
|
thru=thru,
|
||||||
strategy=strategy,
|
plan_options=plan_opts,
|
||||||
tool_options=options,
|
tool_options=tool_opts,
|
||||||
):
|
):
|
||||||
|
try:
|
||||||
result = self.planner.plan_trace_into(
|
result = self.planner.plan_trace_into(
|
||||||
self._route_context(portspec_src),
|
self._route_context(portspec_src),
|
||||||
portspec_dst,
|
portspec_dst,
|
||||||
|
|
@ -1158,9 +1186,12 @@ class Pather(PortList):
|
||||||
out_ptype = out_ptype,
|
out_ptype = out_ptype,
|
||||||
plug_destination = plug_destination,
|
plug_destination = plug_destination,
|
||||||
thru = thru,
|
thru = thru,
|
||||||
strategy = strategy,
|
plan_options = plan_opts,
|
||||||
tool_options = options,
|
tool_options = tool_opts,
|
||||||
)
|
)
|
||||||
|
except RouteError as err:
|
||||||
|
err.__traceback__ = None
|
||||||
|
raise
|
||||||
self._apply_route_result(result)
|
self._apply_route_result(result)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
@ -1376,7 +1407,7 @@ class PortPather:
|
||||||
length: float | None = None,
|
length: float | None = None,
|
||||||
*,
|
*,
|
||||||
spacing: float | ArrayLike | None = None,
|
spacing: float | ArrayLike | None = None,
|
||||||
strategy: RouteTieBreakStrategy | str | None = None,
|
plan_options: Mapping[str, Any] | None = None,
|
||||||
out_ptype: str | None = None,
|
out_ptype: str | None = None,
|
||||||
each: float | None = None,
|
each: float | None = None,
|
||||||
set_rotation: float | None = None,
|
set_rotation: float | None = None,
|
||||||
|
|
@ -1394,7 +1425,7 @@ class PortPather:
|
||||||
if spacing is None and self.default_spacing is not None and len(self.ports) > 1 and ccw is not None:
|
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
|
spacing = self.default_spacing
|
||||||
self.pather.trace(
|
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,
|
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,
|
xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax, min_past_furthest=min_past_furthest,
|
||||||
tool_options=tool_options,
|
tool_options=tool_options,
|
||||||
|
|
@ -1407,7 +1438,7 @@ class PortPather:
|
||||||
*,
|
*,
|
||||||
length: float | None = None,
|
length: float | None = None,
|
||||||
spacing: float | ArrayLike | None = None,
|
spacing: float | ArrayLike | None = None,
|
||||||
strategy: RouteTieBreakStrategy | str | None = None,
|
plan_options: Mapping[str, Any] | None = None,
|
||||||
out_ptype: str | None = None,
|
out_ptype: str | None = None,
|
||||||
each: float | None = None,
|
each: float | None = None,
|
||||||
set_rotation: float | None = None,
|
set_rotation: float | None = None,
|
||||||
|
|
@ -1430,7 +1461,7 @@ class PortPather:
|
||||||
if spacing is None and self.default_spacing is not None and len(self.ports) > 1 and ccw is not None:
|
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
|
spacing = self.default_spacing
|
||||||
self.pather.trace_to(
|
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,
|
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,
|
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,
|
min_past_furthest=min_past_furthest, tool_options=tool_options,
|
||||||
|
|
@ -1442,7 +1473,7 @@ class PortPather:
|
||||||
length: float | None = None,
|
length: float | None = None,
|
||||||
*,
|
*,
|
||||||
spacing: float | ArrayLike | None = None,
|
spacing: float | ArrayLike | None = None,
|
||||||
strategy: RouteTieBreakStrategy | str | None = None,
|
plan_options: Mapping[str, Any] | None = None,
|
||||||
out_ptype: str | None = None,
|
out_ptype: str | None = None,
|
||||||
each: float | None = None,
|
each: float | None = None,
|
||||||
set_rotation: float | None = None,
|
set_rotation: float | None = None,
|
||||||
|
|
@ -1463,7 +1494,7 @@ class PortPather:
|
||||||
tool_options: Mapping[str, Any] | None = None,
|
tool_options: Mapping[str, Any] | None = None,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
return self.trace_to(
|
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,
|
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,
|
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,
|
min_past_furthest=min_past_furthest, tool_options=tool_options,
|
||||||
|
|
@ -1475,7 +1506,7 @@ class PortPather:
|
||||||
length: float | None = None,
|
length: float | None = None,
|
||||||
*,
|
*,
|
||||||
spacing: float | ArrayLike | None = None,
|
spacing: float | ArrayLike | None = None,
|
||||||
strategy: RouteTieBreakStrategy | str | None = None,
|
plan_options: Mapping[str, Any] | None = None,
|
||||||
out_ptype: str | None = None,
|
out_ptype: str | None = None,
|
||||||
each: float | None = None,
|
each: float | None = None,
|
||||||
set_rotation: float | None = None,
|
set_rotation: float | None = None,
|
||||||
|
|
@ -1496,7 +1527,7 @@ class PortPather:
|
||||||
tool_options: Mapping[str, Any] | None = None,
|
tool_options: Mapping[str, Any] | None = None,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
return self.trace_to(
|
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,
|
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,
|
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,
|
min_past_furthest=min_past_furthest, tool_options=tool_options,
|
||||||
|
|
@ -1507,7 +1538,7 @@ class PortPather:
|
||||||
length: float | None = None,
|
length: float | None = None,
|
||||||
*,
|
*,
|
||||||
spacing: float | ArrayLike | None = None,
|
spacing: float | ArrayLike | None = None,
|
||||||
strategy: RouteTieBreakStrategy | str | None = None,
|
plan_options: Mapping[str, Any] | None = None,
|
||||||
out_ptype: str | None = None,
|
out_ptype: str | None = None,
|
||||||
each: float | None = None,
|
each: float | None = None,
|
||||||
set_rotation: float | None = None,
|
set_rotation: float | None = None,
|
||||||
|
|
@ -1528,7 +1559,7 @@ class PortPather:
|
||||||
tool_options: Mapping[str, Any] | None = None,
|
tool_options: Mapping[str, Any] | None = None,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
return self.bend(
|
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,
|
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,
|
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,
|
min_past_furthest=min_past_furthest, tool_options=tool_options,
|
||||||
|
|
@ -1539,7 +1570,7 @@ class PortPather:
|
||||||
length: float | None = None,
|
length: float | None = None,
|
||||||
*,
|
*,
|
||||||
spacing: float | ArrayLike | None = None,
|
spacing: float | ArrayLike | None = None,
|
||||||
strategy: RouteTieBreakStrategy | str | None = None,
|
plan_options: Mapping[str, Any] | None = None,
|
||||||
out_ptype: str | None = None,
|
out_ptype: str | None = None,
|
||||||
each: float | None = None,
|
each: float | None = None,
|
||||||
set_rotation: float | None = None,
|
set_rotation: float | None = None,
|
||||||
|
|
@ -1560,7 +1591,7 @@ class PortPather:
|
||||||
tool_options: Mapping[str, Any] | None = None,
|
tool_options: Mapping[str, Any] | None = None,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
return self.bend(
|
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,
|
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,
|
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,
|
min_past_furthest=min_past_furthest, tool_options=tool_options,
|
||||||
|
|
@ -1572,7 +1603,7 @@ class PortPather:
|
||||||
length: float | None = None,
|
length: float | None = None,
|
||||||
*,
|
*,
|
||||||
spacing: float | ArrayLike | None = None,
|
spacing: float | ArrayLike | None = None,
|
||||||
strategy: RouteTieBreakStrategy | str | None = None,
|
plan_options: Mapping[str, Any] | None = None,
|
||||||
out_ptype: str | None = None,
|
out_ptype: str | None = None,
|
||||||
p: float | None = None,
|
p: float | None = None,
|
||||||
pos: float | None = None,
|
pos: float | None = None,
|
||||||
|
|
@ -1584,7 +1615,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):
|
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
|
spacing = self.default_spacing
|
||||||
self.pather.jog(
|
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,
|
p=p, pos=pos, position=position, x=x, y=y, tool_options=tool_options,
|
||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
|
|
@ -1595,14 +1626,14 @@ class PortPather:
|
||||||
length: float | None = None,
|
length: float | None = None,
|
||||||
*,
|
*,
|
||||||
spacing: float | ArrayLike | None = None,
|
spacing: float | ArrayLike | None = None,
|
||||||
strategy: RouteTieBreakStrategy | str | None = None,
|
plan_options: Mapping[str, Any] | None = None,
|
||||||
out_ptype: str | None = None,
|
out_ptype: str | None = None,
|
||||||
tool_options: Mapping[str, Any] | None = None,
|
tool_options: Mapping[str, Any] | None = None,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
if spacing is None and self.default_spacing is not None and len(self.ports) > 1:
|
if spacing is None and self.default_spacing is not None and len(self.ports) > 1:
|
||||||
spacing = self.default_spacing
|
spacing = self.default_spacing
|
||||||
self.pather.uturn(
|
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,
|
out_ptype=out_ptype, tool_options=tool_options,
|
||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
|
|
@ -1614,13 +1645,13 @@ class PortPather:
|
||||||
out_ptype: str | None = None,
|
out_ptype: str | None = None,
|
||||||
plug_destination: bool = True,
|
plug_destination: bool = True,
|
||||||
thru: str | None = None,
|
thru: str | None = None,
|
||||||
strategy: RouteTieBreakStrategy | str | None = None,
|
plan_options: Mapping[str, Any] | None = None,
|
||||||
tool_options: Mapping[str, Any] | None = None,
|
tool_options: Mapping[str, Any] | None = None,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
port = self._single_port('trace_into')
|
port = self._single_port('trace_into')
|
||||||
self.pather.trace_into(
|
self.pather.trace_into(
|
||||||
port, target_port, out_ptype=out_ptype, plug_destination=plug_destination,
|
port, target_port, out_ptype=out_ptype, plug_destination=plug_destination,
|
||||||
thru=thru, strategy=strategy, tool_options=tool_options,
|
thru=thru, plan_options=plan_options, tool_options=tool_options,
|
||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,4 +13,5 @@ from .interface import (
|
||||||
route_failure_policy as route_failure_policy,
|
route_failure_policy as route_failure_policy,
|
||||||
)
|
)
|
||||||
from .planner import RouteTieBreakStrategy as RouteTieBreakStrategy
|
from .planner import RouteTieBreakStrategy as RouteTieBreakStrategy
|
||||||
|
from .planner import TraceIntoBendPolicy as TraceIntoBendPolicy
|
||||||
from .planner import RoutingPlanner as RoutingPlanner
|
from .planner import RoutingPlanner as RoutingPlanner
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ from .interface import (
|
||||||
)
|
)
|
||||||
|
|
||||||
RouteTieBreakStrategy = Literal['straight_first', 'turn_first']
|
RouteTieBreakStrategy = Literal['straight_first', 'turn_first']
|
||||||
|
TraceIntoBendPolicy = Literal['flexible', 'minimal']
|
||||||
COST_RTOL = 1e-10
|
COST_RTOL = 1e-10
|
||||||
COST_ATOL = 1e-8
|
COST_ATOL = 1e-8
|
||||||
|
|
||||||
|
|
@ -87,6 +88,17 @@ def validate_strategy(strategy: RouteTieBreakStrategy | str) -> RouteTieBreakStr
|
||||||
raise BuildError(f'Invalid route strategy {strategy!r}; expected straight_first or turn_first')
|
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:
|
def is_close(a: float, b: float) -> bool:
|
||||||
"""Compare route-solver scalars with the planner tolerance."""
|
"""Compare route-solver scalars with the planner tolerance."""
|
||||||
return scalar_close(a, b)
|
return scalar_close(a, b)
|
||||||
|
|
@ -1057,9 +1069,16 @@ class RoutingPlanner:
|
||||||
|
|
||||||
TRACE_INTO_MAX_BENDS: int = 4
|
TRACE_INTO_MAX_BENDS: int = 4
|
||||||
DEFAULT_STRATEGY: RouteTieBreakStrategy = 'straight_first'
|
DEFAULT_STRATEGY: RouteTieBreakStrategy = 'straight_first'
|
||||||
|
DEFAULT_TRACE_INTO_BEND_POLICY: TraceIntoBendPolicy = 'minimal'
|
||||||
|
|
||||||
def __init__(self, strategy: RouteTieBreakStrategy = DEFAULT_STRATEGY) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
strategy: RouteTieBreakStrategy = DEFAULT_STRATEGY,
|
||||||
|
*,
|
||||||
|
bend_policy: TraceIntoBendPolicy = DEFAULT_TRACE_INTO_BEND_POLICY,
|
||||||
|
) -> None:
|
||||||
self.strategy = validate_strategy(strategy)
|
self.strategy = validate_strategy(strategy)
|
||||||
|
self.bend_policy = validate_trace_into_bend_policy(bend_policy)
|
||||||
|
|
||||||
def resolve_strategy(self, strategy: RouteTieBreakStrategy | str | None) -> RouteTieBreakStrategy:
|
def resolve_strategy(self, strategy: RouteTieBreakStrategy | str | None) -> RouteTieBreakStrategy:
|
||||||
"""Return the per-route strategy or the planner default."""
|
"""Return the per-route strategy or the planner default."""
|
||||||
|
|
@ -1067,9 +1086,56 @@ class RoutingPlanner:
|
||||||
return getattr(self, 'strategy', self.DEFAULT_STRATEGY)
|
return getattr(self, 'strategy', self.DEFAULT_STRATEGY)
|
||||||
return validate_strategy(strategy)
|
return validate_strategy(strategy)
|
||||||
|
|
||||||
def trace_into_bend_bands(self, family: PrimitiveKind) -> tuple[tuple[int, int], ...]:
|
def resolve_trace_into_bend_policy(
|
||||||
"""Return non-overlapping trace_into bend-budget bands for staged solving."""
|
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."""
|
||||||
max_bends = self.TRACE_INTO_MAX_BENDS
|
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':
|
if family == 'bend':
|
||||||
return tuple(band for band in ((1, 1), (3, 3)) if band[1] <= max_bends)
|
return tuple(band for band in ((1, 1), (3, 3)) if band[1] <= max_bends)
|
||||||
bands: list[tuple[int, int]] = []
|
bands: list[tuple[int, int]] = []
|
||||||
|
|
@ -1286,11 +1352,13 @@ class RoutingPlanner:
|
||||||
length: float | None = None,
|
length: float | None = None,
|
||||||
*,
|
*,
|
||||||
spacing: float | ArrayLike | 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,
|
tool_options: Mapping[str, Any] | None = None,
|
||||||
**bounds: Any,
|
**bounds: Any,
|
||||||
) -> PreparedRouteResult:
|
) -> PreparedRouteResult:
|
||||||
"""Plan straight or single-bend traces, including `each` and bundle-bound modes."""
|
"""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)
|
route_bounds = dict(bounds)
|
||||||
request_details = {'ccw': ccw, **{
|
request_details = {'ccw': ccw, **{
|
||||||
key: value for key, value in route_bounds.items() if value is not None
|
key: value for key, value in route_bounds.items() if value is not None
|
||||||
|
|
@ -1299,8 +1367,8 @@ class RoutingPlanner:
|
||||||
request_details['length'] = length
|
request_details['length'] = length
|
||||||
if spacing is not None:
|
if spacing is not None:
|
||||||
request_details['spacing'] = spacing
|
request_details['spacing'] = spacing
|
||||||
if strategy is not None:
|
if plan_opts:
|
||||||
request_details['strategy'] = strategy
|
request_details['plan_options'] = dict(plan_opts)
|
||||||
if tool_options:
|
if tool_options:
|
||||||
request_details['tool_options'] = dict(tool_options)
|
request_details['tool_options'] = dict(tool_options)
|
||||||
operation: RouteOperation = 'trace'
|
operation: RouteOperation = 'trace'
|
||||||
|
|
@ -1416,19 +1484,21 @@ class RoutingPlanner:
|
||||||
ccw: SupportsBool | None,
|
ccw: SupportsBool | None,
|
||||||
*,
|
*,
|
||||||
spacing: float | ArrayLike | 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,
|
tool_options: Mapping[str, Any] | None = None,
|
||||||
**bounds: Any,
|
**bounds: Any,
|
||||||
) -> PreparedRouteResult:
|
) -> PreparedRouteResult:
|
||||||
"""Plan `trace_to()` by resolving positional targets or delegating to `trace()` modes."""
|
"""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)
|
route_bounds = dict(bounds)
|
||||||
request_details = {'ccw': ccw, **{
|
request_details = {'ccw': ccw, **{
|
||||||
key: value for key, value in route_bounds.items() if value is not None
|
key: value for key, value in route_bounds.items() if value is not None
|
||||||
}}
|
}}
|
||||||
if spacing is not None:
|
if spacing is not None:
|
||||||
request_details['spacing'] = spacing
|
request_details['spacing'] = spacing
|
||||||
if strategy is not None:
|
if plan_opts:
|
||||||
request_details['strategy'] = strategy
|
request_details['plan_options'] = dict(plan_opts)
|
||||||
if tool_options:
|
if tool_options:
|
||||||
request_details['tool_options'] = dict(tool_options)
|
request_details['tool_options'] = dict(tool_options)
|
||||||
operation: RouteOperation = 'trace_to'
|
operation: RouteOperation = 'trace_to'
|
||||||
|
|
@ -1504,11 +1574,13 @@ class RoutingPlanner:
|
||||||
length: float | None = None,
|
length: float | None = None,
|
||||||
*,
|
*,
|
||||||
spacing: float | ArrayLike | 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,
|
tool_options: Mapping[str, Any] | None = None,
|
||||||
**bounds: Any,
|
**bounds: Any,
|
||||||
) -> PreparedRouteResult:
|
) -> PreparedRouteResult:
|
||||||
"""Plan S-bend routes for single ports or spaced bundles."""
|
"""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')
|
offset = planner_bounds.finite_scalar(offset, 'offset')
|
||||||
request_details = {'offset': offset, **{
|
request_details = {'offset': offset, **{
|
||||||
key: value for key, value in bounds.items() if value is not None
|
key: value for key, value in bounds.items() if value is not None
|
||||||
|
|
@ -1517,8 +1589,8 @@ class RoutingPlanner:
|
||||||
request_details['length'] = length
|
request_details['length'] = length
|
||||||
if spacing is not None:
|
if spacing is not None:
|
||||||
request_details['spacing'] = spacing
|
request_details['spacing'] = spacing
|
||||||
if strategy is not None:
|
if plan_opts:
|
||||||
request_details['strategy'] = strategy
|
request_details['plan_options'] = dict(plan_opts)
|
||||||
if tool_options:
|
if tool_options:
|
||||||
request_details['tool_options'] = dict(tool_options)
|
request_details['tool_options'] = dict(tool_options)
|
||||||
operation: RouteOperation = 'jog'
|
operation: RouteOperation = 'jog'
|
||||||
|
|
@ -1580,11 +1652,13 @@ class RoutingPlanner:
|
||||||
length: float | None = None,
|
length: float | None = None,
|
||||||
*,
|
*,
|
||||||
spacing: float | ArrayLike | 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,
|
tool_options: Mapping[str, Any] | None = None,
|
||||||
**bounds: Any,
|
**bounds: Any,
|
||||||
) -> PreparedRouteResult:
|
) -> PreparedRouteResult:
|
||||||
"""Plan U-turn routes for single ports or spaced bundles."""
|
"""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')
|
offset = planner_bounds.finite_scalar(offset, 'offset')
|
||||||
route_bounds = dict(bounds)
|
route_bounds = dict(bounds)
|
||||||
request_details = {'offset': offset, **{
|
request_details = {'offset': offset, **{
|
||||||
|
|
@ -1594,8 +1668,8 @@ class RoutingPlanner:
|
||||||
request_details['length'] = length
|
request_details['length'] = length
|
||||||
if spacing is not None:
|
if spacing is not None:
|
||||||
request_details['spacing'] = spacing
|
request_details['spacing'] = spacing
|
||||||
if strategy is not None:
|
if plan_opts:
|
||||||
request_details['strategy'] = strategy
|
request_details['plan_options'] = dict(plan_opts)
|
||||||
if tool_options:
|
if tool_options:
|
||||||
request_details['tool_options'] = dict(tool_options)
|
request_details['tool_options'] = dict(tool_options)
|
||||||
operation: RouteOperation = 'uturn'
|
operation: RouteOperation = 'uturn'
|
||||||
|
|
@ -1722,10 +1796,14 @@ class RoutingPlanner:
|
||||||
out_ptype: str | None,
|
out_ptype: str | None,
|
||||||
plug_destination: bool,
|
plug_destination: bool,
|
||||||
thru: str | None,
|
thru: str | None,
|
||||||
strategy: RouteTieBreakStrategy | str | None = None,
|
plan_options: Mapping[str, Any] | None = None,
|
||||||
tool_options: Mapping[str, Any] | None = None,
|
tool_options: Mapping[str, Any] | None = None,
|
||||||
) -> PreparedRouteResult:
|
) -> PreparedRouteResult:
|
||||||
"""Plan a bounded route from one source port into a destination port."""
|
"""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:
|
if out_ptype is None:
|
||||||
out_ptype = port_dst.ptype
|
out_ptype = port_dst.ptype
|
||||||
if context_src.port.rotation is None or port_dst.rotation is None:
|
if context_src.port.rotation is None or port_dst.rotation is None:
|
||||||
|
|
@ -1734,6 +1812,8 @@ class RoutingPlanner:
|
||||||
desired.rotation = port_dst.rotation - pi
|
desired.rotation = port_dst.rotation - pi
|
||||||
desired.ptype = out_ptype
|
desired.ptype = out_ptype
|
||||||
family, length, jog, ccw = self.trace_into_spec(context_src.port, desired)
|
family, length, jog, ccw = self.trace_into_spec(context_src.port, desired)
|
||||||
|
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(
|
request = self.solver_request(
|
||||||
family,
|
family,
|
||||||
context_src,
|
context_src,
|
||||||
|
|
@ -1741,7 +1821,7 @@ class RoutingPlanner:
|
||||||
jog=jog,
|
jog=jog,
|
||||||
ccw=ccw,
|
ccw=ccw,
|
||||||
constrain_jog=family == 'bend',
|
constrain_jog=family == 'bend',
|
||||||
max_bends=self.TRACE_INTO_MAX_BENDS,
|
max_bends=max_bends,
|
||||||
strategy=strategy,
|
strategy=strategy,
|
||||||
tool_options=tool_options,
|
tool_options=tool_options,
|
||||||
out_ptype=out_ptype,
|
out_ptype=out_ptype,
|
||||||
|
|
@ -1749,7 +1829,7 @@ class RoutingPlanner:
|
||||||
solver = self.solver_for_request(request)
|
solver = self.solver_for_request(request)
|
||||||
candidate = None
|
candidate = None
|
||||||
last_error: Exception | None = None
|
last_error: Exception | None = None
|
||||||
for min_bends, max_bends in self.trace_into_bend_bands(family):
|
for min_bends, max_bends in bend_bands:
|
||||||
try:
|
try:
|
||||||
candidate = solver.solve(min_bends=min_bends, max_bends=max_bends)
|
candidate = solver.solve(min_bends=min_bends, max_bends=max_bends)
|
||||||
break
|
break
|
||||||
|
|
|
||||||
|
|
@ -996,7 +996,13 @@ def test_autotool_strategy_orders_main_steps_across_adapters(jog: float) -> None
|
||||||
for strategy in ('straight_first', 'turn_first'):
|
for strategy in ('straight_first', 'turn_first'):
|
||||||
pather = Pather(library, tools=tool, render='deferred')
|
pather = Pather(library, tools=tool, render='deferred')
|
||||||
pather.ports['A'] = Port((0, 0), 0, ptype=primary)
|
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']]
|
selected_kinds[strategy] = [step.kind for step in pather._paths['A']]
|
||||||
|
|
||||||
assert selected_kinds['straight_first'] == ['straight', 'straight', 'straight', 's']
|
assert selected_kinds['straight_first'] == ['straight', 'straight', 'straight', 's']
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
|
from types import FunctionType
|
||||||
|
import traceback
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from ..builder import Pather
|
from ..builder import Pather, PathTool, RouteError
|
||||||
from ..error import BuildError, LibraryError
|
from ..error import BuildError, LibraryError
|
||||||
from ..library import (
|
from ..library import (
|
||||||
INameView,
|
INameView,
|
||||||
|
|
@ -28,6 +30,17 @@ 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):
|
class _MetadataSource(ILibraryView):
|
||||||
def __init__(self, mapping: dict[str, Pattern], child_graph: dict[str, set[str]]) -> None:
|
def __init__(self, mapping: dict[str, Pattern], child_graph: dict[str, set[str]]) -> None:
|
||||||
self.mapping = mapping
|
self.mapping = mapping
|
||||||
|
|
@ -263,6 +276,34 @@ def test_build_library_validate_is_retryable_after_failure() -> None:
|
||||||
assert report.dependency_graph["parent"] == frozenset({"child"})
|
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:
|
def test_build_library_depends_on_supports_hidden_dependencies_for_partial_validation() -> None:
|
||||||
builder = LibraryBuilder()
|
builder = LibraryBuilder()
|
||||||
builder["child"] = Pattern()
|
builder["child"] = Pattern()
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,10 @@ 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:
|
def test_builder_init() -> None:
|
||||||
lib = Library()
|
lib = Library()
|
||||||
b = Pather(lib, name="mypat")
|
b = Pather(lib, name="mypat")
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from typing import Any, Literal, Never
|
from typing import Any, Literal, Never
|
||||||
|
from types import FunctionType
|
||||||
import inspect
|
import inspect
|
||||||
|
import traceback
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import numpy
|
import numpy
|
||||||
|
|
@ -34,6 +36,18 @@ class PlanningOnlyTool(Tool):
|
||||||
return tree
|
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):
|
class FirstPortOnlyTraceTool(PlanningOnlyTool):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.render_calls = 0
|
self.render_calls = 0
|
||||||
|
|
@ -270,6 +284,90 @@ def test_route_error_reports_no_route_at_any_length() -> None:
|
||||||
assert 'no legal route exists at any length' in str(exc_info.value)
|
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:
|
def test_route_error_reports_failed_minimum_diagnosis(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
planner = RoutingPlanner()
|
planner = RoutingPlanner()
|
||||||
context = RoutePortContext(
|
context = RoutePortContext(
|
||||||
|
|
@ -986,6 +1084,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)
|
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:
|
def test_routing_typo_fails_before_tool_lookup() -> None:
|
||||||
tool = RequestCountingTool()
|
tool = RequestCountingTool()
|
||||||
p = Pather(Library(), tools=tool, render='deferred')
|
p = Pather(Library(), tools=tool, render='deferred')
|
||||||
|
|
@ -1007,6 +1115,41 @@ def test_tool_options_reject_invalid_or_reserved_keys(tool_options: dict[Any, An
|
||||||
assert tool.offer_calls == 0
|
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(
|
@pytest.mark.parametrize(
|
||||||
'operation',
|
'operation',
|
||||||
[
|
[
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,41 @@ def test_pather_accepts_and_reuses_planner_instance() -> None:
|
||||||
assert planner.trace_to_calls == 2
|
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:
|
def test_port_tool_policy_and_portpather_selection_follow_names() -> None:
|
||||||
default_tool = PathTool(layer=(1, 0), width=1, ptype='wire')
|
default_tool = PathTool(layer=(1, 0), width=1, ptype='wire')
|
||||||
named_tool = PathTool(layer=(2, 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:
|
def test_pather_route_strategy_per_route_overrides_planner_default() -> None:
|
||||||
pather, _tool = pather_with_strategy_tool(RoutingPlanner(strategy='turn_first'))
|
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']
|
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:
|
def test_pather_route_strategy_per_route_can_request_turn_first() -> None:
|
||||||
pather, _tool = pather_with_strategy_tool()
|
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']
|
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, 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 tool.seen_kwargs
|
||||||
assert all('strategy' not in kwargs for kwargs in 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()
|
pather, _tool = pather_with_strategy_tool()
|
||||||
with pytest.raises(BuildError, match='Invalid route strategy'):
|
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:
|
def test_solver_rejects_rotation_impossible_candidates_before_parameter_solving() -> None:
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,78 @@ def test_pather_trace_into_shapes() -> None:
|
||||||
assert numpy.isclose(p.pattern.ports['I'].rotation, pi / 2)
|
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:
|
def test_pather_trace_into_large_composed_manhattan_route_plugs() -> None:
|
||||||
p = Pather(
|
p = Pather(
|
||||||
Library(),
|
Library(),
|
||||||
|
|
@ -305,6 +377,7 @@ class TraceIntoBudgetSolver:
|
||||||
|
|
||||||
class TraceIntoBudgetPlanner(RoutingPlanner):
|
class TraceIntoBudgetPlanner(RoutingPlanner):
|
||||||
def __init__(self, successes: set[tuple[int, int]], fatal_at: set[tuple[int, int]] | None = None) -> None:
|
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 = TraceIntoBudgetSolver(successes, fatal_at=fatal_at)
|
||||||
self.solver_requests = 0
|
self.solver_requests = 0
|
||||||
|
|
||||||
|
|
@ -340,7 +413,15 @@ def test_trace_into_reuses_solver_across_staged_bend_bands(
|
||||||
planner = TraceIntoBudgetPlanner(successes)
|
planner = TraceIntoBudgetPlanner(successes)
|
||||||
context = RoutePortContext('src', Port((0, 0), rotation=0, ptype='wire'), PathTool(layer='M1', width=1, ptype='wire'))
|
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)
|
planner.plan_trace_into(
|
||||||
|
context,
|
||||||
|
'dst',
|
||||||
|
dst,
|
||||||
|
out_ptype=None,
|
||||||
|
plug_destination=True,
|
||||||
|
thru=None,
|
||||||
|
plan_options={'bend_policy': 'flexible'},
|
||||||
|
)
|
||||||
|
|
||||||
assert planner.solver.attempts == attempts
|
assert planner.solver.attempts == attempts
|
||||||
assert planner.solver_requests == 1
|
assert planner.solver_requests == 1
|
||||||
|
|
@ -351,7 +432,15 @@ 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'))
|
context = RoutePortContext('src', Port((0, 0), rotation=0, ptype='wire'), PathTool(layer='M1', width=1, ptype='wire'))
|
||||||
|
|
||||||
with pytest.raises(RoutePlanningError, match='fatal'):
|
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)
|
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'},
|
||||||
|
)
|
||||||
|
|
||||||
assert planner.solver.attempts == [(0, 2)]
|
assert planner.solver.attempts == [(0, 2)]
|
||||||
assert planner.solver_requests == 1
|
assert planner.solver_requests == 1
|
||||||
|
|
@ -361,8 +450,124 @@ def test_trace_into_bend_bands_respect_max_bends() -> None:
|
||||||
class OneBendPlanner(RoutingPlanner):
|
class OneBendPlanner(RoutingPlanner):
|
||||||
TRACE_INTO_MAX_BENDS = 1
|
TRACE_INTO_MAX_BENDS = 1
|
||||||
|
|
||||||
planner = OneBendPlanner()
|
planner = OneBendPlanner(bend_policy='flexible')
|
||||||
|
|
||||||
assert planner.trace_into_bend_bands('straight') == ((0, 0),)
|
assert planner.trace_into_bend_bands('straight') == ((0, 0),)
|
||||||
assert planner.trace_into_bend_bands('s') == ((0, 0),)
|
assert planner.trace_into_bend_bands('s') == ((0, 0),)
|
||||||
assert planner.trace_into_bend_bands('bend') == ((1, 1),)
|
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'})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue