[Pather / AutoTool] rework route kwargs into tool_options, and actualyl pass them through AutoTool
This commit is contained in:
parent
49713896c3
commit
67afee7704
11 changed files with 865 additions and 130 deletions
46
MIGRATION.md
46
MIGRATION.md
|
|
@ -22,13 +22,13 @@ The biggest migration point is that the old routing verbs were renamed:
|
|||
| `Pather.pathU(...)` | `Pather.uturn(...)` |
|
||||
| `Pather.path_into(...)` | `Pather.trace_into(...)` |
|
||||
| `Pather.path_from(src, dst)` | `Pather.at(src).trace_into(dst)` |
|
||||
| `RenderPather.path(...)` | `Pather(..., auto_render=False).trace(...)` |
|
||||
| `RenderPather.path_to(...)` | `Pather(..., auto_render=False).trace_to(...)` |
|
||||
| `RenderPather.mpath(...)` | `Pather(..., auto_render=False).trace(...)` / `Pather(..., auto_render=False).trace_to(...)` |
|
||||
| `RenderPather.pathS(...)` | `Pather(..., auto_render=False).jog(...)` |
|
||||
| `RenderPather.pathU(...)` | `Pather(..., auto_render=False).uturn(...)` |
|
||||
| `RenderPather.path_into(...)` | `Pather(..., auto_render=False).trace_into(...)` |
|
||||
| `RenderPather.path_from(src, dst)` | `Pather(..., auto_render=False).at(src).trace_into(dst)` |
|
||||
| `RenderPather.path(...)` | `Pather(..., render='deferred').trace(...)` |
|
||||
| `RenderPather.path_to(...)` | `Pather(..., render='deferred').trace_to(...)` |
|
||||
| `RenderPather.mpath(...)` | `Pather(..., render='deferred').trace(...)` / `Pather(..., render='deferred').trace_to(...)` |
|
||||
| `RenderPather.pathS(...)` | `Pather(..., render='deferred').jog(...)` |
|
||||
| `RenderPather.pathU(...)` | `Pather(..., render='deferred').uturn(...)` |
|
||||
| `RenderPather.path_into(...)` | `Pather(..., render='deferred').trace_into(...)` |
|
||||
| `RenderPather.path_from(src, dst)` | `Pather(..., render='deferred').at(src).trace_into(dst)` |
|
||||
|
||||
There are also new convenience wrappers:
|
||||
|
||||
|
|
@ -123,13 +123,13 @@ from masque.builder.renderpather import RenderPather
|
|||
from masque.builder import Pather
|
||||
|
||||
builder = Pather(...)
|
||||
deferred = Pather(..., auto_render=False)
|
||||
deferred = Pather(..., render='deferred')
|
||||
```
|
||||
|
||||
Top-level imports from `masque` also continue to work.
|
||||
|
||||
`Pather` now defaults to `auto_render=True`, so plain construction replaces the
|
||||
old `Builder` behavior. Use `Pather(..., auto_render=False)` where you
|
||||
`Pather` now defaults to `render='auto'`, so plain construction replaces the
|
||||
old `Builder` behavior. Use `Pather(..., render='deferred')` where you
|
||||
previously used `RenderPather`.
|
||||
|
||||
## `BasicTool` was replaced
|
||||
|
|
@ -218,7 +218,7 @@ class MyTool(Tool):
|
|||
commit_planner=commit,
|
||||
),)
|
||||
|
||||
def render(self, batch: Sequence[RenderStep], **kwargs: Any):
|
||||
def render(self, batch: Sequence[RenderStep]):
|
||||
...
|
||||
```
|
||||
|
||||
|
|
@ -281,10 +281,32 @@ class MyTool(Tool):
|
|||
commit_planner=commit,
|
||||
),)
|
||||
|
||||
def render(self, batch: Sequence[RenderStep], **kwargs):
|
||||
def render(self, batch: Sequence[RenderStep]):
|
||||
...
|
||||
```
|
||||
|
||||
Routing entry points now name every supported route argument explicitly.
|
||||
Custom per-route planning values must be placed under `tool_options`:
|
||||
|
||||
```python
|
||||
pather.jog('A', 4, length=10, tool_options={'process_corner': 'slow'})
|
||||
```
|
||||
|
||||
The mapping is unpacked only for `Tool.primitive_offers()`. Route arguments do
|
||||
not leak into that namespace, and tool options are not forwarded to
|
||||
`Tool.render()`. An offer that needs route-specific render behavior must capture
|
||||
the selected value in its `commit()` result (`RenderStep.data`). `AutoTool`
|
||||
does this automatically for generated straight and S-bend primitives: the flat
|
||||
mapping is snapshotted per selected primitive and passed to its generator as
|
||||
keyword arguments during rendering. AutoTool options must not change generated
|
||||
ports or endpoint geometry. `PathTool` defines no tool options and rejects
|
||||
nonempty mappings.
|
||||
|
||||
AutoTool does not validate generator keyword signatures during planning. A bad
|
||||
keyword therefore raises when the generator runs, normally during rendering.
|
||||
Generators that require route options must provide explicit port metadata at
|
||||
registration; S-bend generators must also provide an explicit `endpoint`.
|
||||
|
||||
Primitive offers are local planning objects:
|
||||
|
||||
- `endpoint_at(parameter)` returns the local output `Port`
|
||||
|
|
|
|||
|
|
@ -271,13 +271,13 @@ class PrimitiveWireTool(Tool):
|
|||
**kwargs: Any,
|
||||
) -> tuple[StraightOffer | BendOffer, ...]:
|
||||
if kind == 'straight':
|
||||
route_kwargs = dict(kwargs)
|
||||
tool_options = dict(kwargs)
|
||||
|
||||
def endpoint_planner(length: float) -> Port:
|
||||
return Port((length, 0), rotation=pi, ptype=self.ptype)
|
||||
|
||||
def commit_planner(length: float) -> WireStraightData:
|
||||
_ = route_kwargs
|
||||
_ = tool_options
|
||||
return WireStraightData(length)
|
||||
|
||||
native_offer = StraightOffer(
|
||||
|
|
@ -354,7 +354,6 @@ class PrimitiveWireTool(Tool):
|
|||
batch: Sequence[RenderStep],
|
||||
*,
|
||||
port_names: tuple[str, str] = ('A', 'B'),
|
||||
**kwargs: Any, # noqa: ARG002 (no per-render options in this example tool)
|
||||
) -> ILibrary:
|
||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'primitive_wire')
|
||||
pat.add_port_pair(names=port_names, ptype=batch[0].start_port.ptype if batch else self.ptype)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ 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.
|
||||
|
||||
Routing is split into four ownership phases:
|
||||
- snapshot: `Pather` resolves the active Tool for each requested port and
|
||||
passes copied ports to the planner as `RoutePortContext` values,
|
||||
|
|
@ -84,6 +88,29 @@ from .logging import PatherLogger
|
|||
logger = logging.getLogger(__name__)
|
||||
RenderPolicy = Literal['auto', 'immediate', 'deferred', 'warn', 'error', 'ignore']
|
||||
RENDER_POLICIES: tuple[RenderPolicy, ...] = ('auto', 'immediate', 'deferred', 'warn', 'error', 'ignore')
|
||||
RESERVED_TOOL_OPTION_KEYS = frozenset(('kind', 'in_ptype', 'out_ptype', 'ccw'))
|
||||
|
||||
|
||||
def _present_route_args(**kwargs: Any) -> dict[str, Any]:
|
||||
"""Return explicitly supplied route arguments for planner validation."""
|
||||
return {key: value for key, value in kwargs.items() if value is not None}
|
||||
|
||||
|
||||
def _validated_tool_options(tool_options: Mapping[str, Any] | None) -> dict[str, Any]:
|
||||
"""Copy and validate custom planning options before any Tool is queried."""
|
||||
if tool_options is None:
|
||||
return {}
|
||||
try:
|
||||
options = dict(tool_options)
|
||||
except (TypeError, ValueError) as err:
|
||||
raise BuildError('tool_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'tool_options keys must be strings; got {nonstring!r}')
|
||||
collisions = sorted(RESERVED_TOOL_OPTION_KEYS & options.keys())
|
||||
if collisions:
|
||||
raise BuildError(f'tool_options cannot override Tool arguments: {", ".join(collisions)}')
|
||||
return options
|
||||
|
||||
|
||||
class Pather(PortList):
|
||||
|
|
@ -548,7 +575,19 @@ class Pather(PortList):
|
|||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
**bounds: Any,
|
||||
out_ptype: str | None = None,
|
||||
each: float | None = None,
|
||||
set_rotation: float | None = None,
|
||||
emin: float | None = None,
|
||||
emax: float | None = None,
|
||||
pmin: float | None = None,
|
||||
pmax: float | None = None,
|
||||
xmin: float | None = None,
|
||||
xmax: float | None = None,
|
||||
ymin: float | None = None,
|
||||
ymax: float | None = None,
|
||||
min_past_furthest: float | None = None,
|
||||
tool_options: Mapping[str, Any] | None = None,
|
||||
) -> Self:
|
||||
"""
|
||||
Route one or more ports using straight segments or single 90-degree bends.
|
||||
|
|
@ -567,12 +606,33 @@ class Pather(PortList):
|
|||
|
||||
`spacing` and `set_rotation` are only valid when using a bundle bound.
|
||||
"""
|
||||
with self._logger.log_operation(self, 'trace', portspec, ccw=ccw, length=length, spacing=spacing, strategy=strategy, **bounds):
|
||||
bounds = _present_route_args(
|
||||
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,
|
||||
)
|
||||
options = _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,
|
||||
):
|
||||
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, **bounds)
|
||||
result = self.planner.plan_trace_route(
|
||||
contexts, ccw, length, spacing=spacing, strategy=strategy,
|
||||
tool_options=options, **bounds,
|
||||
)
|
||||
except (BuildError, NotImplementedError) as err:
|
||||
if not self._dead or route_failure_policy(err) is RouteFailurePolicy.FATAL:
|
||||
raise
|
||||
|
|
@ -584,7 +644,7 @@ class Pather(PortList):
|
|||
0,
|
||||
ccw,
|
||||
context.port.ptype,
|
||||
out_ptype = bounds.get('out_ptype'),
|
||||
out_ptype = out_ptype,
|
||||
)
|
||||
return self
|
||||
if bounds.get('each') is not None:
|
||||
|
|
@ -596,7 +656,7 @@ class Pather(PortList):
|
|||
0,
|
||||
ccw,
|
||||
context.port.ptype,
|
||||
out_ptype = bounds.get('out_ptype'),
|
||||
out_ptype = out_ptype,
|
||||
)
|
||||
return self
|
||||
raise
|
||||
|
|
@ -608,9 +668,27 @@ class Pather(PortList):
|
|||
portspec: str | Sequence[str],
|
||||
ccw: SupportsBool | None,
|
||||
*,
|
||||
length: float | None = None,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
**bounds: Any,
|
||||
out_ptype: str | None = None,
|
||||
each: float | None = None,
|
||||
set_rotation: float | None = None,
|
||||
p: float | None = None,
|
||||
pos: float | None = None,
|
||||
position: float | None = None,
|
||||
x: float | None = None,
|
||||
y: float | None = None,
|
||||
emin: float | None = None,
|
||||
emax: float | None = None,
|
||||
pmin: float | None = None,
|
||||
pmax: float | None = None,
|
||||
xmin: float | None = None,
|
||||
xmax: float | None = None,
|
||||
ymin: float | None = None,
|
||||
ymax: float | None = None,
|
||||
min_past_furthest: float | None = None,
|
||||
tool_options: Mapping[str, Any] | None = None,
|
||||
) -> Self:
|
||||
"""
|
||||
Route until a single positional bound is reached, or delegate to `trace()` for length/bundle bounds.
|
||||
|
|
@ -624,12 +702,39 @@ class Pather(PortList):
|
|||
`strategy` controls straight-first vs turn-first ordering only after
|
||||
cost and structural tie-breakers.
|
||||
"""
|
||||
with self._logger.log_operation(self, 'trace_to', portspec, ccw=ccw, spacing=spacing, strategy=strategy, **bounds):
|
||||
bounds = _present_route_args(
|
||||
length=length,
|
||||
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,
|
||||
)
|
||||
options = _validated_tool_options(tool_options)
|
||||
with self._logger.log_operation(
|
||||
self, 'trace_to', portspec, ccw=ccw, spacing=spacing,
|
||||
strategy=strategy, tool_options=options, **bounds,
|
||||
):
|
||||
if isinstance(portspec, str):
|
||||
portspec = [portspec]
|
||||
contexts = self._route_contexts(portspec)
|
||||
try:
|
||||
result = self.planner.plan_trace_to_route(contexts, ccw, spacing=spacing, strategy=strategy, **bounds)
|
||||
result = self.planner.plan_trace_to_route(
|
||||
contexts, ccw, spacing=spacing, strategy=strategy,
|
||||
tool_options=options, **bounds,
|
||||
)
|
||||
except (BuildError, NotImplementedError) as err:
|
||||
if (
|
||||
not self._dead
|
||||
|
|
@ -655,23 +760,148 @@ class Pather(PortList):
|
|||
0,
|
||||
ccw,
|
||||
context.port.ptype,
|
||||
out_ptype = bounds.get('out_ptype'),
|
||||
out_ptype = out_ptype,
|
||||
)
|
||||
return self
|
||||
self._apply_route_result(result)
|
||||
return self
|
||||
|
||||
def straight(self, portspec: str | Sequence[str], length: float | None = None, **bounds) -> Self:
|
||||
return self.trace_to(portspec, None, length=length, **bounds)
|
||||
def straight(
|
||||
self,
|
||||
portspec: str | Sequence[str],
|
||||
length: float | None = None,
|
||||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
each: float | None = None,
|
||||
set_rotation: float | None = None,
|
||||
p: float | None = None,
|
||||
pos: float | None = None,
|
||||
position: float | None = None,
|
||||
x: float | None = None,
|
||||
y: float | None = None,
|
||||
emin: float | None = None,
|
||||
emax: float | None = None,
|
||||
pmin: float | None = None,
|
||||
pmax: float | None = None,
|
||||
xmin: float | None = None,
|
||||
xmax: float | None = None,
|
||||
ymin: float | None = None,
|
||||
ymax: float | None = None,
|
||||
min_past_furthest: float | None = None,
|
||||
tool_options: Mapping[str, Any] | None = None,
|
||||
) -> Self:
|
||||
return self.trace_to(
|
||||
portspec, None, length=length, spacing=spacing, strategy=strategy,
|
||||
out_ptype=out_ptype, each=each, set_rotation=set_rotation,
|
||||
p=p, pos=pos, position=position, x=x, y=y,
|
||||
emin=emin, emax=emax, pmin=pmin, pmax=pmax,
|
||||
xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax,
|
||||
min_past_furthest=min_past_furthest, tool_options=tool_options,
|
||||
)
|
||||
|
||||
def bend(self, portspec: str | Sequence[str], ccw: SupportsBool, length: float | None = None, **bounds) -> Self:
|
||||
return self.trace_to(portspec, ccw, length=length, **bounds)
|
||||
def bend(
|
||||
self,
|
||||
portspec: str | Sequence[str],
|
||||
ccw: SupportsBool,
|
||||
length: float | None = None,
|
||||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
each: float | None = None,
|
||||
set_rotation: float | None = None,
|
||||
p: float | None = None,
|
||||
pos: float | None = None,
|
||||
position: float | None = None,
|
||||
x: float | None = None,
|
||||
y: float | None = None,
|
||||
emin: float | None = None,
|
||||
emax: float | None = None,
|
||||
pmin: float | None = None,
|
||||
pmax: float | None = None,
|
||||
xmin: float | None = None,
|
||||
xmax: float | None = None,
|
||||
ymin: float | None = None,
|
||||
ymax: float | None = None,
|
||||
min_past_furthest: float | None = None,
|
||||
tool_options: Mapping[str, Any] | None = None,
|
||||
) -> Self:
|
||||
return self.trace_to(
|
||||
portspec, ccw, length=length, spacing=spacing, strategy=strategy,
|
||||
out_ptype=out_ptype, each=each, set_rotation=set_rotation,
|
||||
p=p, pos=pos, position=position, x=x, y=y,
|
||||
emin=emin, emax=emax, pmin=pmin, pmax=pmax,
|
||||
xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax,
|
||||
min_past_furthest=min_past_furthest, tool_options=tool_options,
|
||||
)
|
||||
|
||||
def ccw(self, portspec: str | Sequence[str], length: float | None = None, **bounds) -> Self:
|
||||
return self.bend(portspec, True, length, **bounds)
|
||||
def ccw(
|
||||
self,
|
||||
portspec: str | Sequence[str],
|
||||
length: float | None = None,
|
||||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
each: float | None = None,
|
||||
set_rotation: float | None = None,
|
||||
p: float | None = None,
|
||||
pos: float | None = None,
|
||||
position: float | None = None,
|
||||
x: float | None = None,
|
||||
y: float | None = None,
|
||||
emin: float | None = None,
|
||||
emax: float | None = None,
|
||||
pmin: float | None = None,
|
||||
pmax: float | None = None,
|
||||
xmin: float | None = None,
|
||||
xmax: float | None = None,
|
||||
ymin: float | None = None,
|
||||
ymax: float | None = None,
|
||||
min_past_furthest: float | None = None,
|
||||
tool_options: Mapping[str, Any] | None = None,
|
||||
) -> Self:
|
||||
return self.bend(
|
||||
portspec, True, length, spacing=spacing, strategy=strategy, out_ptype=out_ptype,
|
||||
each=each, set_rotation=set_rotation, p=p, pos=pos, position=position, x=x, y=y,
|
||||
emin=emin, emax=emax, pmin=pmin, pmax=pmax, xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax,
|
||||
min_past_furthest=min_past_furthest, tool_options=tool_options,
|
||||
)
|
||||
|
||||
def cw(self, portspec: str | Sequence[str], length: float | None = None, **bounds) -> Self:
|
||||
return self.bend(portspec, False, length, **bounds)
|
||||
def cw(
|
||||
self,
|
||||
portspec: str | Sequence[str],
|
||||
length: float | None = None,
|
||||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
each: float | None = None,
|
||||
set_rotation: float | None = None,
|
||||
p: float | None = None,
|
||||
pos: float | None = None,
|
||||
position: float | None = None,
|
||||
x: float | None = None,
|
||||
y: float | None = None,
|
||||
emin: float | None = None,
|
||||
emax: float | None = None,
|
||||
pmin: float | None = None,
|
||||
pmax: float | None = None,
|
||||
xmin: float | None = None,
|
||||
xmax: float | None = None,
|
||||
ymin: float | None = None,
|
||||
ymax: float | None = None,
|
||||
min_past_furthest: float | None = None,
|
||||
tool_options: Mapping[str, Any] | None = None,
|
||||
) -> Self:
|
||||
return self.bend(
|
||||
portspec, False, length, spacing=spacing, strategy=strategy, out_ptype=out_ptype,
|
||||
each=each, set_rotation=set_rotation, p=p, pos=pos, position=position, x=x, y=y,
|
||||
emin=emin, emax=emax, pmin=pmin, pmax=pmax, xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax,
|
||||
min_past_furthest=min_past_furthest, tool_options=tool_options,
|
||||
)
|
||||
|
||||
def jog(
|
||||
self,
|
||||
|
|
@ -681,7 +911,13 @@ class Pather(PortList):
|
|||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
**bounds: Any,
|
||||
out_ptype: str | None = None,
|
||||
p: float | None = None,
|
||||
pos: float | None = None,
|
||||
position: float | None = None,
|
||||
x: float | None = None,
|
||||
y: float | None = None,
|
||||
tool_options: Mapping[str, Any] | None = None,
|
||||
) -> Self:
|
||||
"""
|
||||
Route an S-bend.
|
||||
|
|
@ -700,12 +936,20 @@ class Pather(PortList):
|
|||
controls straight-first vs S-first ordering only after cost and
|
||||
structural tie-breakers.
|
||||
"""
|
||||
with self._logger.log_operation(self, 'jog', portspec, offset=offset, length=length, spacing=spacing, strategy=strategy, **bounds):
|
||||
bounds = _present_route_args(out_ptype=out_ptype, p=p, pos=pos, position=position, x=x, y=y)
|
||||
options = _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,
|
||||
):
|
||||
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, **bounds)
|
||||
result = self.planner.plan_jog_route(
|
||||
contexts, offset, length, spacing=spacing, strategy=strategy,
|
||||
tool_options=options, **bounds,
|
||||
)
|
||||
except (BuildError, NotImplementedError) as err:
|
||||
if (
|
||||
not self._dead
|
||||
|
|
@ -723,7 +967,7 @@ class Pather(PortList):
|
|||
0,
|
||||
None,
|
||||
context.port.ptype,
|
||||
out_ptype = bounds.get('out_ptype'),
|
||||
out_ptype = out_ptype,
|
||||
)
|
||||
return self
|
||||
fallback_length = length if length is not None else 0
|
||||
|
|
@ -735,7 +979,7 @@ class Pather(PortList):
|
|||
None,
|
||||
context.port.ptype,
|
||||
out_rot = pi,
|
||||
out_ptype = bounds.get('out_ptype'),
|
||||
out_ptype = out_ptype,
|
||||
)
|
||||
return self
|
||||
self._apply_route_result(result)
|
||||
|
|
@ -749,7 +993,8 @@ class Pather(PortList):
|
|||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
**bounds: Any,
|
||||
out_ptype: str | None = None,
|
||||
tool_options: Mapping[str, Any] | None = None,
|
||||
) -> Self:
|
||||
"""
|
||||
Route a U-turn.
|
||||
|
|
@ -766,12 +1011,20 @@ class Pather(PortList):
|
|||
controls straight-first vs U-first ordering only after cost and
|
||||
structural tie-breakers.
|
||||
"""
|
||||
with self._logger.log_operation(self, 'uturn', portspec, offset=offset, length=length, spacing=spacing, strategy=strategy, **bounds):
|
||||
bounds = _present_route_args(out_ptype=out_ptype)
|
||||
options = _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,
|
||||
):
|
||||
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, **bounds)
|
||||
result = self.planner.plan_uturn_route(
|
||||
contexts, offset, length, spacing=spacing, strategy=strategy,
|
||||
tool_options=options, **bounds,
|
||||
)
|
||||
except (BuildError, NotImplementedError) as err:
|
||||
if (
|
||||
not self._dead
|
||||
|
|
@ -788,7 +1041,7 @@ class Pather(PortList):
|
|||
None,
|
||||
context.port.ptype,
|
||||
out_rot = 0.0,
|
||||
out_ptype = bounds.get('out_ptype'),
|
||||
out_ptype = out_ptype,
|
||||
)
|
||||
return self
|
||||
self._apply_route_result(result)
|
||||
|
|
@ -803,7 +1056,7 @@ class Pather(PortList):
|
|||
plug_destination: bool = True,
|
||||
thru: str | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
**kwargs: Any,
|
||||
tool_options: Mapping[str, Any] | None = None,
|
||||
) -> Self:
|
||||
"""
|
||||
Route one port into another using a bounded primitive-offer selection.
|
||||
|
|
@ -813,9 +1066,8 @@ class Pather(PortList):
|
|||
dogleg topologies. The lowest-cost legal bounded candidate is selected;
|
||||
bend count, step count, and search order are tie-breakers only. `strategy`
|
||||
controls straight-first vs turn-first search order within those ties.
|
||||
Route-shape kwargs such as `length`, `offset`, `ccw`, positional bounds,
|
||||
bundle bounds, and `strategy` are reserved for this internal solve; other tool
|
||||
kwargs are forwarded to primitive offer generation.
|
||||
Custom planning options may be supplied through `tool_options`; they are
|
||||
forwarded only to primitive offer generation.
|
||||
|
||||
If `plug_destination` is `True`, the destination port is consumed by the final step.
|
||||
If `thru` is provided, that port is renamed to the source name after the route is complete.
|
||||
|
|
@ -824,6 +1076,7 @@ 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)
|
||||
with self._logger.log_operation(
|
||||
self,
|
||||
'trace_into',
|
||||
|
|
@ -832,7 +1085,7 @@ class Pather(PortList):
|
|||
plug_destination=plug_destination,
|
||||
thru=thru,
|
||||
strategy=strategy,
|
||||
**kwargs,
|
||||
tool_options=options,
|
||||
):
|
||||
result = self.planner.plan_trace_into(
|
||||
self._route_context(portspec_src),
|
||||
|
|
@ -842,7 +1095,7 @@ class Pather(PortList):
|
|||
plug_destination = plug_destination,
|
||||
thru = thru,
|
||||
strategy = strategy,
|
||||
**kwargs,
|
||||
tool_options = options,
|
||||
)
|
||||
self._apply_route_result(result)
|
||||
return self
|
||||
|
|
@ -1045,46 +1298,259 @@ class PortPather:
|
|||
with self.pather.toolctx(tool, keys=self.ports):
|
||||
yield self
|
||||
|
||||
def trace(self, ccw: SupportsBool | None, length: float | None = None, **kw: Any) -> Self:
|
||||
if 'spacing' not in kw and self.default_spacing is not None and len(self.ports) > 1 and ccw is not None:
|
||||
kw['spacing'] = self.default_spacing
|
||||
self.pather.trace(self.ports, ccw, length, **kw)
|
||||
def trace(
|
||||
self,
|
||||
ccw: SupportsBool | None,
|
||||
length: float | None = None,
|
||||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
each: float | None = None,
|
||||
set_rotation: float | None = None,
|
||||
emin: float | None = None,
|
||||
emax: float | None = None,
|
||||
pmin: float | None = None,
|
||||
pmax: float | None = None,
|
||||
xmin: float | None = None,
|
||||
xmax: float | None = None,
|
||||
ymin: float | None = None,
|
||||
ymax: float | None = None,
|
||||
min_past_furthest: float | 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 and ccw is not None:
|
||||
spacing = self.default_spacing
|
||||
self.pather.trace(
|
||||
self.ports, ccw, length, spacing=spacing, strategy=strategy, out_ptype=out_ptype,
|
||||
each=each, set_rotation=set_rotation, emin=emin, emax=emax, pmin=pmin, pmax=pmax,
|
||||
xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax, min_past_furthest=min_past_furthest,
|
||||
tool_options=tool_options,
|
||||
)
|
||||
return self
|
||||
|
||||
def trace_to(self, ccw: SupportsBool | None, **kw: Any) -> Self:
|
||||
if 'spacing' not in kw and self.default_spacing is not None and len(self.ports) > 1 and ccw is not None:
|
||||
kw['spacing'] = self.default_spacing
|
||||
self.pather.trace_to(self.ports, ccw, **kw)
|
||||
def trace_to(
|
||||
self,
|
||||
ccw: SupportsBool | None,
|
||||
*,
|
||||
length: float | None = None,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
each: float | None = None,
|
||||
set_rotation: float | None = None,
|
||||
p: float | None = None,
|
||||
pos: float | None = None,
|
||||
position: float | None = None,
|
||||
x: float | None = None,
|
||||
y: float | None = None,
|
||||
emin: float | None = None,
|
||||
emax: float | None = None,
|
||||
pmin: float | None = None,
|
||||
pmax: float | None = None,
|
||||
xmin: float | None = None,
|
||||
xmax: float | None = None,
|
||||
ymin: float | None = None,
|
||||
ymax: float | None = None,
|
||||
min_past_furthest: float | 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 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,
|
||||
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,
|
||||
)
|
||||
return self
|
||||
|
||||
def straight(self, length: float | None = None, **kw: Any) -> Self:
|
||||
return self.trace_to(None, length=length, **kw)
|
||||
def straight(
|
||||
self,
|
||||
length: float | None = None,
|
||||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
each: float | None = None,
|
||||
set_rotation: float | None = None,
|
||||
p: float | None = None,
|
||||
pos: float | None = None,
|
||||
position: float | None = None,
|
||||
x: float | None = None,
|
||||
y: float | None = None,
|
||||
emin: float | None = None,
|
||||
emax: float | None = None,
|
||||
pmin: float | None = None,
|
||||
pmax: float | None = None,
|
||||
xmin: float | None = None,
|
||||
xmax: float | None = None,
|
||||
ymin: float | None = None,
|
||||
ymax: float | None = None,
|
||||
min_past_furthest: float | None = None,
|
||||
tool_options: Mapping[str, Any] | None = None,
|
||||
) -> Self:
|
||||
return self.trace_to(
|
||||
None, length=length, spacing=spacing, strategy=strategy, out_ptype=out_ptype,
|
||||
each=each, set_rotation=set_rotation, p=p, pos=pos, position=position, x=x, y=y,
|
||||
emin=emin, emax=emax, pmin=pmin, pmax=pmax, xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax,
|
||||
min_past_furthest=min_past_furthest, tool_options=tool_options,
|
||||
)
|
||||
|
||||
def bend(self, ccw: SupportsBool, length: float | None = None, **kw: Any) -> Self:
|
||||
return self.trace_to(ccw, length=length, **kw)
|
||||
def bend(
|
||||
self,
|
||||
ccw: SupportsBool,
|
||||
length: float | None = None,
|
||||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
each: float | None = None,
|
||||
set_rotation: float | None = None,
|
||||
p: float | None = None,
|
||||
pos: float | None = None,
|
||||
position: float | None = None,
|
||||
x: float | None = None,
|
||||
y: float | None = None,
|
||||
emin: float | None = None,
|
||||
emax: float | None = None,
|
||||
pmin: float | None = None,
|
||||
pmax: float | None = None,
|
||||
xmin: float | None = None,
|
||||
xmax: float | None = None,
|
||||
ymin: float | None = None,
|
||||
ymax: float | None = None,
|
||||
min_past_furthest: float | None = None,
|
||||
tool_options: Mapping[str, Any] | None = None,
|
||||
) -> Self:
|
||||
return self.trace_to(
|
||||
ccw, length=length, spacing=spacing, strategy=strategy, out_ptype=out_ptype,
|
||||
each=each, set_rotation=set_rotation, p=p, pos=pos, position=position, x=x, y=y,
|
||||
emin=emin, emax=emax, pmin=pmin, pmax=pmax, xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax,
|
||||
min_past_furthest=min_past_furthest, tool_options=tool_options,
|
||||
)
|
||||
|
||||
def ccw(self, length: float | None = None, **kw: Any) -> Self:
|
||||
return self.bend(True, length, **kw)
|
||||
def ccw(
|
||||
self,
|
||||
length: float | None = None,
|
||||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
each: float | None = None,
|
||||
set_rotation: float | None = None,
|
||||
p: float | None = None,
|
||||
pos: float | None = None,
|
||||
position: float | None = None,
|
||||
x: float | None = None,
|
||||
y: float | None = None,
|
||||
emin: float | None = None,
|
||||
emax: float | None = None,
|
||||
pmin: float | None = None,
|
||||
pmax: float | None = None,
|
||||
xmin: float | None = None,
|
||||
xmax: float | None = None,
|
||||
ymin: float | None = None,
|
||||
ymax: float | None = None,
|
||||
min_past_furthest: float | None = None,
|
||||
tool_options: Mapping[str, Any] | None = None,
|
||||
) -> Self:
|
||||
return self.bend(
|
||||
True, length, spacing=spacing, strategy=strategy, out_ptype=out_ptype,
|
||||
each=each, set_rotation=set_rotation, p=p, pos=pos, position=position, x=x, y=y,
|
||||
emin=emin, emax=emax, pmin=pmin, pmax=pmax, xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax,
|
||||
min_past_furthest=min_past_furthest, tool_options=tool_options,
|
||||
)
|
||||
|
||||
def cw(self, length: float | None = None, **kw: Any) -> Self:
|
||||
return self.bend(False, length, **kw)
|
||||
def cw(
|
||||
self,
|
||||
length: float | None = None,
|
||||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
each: float | None = None,
|
||||
set_rotation: float | None = None,
|
||||
p: float | None = None,
|
||||
pos: float | None = None,
|
||||
position: float | None = None,
|
||||
x: float | None = None,
|
||||
y: float | None = None,
|
||||
emin: float | None = None,
|
||||
emax: float | None = None,
|
||||
pmin: float | None = None,
|
||||
pmax: float | None = None,
|
||||
xmin: float | None = None,
|
||||
xmax: float | None = None,
|
||||
ymin: float | None = None,
|
||||
ymax: float | None = None,
|
||||
min_past_furthest: float | None = None,
|
||||
tool_options: Mapping[str, Any] | None = None,
|
||||
) -> Self:
|
||||
return self.bend(
|
||||
False, length, spacing=spacing, strategy=strategy, out_ptype=out_ptype,
|
||||
each=each, set_rotation=set_rotation, p=p, pos=pos, position=position, x=x, y=y,
|
||||
emin=emin, emax=emax, pmin=pmin, pmax=pmax, xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax,
|
||||
min_past_furthest=min_past_furthest, tool_options=tool_options,
|
||||
)
|
||||
|
||||
def jog(self, offset: float, length: float | None = None, **kw: Any) -> Self:
|
||||
if 'spacing' not in kw and self.default_spacing is not None and len(self.ports) > 1 and not numpy.isclose(offset, 0):
|
||||
kw['spacing'] = self.default_spacing
|
||||
self.pather.jog(self.ports, offset, length, **kw)
|
||||
def jog(
|
||||
self,
|
||||
offset: float,
|
||||
length: float | None = None,
|
||||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
p: float | None = None,
|
||||
pos: float | None = None,
|
||||
position: float | None = None,
|
||||
x: float | None = None,
|
||||
y: float | 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 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,
|
||||
p=p, pos=pos, position=position, x=x, y=y, tool_options=tool_options,
|
||||
)
|
||||
return self
|
||||
|
||||
def uturn(self, offset: float, length: float | None = None, **kw: Any) -> Self:
|
||||
if 'spacing' not in kw and self.default_spacing is not None and len(self.ports) > 1:
|
||||
kw['spacing'] = self.default_spacing
|
||||
self.pather.uturn(self.ports, offset, length, **kw)
|
||||
def uturn(
|
||||
self,
|
||||
offset: float,
|
||||
length: float | None = None,
|
||||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
tool_options: Mapping[str, Any] | None = None,
|
||||
) -> Self:
|
||||
if spacing is None and self.default_spacing is not None and len(self.ports) > 1:
|
||||
spacing = self.default_spacing
|
||||
self.pather.uturn(
|
||||
self.ports, offset, length, spacing=spacing, strategy=strategy,
|
||||
out_ptype=out_ptype, tool_options=tool_options,
|
||||
)
|
||||
return self
|
||||
|
||||
def trace_into(self, target_port: str, **kwargs) -> Self:
|
||||
if len(self.ports) > 1:
|
||||
raise BuildError(f'Unable use implicit trace_into() with {len(self.ports)} (>1) ports.')
|
||||
self.pather.trace_into(self.ports[0], target_port, **kwargs)
|
||||
def trace_into(
|
||||
self,
|
||||
target_port: str,
|
||||
*,
|
||||
out_ptype: str | None = None,
|
||||
plug_destination: bool = True,
|
||||
thru: str | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
tool_options: Mapping[str, Any] | None = None,
|
||||
) -> Self:
|
||||
if len(self.ports) != 1:
|
||||
raise BuildError(f'Unable to use implicit trace_into() with {len(self.ports)} ports; expected exactly one.')
|
||||
self.pather.trace_into(
|
||||
self.ports[0], target_port, out_ptype=out_ptype, plug_destination=plug_destination,
|
||||
thru=thru, strategy=strategy, tool_options=tool_options,
|
||||
)
|
||||
return self
|
||||
|
||||
def plug(self, other: Abstract | str, other_port: str, **kwargs) -> Self:
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ from pprint import pformat
|
|||
|
||||
import numpy
|
||||
from numpy import pi
|
||||
from numpy.typing import ArrayLike
|
||||
from numpy.typing import ArrayLike, NDArray
|
||||
|
||||
from ...error import BuildError, PortError
|
||||
from ...ports import Port
|
||||
|
|
@ -34,6 +34,22 @@ BUNDLE_BOUND_KEYS: tuple[str, ...] = (
|
|||
)
|
||||
|
||||
|
||||
def finite_scalar(value: Any, name: str, *, nonnegative: bool = False) -> float:
|
||||
"""Return a finite scalar, preserving duck-typed numeric inputs."""
|
||||
try:
|
||||
array = numpy.asarray(value, dtype=float)
|
||||
except (TypeError, ValueError) as err:
|
||||
raise BuildError(f'{name} must be a finite numeric scalar') from err
|
||||
if array.size != 1:
|
||||
raise BuildError(f'{name} must be a scalar; got {array.size} values')
|
||||
result = float(array.reshape(-1)[0])
|
||||
if not numpy.isfinite(result):
|
||||
raise BuildError(f'{name} must be finite')
|
||||
if nonnegative and result < 0:
|
||||
raise BuildError(f'{name} must be nonnegative')
|
||||
return result
|
||||
|
||||
|
||||
def resolved_position_bound(
|
||||
port: Port,
|
||||
bounds: Mapping[str, Any],
|
||||
|
|
@ -50,7 +66,8 @@ def resolved_position_bound(
|
|||
if not allow_length and bounds.get('length') is not None:
|
||||
raise BuildError('length cannot be combined with a positional bound')
|
||||
|
||||
key, value = present[0]
|
||||
key, raw_value = present[0]
|
||||
value = finite_scalar(raw_value, f'{key} positional bound')
|
||||
if port.rotation is None:
|
||||
raise BuildError('Ports must have rotation')
|
||||
is_horiz = bool(numpy.isclose(port.rotation % pi, 0, rtol=1e-9, atol=1e-9))
|
||||
|
|
@ -206,6 +223,8 @@ def su_bundle_specs(
|
|||
"""
|
||||
if spacing is None:
|
||||
raise BuildError(f'Must provide spacing for multi-port {route_name}()')
|
||||
finite_scalar(offset, 'offset')
|
||||
finite_scalar(length, 'length', nonnegative=True)
|
||||
|
||||
ports = {context.portspec: context.port for context in contexts}
|
||||
has_rotation = numpy.array([port.rotation is not None for port in ports.values()], dtype=bool)
|
||||
|
|
@ -229,7 +248,9 @@ def su_bundle_specs(
|
|||
y_order = ((-1 if first_ccw else 1) * rot_offsets[:, 1]).argsort(kind='stable')
|
||||
|
||||
spacing_arr = numpy.asarray(spacing, dtype=float).reshape(-1)
|
||||
steps = numpy.zeros(len(ports), dtype=float)
|
||||
if numpy.any(spacing_arr < 0):
|
||||
raise BuildError('spacing must be nonnegative')
|
||||
steps: NDArray[numpy.float64] = numpy.zeros(len(ports), dtype=float)
|
||||
if spacing_arr.size == 1:
|
||||
steps[1:] = spacing_arr[0]
|
||||
elif spacing_arr.size == len(ports) - 1:
|
||||
|
|
|
|||
|
|
@ -251,7 +251,7 @@ class SolverRequest:
|
|||
|
||||
Public Pather calls are converted into this smaller shape before grammar
|
||||
enumeration. `length`, `jog`, and `out_ptype` become endpoint constraints;
|
||||
`route_kwargs` are forwarded to Tool primitive-offer generation.
|
||||
`tool_options` are forwarded to Tool primitive-offer generation.
|
||||
"""
|
||||
family: PrimitiveKind
|
||||
"""High-level route family being solved."""
|
||||
|
|
@ -259,8 +259,8 @@ class SolverRequest:
|
|||
"""Tool queried for primitive offers."""
|
||||
in_ptype: str | None
|
||||
"""Input ptype at the start of the route."""
|
||||
route_kwargs: Mapping[str, Any]
|
||||
"""Tool kwargs forwarded to primitive-offer generation."""
|
||||
tool_options: Mapping[str, Any]
|
||||
"""Custom Tool kwargs forwarded to primitive-offer generation."""
|
||||
length: float | None = None
|
||||
"""Requested local x displacement, when constrained."""
|
||||
jog: float | None = None
|
||||
|
|
@ -439,9 +439,8 @@ class Solver:
|
|||
out_ptype: str | None = None,
|
||||
extra: Mapping[str, Any] | None = None,
|
||||
) -> tuple[PrimitiveOffer, ...]:
|
||||
"""Query the active Tool with route kwargs and per-query overrides."""
|
||||
kwargs = dict(self.request.route_kwargs)
|
||||
kwargs.pop('out_ptype', None)
|
||||
"""Query the active Tool with custom planning options and internal overrides."""
|
||||
kwargs = dict(self.request.tool_options)
|
||||
if extra:
|
||||
kwargs.update(extra)
|
||||
try:
|
||||
|
|
@ -1018,6 +1017,7 @@ class RoutingPlanner:
|
|||
constrain_jog: bool = False,
|
||||
max_bends: int | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
tool_options: Mapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> SolverRequest:
|
||||
"""Build normalized solver input for one route leg."""
|
||||
|
|
@ -1025,7 +1025,7 @@ class RoutingPlanner:
|
|||
family=family,
|
||||
tool=context.tool,
|
||||
in_ptype=context.port.ptype,
|
||||
route_kwargs=kwargs,
|
||||
tool_options={} if tool_options is None else dict(tool_options),
|
||||
length=length,
|
||||
jog=jog,
|
||||
ccw=ccw,
|
||||
|
|
@ -1070,6 +1070,7 @@ class RoutingPlanner:
|
|||
constrain_jog: bool = False,
|
||||
max_bends: int | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
tool_options: Mapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> RouteLeg:
|
||||
"""Solve one route leg and attach it to its source Pather context."""
|
||||
|
|
@ -1107,6 +1108,7 @@ class RoutingPlanner:
|
|||
constrain_jog=constrain_jog,
|
||||
max_bends=max_bends,
|
||||
strategy=strategy,
|
||||
tool_options=tool_options,
|
||||
**kwargs,
|
||||
)
|
||||
try:
|
||||
|
|
@ -1212,6 +1214,7 @@ class RoutingPlanner:
|
|||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
tool_options: Mapping[str, Any] | None = None,
|
||||
**bounds: Any,
|
||||
) -> PreparedRouteResult:
|
||||
"""Plan straight or single-bend traces, including `each` and bundle-bound modes."""
|
||||
|
|
@ -1225,6 +1228,8 @@ class RoutingPlanner:
|
|||
request_details['spacing'] = spacing
|
||||
if strategy is not None:
|
||||
request_details['strategy'] = strategy
|
||||
if tool_options:
|
||||
request_details['tool_options'] = dict(tool_options)
|
||||
operation: RouteOperation = 'trace'
|
||||
diagnostic_request = request_details
|
||||
return self._plan_trace_route(
|
||||
|
|
@ -1235,6 +1240,7 @@ class RoutingPlanner:
|
|||
diagnostic_request,
|
||||
spacing=spacing,
|
||||
strategy=strategy,
|
||||
tool_options=tool_options,
|
||||
**route_bounds,
|
||||
)
|
||||
|
||||
|
|
@ -1249,6 +1255,7 @@ class RoutingPlanner:
|
|||
*,
|
||||
spacing: float | ArrayLike | None,
|
||||
strategy: RouteTieBreakStrategy | str | None,
|
||||
tool_options: Mapping[str, Any] | None,
|
||||
**route_bounds: Any,
|
||||
) -> PreparedRouteResult:
|
||||
portspec = tuple(context.portspec for context in contexts)
|
||||
|
|
@ -1263,6 +1270,7 @@ class RoutingPlanner:
|
|||
length=length,
|
||||
ccw=ccw,
|
||||
strategy=strategy,
|
||||
tool_options=tool_options,
|
||||
**route_bounds,
|
||||
)
|
||||
return self.prepared_result_from_legs((leg,))
|
||||
|
|
@ -1279,6 +1287,7 @@ class RoutingPlanner:
|
|||
length=each,
|
||||
ccw=ccw,
|
||||
strategy=strategy,
|
||||
tool_options=tool_options,
|
||||
**route_bounds,
|
||||
),
|
||||
)
|
||||
|
|
@ -1295,6 +1304,7 @@ class RoutingPlanner:
|
|||
length=None,
|
||||
ccw=ccw,
|
||||
strategy=strategy,
|
||||
tool_options=tool_options,
|
||||
**route_bounds,
|
||||
)
|
||||
return self.prepared_result_from_legs((leg,))
|
||||
|
|
@ -1321,6 +1331,7 @@ class RoutingPlanner:
|
|||
length=route_length,
|
||||
ccw=ccw,
|
||||
strategy=strategy,
|
||||
tool_options=tool_options,
|
||||
**route_bounds,
|
||||
)
|
||||
actions.append(self.prepared_route_action_from_leg(leg))
|
||||
|
|
@ -1333,6 +1344,7 @@ class RoutingPlanner:
|
|||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
tool_options: Mapping[str, Any] | None = None,
|
||||
**bounds: Any,
|
||||
) -> PreparedRouteResult:
|
||||
"""Plan `trace_to()` by resolving positional targets or delegating to `trace()` modes."""
|
||||
|
|
@ -1344,6 +1356,8 @@ class RoutingPlanner:
|
|||
request_details['spacing'] = spacing
|
||||
if strategy is not None:
|
||||
request_details['strategy'] = strategy
|
||||
if tool_options:
|
||||
request_details['tool_options'] = dict(tool_options)
|
||||
operation: RouteOperation = 'trace_to'
|
||||
diagnostic_request = request_details
|
||||
return self._plan_trace_to_route(
|
||||
|
|
@ -1353,6 +1367,7 @@ class RoutingPlanner:
|
|||
diagnostic_request,
|
||||
spacing=spacing,
|
||||
strategy=strategy,
|
||||
tool_options=tool_options,
|
||||
**route_bounds,
|
||||
)
|
||||
|
||||
|
|
@ -1366,6 +1381,7 @@ class RoutingPlanner:
|
|||
*,
|
||||
spacing: float | ArrayLike | None,
|
||||
strategy: RouteTieBreakStrategy | str | None,
|
||||
tool_options: Mapping[str, Any] | None,
|
||||
**route_bounds: Any,
|
||||
) -> PreparedRouteResult:
|
||||
if len(contexts) == 1:
|
||||
|
|
@ -1383,6 +1399,7 @@ class RoutingPlanner:
|
|||
diagnostic_request,
|
||||
spacing=spacing,
|
||||
strategy=strategy,
|
||||
tool_options=tool_options,
|
||||
**route_bounds,
|
||||
)
|
||||
|
||||
|
|
@ -1402,6 +1419,7 @@ class RoutingPlanner:
|
|||
length=length,
|
||||
ccw=ccw,
|
||||
strategy=strategy,
|
||||
tool_options=tool_options,
|
||||
**other_bounds,
|
||||
)
|
||||
return self.prepared_result_from_legs((leg,))
|
||||
|
|
@ -1414,9 +1432,11 @@ class RoutingPlanner:
|
|||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
tool_options: Mapping[str, Any] | None = None,
|
||||
**bounds: Any,
|
||||
) -> PreparedRouteResult:
|
||||
"""Plan S-bend routes for single ports or spaced bundles."""
|
||||
offset = planner_bounds.finite_scalar(offset, 'offset')
|
||||
request_details = {'offset': offset, **{
|
||||
key: value for key, value in bounds.items() if value is not None
|
||||
}}
|
||||
|
|
@ -1426,6 +1446,8 @@ class RoutingPlanner:
|
|||
request_details['spacing'] = spacing
|
||||
if strategy is not None:
|
||||
request_details['strategy'] = strategy
|
||||
if tool_options:
|
||||
request_details['tool_options'] = dict(tool_options)
|
||||
operation: RouteOperation = 'jog'
|
||||
diagnostic_request = request_details
|
||||
if numpy.isclose(offset, 0):
|
||||
|
|
@ -1437,6 +1459,7 @@ class RoutingPlanner:
|
|||
length=length,
|
||||
spacing=spacing,
|
||||
strategy=strategy,
|
||||
tool_options=tool_options,
|
||||
**bounds,
|
||||
)
|
||||
route_bounds = dict(bounds)
|
||||
|
|
@ -1460,6 +1483,7 @@ class RoutingPlanner:
|
|||
operation,
|
||||
diagnostic_request,
|
||||
strategy=strategy,
|
||||
tool_options=tool_options,
|
||||
**other_bounds,
|
||||
)
|
||||
))
|
||||
|
|
@ -1471,6 +1495,7 @@ class RoutingPlanner:
|
|||
length=length,
|
||||
jog=offset,
|
||||
strategy=strategy,
|
||||
tool_options=tool_options,
|
||||
**other_bounds,
|
||||
)
|
||||
return self.prepared_result_from_legs((leg,))
|
||||
|
|
@ -1483,9 +1508,11 @@ class RoutingPlanner:
|
|||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
tool_options: Mapping[str, Any] | None = None,
|
||||
**bounds: Any,
|
||||
) -> PreparedRouteResult:
|
||||
"""Plan U-turn routes for single ports or spaced bundles."""
|
||||
offset = planner_bounds.finite_scalar(offset, 'offset')
|
||||
route_bounds = dict(bounds)
|
||||
request_details = {'offset': offset, **{
|
||||
key: value for key, value in route_bounds.items() if value is not None
|
||||
|
|
@ -1496,6 +1523,8 @@ class RoutingPlanner:
|
|||
request_details['spacing'] = spacing
|
||||
if strategy is not None:
|
||||
request_details['strategy'] = strategy
|
||||
if tool_options:
|
||||
request_details['tool_options'] = dict(tool_options)
|
||||
operation: RouteOperation = 'uturn'
|
||||
diagnostic_request = request_details
|
||||
portspec = tuple(context.portspec for context in contexts)
|
||||
|
|
@ -1512,6 +1541,7 @@ class RoutingPlanner:
|
|||
operation,
|
||||
diagnostic_request,
|
||||
strategy=strategy,
|
||||
tool_options=tool_options,
|
||||
**route_bounds,
|
||||
)
|
||||
))
|
||||
|
|
@ -1523,6 +1553,7 @@ class RoutingPlanner:
|
|||
length=length,
|
||||
jog=offset,
|
||||
strategy=strategy,
|
||||
tool_options=tool_options,
|
||||
**route_bounds,
|
||||
)
|
||||
return self.prepared_result_from_legs((leg,))
|
||||
|
|
@ -1539,6 +1570,7 @@ class RoutingPlanner:
|
|||
/,
|
||||
*,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
tool_options: Mapping[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> tuple[RouteLeg, ...]:
|
||||
"""
|
||||
|
|
@ -1557,6 +1589,7 @@ class RoutingPlanner:
|
|||
length=length,
|
||||
jog=offset,
|
||||
strategy=strategy,
|
||||
tool_options=tool_options,
|
||||
**kwargs,
|
||||
),)
|
||||
route_name = 'jog' if kind == 's' else 'uturn'
|
||||
|
|
@ -1573,6 +1606,7 @@ class RoutingPlanner:
|
|||
length=length,
|
||||
jog=offset,
|
||||
strategy=strategy,
|
||||
tool_options=tool_options,
|
||||
**kwargs,
|
||||
)
|
||||
base_length = anchor.candidate.public_length
|
||||
|
|
@ -1589,6 +1623,7 @@ class RoutingPlanner:
|
|||
length=spec_length,
|
||||
jog=spec_offset,
|
||||
strategy=strategy,
|
||||
tool_options=tool_options,
|
||||
**kwargs,
|
||||
)
|
||||
else:
|
||||
|
|
@ -1600,6 +1635,7 @@ class RoutingPlanner:
|
|||
length=spec_length,
|
||||
jog=spec_offset,
|
||||
strategy=strategy,
|
||||
tool_options=tool_options,
|
||||
**kwargs,
|
||||
)
|
||||
return tuple(routes_by_name[spec_portspec] for spec_portspec, _length, _offset in specs)
|
||||
|
|
@ -1614,17 +1650,9 @@ class RoutingPlanner:
|
|||
plug_destination: bool,
|
||||
thru: str | None,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
**kwargs: Any,
|
||||
tool_options: Mapping[str, Any] | None = None,
|
||||
) -> PreparedRouteResult:
|
||||
"""Plan a bounded route from one source port into a destination port."""
|
||||
reserved = {
|
||||
'portspec', 'ccw', 'length', 'offset', 'plug_into', 'spacing', 'each', 'set_rotation',
|
||||
*planner_bounds.POSITION_KEYS,
|
||||
*planner_bounds.BUNDLE_BOUND_KEYS,
|
||||
}
|
||||
collisions = sorted(set(kwargs) & reserved)
|
||||
if collisions:
|
||||
raise BuildError(f'trace_into() kwargs cannot override route arguments: {", ".join(collisions)}')
|
||||
if out_ptype is None:
|
||||
out_ptype = port_dst.ptype
|
||||
if context_src.port.rotation is None or port_dst.rotation is None:
|
||||
|
|
@ -1642,7 +1670,8 @@ class RoutingPlanner:
|
|||
constrain_jog=family == 'bend',
|
||||
max_bends=self.TRACE_INTO_MAX_BENDS,
|
||||
strategy=strategy,
|
||||
**(dict(kwargs) | {'out_ptype': out_ptype}),
|
||||
tool_options=tool_options,
|
||||
out_ptype=out_ptype,
|
||||
)
|
||||
solver = self.solver_for_request(request)
|
||||
candidate = None
|
||||
|
|
|
|||
|
|
@ -49,8 +49,9 @@ from typing import Literal, Any, Self
|
|||
from collections import ChainMap
|
||||
from collections.abc import Sequence, Callable, Mapping
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass, field, replace
|
||||
from math import isclose as scalar_isclose, isfinite as scalar_isfinite, isnan as scalar_isnan, sqrt
|
||||
from types import MappingProxyType
|
||||
|
||||
import numpy
|
||||
from numpy.typing import NDArray
|
||||
|
|
@ -693,6 +694,10 @@ class Tool(ABC):
|
|||
- `'s'`: a non-turning `SOffer`
|
||||
- `'u'`: an `UOffer`
|
||||
|
||||
Other `kwargs` come only from the Pather call's explicit
|
||||
`tool_options` mapping. Tools are responsible for validating the keys
|
||||
they support.
|
||||
|
||||
`Pather` applies any requested `out_ptype` to the final route endpoint,
|
||||
not to every primitive in the route. Intermediate ptypes are
|
||||
solver-selected, and heterogeneous straight/S offers may be used as
|
||||
|
|
@ -706,7 +711,6 @@ class Tool(ABC):
|
|||
batch: Sequence[RenderStep],
|
||||
*,
|
||||
port_names: tuple[str, str] = ('A', 'B'),
|
||||
**kwargs,
|
||||
) -> ILibrary:
|
||||
"""
|
||||
Render a compatible batch of selected route steps into geometry.
|
||||
|
|
@ -722,7 +726,6 @@ class Tool(ABC):
|
|||
primitive render data.
|
||||
port_names: The topcell's input and output ports should be named
|
||||
`port_names[0]` and `port_names[1]` respectively.
|
||||
kwargs: Custom tool-specific parameters.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
|
@ -778,17 +781,20 @@ class AutoTool(Tool):
|
|||
when only the declared direction should be available.
|
||||
|
||||
Straight and S-bend generator functions may return either a `Pattern` or a
|
||||
single-top `Library`. Extra keyword arguments passed to `render()` are
|
||||
forwarded to those generators.
|
||||
single-top `Library`. Route `tool_options` are snapshotted into committed
|
||||
generated data and passed to each selected generator as keyword arguments.
|
||||
These options are render-only: they may change geometry, layers, or
|
||||
annotations, but must not change generated port topology or endpoints.
|
||||
"""
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class GeneratedData:
|
||||
""" Deferred render data for one generated primitive offer. """
|
||||
"""Deferred generator call, including its route-specific option snapshot."""
|
||||
fn: GeneratedPrimitiveFn
|
||||
port_name: str
|
||||
parameter: float
|
||||
mirrored: bool = False
|
||||
tool_options: Mapping[str, Any] = field(default_factory=lambda: MappingProxyType({}))
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ReusableData:
|
||||
|
|
@ -972,7 +978,7 @@ class AutoTool(Tool):
|
|||
|
||||
If `ptype` or `in_port_name` is omitted, one in-domain example is
|
||||
generated and the missing metadata is inferred from an equivalent
|
||||
two-port straight.
|
||||
two-port straight. Metadata inference does not receive route options.
|
||||
"""
|
||||
ptype, in_port_name = self._infer_straight_metadata(fn, length_range, ptype, in_port_name)
|
||||
priority_bias = len(self._straight_offers) * BUILTIN_PRIORITY_STEP
|
||||
|
|
@ -1062,6 +1068,7 @@ class AutoTool(Tool):
|
|||
|
||||
If `ptype` or port names are omitted, one in-domain example is generated
|
||||
and the missing metadata is inferred from an equivalent two-port S-bend.
|
||||
Metadata inference and endpoint planning do not receive route options.
|
||||
"""
|
||||
ptype, in_port_name, out_port_name = self._infer_sbend_metadata(
|
||||
fn,
|
||||
|
|
@ -1225,7 +1232,7 @@ class AutoTool(Tool):
|
|||
dxy, _ = sbpat[in_port_name].measure_travel(sbpat[out_port_name])
|
||||
return dxy
|
||||
|
||||
def _rendered_bbox(self, render: Callable[[ILibrary, tuple[str, str]], None]) -> NDArray[numpy.float64]:
|
||||
def _rendered_bbox(self, render: Callable[[ILibrary, tuple[str, str]], Any]) -> NDArray[numpy.float64]:
|
||||
port_names = ('A', 'B')
|
||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'primitive_bbox')
|
||||
pat.add_port_pair(names=port_names)
|
||||
|
|
@ -1247,7 +1254,7 @@ class AutoTool(Tool):
|
|||
return numpy.asarray(bounds, dtype=float)
|
||||
|
||||
def _bbox_for_data(self, data: Any) -> NDArray[numpy.float64]:
|
||||
return self._rendered_bbox(lambda tree, names: self._render_data(data, tree, names, {}))
|
||||
return self._rendered_bbox(lambda tree, names: self._render_data(data, tree, names))
|
||||
|
||||
def _add_transition_direction(
|
||||
self,
|
||||
|
|
@ -1330,21 +1337,45 @@ class AutoTool(Tool):
|
|||
**kwargs,
|
||||
) -> tuple[PrimitiveOffer, ...]:
|
||||
_ = out_ptype
|
||||
ccw = kwargs.pop('ccw', None)
|
||||
tool_options = MappingProxyType(dict(kwargs))
|
||||
|
||||
def configured(offer: PrimitiveOffer) -> PrimitiveOffer:
|
||||
if not tool_options:
|
||||
return offer
|
||||
|
||||
def commit(parameter: float) -> Any:
|
||||
data = offer.commit(parameter)
|
||||
if not isinstance(data, self.GeneratedData):
|
||||
raise BuildError('AutoTool generated offer returned unexpected commit data')
|
||||
return replace(data, tool_options=tool_options)
|
||||
|
||||
def bbox(parameter: float) -> NDArray[numpy.float64]:
|
||||
return self._bbox_for_data(commit(parameter))
|
||||
|
||||
return replace(
|
||||
offer,
|
||||
commit_planner=commit,
|
||||
bbox_planner=bbox if offer.bbox_planner is not None else None,
|
||||
)
|
||||
|
||||
if kind == 'straight':
|
||||
in_key = self._wildcard_ptype_key(in_ptype)
|
||||
return (
|
||||
*self._transition_adapter_offers_by_key.get(('straight', in_key), ()),
|
||||
*self._straight_offers,
|
||||
*(configured(offer) for offer in self._straight_offers),
|
||||
)
|
||||
|
||||
if kind == 'bend':
|
||||
return tuple(self._bend_offers[int(bool(kwargs['ccw']))])
|
||||
if ccw is None:
|
||||
raise BuildError('AutoTool bend offer discovery requires ccw')
|
||||
return tuple(self._bend_offers[int(bool(ccw))])
|
||||
|
||||
if kind == 's':
|
||||
in_key = self._wildcard_ptype_key(in_ptype)
|
||||
return (
|
||||
*self._transition_adapter_offers_by_key.get(('s', in_key), ()),
|
||||
*self._s_offers,
|
||||
*(configured(offer) for offer in self._s_offers),
|
||||
)
|
||||
|
||||
if kind == 'u':
|
||||
|
|
@ -1356,13 +1387,12 @@ class AutoTool(Tool):
|
|||
data: GeneratedData,
|
||||
tree: ILibrary,
|
||||
port_names: tuple[str, str],
|
||||
gen_kwargs: dict[str, Any],
|
||||
) -> ILibrary:
|
||||
if numpy.isclose(data.parameter, 0):
|
||||
return tree
|
||||
|
||||
pat = tree.top_pattern()
|
||||
generated = data.fn(data.parameter, **gen_kwargs)
|
||||
generated = data.fn(data.parameter, **data.tool_options)
|
||||
pmap = {port_names[1]: data.port_name}
|
||||
if isinstance(generated, Pattern):
|
||||
pat.plug(generated, pmap, append=True, mirrored=data.mirrored)
|
||||
|
|
@ -1387,10 +1417,9 @@ class AutoTool(Tool):
|
|||
data: Any,
|
||||
tree: ILibrary,
|
||||
port_names: tuple[str, str],
|
||||
gen_kwargs: dict[str, Any],
|
||||
) -> ILibrary:
|
||||
if isinstance(data, self.GeneratedData):
|
||||
return self._render_generated(data=data, tree=tree, port_names=port_names, gen_kwargs=gen_kwargs)
|
||||
return self._render_generated(data=data, tree=tree, port_names=port_names)
|
||||
if isinstance(data, self.ReusableData):
|
||||
return self._render_reusable(data=data, tree=tree, port_names=port_names)
|
||||
raise BuildError(f'Unexpected AutoTool render data {type(data).__name__}')
|
||||
|
|
@ -1400,7 +1429,6 @@ class AutoTool(Tool):
|
|||
batch: Sequence[RenderStep],
|
||||
*,
|
||||
port_names: tuple[str, str] = ('A', 'B'),
|
||||
**kwargs,
|
||||
) -> ILibrary:
|
||||
|
||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL')
|
||||
|
|
@ -1408,7 +1436,7 @@ class AutoTool(Tool):
|
|||
|
||||
for step in batch:
|
||||
assert step.tool == self
|
||||
self._render_data(step.data, tree, port_names, kwargs)
|
||||
self._render_data(step.data, tree, port_names)
|
||||
return tree
|
||||
|
||||
|
||||
|
|
@ -1470,6 +1498,10 @@ class PathTool(Tool):
|
|||
out_ptype: str | None = None,
|
||||
**kwargs,
|
||||
) -> tuple[PrimitiveOffer, ...]:
|
||||
allowed = {'ccw'} if kind == 'bend' else set()
|
||||
unsupported = sorted(set(kwargs) - allowed)
|
||||
if unsupported:
|
||||
raise BuildError(f'PathTool does not support tool options: {", ".join(unsupported)}')
|
||||
if kind == 'u':
|
||||
return ()
|
||||
|
||||
|
|
@ -1559,7 +1591,6 @@ class PathTool(Tool):
|
|||
batch: Sequence[RenderStep],
|
||||
*,
|
||||
port_names: tuple[str, str] = ('A', 'B'),
|
||||
**kwargs, # noqa: ARG002 (unused)
|
||||
) -> ILibrary:
|
||||
|
||||
# Transform the batch so the first port is local (at 0,0) but retains its global rotation.
|
||||
|
|
|
|||
|
|
@ -131,6 +131,8 @@ def ell(
|
|||
else:
|
||||
if set_rotation is None:
|
||||
raise BuildError('set_rotation must be specified if no ports have rotations!')
|
||||
if not numpy.isfinite(set_rotation):
|
||||
raise BuildError('set_rotation must be finite')
|
||||
rotations = numpy.full_like(has_rotation, set_rotation, dtype=float)
|
||||
|
||||
is_horizontal = numpy.isclose(rotations[0] % pi, 0)
|
||||
|
|
@ -156,7 +158,11 @@ def ell(
|
|||
ch_offsets = numpy.zeros_like(y_order)
|
||||
else:
|
||||
spacing_arr = numpy.asarray(spacing, dtype=float).reshape(-1)
|
||||
steps = numpy.zeros_like(y_order)
|
||||
if not numpy.all(numpy.isfinite(spacing_arr)):
|
||||
raise BuildError('spacing must contain only finite values')
|
||||
if numpy.any(spacing_arr < 0):
|
||||
raise BuildError('spacing must be nonnegative')
|
||||
steps: NDArray[numpy.float64] = numpy.zeros(len(y_order), dtype=float)
|
||||
if spacing_arr.size == 1:
|
||||
steps[1:] = spacing_arr[0]
|
||||
elif spacing_arr.size == len(ports) - 1:
|
||||
|
|
|
|||
|
|
@ -800,7 +800,7 @@ def test_autotool_s_offer_bbox_matches_rendered_sbend() -> None:
|
|||
.add_sbend(make_wide_sbend, "core", "A", "B", jog_range=(0, 1e8))
|
||||
.add_transition(lib.abstract("xin"), "EXT", "CORE")
|
||||
)
|
||||
offer, _out_port, _data = selected_offer(tool, "s", 4, length=15, in_ptype="core")
|
||||
offer, _out_port, _data = selected_offer(tool, "s", 4, in_ptype="core")
|
||||
|
||||
assert_offer_bbox_matches_rendered_offer(tool, offer, 4, lib)
|
||||
|
||||
|
|
@ -1006,7 +1006,7 @@ def test_pather_autotool_uses_l_offer_domains(multi_bend_tool: tuple[AutoTool, L
|
|||
assert bend_step.data.abstract.name == "b1"
|
||||
|
||||
|
||||
def test_autotool_generated_primitives_do_not_capture_route_kwargs() -> None:
|
||||
def test_autotool_generated_primitives_snapshot_route_options() -> None:
|
||||
markers: list[str | None] = []
|
||||
|
||||
def make_marked_straight(length: float, marker: str | None = None) -> Pattern:
|
||||
|
|
@ -1017,10 +1017,90 @@ def test_autotool_generated_primitives_do_not_capture_route_kwargs() -> None:
|
|||
p = Pather(Library(), tools=tool, render='deferred')
|
||||
p.ports["A"] = Port((0, 0), 0, ptype="wire")
|
||||
|
||||
p.straight("A", 5, marker="route")
|
||||
first_options = {'marker': 'first'}
|
||||
p.straight("A", 5, tool_options=first_options)
|
||||
first_options['marker'] = 'mutated'
|
||||
p.straight("A", 6, tool_options={'marker': 'second'})
|
||||
|
||||
first_data, second_data = (step.data for step in p._paths['A'])
|
||||
assert isinstance(first_data, AutoTool.GeneratedData)
|
||||
assert isinstance(second_data, AutoTool.GeneratedData)
|
||||
assert dict(first_data.tool_options) == {'marker': 'first'}
|
||||
assert dict(second_data.tool_options) == {'marker': 'second'}
|
||||
|
||||
p.render()
|
||||
|
||||
assert markers == [None]
|
||||
assert markers == ['first', 'second']
|
||||
|
||||
|
||||
def test_autotool_route_options_do_not_attach_to_reusable_bends(
|
||||
multi_bend_tool: tuple[AutoTool, Library],
|
||||
) -> None:
|
||||
tool, _lib = multi_bend_tool
|
||||
offers = tool.primitive_offers('bend', in_ptype='wire', ccw=True, marker='ignored')
|
||||
|
||||
assert offers
|
||||
assert all(isinstance(offer.commit(offer.parameter_domain[0]), AutoTool.ReusableData) for offer in offers)
|
||||
|
||||
|
||||
def test_autotool_sbend_generator_receives_route_options() -> None:
|
||||
markers: list[str] = []
|
||||
|
||||
def make_marked_sbend(jog: float, *, marker: str) -> Pattern:
|
||||
markers.append(marker)
|
||||
pat = Pattern()
|
||||
pat.ports['A'] = Port((0, 0), 0, ptype='wire')
|
||||
pat.ports['B'] = Port((3, jog), pi, ptype='wire')
|
||||
return pat
|
||||
|
||||
tool = AutoTool().add_sbend(
|
||||
make_marked_sbend,
|
||||
'wire',
|
||||
'A',
|
||||
'B',
|
||||
endpoint=lambda jog: Port((3, jog), pi, ptype='wire'),
|
||||
)
|
||||
p = Pather(Library(), tools=tool, render='deferred')
|
||||
p.ports['A'] = Port((0, 0), 0, ptype='wire')
|
||||
|
||||
p.jog('A', 4, length=3, tool_options={'marker': 'sbend'})
|
||||
p.render()
|
||||
|
||||
assert markers == ['sbend']
|
||||
|
||||
|
||||
def test_autotool_generated_bbox_uses_route_options() -> None:
|
||||
tool = AutoTool().add_straight(make_straight, 'wire', 'A')
|
||||
offer = tool.primitive_offers('straight', width=6)[0]
|
||||
|
||||
assert_allclose(offer.bbox_at(10), [[0, -3], [10, 3]])
|
||||
|
||||
|
||||
def test_autotool_unsupported_generator_option_fails_during_render() -> None:
|
||||
tool = AutoTool().add_straight(make_straight, 'wire', 'A')
|
||||
p = Pather(Library(), tools=tool, render='deferred')
|
||||
p.ports['A'] = Port((0, 0), 0, ptype='wire')
|
||||
|
||||
p.straight('A', 5, tool_options={'unknown_generator_option': True})
|
||||
|
||||
with pytest.raises(TypeError, match='unknown_generator_option'):
|
||||
p.render()
|
||||
|
||||
|
||||
def test_autotool_generator_options_must_preserve_planned_endpoint() -> None:
|
||||
def shifted_straight(length: float, endpoint_shift: float = 0) -> Pattern:
|
||||
pat = make_straight(length)
|
||||
pat.ports['B'].x += endpoint_shift
|
||||
return pat
|
||||
|
||||
tool = AutoTool().add_straight(shifted_straight, 'wire', 'A')
|
||||
p = Pather(Library(), tools=tool, render='deferred')
|
||||
p.ports['A'] = Port((0, 0), 0, ptype='wire')
|
||||
|
||||
p.straight('A', 5, tool_options={'endpoint_shift': 1})
|
||||
|
||||
with pytest.raises(BuildError, match='does not match planned endpoint'):
|
||||
p.render()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ccw", [False, True])
|
||||
|
|
@ -1204,7 +1284,7 @@ def test_pather_autotool_pure_sbend_with_transition_dx() -> None:
|
|||
assert_allclose(trans_port.offset, [5, 0])
|
||||
assert isinstance(trans_data, AutoTool.ReusableData)
|
||||
|
||||
s_offer, s_port, s_data = selected_offer(tool, "s", 4, length=15, in_ptype="core")
|
||||
s_offer, s_port, s_data = selected_offer(tool, "s", 4, in_ptype="core")
|
||||
assert_allclose(s_port.offset, [10, 4])
|
||||
assert isinstance(s_data, AutoTool.GeneratedData)
|
||||
assert s_offer.out_ptype == "core"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
from collections.abc import Sequence
|
||||
from typing import Any, Literal, Never
|
||||
import inspect
|
||||
|
||||
import pytest
|
||||
import numpy
|
||||
from numpy import pi
|
||||
|
||||
from masque import MinimumStatus, Pather, Library, Port, RouteError, RouteFailurePolicy
|
||||
from masque import MinimumStatus, Pather, PortPather, Library, Port, RouteError, RouteFailurePolicy
|
||||
from masque.builder.planner import RoutePortContext, RoutingPlanner
|
||||
from masque.builder.planner.planner import NoLegalRouteError
|
||||
from masque.builder.tools import BendOffer, PathTool, RenderStep, StraightOffer, Tool, UOffer
|
||||
|
|
@ -199,7 +200,7 @@ def test_route_error_reports_preferred_minimum_and_request_details() -> None:
|
|||
)
|
||||
|
||||
with pytest.raises(RouteError) as exc_info:
|
||||
p.ccw('A', 1, out_ptype='wide', diagnostic_context='tool-value')
|
||||
p.ccw('A', 1, out_ptype='wide', tool_options={'diagnostic_context': 'tool-value'})
|
||||
|
||||
details = exc_info.value.details
|
||||
assert isinstance(exc_info.value, BuildError)
|
||||
|
|
@ -211,7 +212,7 @@ def test_route_error_reports_preferred_minimum_and_request_details() -> None:
|
|||
'ccw': True,
|
||||
'length': 1,
|
||||
'out_ptype': 'wide',
|
||||
'diagnostic_context': 'tool-value',
|
||||
'tool_options': {'diagnostic_context': 'tool-value'},
|
||||
}
|
||||
assert details.resolved_length == 1
|
||||
assert details.resolved_jog is None
|
||||
|
|
@ -705,7 +706,7 @@ def test_pather_uturn_rejects_routing_bounds(kwargs: dict[str, int]) -> None:
|
|||
p = Pather(Library(), tools=PathTool(layer='M1', width=1, ptype='wire'))
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
with pytest.raises(BuildError, match='Unsupported routing bounds for uturn'):
|
||||
with pytest.raises(TypeError, match='unexpected keyword argument'):
|
||||
p.uturn('A', 4, **kwargs)
|
||||
|
||||
def test_pather_uturn_omitted_length_uses_minimum_length_route() -> None:
|
||||
|
|
@ -935,3 +936,74 @@ def test_pather_uturn_failed_two_bend_route_is_atomic() -> None:
|
|||
assert numpy.allclose(p.pattern.ports['A'].offset, (0, 0))
|
||||
assert p.pattern.ports['A'].rotation == 0
|
||||
assert len(p._paths['A']) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize('route_type', [Pather, PortPather])
|
||||
@pytest.mark.parametrize('name', ['trace', 'trace_to', 'straight', 'bend', 'ccw', 'cw', 'jog', 'uturn', 'trace_into'])
|
||||
def test_routing_entry_points_have_no_catch_all_kwargs(route_type: type, name: str) -> None:
|
||||
parameters = inspect.signature(getattr(route_type, name)).parameters.values()
|
||||
assert all(parameter.kind is not inspect.Parameter.VAR_KEYWORD for parameter in parameters)
|
||||
|
||||
|
||||
def test_routing_typo_fails_before_tool_lookup() -> None:
|
||||
tool = RequestCountingTool()
|
||||
p = Pather(Library(), tools=tool, render='deferred')
|
||||
|
||||
with pytest.raises(TypeError, match='unexpected keyword argument'):
|
||||
p.straight('A', lenght=10) # type: ignore[call-arg]
|
||||
|
||||
assert tool.offer_calls == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize('tool_options', [{1: 'bad'}, {'ccw': False}, {'out_ptype': 'bad'}])
|
||||
def test_tool_options_reject_invalid_or_reserved_keys(tool_options: dict[Any, Any]) -> None:
|
||||
tool = RequestCountingTool()
|
||||
p = Pather(Library(), tools=tool, render='deferred')
|
||||
|
||||
with pytest.raises(BuildError, match='tool_options'):
|
||||
p.trace('A', None, tool_options=tool_options)
|
||||
|
||||
assert tool.offer_calls == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'operation',
|
||||
[
|
||||
lambda p: p.jog('A', numpy.nan, length=1),
|
||||
lambda p: p.uturn('A', numpy.inf, length=1),
|
||||
lambda p: p.straight('A', x=numpy.nan),
|
||||
],
|
||||
ids=['jog-offset', 'uturn-offset', 'position-bound'],
|
||||
)
|
||||
def test_nonfinite_route_geometry_fails_before_offer_query(operation: Any) -> None:
|
||||
tool = RequestCountingTool()
|
||||
p = Pather(
|
||||
Library(),
|
||||
ports={'A': Port((0, 0), rotation=0, ptype='wire')},
|
||||
tools=tool,
|
||||
render='deferred',
|
||||
)
|
||||
|
||||
with pytest.raises(BuildError, match='finite'):
|
||||
operation(p)
|
||||
|
||||
assert tool.offer_calls == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize('spacing', [-1, numpy.nan, numpy.inf])
|
||||
def test_invalid_bundle_spacing_fails_before_offer_query(spacing: float) -> None:
|
||||
tool = RequestCountingTool()
|
||||
p = Pather(
|
||||
Library(),
|
||||
ports={
|
||||
'A': Port((0, 0), rotation=0, ptype='wire'),
|
||||
'B': Port((0, 4), rotation=0, ptype='wire'),
|
||||
},
|
||||
tools=tool,
|
||||
render='deferred',
|
||||
)
|
||||
|
||||
with pytest.raises(BuildError, match='spacing'):
|
||||
p.trace(['A', 'B'], True, xmin=-10, spacing=spacing)
|
||||
|
||||
assert tool.offer_calls == 0
|
||||
|
|
|
|||
|
|
@ -313,7 +313,7 @@ def test_pathtool_bend_offer_bbox_matches_path_bounds() -> None:
|
|||
|
||||
def test_pathtool_s_offer_bbox_uses_intrinsic_minimum_length() -> None:
|
||||
tool = PathTool(layer=(1, 0), width=2, ptype='wire')
|
||||
offer = tool.primitive_offers('s', in_ptype='wire', length=6)[0]
|
||||
offer = tool.primitive_offers('s', in_ptype='wire')[0]
|
||||
|
||||
bounds = offer.bbox_at(3)
|
||||
expected = Path(vertices=[(0, 0), (1, 0), (1, 3), (2, 3)], width=2).get_bounds_single()
|
||||
|
|
@ -333,7 +333,7 @@ def test_pathtool_u_offers_remain_unsupported() -> None:
|
|||
[
|
||||
('straight', {}),
|
||||
('bend', {'ccw': True}),
|
||||
('s', {'length': 6}),
|
||||
('s', {}),
|
||||
],
|
||||
)
|
||||
def test_pathtool_out_ptype_unk_is_wildcard(
|
||||
|
|
@ -348,6 +348,12 @@ def test_pathtool_out_ptype_unk_is_wildcard(
|
|||
assert offers[0].out_ptype == 'wire'
|
||||
|
||||
|
||||
def test_pathtool_rejects_unsupported_planning_options() -> None:
|
||||
tool = PathTool(layer='M1', width=1)
|
||||
with pytest.raises(BuildError, match='does not support tool options: marker'):
|
||||
tool.primitive_offers('straight', marker='sentinel')
|
||||
|
||||
|
||||
def test_pather_treats_notimplemented_offer_query_as_no_offers() -> None:
|
||||
class NoOfferTool(PlanningOnlyTool):
|
||||
def primitive_offers(
|
||||
|
|
@ -437,13 +443,14 @@ class StrategyTieTool(PlanningOnlyTool):
|
|||
) -> tuple[PrimitiveOffer, ...]:
|
||||
self.seen_kwargs.append(dict(kwargs))
|
||||
endpoint_ptype = out_ptype or in_ptype
|
||||
marker = kwargs.get('marker')
|
||||
if kind == 'straight':
|
||||
return (StraightOffer(
|
||||
in_ptype=in_ptype,
|
||||
out_ptype=endpoint_ptype,
|
||||
**offer_callbacks(lambda length: (
|
||||
Port((length, 0), rotation=pi, ptype=endpoint_ptype),
|
||||
{'kind': 'straight', 'length': length},
|
||||
{'kind': 'straight', 'length': length, 'marker': marker},
|
||||
)),
|
||||
),)
|
||||
if kind == 's':
|
||||
|
|
@ -452,7 +459,7 @@ class StrategyTieTool(PlanningOnlyTool):
|
|||
out_ptype=endpoint_ptype,
|
||||
**offer_callbacks(lambda jog: (
|
||||
Port((3, jog), rotation=pi, ptype=endpoint_ptype),
|
||||
{'kind': 's', 'jog': jog},
|
||||
{'kind': 's', 'jog': jog, 'marker': marker},
|
||||
)),
|
||||
),)
|
||||
return ()
|
||||
|
|
@ -504,11 +511,12 @@ def test_pather_route_strategy_per_route_can_request_turn_first() -> None:
|
|||
def test_pather_route_strategy_is_not_forwarded_to_tool() -> None:
|
||||
pather, tool = pather_with_strategy_tool()
|
||||
|
||||
pather.jog('A', 4, length=10, strategy='turn_first', marker='sentinel')
|
||||
pather.jog('A', 4, length=10, strategy='turn_first', tool_options={'marker': 'sentinel'})
|
||||
|
||||
assert tool.seen_kwargs
|
||||
assert all('strategy' not in kwargs for kwargs in tool.seen_kwargs)
|
||||
assert any(kwargs.get('marker') == 'sentinel' for kwargs in tool.seen_kwargs)
|
||||
assert all(step.data['marker'] == 'sentinel' for step in pather._paths['A'])
|
||||
|
||||
|
||||
def test_pather_route_strategy_rejects_invalid_values() -> None:
|
||||
|
|
|
|||
|
|
@ -252,7 +252,8 @@ def test_pather_trace_into_rejects_reserved_route_kwargs(
|
|||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
p.pattern.ports['B'] = dst
|
||||
|
||||
with pytest.raises(BuildError, match=match):
|
||||
_ = match
|
||||
with pytest.raises(TypeError, match='unexpected keyword argument'):
|
||||
p.trace_into('A', 'B', plug_destination=False, **kwargs)
|
||||
|
||||
assert numpy.allclose(p.pattern.ports['A'].offset, (0, 0))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue