[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.pathU(...)` | `Pather.uturn(...)` |
|
||||||
| `Pather.path_into(...)` | `Pather.trace_into(...)` |
|
| `Pather.path_into(...)` | `Pather.trace_into(...)` |
|
||||||
| `Pather.path_from(src, dst)` | `Pather.at(src).trace_into(dst)` |
|
| `Pather.path_from(src, dst)` | `Pather.at(src).trace_into(dst)` |
|
||||||
| `RenderPather.path(...)` | `Pather(..., auto_render=False).trace(...)` |
|
| `RenderPather.path(...)` | `Pather(..., render='deferred').trace(...)` |
|
||||||
| `RenderPather.path_to(...)` | `Pather(..., auto_render=False).trace_to(...)` |
|
| `RenderPather.path_to(...)` | `Pather(..., render='deferred').trace_to(...)` |
|
||||||
| `RenderPather.mpath(...)` | `Pather(..., auto_render=False).trace(...)` / `Pather(..., auto_render=False).trace_to(...)` |
|
| `RenderPather.mpath(...)` | `Pather(..., render='deferred').trace(...)` / `Pather(..., render='deferred').trace_to(...)` |
|
||||||
| `RenderPather.pathS(...)` | `Pather(..., auto_render=False).jog(...)` |
|
| `RenderPather.pathS(...)` | `Pather(..., render='deferred').jog(...)` |
|
||||||
| `RenderPather.pathU(...)` | `Pather(..., auto_render=False).uturn(...)` |
|
| `RenderPather.pathU(...)` | `Pather(..., render='deferred').uturn(...)` |
|
||||||
| `RenderPather.path_into(...)` | `Pather(..., auto_render=False).trace_into(...)` |
|
| `RenderPather.path_into(...)` | `Pather(..., render='deferred').trace_into(...)` |
|
||||||
| `RenderPather.path_from(src, dst)` | `Pather(..., auto_render=False).at(src).trace_into(dst)` |
|
| `RenderPather.path_from(src, dst)` | `Pather(..., render='deferred').at(src).trace_into(dst)` |
|
||||||
|
|
||||||
There are also new convenience wrappers:
|
There are also new convenience wrappers:
|
||||||
|
|
||||||
|
|
@ -123,13 +123,13 @@ from masque.builder.renderpather import RenderPather
|
||||||
from masque.builder import Pather
|
from masque.builder import Pather
|
||||||
|
|
||||||
builder = Pather(...)
|
builder = Pather(...)
|
||||||
deferred = Pather(..., auto_render=False)
|
deferred = Pather(..., render='deferred')
|
||||||
```
|
```
|
||||||
|
|
||||||
Top-level imports from `masque` also continue to work.
|
Top-level imports from `masque` also continue to work.
|
||||||
|
|
||||||
`Pather` now defaults to `auto_render=True`, so plain construction replaces the
|
`Pather` now defaults to `render='auto'`, so plain construction replaces the
|
||||||
old `Builder` behavior. Use `Pather(..., auto_render=False)` where you
|
old `Builder` behavior. Use `Pather(..., render='deferred')` where you
|
||||||
previously used `RenderPather`.
|
previously used `RenderPather`.
|
||||||
|
|
||||||
## `BasicTool` was replaced
|
## `BasicTool` was replaced
|
||||||
|
|
@ -218,7 +218,7 @@ class MyTool(Tool):
|
||||||
commit_planner=commit,
|
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,
|
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:
|
Primitive offers are local planning objects:
|
||||||
|
|
||||||
- `endpoint_at(parameter)` returns the local output `Port`
|
- `endpoint_at(parameter)` returns the local output `Port`
|
||||||
|
|
|
||||||
|
|
@ -271,13 +271,13 @@ class PrimitiveWireTool(Tool):
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> tuple[StraightOffer | BendOffer, ...]:
|
) -> tuple[StraightOffer | BendOffer, ...]:
|
||||||
if kind == 'straight':
|
if kind == 'straight':
|
||||||
route_kwargs = dict(kwargs)
|
tool_options = dict(kwargs)
|
||||||
|
|
||||||
def endpoint_planner(length: float) -> Port:
|
def endpoint_planner(length: float) -> Port:
|
||||||
return Port((length, 0), rotation=pi, ptype=self.ptype)
|
return Port((length, 0), rotation=pi, ptype=self.ptype)
|
||||||
|
|
||||||
def commit_planner(length: float) -> WireStraightData:
|
def commit_planner(length: float) -> WireStraightData:
|
||||||
_ = route_kwargs
|
_ = tool_options
|
||||||
return WireStraightData(length)
|
return WireStraightData(length)
|
||||||
|
|
||||||
native_offer = StraightOffer(
|
native_offer = StraightOffer(
|
||||||
|
|
@ -354,7 +354,6 @@ class PrimitiveWireTool(Tool):
|
||||||
batch: Sequence[RenderStep],
|
batch: Sequence[RenderStep],
|
||||||
*,
|
*,
|
||||||
port_names: tuple[str, str] = ('A', 'B'),
|
port_names: tuple[str, str] = ('A', 'B'),
|
||||||
**kwargs: Any, # noqa: ARG002 (no per-render options in this example tool)
|
|
||||||
) -> ILibrary:
|
) -> ILibrary:
|
||||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'primitive_wire')
|
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)
|
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
|
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
|
||||||
|
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:
|
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
|
||||||
passes copied ports to the planner as `RoutePortContext` values,
|
passes copied ports to the planner as `RoutePortContext` values,
|
||||||
|
|
@ -84,6 +88,29 @@ from .logging import PatherLogger
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
RenderPolicy = Literal['auto', 'immediate', 'deferred', 'warn', 'error', 'ignore']
|
RenderPolicy = Literal['auto', 'immediate', 'deferred', 'warn', 'error', 'ignore']
|
||||||
RENDER_POLICIES: tuple[RenderPolicy, ...] = ('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):
|
class Pather(PortList):
|
||||||
|
|
@ -548,7 +575,19 @@ class Pather(PortList):
|
||||||
*,
|
*,
|
||||||
spacing: float | ArrayLike | None = None,
|
spacing: float | ArrayLike | None = None,
|
||||||
strategy: RouteTieBreakStrategy | str | 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:
|
) -> Self:
|
||||||
"""
|
"""
|
||||||
Route one or more ports using straight segments or single 90-degree bends.
|
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.
|
`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):
|
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(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:
|
except (BuildError, NotImplementedError) as err:
|
||||||
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
|
||||||
|
|
@ -584,7 +644,7 @@ class Pather(PortList):
|
||||||
0,
|
0,
|
||||||
ccw,
|
ccw,
|
||||||
context.port.ptype,
|
context.port.ptype,
|
||||||
out_ptype = bounds.get('out_ptype'),
|
out_ptype = out_ptype,
|
||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
if bounds.get('each') is not None:
|
if bounds.get('each') is not None:
|
||||||
|
|
@ -596,7 +656,7 @@ class Pather(PortList):
|
||||||
0,
|
0,
|
||||||
ccw,
|
ccw,
|
||||||
context.port.ptype,
|
context.port.ptype,
|
||||||
out_ptype = bounds.get('out_ptype'),
|
out_ptype = out_ptype,
|
||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
raise
|
raise
|
||||||
|
|
@ -608,9 +668,27 @@ class Pather(PortList):
|
||||||
portspec: str | Sequence[str],
|
portspec: str | Sequence[str],
|
||||||
ccw: SupportsBool | None,
|
ccw: SupportsBool | None,
|
||||||
*,
|
*,
|
||||||
|
length: float | None = None,
|
||||||
spacing: float | ArrayLike | None = None,
|
spacing: float | ArrayLike | None = None,
|
||||||
strategy: RouteTieBreakStrategy | str | 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:
|
) -> Self:
|
||||||
"""
|
"""
|
||||||
Route until a single positional bound is reached, or delegate to `trace()` for length/bundle bounds.
|
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
|
`strategy` controls straight-first vs turn-first ordering only after
|
||||||
cost and structural tie-breakers.
|
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):
|
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(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:
|
except (BuildError, NotImplementedError) as err:
|
||||||
if (
|
if (
|
||||||
not self._dead
|
not self._dead
|
||||||
|
|
@ -655,23 +760,148 @@ class Pather(PortList):
|
||||||
0,
|
0,
|
||||||
ccw,
|
ccw,
|
||||||
context.port.ptype,
|
context.port.ptype,
|
||||||
out_ptype = bounds.get('out_ptype'),
|
out_ptype = out_ptype,
|
||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
self._apply_route_result(result)
|
self._apply_route_result(result)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def straight(self, portspec: str | Sequence[str], length: float | None = None, **bounds) -> Self:
|
def straight(
|
||||||
return self.trace_to(portspec, None, length=length, **bounds)
|
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:
|
def bend(
|
||||||
return self.trace_to(portspec, ccw, length=length, **bounds)
|
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:
|
def ccw(
|
||||||
return self.bend(portspec, True, length, **bounds)
|
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:
|
def cw(
|
||||||
return self.bend(portspec, False, length, **bounds)
|
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(
|
def jog(
|
||||||
self,
|
self,
|
||||||
|
|
@ -681,7 +911,13 @@ class Pather(PortList):
|
||||||
*,
|
*,
|
||||||
spacing: float | ArrayLike | None = None,
|
spacing: float | ArrayLike | None = None,
|
||||||
strategy: RouteTieBreakStrategy | str | 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:
|
) -> Self:
|
||||||
"""
|
"""
|
||||||
Route an S-bend.
|
Route an S-bend.
|
||||||
|
|
@ -700,12 +936,20 @@ class Pather(PortList):
|
||||||
controls straight-first vs S-first ordering only after cost and
|
controls straight-first vs S-first ordering only after cost and
|
||||||
structural tie-breakers.
|
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):
|
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(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:
|
except (BuildError, NotImplementedError) as err:
|
||||||
if (
|
if (
|
||||||
not self._dead
|
not self._dead
|
||||||
|
|
@ -723,7 +967,7 @@ class Pather(PortList):
|
||||||
0,
|
0,
|
||||||
None,
|
None,
|
||||||
context.port.ptype,
|
context.port.ptype,
|
||||||
out_ptype = bounds.get('out_ptype'),
|
out_ptype = out_ptype,
|
||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
fallback_length = length if length is not None else 0
|
fallback_length = length if length is not None else 0
|
||||||
|
|
@ -735,7 +979,7 @@ class Pather(PortList):
|
||||||
None,
|
None,
|
||||||
context.port.ptype,
|
context.port.ptype,
|
||||||
out_rot = pi,
|
out_rot = pi,
|
||||||
out_ptype = bounds.get('out_ptype'),
|
out_ptype = out_ptype,
|
||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
self._apply_route_result(result)
|
self._apply_route_result(result)
|
||||||
|
|
@ -749,7 +993,8 @@ class Pather(PortList):
|
||||||
*,
|
*,
|
||||||
spacing: float | ArrayLike | None = None,
|
spacing: float | ArrayLike | None = None,
|
||||||
strategy: RouteTieBreakStrategy | str | None = None,
|
strategy: RouteTieBreakStrategy | str | None = None,
|
||||||
**bounds: Any,
|
out_ptype: str | None = None,
|
||||||
|
tool_options: Mapping[str, Any] | None = None,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
"""
|
"""
|
||||||
Route a U-turn.
|
Route a U-turn.
|
||||||
|
|
@ -766,12 +1011,20 @@ class Pather(PortList):
|
||||||
controls straight-first vs U-first ordering only after cost and
|
controls straight-first vs U-first ordering only after cost and
|
||||||
structural tie-breakers.
|
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):
|
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(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:
|
except (BuildError, NotImplementedError) as err:
|
||||||
if (
|
if (
|
||||||
not self._dead
|
not self._dead
|
||||||
|
|
@ -788,7 +1041,7 @@ class Pather(PortList):
|
||||||
None,
|
None,
|
||||||
context.port.ptype,
|
context.port.ptype,
|
||||||
out_rot = 0.0,
|
out_rot = 0.0,
|
||||||
out_ptype = bounds.get('out_ptype'),
|
out_ptype = out_ptype,
|
||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
self._apply_route_result(result)
|
self._apply_route_result(result)
|
||||||
|
|
@ -803,7 +1056,7 @@ class Pather(PortList):
|
||||||
plug_destination: bool = True,
|
plug_destination: bool = True,
|
||||||
thru: str | None = None,
|
thru: str | None = None,
|
||||||
strategy: RouteTieBreakStrategy | str | None = None,
|
strategy: RouteTieBreakStrategy | str | None = None,
|
||||||
**kwargs: Any,
|
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.
|
||||||
|
|
@ -813,9 +1066,8 @@ class Pather(PortList):
|
||||||
dogleg topologies. The lowest-cost legal bounded candidate is selected;
|
dogleg topologies. The lowest-cost legal bounded candidate is selected;
|
||||||
bend count, step count, and search order are tie-breakers only. `strategy`
|
bend count, step count, and search order are tie-breakers only. `strategy`
|
||||||
controls straight-first vs turn-first search order within those ties.
|
controls straight-first vs turn-first search order within those ties.
|
||||||
Route-shape kwargs such as `length`, `offset`, `ccw`, positional bounds,
|
Custom planning options may be supplied through `tool_options`; they are
|
||||||
bundle bounds, and `strategy` are reserved for this internal solve; other tool
|
forwarded only to primitive offer generation.
|
||||||
kwargs are forwarded to primitive offer generation.
|
|
||||||
|
|
||||||
If `plug_destination` is `True`, the destination port is consumed by the final step.
|
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.
|
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
|
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)
|
||||||
with self._logger.log_operation(
|
with self._logger.log_operation(
|
||||||
self,
|
self,
|
||||||
'trace_into',
|
'trace_into',
|
||||||
|
|
@ -832,7 +1085,7 @@ class Pather(PortList):
|
||||||
plug_destination=plug_destination,
|
plug_destination=plug_destination,
|
||||||
thru=thru,
|
thru=thru,
|
||||||
strategy=strategy,
|
strategy=strategy,
|
||||||
**kwargs,
|
tool_options=options,
|
||||||
):
|
):
|
||||||
result = self.planner.plan_trace_into(
|
result = self.planner.plan_trace_into(
|
||||||
self._route_context(portspec_src),
|
self._route_context(portspec_src),
|
||||||
|
|
@ -842,7 +1095,7 @@ class Pather(PortList):
|
||||||
plug_destination = plug_destination,
|
plug_destination = plug_destination,
|
||||||
thru = thru,
|
thru = thru,
|
||||||
strategy = strategy,
|
strategy = strategy,
|
||||||
**kwargs,
|
tool_options = options,
|
||||||
)
|
)
|
||||||
self._apply_route_result(result)
|
self._apply_route_result(result)
|
||||||
return self
|
return self
|
||||||
|
|
@ -1045,46 +1298,259 @@ class PortPather:
|
||||||
with self.pather.toolctx(tool, keys=self.ports):
|
with self.pather.toolctx(tool, keys=self.ports):
|
||||||
yield self
|
yield self
|
||||||
|
|
||||||
def trace(self, ccw: SupportsBool | None, length: float | None = None, **kw: Any) -> Self:
|
def trace(
|
||||||
if 'spacing' not in kw and self.default_spacing is not None and len(self.ports) > 1 and ccw is not None:
|
self,
|
||||||
kw['spacing'] = self.default_spacing
|
ccw: SupportsBool | None,
|
||||||
self.pather.trace(self.ports, ccw, length, **kw)
|
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
|
return self
|
||||||
|
|
||||||
def trace_to(self, ccw: SupportsBool | None, **kw: Any) -> Self:
|
def trace_to(
|
||||||
if 'spacing' not in kw and self.default_spacing is not None and len(self.ports) > 1 and ccw is not None:
|
self,
|
||||||
kw['spacing'] = self.default_spacing
|
ccw: SupportsBool | None,
|
||||||
self.pather.trace_to(self.ports, ccw, **kw)
|
*,
|
||||||
|
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
|
return self
|
||||||
|
|
||||||
def straight(self, length: float | None = None, **kw: Any) -> Self:
|
def straight(
|
||||||
return self.trace_to(None, length=length, **kw)
|
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:
|
def bend(
|
||||||
return self.trace_to(ccw, length=length, **kw)
|
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:
|
def ccw(
|
||||||
return self.bend(True, length, **kw)
|
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:
|
def cw(
|
||||||
return self.bend(False, length, **kw)
|
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:
|
def jog(
|
||||||
if 'spacing' not in kw and self.default_spacing is not None and len(self.ports) > 1 and not numpy.isclose(offset, 0):
|
self,
|
||||||
kw['spacing'] = self.default_spacing
|
offset: float,
|
||||||
self.pather.jog(self.ports, offset, length, **kw)
|
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
|
return self
|
||||||
|
|
||||||
def uturn(self, offset: float, length: float | None = None, **kw: Any) -> Self:
|
def uturn(
|
||||||
if 'spacing' not in kw and self.default_spacing is not None and len(self.ports) > 1:
|
self,
|
||||||
kw['spacing'] = self.default_spacing
|
offset: float,
|
||||||
self.pather.uturn(self.ports, offset, length, **kw)
|
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
|
return self
|
||||||
|
|
||||||
def trace_into(self, target_port: str, **kwargs) -> Self:
|
def trace_into(
|
||||||
if len(self.ports) > 1:
|
self,
|
||||||
raise BuildError(f'Unable use implicit trace_into() with {len(self.ports)} (>1) ports.')
|
target_port: str,
|
||||||
self.pather.trace_into(self.ports[0], target_port, **kwargs)
|
*,
|
||||||
|
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
|
return self
|
||||||
|
|
||||||
def plug(self, other: Abstract | str, other_port: str, **kwargs) -> Self:
|
def plug(self, other: Abstract | str, other_port: str, **kwargs) -> Self:
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ from pprint import pformat
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy import pi
|
from numpy import pi
|
||||||
from numpy.typing import ArrayLike
|
from numpy.typing import ArrayLike, NDArray
|
||||||
|
|
||||||
from ...error import BuildError, PortError
|
from ...error import BuildError, PortError
|
||||||
from ...ports import Port
|
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(
|
def resolved_position_bound(
|
||||||
port: Port,
|
port: Port,
|
||||||
bounds: Mapping[str, Any],
|
bounds: Mapping[str, Any],
|
||||||
|
|
@ -50,7 +66,8 @@ def resolved_position_bound(
|
||||||
if not allow_length and bounds.get('length') is not None:
|
if not allow_length and bounds.get('length') is not None:
|
||||||
raise BuildError('length cannot be combined with a positional bound')
|
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:
|
if port.rotation is None:
|
||||||
raise BuildError('Ports must have rotation')
|
raise BuildError('Ports must have rotation')
|
||||||
is_horiz = bool(numpy.isclose(port.rotation % pi, 0, rtol=1e-9, atol=1e-9))
|
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:
|
if spacing is None:
|
||||||
raise BuildError(f'Must provide spacing for multi-port {route_name}()')
|
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}
|
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)
|
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')
|
y_order = ((-1 if first_ccw else 1) * rot_offsets[:, 1]).argsort(kind='stable')
|
||||||
|
|
||||||
spacing_arr = numpy.asarray(spacing, dtype=float).reshape(-1)
|
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:
|
if spacing_arr.size == 1:
|
||||||
steps[1:] = spacing_arr[0]
|
steps[1:] = spacing_arr[0]
|
||||||
elif spacing_arr.size == len(ports) - 1:
|
elif spacing_arr.size == len(ports) - 1:
|
||||||
|
|
|
||||||
|
|
@ -251,7 +251,7 @@ class SolverRequest:
|
||||||
|
|
||||||
Public Pather calls are converted into this smaller shape before grammar
|
Public Pather calls are converted into this smaller shape before grammar
|
||||||
enumeration. `length`, `jog`, and `out_ptype` become endpoint constraints;
|
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
|
family: PrimitiveKind
|
||||||
"""High-level route family being solved."""
|
"""High-level route family being solved."""
|
||||||
|
|
@ -259,8 +259,8 @@ class SolverRequest:
|
||||||
"""Tool queried for primitive offers."""
|
"""Tool queried for primitive offers."""
|
||||||
in_ptype: str | None
|
in_ptype: str | None
|
||||||
"""Input ptype at the start of the route."""
|
"""Input ptype at the start of the route."""
|
||||||
route_kwargs: Mapping[str, Any]
|
tool_options: Mapping[str, Any]
|
||||||
"""Tool kwargs forwarded to primitive-offer generation."""
|
"""Custom Tool kwargs forwarded to primitive-offer generation."""
|
||||||
length: float | None = None
|
length: float | None = None
|
||||||
"""Requested local x displacement, when constrained."""
|
"""Requested local x displacement, when constrained."""
|
||||||
jog: float | None = None
|
jog: float | None = None
|
||||||
|
|
@ -439,9 +439,8 @@ class Solver:
|
||||||
out_ptype: str | None = None,
|
out_ptype: str | None = None,
|
||||||
extra: Mapping[str, Any] | None = None,
|
extra: Mapping[str, Any] | None = None,
|
||||||
) -> tuple[PrimitiveOffer, ...]:
|
) -> tuple[PrimitiveOffer, ...]:
|
||||||
"""Query the active Tool with route kwargs and per-query overrides."""
|
"""Query the active Tool with custom planning options and internal overrides."""
|
||||||
kwargs = dict(self.request.route_kwargs)
|
kwargs = dict(self.request.tool_options)
|
||||||
kwargs.pop('out_ptype', None)
|
|
||||||
if extra:
|
if extra:
|
||||||
kwargs.update(extra)
|
kwargs.update(extra)
|
||||||
try:
|
try:
|
||||||
|
|
@ -1018,6 +1017,7 @@ class RoutingPlanner:
|
||||||
constrain_jog: bool = False,
|
constrain_jog: bool = False,
|
||||||
max_bends: int | None = None,
|
max_bends: int | None = None,
|
||||||
strategy: RouteTieBreakStrategy | str | None = None,
|
strategy: RouteTieBreakStrategy | str | None = None,
|
||||||
|
tool_options: Mapping[str, Any] | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> SolverRequest:
|
) -> SolverRequest:
|
||||||
"""Build normalized solver input for one route leg."""
|
"""Build normalized solver input for one route leg."""
|
||||||
|
|
@ -1025,7 +1025,7 @@ class RoutingPlanner:
|
||||||
family=family,
|
family=family,
|
||||||
tool=context.tool,
|
tool=context.tool,
|
||||||
in_ptype=context.port.ptype,
|
in_ptype=context.port.ptype,
|
||||||
route_kwargs=kwargs,
|
tool_options={} if tool_options is None else dict(tool_options),
|
||||||
length=length,
|
length=length,
|
||||||
jog=jog,
|
jog=jog,
|
||||||
ccw=ccw,
|
ccw=ccw,
|
||||||
|
|
@ -1070,6 +1070,7 @@ class RoutingPlanner:
|
||||||
constrain_jog: bool = False,
|
constrain_jog: bool = False,
|
||||||
max_bends: int | None = None,
|
max_bends: int | None = None,
|
||||||
strategy: RouteTieBreakStrategy | str | None = None,
|
strategy: RouteTieBreakStrategy | str | None = None,
|
||||||
|
tool_options: Mapping[str, Any] | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> RouteLeg:
|
) -> RouteLeg:
|
||||||
"""Solve one route leg and attach it to its source Pather context."""
|
"""Solve one route leg and attach it to its source Pather context."""
|
||||||
|
|
@ -1107,6 +1108,7 @@ class RoutingPlanner:
|
||||||
constrain_jog=constrain_jog,
|
constrain_jog=constrain_jog,
|
||||||
max_bends=max_bends,
|
max_bends=max_bends,
|
||||||
strategy=strategy,
|
strategy=strategy,
|
||||||
|
tool_options=tool_options,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
|
|
@ -1212,6 +1214,7 @@ class RoutingPlanner:
|
||||||
*,
|
*,
|
||||||
spacing: float | ArrayLike | None = None,
|
spacing: float | ArrayLike | None = None,
|
||||||
strategy: RouteTieBreakStrategy | str | None = None,
|
strategy: RouteTieBreakStrategy | str | 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."""
|
||||||
|
|
@ -1225,6 +1228,8 @@ class RoutingPlanner:
|
||||||
request_details['spacing'] = spacing
|
request_details['spacing'] = spacing
|
||||||
if strategy is not None:
|
if strategy is not None:
|
||||||
request_details['strategy'] = strategy
|
request_details['strategy'] = strategy
|
||||||
|
if tool_options:
|
||||||
|
request_details['tool_options'] = dict(tool_options)
|
||||||
operation: RouteOperation = 'trace'
|
operation: RouteOperation = 'trace'
|
||||||
diagnostic_request = request_details
|
diagnostic_request = request_details
|
||||||
return self._plan_trace_route(
|
return self._plan_trace_route(
|
||||||
|
|
@ -1235,6 +1240,7 @@ class RoutingPlanner:
|
||||||
diagnostic_request,
|
diagnostic_request,
|
||||||
spacing=spacing,
|
spacing=spacing,
|
||||||
strategy=strategy,
|
strategy=strategy,
|
||||||
|
tool_options=tool_options,
|
||||||
**route_bounds,
|
**route_bounds,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1249,6 +1255,7 @@ class RoutingPlanner:
|
||||||
*,
|
*,
|
||||||
spacing: float | ArrayLike | None,
|
spacing: float | ArrayLike | None,
|
||||||
strategy: RouteTieBreakStrategy | str | None,
|
strategy: RouteTieBreakStrategy | str | None,
|
||||||
|
tool_options: Mapping[str, Any] | None,
|
||||||
**route_bounds: Any,
|
**route_bounds: Any,
|
||||||
) -> PreparedRouteResult:
|
) -> PreparedRouteResult:
|
||||||
portspec = tuple(context.portspec for context in contexts)
|
portspec = tuple(context.portspec for context in contexts)
|
||||||
|
|
@ -1263,6 +1270,7 @@ class RoutingPlanner:
|
||||||
length=length,
|
length=length,
|
||||||
ccw=ccw,
|
ccw=ccw,
|
||||||
strategy=strategy,
|
strategy=strategy,
|
||||||
|
tool_options=tool_options,
|
||||||
**route_bounds,
|
**route_bounds,
|
||||||
)
|
)
|
||||||
return self.prepared_result_from_legs((leg,))
|
return self.prepared_result_from_legs((leg,))
|
||||||
|
|
@ -1279,6 +1287,7 @@ class RoutingPlanner:
|
||||||
length=each,
|
length=each,
|
||||||
ccw=ccw,
|
ccw=ccw,
|
||||||
strategy=strategy,
|
strategy=strategy,
|
||||||
|
tool_options=tool_options,
|
||||||
**route_bounds,
|
**route_bounds,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -1295,6 +1304,7 @@ class RoutingPlanner:
|
||||||
length=None,
|
length=None,
|
||||||
ccw=ccw,
|
ccw=ccw,
|
||||||
strategy=strategy,
|
strategy=strategy,
|
||||||
|
tool_options=tool_options,
|
||||||
**route_bounds,
|
**route_bounds,
|
||||||
)
|
)
|
||||||
return self.prepared_result_from_legs((leg,))
|
return self.prepared_result_from_legs((leg,))
|
||||||
|
|
@ -1321,6 +1331,7 @@ class RoutingPlanner:
|
||||||
length=route_length,
|
length=route_length,
|
||||||
ccw=ccw,
|
ccw=ccw,
|
||||||
strategy=strategy,
|
strategy=strategy,
|
||||||
|
tool_options=tool_options,
|
||||||
**route_bounds,
|
**route_bounds,
|
||||||
)
|
)
|
||||||
actions.append(self.prepared_route_action_from_leg(leg))
|
actions.append(self.prepared_route_action_from_leg(leg))
|
||||||
|
|
@ -1333,6 +1344,7 @@ class RoutingPlanner:
|
||||||
*,
|
*,
|
||||||
spacing: float | ArrayLike | None = None,
|
spacing: float | ArrayLike | None = None,
|
||||||
strategy: RouteTieBreakStrategy | str | None = None,
|
strategy: RouteTieBreakStrategy | str | 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."""
|
||||||
|
|
@ -1344,6 +1356,8 @@ class RoutingPlanner:
|
||||||
request_details['spacing'] = spacing
|
request_details['spacing'] = spacing
|
||||||
if strategy is not None:
|
if strategy is not None:
|
||||||
request_details['strategy'] = strategy
|
request_details['strategy'] = strategy
|
||||||
|
if tool_options:
|
||||||
|
request_details['tool_options'] = dict(tool_options)
|
||||||
operation: RouteOperation = 'trace_to'
|
operation: RouteOperation = 'trace_to'
|
||||||
diagnostic_request = request_details
|
diagnostic_request = request_details
|
||||||
return self._plan_trace_to_route(
|
return self._plan_trace_to_route(
|
||||||
|
|
@ -1353,6 +1367,7 @@ class RoutingPlanner:
|
||||||
diagnostic_request,
|
diagnostic_request,
|
||||||
spacing=spacing,
|
spacing=spacing,
|
||||||
strategy=strategy,
|
strategy=strategy,
|
||||||
|
tool_options=tool_options,
|
||||||
**route_bounds,
|
**route_bounds,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1366,6 +1381,7 @@ class RoutingPlanner:
|
||||||
*,
|
*,
|
||||||
spacing: float | ArrayLike | None,
|
spacing: float | ArrayLike | None,
|
||||||
strategy: RouteTieBreakStrategy | str | None,
|
strategy: RouteTieBreakStrategy | str | None,
|
||||||
|
tool_options: Mapping[str, Any] | None,
|
||||||
**route_bounds: Any,
|
**route_bounds: Any,
|
||||||
) -> PreparedRouteResult:
|
) -> PreparedRouteResult:
|
||||||
if len(contexts) == 1:
|
if len(contexts) == 1:
|
||||||
|
|
@ -1383,6 +1399,7 @@ class RoutingPlanner:
|
||||||
diagnostic_request,
|
diagnostic_request,
|
||||||
spacing=spacing,
|
spacing=spacing,
|
||||||
strategy=strategy,
|
strategy=strategy,
|
||||||
|
tool_options=tool_options,
|
||||||
**route_bounds,
|
**route_bounds,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1402,6 +1419,7 @@ class RoutingPlanner:
|
||||||
length=length,
|
length=length,
|
||||||
ccw=ccw,
|
ccw=ccw,
|
||||||
strategy=strategy,
|
strategy=strategy,
|
||||||
|
tool_options=tool_options,
|
||||||
**other_bounds,
|
**other_bounds,
|
||||||
)
|
)
|
||||||
return self.prepared_result_from_legs((leg,))
|
return self.prepared_result_from_legs((leg,))
|
||||||
|
|
@ -1414,9 +1432,11 @@ class RoutingPlanner:
|
||||||
*,
|
*,
|
||||||
spacing: float | ArrayLike | None = None,
|
spacing: float | ArrayLike | None = None,
|
||||||
strategy: RouteTieBreakStrategy | str | None = None,
|
strategy: RouteTieBreakStrategy | str | 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."""
|
||||||
|
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
|
||||||
}}
|
}}
|
||||||
|
|
@ -1426,6 +1446,8 @@ class RoutingPlanner:
|
||||||
request_details['spacing'] = spacing
|
request_details['spacing'] = spacing
|
||||||
if strategy is not None:
|
if strategy is not None:
|
||||||
request_details['strategy'] = strategy
|
request_details['strategy'] = strategy
|
||||||
|
if tool_options:
|
||||||
|
request_details['tool_options'] = dict(tool_options)
|
||||||
operation: RouteOperation = 'jog'
|
operation: RouteOperation = 'jog'
|
||||||
diagnostic_request = request_details
|
diagnostic_request = request_details
|
||||||
if numpy.isclose(offset, 0):
|
if numpy.isclose(offset, 0):
|
||||||
|
|
@ -1437,6 +1459,7 @@ class RoutingPlanner:
|
||||||
length=length,
|
length=length,
|
||||||
spacing=spacing,
|
spacing=spacing,
|
||||||
strategy=strategy,
|
strategy=strategy,
|
||||||
|
tool_options=tool_options,
|
||||||
**bounds,
|
**bounds,
|
||||||
)
|
)
|
||||||
route_bounds = dict(bounds)
|
route_bounds = dict(bounds)
|
||||||
|
|
@ -1460,6 +1483,7 @@ class RoutingPlanner:
|
||||||
operation,
|
operation,
|
||||||
diagnostic_request,
|
diagnostic_request,
|
||||||
strategy=strategy,
|
strategy=strategy,
|
||||||
|
tool_options=tool_options,
|
||||||
**other_bounds,
|
**other_bounds,
|
||||||
)
|
)
|
||||||
))
|
))
|
||||||
|
|
@ -1471,6 +1495,7 @@ class RoutingPlanner:
|
||||||
length=length,
|
length=length,
|
||||||
jog=offset,
|
jog=offset,
|
||||||
strategy=strategy,
|
strategy=strategy,
|
||||||
|
tool_options=tool_options,
|
||||||
**other_bounds,
|
**other_bounds,
|
||||||
)
|
)
|
||||||
return self.prepared_result_from_legs((leg,))
|
return self.prepared_result_from_legs((leg,))
|
||||||
|
|
@ -1483,9 +1508,11 @@ class RoutingPlanner:
|
||||||
*,
|
*,
|
||||||
spacing: float | ArrayLike | None = None,
|
spacing: float | ArrayLike | None = None,
|
||||||
strategy: RouteTieBreakStrategy | str | None = None,
|
strategy: RouteTieBreakStrategy | str | 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."""
|
||||||
|
offset = planner_bounds.finite_scalar(offset, 'offset')
|
||||||
route_bounds = dict(bounds)
|
route_bounds = dict(bounds)
|
||||||
request_details = {'offset': offset, **{
|
request_details = {'offset': offset, **{
|
||||||
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
|
||||||
|
|
@ -1496,6 +1523,8 @@ class RoutingPlanner:
|
||||||
request_details['spacing'] = spacing
|
request_details['spacing'] = spacing
|
||||||
if strategy is not None:
|
if strategy is not None:
|
||||||
request_details['strategy'] = strategy
|
request_details['strategy'] = strategy
|
||||||
|
if tool_options:
|
||||||
|
request_details['tool_options'] = dict(tool_options)
|
||||||
operation: RouteOperation = 'uturn'
|
operation: RouteOperation = 'uturn'
|
||||||
diagnostic_request = request_details
|
diagnostic_request = request_details
|
||||||
portspec = tuple(context.portspec for context in contexts)
|
portspec = tuple(context.portspec for context in contexts)
|
||||||
|
|
@ -1512,6 +1541,7 @@ class RoutingPlanner:
|
||||||
operation,
|
operation,
|
||||||
diagnostic_request,
|
diagnostic_request,
|
||||||
strategy=strategy,
|
strategy=strategy,
|
||||||
|
tool_options=tool_options,
|
||||||
**route_bounds,
|
**route_bounds,
|
||||||
)
|
)
|
||||||
))
|
))
|
||||||
|
|
@ -1523,6 +1553,7 @@ class RoutingPlanner:
|
||||||
length=length,
|
length=length,
|
||||||
jog=offset,
|
jog=offset,
|
||||||
strategy=strategy,
|
strategy=strategy,
|
||||||
|
tool_options=tool_options,
|
||||||
**route_bounds,
|
**route_bounds,
|
||||||
)
|
)
|
||||||
return self.prepared_result_from_legs((leg,))
|
return self.prepared_result_from_legs((leg,))
|
||||||
|
|
@ -1539,6 +1570,7 @@ class RoutingPlanner:
|
||||||
/,
|
/,
|
||||||
*,
|
*,
|
||||||
strategy: RouteTieBreakStrategy | str | None = None,
|
strategy: RouteTieBreakStrategy | str | None = None,
|
||||||
|
tool_options: Mapping[str, Any] | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> tuple[RouteLeg, ...]:
|
) -> tuple[RouteLeg, ...]:
|
||||||
"""
|
"""
|
||||||
|
|
@ -1557,6 +1589,7 @@ class RoutingPlanner:
|
||||||
length=length,
|
length=length,
|
||||||
jog=offset,
|
jog=offset,
|
||||||
strategy=strategy,
|
strategy=strategy,
|
||||||
|
tool_options=tool_options,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
),)
|
),)
|
||||||
route_name = 'jog' if kind == 's' else 'uturn'
|
route_name = 'jog' if kind == 's' else 'uturn'
|
||||||
|
|
@ -1573,6 +1606,7 @@ class RoutingPlanner:
|
||||||
length=length,
|
length=length,
|
||||||
jog=offset,
|
jog=offset,
|
||||||
strategy=strategy,
|
strategy=strategy,
|
||||||
|
tool_options=tool_options,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
base_length = anchor.candidate.public_length
|
base_length = anchor.candidate.public_length
|
||||||
|
|
@ -1589,6 +1623,7 @@ class RoutingPlanner:
|
||||||
length=spec_length,
|
length=spec_length,
|
||||||
jog=spec_offset,
|
jog=spec_offset,
|
||||||
strategy=strategy,
|
strategy=strategy,
|
||||||
|
tool_options=tool_options,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|
@ -1600,6 +1635,7 @@ class RoutingPlanner:
|
||||||
length=spec_length,
|
length=spec_length,
|
||||||
jog=spec_offset,
|
jog=spec_offset,
|
||||||
strategy=strategy,
|
strategy=strategy,
|
||||||
|
tool_options=tool_options,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
return tuple(routes_by_name[spec_portspec] for spec_portspec, _length, _offset in specs)
|
return tuple(routes_by_name[spec_portspec] for spec_portspec, _length, _offset in specs)
|
||||||
|
|
@ -1614,17 +1650,9 @@ class RoutingPlanner:
|
||||||
plug_destination: bool,
|
plug_destination: bool,
|
||||||
thru: str | None,
|
thru: str | None,
|
||||||
strategy: RouteTieBreakStrategy | str | None = None,
|
strategy: RouteTieBreakStrategy | str | None = None,
|
||||||
**kwargs: Any,
|
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."""
|
||||||
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:
|
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:
|
||||||
|
|
@ -1642,7 +1670,8 @@ class RoutingPlanner:
|
||||||
constrain_jog=family == 'bend',
|
constrain_jog=family == 'bend',
|
||||||
max_bends=self.TRACE_INTO_MAX_BENDS,
|
max_bends=self.TRACE_INTO_MAX_BENDS,
|
||||||
strategy=strategy,
|
strategy=strategy,
|
||||||
**(dict(kwargs) | {'out_ptype': out_ptype}),
|
tool_options=tool_options,
|
||||||
|
out_ptype=out_ptype,
|
||||||
)
|
)
|
||||||
solver = self.solver_for_request(request)
|
solver = self.solver_for_request(request)
|
||||||
candidate = None
|
candidate = None
|
||||||
|
|
|
||||||
|
|
@ -49,8 +49,9 @@ from typing import Literal, Any, Self
|
||||||
from collections import ChainMap
|
from collections import ChainMap
|
||||||
from collections.abc import Sequence, Callable, Mapping
|
from collections.abc import Sequence, Callable, Mapping
|
||||||
from abc import ABC, abstractmethod
|
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 math import isclose as scalar_isclose, isfinite as scalar_isfinite, isnan as scalar_isnan, sqrt
|
||||||
|
from types import MappingProxyType
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy.typing import NDArray
|
from numpy.typing import NDArray
|
||||||
|
|
@ -693,6 +694,10 @@ class Tool(ABC):
|
||||||
- `'s'`: a non-turning `SOffer`
|
- `'s'`: a non-turning `SOffer`
|
||||||
- `'u'`: an `UOffer`
|
- `'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,
|
`Pather` applies any requested `out_ptype` to the final route endpoint,
|
||||||
not to every primitive in the route. Intermediate ptypes are
|
not to every primitive in the route. Intermediate ptypes are
|
||||||
solver-selected, and heterogeneous straight/S offers may be used as
|
solver-selected, and heterogeneous straight/S offers may be used as
|
||||||
|
|
@ -706,7 +711,6 @@ class Tool(ABC):
|
||||||
batch: Sequence[RenderStep],
|
batch: Sequence[RenderStep],
|
||||||
*,
|
*,
|
||||||
port_names: tuple[str, str] = ('A', 'B'),
|
port_names: tuple[str, str] = ('A', 'B'),
|
||||||
**kwargs,
|
|
||||||
) -> ILibrary:
|
) -> ILibrary:
|
||||||
"""
|
"""
|
||||||
Render a compatible batch of selected route steps into geometry.
|
Render a compatible batch of selected route steps into geometry.
|
||||||
|
|
@ -722,7 +726,6 @@ class Tool(ABC):
|
||||||
primitive render data.
|
primitive render data.
|
||||||
port_names: The topcell's input and output ports should be named
|
port_names: The topcell's input and output ports should be named
|
||||||
`port_names[0]` and `port_names[1]` respectively.
|
`port_names[0]` and `port_names[1]` respectively.
|
||||||
kwargs: Custom tool-specific parameters.
|
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
@ -778,17 +781,20 @@ class AutoTool(Tool):
|
||||||
when only the declared direction should be available.
|
when only the declared direction should be available.
|
||||||
|
|
||||||
Straight and S-bend generator functions may return either a `Pattern` or a
|
Straight and S-bend generator functions may return either a `Pattern` or a
|
||||||
single-top `Library`. Extra keyword arguments passed to `render()` are
|
single-top `Library`. Route `tool_options` are snapshotted into committed
|
||||||
forwarded to those generators.
|
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)
|
@dataclass(frozen=True, slots=True)
|
||||||
class GeneratedData:
|
class GeneratedData:
|
||||||
""" Deferred render data for one generated primitive offer. """
|
"""Deferred generator call, including its route-specific option snapshot."""
|
||||||
fn: GeneratedPrimitiveFn
|
fn: GeneratedPrimitiveFn
|
||||||
port_name: str
|
port_name: str
|
||||||
parameter: float
|
parameter: float
|
||||||
mirrored: bool = False
|
mirrored: bool = False
|
||||||
|
tool_options: Mapping[str, Any] = field(default_factory=lambda: MappingProxyType({}))
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class ReusableData:
|
class ReusableData:
|
||||||
|
|
@ -972,7 +978,7 @@ class AutoTool(Tool):
|
||||||
|
|
||||||
If `ptype` or `in_port_name` is omitted, one in-domain example is
|
If `ptype` or `in_port_name` is omitted, one in-domain example is
|
||||||
generated and the missing metadata is inferred from an equivalent
|
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)
|
ptype, in_port_name = self._infer_straight_metadata(fn, length_range, ptype, in_port_name)
|
||||||
priority_bias = len(self._straight_offers) * BUILTIN_PRIORITY_STEP
|
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
|
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.
|
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(
|
ptype, in_port_name, out_port_name = self._infer_sbend_metadata(
|
||||||
fn,
|
fn,
|
||||||
|
|
@ -1225,7 +1232,7 @@ class AutoTool(Tool):
|
||||||
dxy, _ = sbpat[in_port_name].measure_travel(sbpat[out_port_name])
|
dxy, _ = sbpat[in_port_name].measure_travel(sbpat[out_port_name])
|
||||||
return dxy
|
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')
|
port_names = ('A', 'B')
|
||||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'primitive_bbox')
|
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'primitive_bbox')
|
||||||
pat.add_port_pair(names=port_names)
|
pat.add_port_pair(names=port_names)
|
||||||
|
|
@ -1247,7 +1254,7 @@ class AutoTool(Tool):
|
||||||
return numpy.asarray(bounds, dtype=float)
|
return numpy.asarray(bounds, dtype=float)
|
||||||
|
|
||||||
def _bbox_for_data(self, data: Any) -> NDArray[numpy.float64]:
|
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(
|
def _add_transition_direction(
|
||||||
self,
|
self,
|
||||||
|
|
@ -1330,21 +1337,45 @@ class AutoTool(Tool):
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> tuple[PrimitiveOffer, ...]:
|
) -> tuple[PrimitiveOffer, ...]:
|
||||||
_ = out_ptype
|
_ = 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':
|
if kind == 'straight':
|
||||||
in_key = self._wildcard_ptype_key(in_ptype)
|
in_key = self._wildcard_ptype_key(in_ptype)
|
||||||
return (
|
return (
|
||||||
*self._transition_adapter_offers_by_key.get(('straight', in_key), ()),
|
*self._transition_adapter_offers_by_key.get(('straight', in_key), ()),
|
||||||
*self._straight_offers,
|
*(configured(offer) for offer in self._straight_offers),
|
||||||
)
|
)
|
||||||
|
|
||||||
if kind == 'bend':
|
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':
|
if kind == 's':
|
||||||
in_key = self._wildcard_ptype_key(in_ptype)
|
in_key = self._wildcard_ptype_key(in_ptype)
|
||||||
return (
|
return (
|
||||||
*self._transition_adapter_offers_by_key.get(('s', in_key), ()),
|
*self._transition_adapter_offers_by_key.get(('s', in_key), ()),
|
||||||
*self._s_offers,
|
*(configured(offer) for offer in self._s_offers),
|
||||||
)
|
)
|
||||||
|
|
||||||
if kind == 'u':
|
if kind == 'u':
|
||||||
|
|
@ -1356,13 +1387,12 @@ class AutoTool(Tool):
|
||||||
data: GeneratedData,
|
data: GeneratedData,
|
||||||
tree: ILibrary,
|
tree: ILibrary,
|
||||||
port_names: tuple[str, str],
|
port_names: tuple[str, str],
|
||||||
gen_kwargs: dict[str, Any],
|
|
||||||
) -> ILibrary:
|
) -> ILibrary:
|
||||||
if numpy.isclose(data.parameter, 0):
|
if numpy.isclose(data.parameter, 0):
|
||||||
return tree
|
return tree
|
||||||
|
|
||||||
pat = tree.top_pattern()
|
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}
|
pmap = {port_names[1]: data.port_name}
|
||||||
if isinstance(generated, Pattern):
|
if isinstance(generated, Pattern):
|
||||||
pat.plug(generated, pmap, append=True, mirrored=data.mirrored)
|
pat.plug(generated, pmap, append=True, mirrored=data.mirrored)
|
||||||
|
|
@ -1387,10 +1417,9 @@ class AutoTool(Tool):
|
||||||
data: Any,
|
data: Any,
|
||||||
tree: ILibrary,
|
tree: ILibrary,
|
||||||
port_names: tuple[str, str],
|
port_names: tuple[str, str],
|
||||||
gen_kwargs: dict[str, Any],
|
|
||||||
) -> ILibrary:
|
) -> ILibrary:
|
||||||
if isinstance(data, self.GeneratedData):
|
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):
|
if isinstance(data, self.ReusableData):
|
||||||
return self._render_reusable(data=data, tree=tree, port_names=port_names)
|
return self._render_reusable(data=data, tree=tree, port_names=port_names)
|
||||||
raise BuildError(f'Unexpected AutoTool render data {type(data).__name__}')
|
raise BuildError(f'Unexpected AutoTool render data {type(data).__name__}')
|
||||||
|
|
@ -1400,7 +1429,6 @@ class AutoTool(Tool):
|
||||||
batch: Sequence[RenderStep],
|
batch: Sequence[RenderStep],
|
||||||
*,
|
*,
|
||||||
port_names: tuple[str, str] = ('A', 'B'),
|
port_names: tuple[str, str] = ('A', 'B'),
|
||||||
**kwargs,
|
|
||||||
) -> ILibrary:
|
) -> ILibrary:
|
||||||
|
|
||||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL')
|
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL')
|
||||||
|
|
@ -1408,7 +1436,7 @@ class AutoTool(Tool):
|
||||||
|
|
||||||
for step in batch:
|
for step in batch:
|
||||||
assert step.tool == self
|
assert step.tool == self
|
||||||
self._render_data(step.data, tree, port_names, kwargs)
|
self._render_data(step.data, tree, port_names)
|
||||||
return tree
|
return tree
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1470,6 +1498,10 @@ class PathTool(Tool):
|
||||||
out_ptype: str | None = None,
|
out_ptype: str | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> tuple[PrimitiveOffer, ...]:
|
) -> 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':
|
if kind == 'u':
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
|
|
@ -1559,7 +1591,6 @@ class PathTool(Tool):
|
||||||
batch: Sequence[RenderStep],
|
batch: Sequence[RenderStep],
|
||||||
*,
|
*,
|
||||||
port_names: tuple[str, str] = ('A', 'B'),
|
port_names: tuple[str, str] = ('A', 'B'),
|
||||||
**kwargs, # noqa: ARG002 (unused)
|
|
||||||
) -> ILibrary:
|
) -> ILibrary:
|
||||||
|
|
||||||
# Transform the batch so the first port is local (at 0,0) but retains its global rotation.
|
# Transform the batch so the first port is local (at 0,0) but retains its global rotation.
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,8 @@ def ell(
|
||||||
else:
|
else:
|
||||||
if set_rotation is None:
|
if set_rotation is None:
|
||||||
raise BuildError('set_rotation must be specified if no ports have rotations!')
|
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)
|
rotations = numpy.full_like(has_rotation, set_rotation, dtype=float)
|
||||||
|
|
||||||
is_horizontal = numpy.isclose(rotations[0] % pi, 0)
|
is_horizontal = numpy.isclose(rotations[0] % pi, 0)
|
||||||
|
|
@ -156,7 +158,11 @@ def ell(
|
||||||
ch_offsets = numpy.zeros_like(y_order)
|
ch_offsets = numpy.zeros_like(y_order)
|
||||||
else:
|
else:
|
||||||
spacing_arr = numpy.asarray(spacing, dtype=float).reshape(-1)
|
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:
|
if spacing_arr.size == 1:
|
||||||
steps[1:] = spacing_arr[0]
|
steps[1:] = spacing_arr[0]
|
||||||
elif spacing_arr.size == len(ports) - 1:
|
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_sbend(make_wide_sbend, "core", "A", "B", jog_range=(0, 1e8))
|
||||||
.add_transition(lib.abstract("xin"), "EXT", "CORE")
|
.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)
|
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"
|
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] = []
|
markers: list[str | None] = []
|
||||||
|
|
||||||
def make_marked_straight(length: float, marker: str | None = None) -> Pattern:
|
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 = Pather(Library(), tools=tool, render='deferred')
|
||||||
p.ports["A"] = Port((0, 0), 0, ptype="wire")
|
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()
|
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])
|
@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_allclose(trans_port.offset, [5, 0])
|
||||||
assert isinstance(trans_data, AutoTool.ReusableData)
|
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_allclose(s_port.offset, [10, 4])
|
||||||
assert isinstance(s_data, AutoTool.GeneratedData)
|
assert isinstance(s_data, AutoTool.GeneratedData)
|
||||||
assert s_offer.out_ptype == "core"
|
assert s_offer.out_ptype == "core"
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from typing import Any, Literal, Never
|
from typing import Any, Literal, Never
|
||||||
|
import inspect
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import numpy
|
import numpy
|
||||||
from numpy import pi
|
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 import RoutePortContext, RoutingPlanner
|
||||||
from masque.builder.planner.planner import NoLegalRouteError
|
from masque.builder.planner.planner import NoLegalRouteError
|
||||||
from masque.builder.tools import BendOffer, PathTool, RenderStep, StraightOffer, Tool, UOffer
|
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:
|
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
|
details = exc_info.value.details
|
||||||
assert isinstance(exc_info.value, BuildError)
|
assert isinstance(exc_info.value, BuildError)
|
||||||
|
|
@ -211,7 +212,7 @@ def test_route_error_reports_preferred_minimum_and_request_details() -> None:
|
||||||
'ccw': True,
|
'ccw': True,
|
||||||
'length': 1,
|
'length': 1,
|
||||||
'out_ptype': 'wide',
|
'out_ptype': 'wide',
|
||||||
'diagnostic_context': 'tool-value',
|
'tool_options': {'diagnostic_context': 'tool-value'},
|
||||||
}
|
}
|
||||||
assert details.resolved_length == 1
|
assert details.resolved_length == 1
|
||||||
assert details.resolved_jog is None
|
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 = Pather(Library(), tools=PathTool(layer='M1', width=1, ptype='wire'))
|
||||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, 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)
|
p.uturn('A', 4, **kwargs)
|
||||||
|
|
||||||
def test_pather_uturn_omitted_length_uses_minimum_length_route() -> None:
|
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 numpy.allclose(p.pattern.ports['A'].offset, (0, 0))
|
||||||
assert p.pattern.ports['A'].rotation == 0
|
assert p.pattern.ports['A'].rotation == 0
|
||||||
assert len(p._paths['A']) == 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:
|
def test_pathtool_s_offer_bbox_uses_intrinsic_minimum_length() -> None:
|
||||||
tool = PathTool(layer=(1, 0), width=2, ptype='wire')
|
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)
|
bounds = offer.bbox_at(3)
|
||||||
expected = Path(vertices=[(0, 0), (1, 0), (1, 3), (2, 3)], width=2).get_bounds_single()
|
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', {}),
|
('straight', {}),
|
||||||
('bend', {'ccw': True}),
|
('bend', {'ccw': True}),
|
||||||
('s', {'length': 6}),
|
('s', {}),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_pathtool_out_ptype_unk_is_wildcard(
|
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'
|
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:
|
def test_pather_treats_notimplemented_offer_query_as_no_offers() -> None:
|
||||||
class NoOfferTool(PlanningOnlyTool):
|
class NoOfferTool(PlanningOnlyTool):
|
||||||
def primitive_offers(
|
def primitive_offers(
|
||||||
|
|
@ -437,13 +443,14 @@ class StrategyTieTool(PlanningOnlyTool):
|
||||||
) -> tuple[PrimitiveOffer, ...]:
|
) -> tuple[PrimitiveOffer, ...]:
|
||||||
self.seen_kwargs.append(dict(kwargs))
|
self.seen_kwargs.append(dict(kwargs))
|
||||||
endpoint_ptype = out_ptype or in_ptype
|
endpoint_ptype = out_ptype or in_ptype
|
||||||
|
marker = kwargs.get('marker')
|
||||||
if kind == 'straight':
|
if kind == 'straight':
|
||||||
return (StraightOffer(
|
return (StraightOffer(
|
||||||
in_ptype=in_ptype,
|
in_ptype=in_ptype,
|
||||||
out_ptype=endpoint_ptype,
|
out_ptype=endpoint_ptype,
|
||||||
**offer_callbacks(lambda length: (
|
**offer_callbacks(lambda length: (
|
||||||
Port((length, 0), rotation=pi, ptype=endpoint_ptype),
|
Port((length, 0), rotation=pi, ptype=endpoint_ptype),
|
||||||
{'kind': 'straight', 'length': length},
|
{'kind': 'straight', 'length': length, 'marker': marker},
|
||||||
)),
|
)),
|
||||||
),)
|
),)
|
||||||
if kind == 's':
|
if kind == 's':
|
||||||
|
|
@ -452,7 +459,7 @@ class StrategyTieTool(PlanningOnlyTool):
|
||||||
out_ptype=endpoint_ptype,
|
out_ptype=endpoint_ptype,
|
||||||
**offer_callbacks(lambda jog: (
|
**offer_callbacks(lambda jog: (
|
||||||
Port((3, jog), rotation=pi, ptype=endpoint_ptype),
|
Port((3, jog), rotation=pi, ptype=endpoint_ptype),
|
||||||
{'kind': 's', 'jog': jog},
|
{'kind': 's', 'jog': jog, 'marker': marker},
|
||||||
)),
|
)),
|
||||||
),)
|
),)
|
||||||
return ()
|
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:
|
def test_pather_route_strategy_is_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', marker='sentinel')
|
pather.jog('A', 4, length=10, 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)
|
||||||
assert any(kwargs.get('marker') == 'sentinel' 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:
|
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['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||||
p.pattern.ports['B'] = dst
|
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)
|
p.trace_into('A', 'B', plug_destination=False, **kwargs)
|
||||||
|
|
||||||
assert numpy.allclose(p.pattern.ports['A'].offset, (0, 0))
|
assert numpy.allclose(p.pattern.ports['A'].offset, (0, 0))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue