[Pather / RenderPather] move common functionality into PatherMixin; redo hierarchy
- (BREAKING change) Pather.mpath no longer wraps the whole bus into a container, since this has no equivalent in RenderPather. Possible this functionality will return in the future - Removed `tool_port_names` arg from Pather functions - In general RenderPather should be much closer to Pather now
This commit is contained in:
parent
d71ede927c
commit
da35019dc8
@ -2,10 +2,9 @@
|
|||||||
Manual wire/waveguide routing (`Pather`)
|
Manual wire/waveguide routing (`Pather`)
|
||||||
"""
|
"""
|
||||||
from typing import Self
|
from typing import Self
|
||||||
from collections.abc import Sequence, MutableMapping, Mapping, Iterator
|
from collections.abc import Sequence, Mapping, MutableMapping
|
||||||
import copy
|
import copy
|
||||||
import logging
|
import logging
|
||||||
from contextlib import contextmanager
|
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
@ -13,20 +12,19 @@ from numpy import pi
|
|||||||
from numpy.typing import ArrayLike
|
from numpy.typing import ArrayLike
|
||||||
|
|
||||||
from ..pattern import Pattern
|
from ..pattern import Pattern
|
||||||
from ..library import ILibrary, SINGLE_USE_PREFIX
|
from ..library import ILibrary
|
||||||
from ..error import PortError, BuildError
|
from ..error import PortError, BuildError
|
||||||
from ..ports import PortList, Port
|
from ..ports import PortList, Port
|
||||||
from ..abstract import Abstract
|
from ..utils import SupportsBool
|
||||||
from ..utils import SupportsBool, rotation_matrix_2d
|
|
||||||
from .tools import Tool
|
from .tools import Tool
|
||||||
from .utils import ell
|
from .pather_mixin import PatherMixin
|
||||||
from .builder import Builder
|
from .builder import Builder
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Pather(Builder):
|
class Pather(Builder, PatherMixin):
|
||||||
"""
|
"""
|
||||||
An extension of `Builder` which provides functionality for routing and attaching
|
An extension of `Builder` which provides functionality for routing and attaching
|
||||||
single-use patterns (e.g. wires or waveguides) and bundles / buses of such patterns.
|
single-use patterns (e.g. wires or waveguides) and bundles / buses of such patterns.
|
||||||
@ -258,60 +256,6 @@ class Pather(Builder):
|
|||||||
s = f'<Pather {self.pattern} L({len(self.library)}) {pformat(self.tools)}>'
|
s = f'<Pather {self.pattern} L({len(self.library)}) {pformat(self.tools)}>'
|
||||||
return s
|
return s
|
||||||
|
|
||||||
def retool(
|
|
||||||
self,
|
|
||||||
tool: Tool,
|
|
||||||
keys: str | Sequence[str | None] | None = None,
|
|
||||||
) -> Self:
|
|
||||||
"""
|
|
||||||
Update the `Tool` which will be used when generating `Pattern`s for the ports
|
|
||||||
given by `keys`.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
tool: The new `Tool` to use for the given ports.
|
|
||||||
keys: Which ports the tool should apply to. `None` indicates the default tool,
|
|
||||||
used when there is no matching entry in `self.tools` for the port in question.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
if keys is None or isinstance(keys, str):
|
|
||||||
self.tools[keys] = tool
|
|
||||||
else:
|
|
||||||
for key in keys:
|
|
||||||
self.tools[key] = tool
|
|
||||||
return self
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def toolctx(
|
|
||||||
self,
|
|
||||||
tool: Tool,
|
|
||||||
keys: str | Sequence[str | None] | None = None,
|
|
||||||
) -> Iterator[Self]:
|
|
||||||
"""
|
|
||||||
Context manager for temporarily `retool`-ing and reverting the `retool`
|
|
||||||
upon exiting the context.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
tool: The new `Tool` to use for the given ports.
|
|
||||||
keys: Which ports the tool should apply to. `None` indicates the default tool,
|
|
||||||
used when there is no matching entry in `self.tools` for the port in question.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
if keys is None or isinstance(keys, str):
|
|
||||||
keys = [keys]
|
|
||||||
saved_tools = {kk: self.tools.get(kk, None) for kk in keys} # If not in self.tools, save `None`
|
|
||||||
try:
|
|
||||||
yield self.retool(tool=tool, keys=keys)
|
|
||||||
finally:
|
|
||||||
for kk, tt in saved_tools.items():
|
|
||||||
if tt is None:
|
|
||||||
# delete if present
|
|
||||||
self.tools.pop(kk, None)
|
|
||||||
else:
|
|
||||||
self.tools[kk] = tt
|
|
||||||
|
|
||||||
def path(
|
def path(
|
||||||
self,
|
self,
|
||||||
@ -319,7 +263,6 @@ class Pather(Builder):
|
|||||||
ccw: SupportsBool | None,
|
ccw: SupportsBool | None,
|
||||||
length: float,
|
length: float,
|
||||||
*,
|
*,
|
||||||
tool_port_names: tuple[str, str] = ('A', 'B'),
|
|
||||||
plug_into: str | None = None,
|
plug_into: str | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
@ -338,9 +281,6 @@ class Pather(Builder):
|
|||||||
and clockwise otherwise.
|
and clockwise otherwise.
|
||||||
length: The total distance from input to output, along the input's axis only.
|
length: The total distance from input to output, along the input's axis only.
|
||||||
(There may be a tool-dependent offset along the other axis.)
|
(There may be a tool-dependent offset along the other axis.)
|
||||||
tool_port_names: The names of the ports on the generated pattern. It is unlikely
|
|
||||||
that you will need to change these. The first port is the input (to be
|
|
||||||
connected to `portspec`).
|
|
||||||
plug_into: If not None, attempts to plug the wire's output port into the provided
|
plug_into: If not None, attempts to plug the wire's output port into the provided
|
||||||
port on `self`.
|
port on `self`.
|
||||||
|
|
||||||
@ -355,369 +295,16 @@ class Pather(Builder):
|
|||||||
logger.error('Skipping path() since device is dead')
|
logger.error('Skipping path() since device is dead')
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
tool_port_names = ('A', 'B')
|
||||||
|
|
||||||
tool = self.tools.get(portspec, self.tools[None])
|
tool = self.tools.get(portspec, self.tools[None])
|
||||||
in_ptype = self.pattern[portspec].ptype
|
in_ptype = self.pattern[portspec].ptype
|
||||||
tree = tool.path(ccw, length, in_ptype=in_ptype, port_names=tool_port_names, **kwargs)
|
tree = tool.path(ccw, length, in_ptype=in_ptype, port_names=tool_port_names, **kwargs)
|
||||||
abstract = self.library << tree
|
abstract = self.library << tree # TODO this seems like a name, not an abstract
|
||||||
if plug_into is not None:
|
if plug_into is not None:
|
||||||
output = {plug_into: tool_port_names[1]}
|
output = {plug_into: tool_port_names[1]}
|
||||||
else:
|
else:
|
||||||
output = {}
|
output = {}
|
||||||
return self.plug(abstract, {portspec: tool_port_names[0], **output})
|
self.plug(abstract, {portspec: tool_port_names[0], **output})
|
||||||
|
|
||||||
def path_to(
|
|
||||||
self,
|
|
||||||
portspec: str,
|
|
||||||
ccw: SupportsBool | None,
|
|
||||||
position: float | None = None,
|
|
||||||
*,
|
|
||||||
x: float | None = None,
|
|
||||||
y: float | None = None,
|
|
||||||
tool_port_names: tuple[str, str] = ('A', 'B'),
|
|
||||||
plug_into: str | None = None,
|
|
||||||
**kwargs,
|
|
||||||
) -> Self:
|
|
||||||
"""
|
|
||||||
Create a "wire"/"waveguide" and `plug` it into the port `portspec`, with the aim
|
|
||||||
of ending exactly at a target position.
|
|
||||||
|
|
||||||
The wire will travel so that the output port will be placed at exactly the target
|
|
||||||
position along the input port's axis. There can be an unspecified (tool-dependent)
|
|
||||||
offset in the perpendicular direction. The output port will be rotated (or not)
|
|
||||||
based on the `ccw` parameter.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
portspec: The name of the port into which the wire will be plugged.
|
|
||||||
ccw: If `None`, the output should be along the same axis as the input.
|
|
||||||
Otherwise, cast to bool and turn counterclockwise if True
|
|
||||||
and clockwise otherwise.
|
|
||||||
position: The final port position, along the input's axis only.
|
|
||||||
(There may be a tool-dependent offset along the other axis.)
|
|
||||||
Only one of `position`, `x`, and `y` may be specified.
|
|
||||||
x: The final port position along the x axis.
|
|
||||||
`portspec` must refer to a horizontal port if `x` is passed, otherwise a
|
|
||||||
BuildError will be raised.
|
|
||||||
y: The final port position along the y axis.
|
|
||||||
`portspec` must refer to a vertical port if `y` is passed, otherwise a
|
|
||||||
BuildError will be raised.
|
|
||||||
tool_port_names: The names of the ports on the generated pattern. It is unlikely
|
|
||||||
that you will need to change these. The first port is the input (to be
|
|
||||||
connected to `portspec`).
|
|
||||||
plug_into: If not None, attempts to plug the wire's output port into the provided
|
|
||||||
port on `self`.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
BuildError if `position`, `x`, or `y` is too close to fit the bend (if a bend
|
|
||||||
is present).
|
|
||||||
BuildError if `x` or `y` is specified but does not match the axis of `portspec`.
|
|
||||||
BuildError if more than one of `x`, `y`, and `position` is specified.
|
|
||||||
"""
|
|
||||||
if self._dead:
|
|
||||||
logger.error('Skipping path_to() since device is dead')
|
|
||||||
return self
|
|
||||||
|
|
||||||
pos_count = sum(vv is not None for vv in (position, x, y))
|
|
||||||
if pos_count > 1:
|
|
||||||
raise BuildError('Only one of `position`, `x`, and `y` may be specified at once')
|
|
||||||
if pos_count < 1:
|
|
||||||
raise BuildError('One of `position`, `x`, and `y` must be specified')
|
|
||||||
|
|
||||||
port = self.pattern[portspec]
|
|
||||||
if port.rotation is None:
|
|
||||||
raise PortError(f'Port {portspec} has no rotation and cannot be used for path_to()')
|
|
||||||
|
|
||||||
if not numpy.isclose(port.rotation % (pi / 2), 0):
|
|
||||||
raise BuildError('path_to was asked to route from non-manhattan port')
|
|
||||||
|
|
||||||
is_horizontal = numpy.isclose(port.rotation % pi, 0)
|
|
||||||
if is_horizontal:
|
|
||||||
if y is not None:
|
|
||||||
raise BuildError('Asked to path to y-coordinate, but port is horizontal')
|
|
||||||
if position is None:
|
|
||||||
position = x
|
|
||||||
else:
|
|
||||||
if x is not None:
|
|
||||||
raise BuildError('Asked to path to x-coordinate, but port is vertical')
|
|
||||||
if position is None:
|
|
||||||
position = y
|
|
||||||
|
|
||||||
x0, y0 = port.offset
|
|
||||||
if is_horizontal:
|
|
||||||
if numpy.sign(numpy.cos(port.rotation)) == numpy.sign(position - x0):
|
|
||||||
raise BuildError(f'path_to routing to behind source port: x0={x0:g} to {position:g}')
|
|
||||||
length = numpy.abs(position - x0)
|
|
||||||
else:
|
|
||||||
if numpy.sign(numpy.sin(port.rotation)) == numpy.sign(position - y0):
|
|
||||||
raise BuildError(f'path_to routing to behind source port: y0={y0:g} to {position:g}')
|
|
||||||
length = numpy.abs(position - y0)
|
|
||||||
|
|
||||||
return self.path(
|
|
||||||
portspec,
|
|
||||||
ccw,
|
|
||||||
length,
|
|
||||||
tool_port_names = tool_port_names,
|
|
||||||
plug_into = plug_into,
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
|
|
||||||
def path_into(
|
|
||||||
self,
|
|
||||||
portspec_src: str,
|
|
||||||
portspec_dst: str,
|
|
||||||
*,
|
|
||||||
tool_port_names: tuple[str, str] = ('A', 'B'),
|
|
||||||
out_ptype: str | None = None,
|
|
||||||
plug_destination: bool = True,
|
|
||||||
**kwargs,
|
|
||||||
) -> Self:
|
|
||||||
"""
|
|
||||||
Create a "wire"/"waveguide" and traveling between the ports `portspec_src` and
|
|
||||||
`portspec_dst`, and `plug` it into both (or just the source port).
|
|
||||||
|
|
||||||
Only unambiguous scenarios are allowed:
|
|
||||||
- Straight connector between facing ports
|
|
||||||
- Single 90 degree bend
|
|
||||||
- Jog between facing ports
|
|
||||||
(jog is done as late as possible, i.e. only 2 L-shaped segments are used)
|
|
||||||
|
|
||||||
By default, the destination's `pytpe` will be used as the `out_ptype` for the
|
|
||||||
wire, and the `portspec_dst` will be plugged (i.e. removed).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
portspec_src: The name of the starting port into which the wire will be plugged.
|
|
||||||
portspec_dst: The name of the destination port.
|
|
||||||
tool_port_names: The names of the ports on the generated pattern. It is unlikely
|
|
||||||
that you will need to change these. The first port is the input (to be
|
|
||||||
connected to `portspec`).
|
|
||||||
out_ptype: Passed to the pathing tool in order to specify the desired port type
|
|
||||||
to be generated at the destination end. If `None` (default), the destination
|
|
||||||
port's `ptype` will be used.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
PortError if either port does not have a specified rotation.
|
|
||||||
BuildError if and invalid port config is encountered:
|
|
||||||
- Non-manhattan ports
|
|
||||||
- U-bend
|
|
||||||
- Destination too close to (or behind) source
|
|
||||||
"""
|
|
||||||
if self._dead:
|
|
||||||
logger.error('Skipping path_into() since device is dead')
|
|
||||||
return self
|
|
||||||
|
|
||||||
port_src = self.pattern[portspec_src]
|
|
||||||
port_dst = self.pattern[portspec_dst]
|
|
||||||
|
|
||||||
if out_ptype is None:
|
|
||||||
out_ptype = port_dst.ptype
|
|
||||||
|
|
||||||
if port_src.rotation is None:
|
|
||||||
raise PortError(f'Port {portspec_src} has no rotation and cannot be used for path_into()')
|
|
||||||
if port_dst.rotation is None:
|
|
||||||
raise PortError(f'Port {portspec_dst} has no rotation and cannot be used for path_into()')
|
|
||||||
|
|
||||||
if not numpy.isclose(port_src.rotation % (pi / 2), 0):
|
|
||||||
raise BuildError('path_into was asked to route from non-manhattan port')
|
|
||||||
if not numpy.isclose(port_dst.rotation % (pi / 2), 0):
|
|
||||||
raise BuildError('path_into was asked to route to non-manhattan port')
|
|
||||||
|
|
||||||
src_is_horizontal = numpy.isclose(port_src.rotation % pi, 0)
|
|
||||||
dst_is_horizontal = numpy.isclose(port_dst.rotation % pi, 0)
|
|
||||||
xs, ys = port_src.offset
|
|
||||||
xd, yd = port_dst.offset
|
|
||||||
|
|
||||||
angle = (port_dst.rotation - port_src.rotation) % (2 * pi)
|
|
||||||
|
|
||||||
src_ne = port_src.rotation % (2 * pi) > (3 * pi / 4) # path from src will go north or east
|
|
||||||
|
|
||||||
def get_jog(ccw: SupportsBool, length: float) -> float:
|
|
||||||
tool = self.tools.get(portspec_src, self.tools[None])
|
|
||||||
in_ptype = 'unk' # Could use port_src.ptype, but we're assuming this is after one bend already...
|
|
||||||
tree2 = tool.path(ccw, length, in_ptype=in_ptype, port_names=('A', 'B'), out_ptype=out_ptype, **kwargs)
|
|
||||||
top2 = tree2.top_pattern()
|
|
||||||
jog = rotation_matrix_2d(top2['A'].rotation) @ (top2['B'].offset - top2['A'].offset)
|
|
||||||
return jog[1] * [-1, 1][int(bool(ccw))]
|
|
||||||
|
|
||||||
dst_extra_args = {'out_ptype': out_ptype}
|
|
||||||
if plug_destination:
|
|
||||||
dst_extra_args['plug_into'] = portspec_dst
|
|
||||||
|
|
||||||
src_args = {**kwargs, 'tool_port_names': tool_port_names}
|
|
||||||
dst_args = {**src_args, **dst_extra_args}
|
|
||||||
if src_is_horizontal and not dst_is_horizontal:
|
|
||||||
# single bend should suffice
|
|
||||||
self.path_to(portspec_src, angle > pi, x=xd, **src_args)
|
|
||||||
self.path_to(portspec_src, None, y=yd, **dst_args)
|
|
||||||
elif dst_is_horizontal and not src_is_horizontal:
|
|
||||||
# single bend should suffice
|
|
||||||
self.path_to(portspec_src, angle > pi, y=yd, **src_args)
|
|
||||||
self.path_to(portspec_src, None, x=xd, **dst_args)
|
|
||||||
elif numpy.isclose(angle, pi):
|
|
||||||
if src_is_horizontal and ys == yd:
|
|
||||||
# straight connector
|
|
||||||
self.path_to(portspec_src, None, x=xd, **dst_args)
|
|
||||||
elif not src_is_horizontal and xs == xd:
|
|
||||||
# straight connector
|
|
||||||
self.path_to(portspec_src, None, y=yd, **dst_args)
|
|
||||||
elif src_is_horizontal:
|
|
||||||
# figure out how much x our y-segment (2nd) takes up, then path based on that
|
|
||||||
y_len = numpy.abs(yd - ys)
|
|
||||||
ccw2 = src_ne != (yd > ys)
|
|
||||||
jog = get_jog(ccw2, y_len) * numpy.sign(xd - xs)
|
|
||||||
self.path_to(portspec_src, not ccw2, x=xd - jog, **src_args)
|
|
||||||
self.path_to(portspec_src, ccw2, y=yd, **dst_args)
|
|
||||||
else:
|
|
||||||
# figure out how much y our x-segment (2nd) takes up, then path based on that
|
|
||||||
x_len = numpy.abs(xd - xs)
|
|
||||||
ccw2 = src_ne != (xd < xs)
|
|
||||||
jog = get_jog(ccw2, x_len) * numpy.sign(yd - ys)
|
|
||||||
self.path_to(portspec_src, not ccw2, y=yd - jog, **src_args)
|
|
||||||
self.path_to(portspec_src, ccw2, x=xd, **dst_args)
|
|
||||||
elif numpy.isclose(angle, 0):
|
|
||||||
raise BuildError('Don\'t know how to route a U-bend at this time!')
|
|
||||||
else:
|
|
||||||
raise BuildError(f'Don\'t know how to route ports with relative angle {angle}')
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
def mpath(
|
|
||||||
self,
|
|
||||||
portspec: str | Sequence[str],
|
|
||||||
ccw: SupportsBool | None,
|
|
||||||
*,
|
|
||||||
spacing: float | ArrayLike | None = None,
|
|
||||||
set_rotation: float | None = None,
|
|
||||||
tool_port_names: tuple[str, str] = ('A', 'B'),
|
|
||||||
force_container: bool = False,
|
|
||||||
base_name: str = SINGLE_USE_PREFIX + 'mpath',
|
|
||||||
**kwargs,
|
|
||||||
) -> Self:
|
|
||||||
"""
|
|
||||||
`mpath` is a superset of `path` and `path_to` which can act on bundles or buses
|
|
||||||
of "wires or "waveguides".
|
|
||||||
|
|
||||||
The wires will travel so that the output ports will be placed at well-defined
|
|
||||||
locations along the axis of their input ports, but may have arbitrary (tool-
|
|
||||||
dependent) offsets in the perpendicular direction.
|
|
||||||
|
|
||||||
If `ccw` is not `None`, the wire bundle will turn 90 degres in either the
|
|
||||||
clockwise (`ccw=False`) or counter-clockwise (`ccw=True`) direction. Within the
|
|
||||||
bundle, the center-to-center wire spacings after the turn are set by `spacing`,
|
|
||||||
which is required when `ccw` is not `None`. The final position of bundle as a
|
|
||||||
whole can be set in a number of ways:
|
|
||||||
|
|
||||||
=A>---------------------------V turn direction: `ccw=False`
|
|
||||||
=B>-------------V |
|
|
||||||
=C>-----------------------V |
|
|
||||||
=D=>----------------V |
|
|
||||||
|
|
|
||||||
|
|
||||||
x---x---x---x `spacing` (can be scalar or array)
|
|
||||||
|
|
||||||
<--------------> `emin=`
|
|
||||||
<------> `bound_type='min_past_furthest', bound=`
|
|
||||||
<--------------------------------> `emax=`
|
|
||||||
x `pmin=`
|
|
||||||
x `pmax=`
|
|
||||||
|
|
||||||
- `emin=`, equivalent to `bound_type='min_extension', bound=`
|
|
||||||
The total extension value for the furthest-out port (B in the diagram).
|
|
||||||
- `emax=`, equivalent to `bound_type='max_extension', bound=`:
|
|
||||||
The total extension value for the closest-in port (C in the diagram).
|
|
||||||
- `pmin=`, equivalent to `xmin=`, `ymin=`, or `bound_type='min_position', bound=`:
|
|
||||||
The coordinate of the innermost bend (D's bend).
|
|
||||||
The x/y versions throw an error if they do not match the port axis (for debug)
|
|
||||||
- `pmax=`, `xmax=`, `ymax=`, or `bound_type='max_position', bound=`:
|
|
||||||
The coordinate of the outermost bend (A's bend).
|
|
||||||
The x/y versions throw an error if they do not match the port axis (for debug)
|
|
||||||
- `bound_type='min_past_furthest', bound=`:
|
|
||||||
The distance between furthest out-port (B) and the innermost bend (D's bend).
|
|
||||||
|
|
||||||
If `ccw=None`, final output positions (along the input axis) of all wires will be
|
|
||||||
identical (i.e. wires will all be cut off evenly). In this case, `spacing=None` is
|
|
||||||
required. In this case, `emin=` and `emax=` are equivalent to each other, and
|
|
||||||
`pmin=`, `pmax=`, `xmin=`, etc. are also equivalent to each other.
|
|
||||||
|
|
||||||
|
|
||||||
Args:
|
|
||||||
portspec: The names of the ports which are to be routed.
|
|
||||||
ccw: If `None`, the outputs should be along the same axis as the inputs.
|
|
||||||
Otherwise, cast to bool and turn 90 degrees counterclockwise if `True`
|
|
||||||
and clockwise otherwise.
|
|
||||||
spacing: Center-to-center distance between output ports along the input port's axis.
|
|
||||||
Must be provided if (and only if) `ccw` is not `None`.
|
|
||||||
set_rotation: If the provided ports have `rotation=None`, this can be used
|
|
||||||
to set a rotation for them.
|
|
||||||
tool_port_names: The names of the ports on the generated pattern. It is unlikely
|
|
||||||
that you will need to change these. The first port is the input (to be
|
|
||||||
connected to `portspec`).
|
|
||||||
force_container: If `False` (default), and only a single port is provided, the
|
|
||||||
generated wire for that port will be referenced directly, rather than being
|
|
||||||
wrapped in an additonal `Pattern`.
|
|
||||||
base_name: Name to use for the generated `Pattern`. This will be passed through
|
|
||||||
`self.library.get_name()` to get a unique name for each new `Pattern`.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
BuildError if the implied length for any wire is too close to fit the bend
|
|
||||||
(if a bend is requested).
|
|
||||||
BuildError if `xmin`/`xmax` or `ymin`/`ymax` is specified but does not
|
|
||||||
match the axis of `portspec`.
|
|
||||||
BuildError if an incorrect bound type or spacing is specified.
|
|
||||||
"""
|
|
||||||
if self._dead:
|
|
||||||
logger.error('Skipping mpath() since device is dead')
|
|
||||||
return self
|
|
||||||
|
|
||||||
bound_types = set()
|
|
||||||
if 'bound_type' in kwargs:
|
|
||||||
bound_types.add(kwargs.pop('bound_type'))
|
|
||||||
bound = kwargs.pop('bound')
|
|
||||||
for bt in ('emin', 'emax', 'pmin', 'pmax', 'xmin', 'xmax', 'ymin', 'ymax', 'min_past_furthest'):
|
|
||||||
if bt in kwargs:
|
|
||||||
bound_types.add(bt)
|
|
||||||
bound = kwargs.pop('bt')
|
|
||||||
|
|
||||||
if not bound_types:
|
|
||||||
raise BuildError('No bound type specified for mpath')
|
|
||||||
if len(bound_types) > 1:
|
|
||||||
raise BuildError(f'Too many bound types specified for mpath: {bound_types}')
|
|
||||||
bound_type = tuple(bound_types)[0]
|
|
||||||
|
|
||||||
if isinstance(portspec, str):
|
|
||||||
portspec = [portspec]
|
|
||||||
ports = self.pattern[tuple(portspec)]
|
|
||||||
|
|
||||||
extensions = ell(ports, ccw, spacing=spacing, bound=bound, bound_type=bound_type, set_rotation=set_rotation)
|
|
||||||
|
|
||||||
if len(ports) == 1 and not force_container:
|
|
||||||
# Not a bus, so having a container just adds noise to the layout
|
|
||||||
port_name = tuple(portspec)[0]
|
|
||||||
return self.path(port_name, ccw, extensions[port_name], tool_port_names=tool_port_names, **kwargs)
|
|
||||||
|
|
||||||
bld = Pather.interface(source=ports, library=self.library, tools=self.tools)
|
|
||||||
for port_name, length in extensions.items():
|
|
||||||
bld.path(port_name, ccw, length, tool_port_names=tool_port_names, **kwargs)
|
|
||||||
name = self.library.get_name(base_name)
|
|
||||||
self.library[name] = bld.pattern
|
|
||||||
return self.plug(Abstract(name, bld.pattern.ports), {sp: 'in_' + sp for sp in ports}) # TODO safe to use 'in_'?
|
|
||||||
|
|
||||||
# TODO def bus_join()?
|
|
||||||
|
|
||||||
def flatten(self) -> Self:
|
|
||||||
"""
|
|
||||||
Flatten the contained pattern, using the contained library to resolve references.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
self.pattern.flatten(self.library)
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|||||||
450
masque/builder/pather_mixin.py
Normal file
450
masque/builder/pather_mixin.py
Normal file
@ -0,0 +1,450 @@
|
|||||||
|
from typing import SupportsFloat, cast, TYPE_CHECKING, Iterator, Self
|
||||||
|
from collections.abc import Sequence, Mapping, MutableMapping
|
||||||
|
import logging
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from abc import abstractmethod, ABCMeta
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
from numpy import pi
|
||||||
|
from numpy.typing import ArrayLike, NDArray
|
||||||
|
|
||||||
|
from ..pattern import Pattern
|
||||||
|
from ..library import ILibrary
|
||||||
|
from ..error import PortError, BuildError
|
||||||
|
from ..ports import PortList, Port
|
||||||
|
from ..utils import rotation_matrix_2d, SupportsBool
|
||||||
|
from ..abstract import Abstract
|
||||||
|
from .tools import Tool
|
||||||
|
from .utils import ell
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PatherMixin(metaclass=ABCMeta):
|
||||||
|
pattern: Pattern
|
||||||
|
""" Layout of this device """
|
||||||
|
|
||||||
|
library: ILibrary
|
||||||
|
""" Library from which patterns should be referenced """
|
||||||
|
|
||||||
|
_dead: bool
|
||||||
|
""" If True, plug()/place() are skipped (for debugging) """
|
||||||
|
|
||||||
|
tools: dict[str | None, Tool]
|
||||||
|
"""
|
||||||
|
Tool objects are used to dynamically generate new single-use Devices
|
||||||
|
(e.g wires or waveguides) to be plugged into this device.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def path(
|
||||||
|
self,
|
||||||
|
portspec: str,
|
||||||
|
ccw: SupportsBool | None,
|
||||||
|
length: float,
|
||||||
|
*,
|
||||||
|
plug_into: str | None = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> Self:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def retool(
|
||||||
|
self,
|
||||||
|
tool: Tool,
|
||||||
|
keys: str | Sequence[str | None] | None = None,
|
||||||
|
) -> Self:
|
||||||
|
"""
|
||||||
|
Update the `Tool` which will be used when generating `Pattern`s for the ports
|
||||||
|
given by `keys`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tool: The new `Tool` to use for the given ports.
|
||||||
|
keys: Which ports the tool should apply to. `None` indicates the default tool,
|
||||||
|
used when there is no matching entry in `self.tools` for the port in question.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
"""
|
||||||
|
if keys is None or isinstance(keys, str):
|
||||||
|
self.tools[keys] = tool
|
||||||
|
else:
|
||||||
|
for key in keys:
|
||||||
|
self.tools[key] = tool
|
||||||
|
return self
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def toolctx(
|
||||||
|
self,
|
||||||
|
tool: Tool,
|
||||||
|
keys: str | Sequence[str | None] | None = None,
|
||||||
|
) -> Iterator[Self]:
|
||||||
|
"""
|
||||||
|
Context manager for temporarily `retool`-ing and reverting the `retool`
|
||||||
|
upon exiting the context.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tool: The new `Tool` to use for the given ports.
|
||||||
|
keys: Which ports the tool should apply to. `None` indicates the default tool,
|
||||||
|
used when there is no matching entry in `self.tools` for the port in question.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
"""
|
||||||
|
if keys is None or isinstance(keys, str):
|
||||||
|
keys = [keys]
|
||||||
|
saved_tools = {kk: self.tools.get(kk, None) for kk in keys} # If not in self.tools, save `None`
|
||||||
|
try:
|
||||||
|
yield self.retool(tool=tool, keys=keys)
|
||||||
|
finally:
|
||||||
|
for kk, tt in saved_tools.items():
|
||||||
|
if tt is None:
|
||||||
|
# delete if present
|
||||||
|
self.tools.pop(kk, None)
|
||||||
|
else:
|
||||||
|
self.tools[kk] = tt
|
||||||
|
|
||||||
|
def path_to(
|
||||||
|
self,
|
||||||
|
portspec: str,
|
||||||
|
ccw: SupportsBool | None,
|
||||||
|
position: float | None = None,
|
||||||
|
*,
|
||||||
|
x: float | None = None,
|
||||||
|
y: float | None = None,
|
||||||
|
plug_into: str | None = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> Self:
|
||||||
|
"""
|
||||||
|
Build a "wire"/"waveguide" extending from the port `portspec`, with the aim
|
||||||
|
of ending exactly at a target position.
|
||||||
|
|
||||||
|
The wire will travel so that the output port will be placed at exactly the target
|
||||||
|
position along the input port's axis. There can be an unspecified (tool-dependent)
|
||||||
|
offset in the perpendicular direction. The output port will be rotated (or not)
|
||||||
|
based on the `ccw` parameter.
|
||||||
|
|
||||||
|
If using `RenderPather`, `RenderPather.render` must be called after all paths have been fully planned.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
portspec: The name of the port into which the wire will be plugged.
|
||||||
|
ccw: If `None`, the output should be along the same axis as the input.
|
||||||
|
Otherwise, cast to bool and turn counterclockwise if True
|
||||||
|
and clockwise otherwise.
|
||||||
|
position: The final port position, along the input's axis only.
|
||||||
|
(There may be a tool-dependent offset along the other axis.)
|
||||||
|
Only one of `position`, `x`, and `y` may be specified.
|
||||||
|
x: The final port position along the x axis.
|
||||||
|
`portspec` must refer to a horizontal port if `x` is passed, otherwise a
|
||||||
|
BuildError will be raised.
|
||||||
|
y: The final port position along the y axis.
|
||||||
|
`portspec` must refer to a vertical port if `y` is passed, otherwise a
|
||||||
|
BuildError will be raised.
|
||||||
|
plug_into: If not None, attempts to plug the wire's output port into the provided
|
||||||
|
port on `self`.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
BuildError if `position`, `x`, or `y` is too close to fit the bend (if a bend
|
||||||
|
is present).
|
||||||
|
BuildError if `x` or `y` is specified but does not match the axis of `portspec`.
|
||||||
|
BuildError if more than one of `x`, `y`, and `position` is specified.
|
||||||
|
"""
|
||||||
|
if self._dead:
|
||||||
|
logger.error('Skipping path_to() since device is dead')
|
||||||
|
return self
|
||||||
|
|
||||||
|
pos_count = sum(vv is not None for vv in (position, x, y))
|
||||||
|
if pos_count > 1:
|
||||||
|
raise BuildError('Only one of `position`, `x`, and `y` may be specified at once')
|
||||||
|
if pos_count < 1:
|
||||||
|
raise BuildError('One of `position`, `x`, and `y` must be specified')
|
||||||
|
|
||||||
|
port = self.pattern[portspec]
|
||||||
|
if port.rotation is None:
|
||||||
|
raise PortError(f'Port {portspec} has no rotation and cannot be used for path_to()')
|
||||||
|
|
||||||
|
if not numpy.isclose(port.rotation % (pi / 2), 0):
|
||||||
|
raise BuildError('path_to was asked to route from non-manhattan port')
|
||||||
|
|
||||||
|
is_horizontal = numpy.isclose(port.rotation % pi, 0)
|
||||||
|
if is_horizontal:
|
||||||
|
if y is not None:
|
||||||
|
raise BuildError('Asked to path to y-coordinate, but port is horizontal')
|
||||||
|
if position is None:
|
||||||
|
position = x
|
||||||
|
else:
|
||||||
|
if x is not None:
|
||||||
|
raise BuildError('Asked to path to x-coordinate, but port is vertical')
|
||||||
|
if position is None:
|
||||||
|
position = y
|
||||||
|
|
||||||
|
x0, y0 = port.offset
|
||||||
|
if is_horizontal:
|
||||||
|
if numpy.sign(numpy.cos(port.rotation)) == numpy.sign(position - x0):
|
||||||
|
raise BuildError(f'path_to routing to behind source port: x0={x0:g} to {position:g}')
|
||||||
|
length = numpy.abs(position - x0)
|
||||||
|
else:
|
||||||
|
if numpy.sign(numpy.sin(port.rotation)) == numpy.sign(position - y0):
|
||||||
|
raise BuildError(f'path_to routing to behind source port: y0={y0:g} to {position:g}')
|
||||||
|
length = numpy.abs(position - y0)
|
||||||
|
|
||||||
|
return self.path(
|
||||||
|
portspec,
|
||||||
|
ccw,
|
||||||
|
length,
|
||||||
|
plug_into = plug_into,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
def path_into(
|
||||||
|
self,
|
||||||
|
portspec_src: str,
|
||||||
|
portspec_dst: str,
|
||||||
|
*,
|
||||||
|
tool_port_names: tuple[str, str] = ('A', 'B'),
|
||||||
|
out_ptype: str | None = None,
|
||||||
|
plug_destination: bool = True,
|
||||||
|
**kwargs,
|
||||||
|
) -> Self:
|
||||||
|
"""
|
||||||
|
Create a "wire"/"waveguide" traveling between the ports `portspec_src` and
|
||||||
|
`portspec_dst`, and `plug` it into both (or just the source port).
|
||||||
|
|
||||||
|
Only unambiguous scenarios are allowed:
|
||||||
|
- Straight connector between facing ports
|
||||||
|
- Single 90 degree bend
|
||||||
|
- Jog between facing ports
|
||||||
|
(jog is done as late as possible, i.e. only 2 L-shaped segments are used)
|
||||||
|
|
||||||
|
By default, the destination's `pytpe` will be used as the `out_ptype` for the
|
||||||
|
wire, and the `portspec_dst` will be plugged (i.e. removed).
|
||||||
|
|
||||||
|
If using `RenderPather`, `RenderPather.render` must be called after all paths have been fully planned.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
portspec_src: The name of the starting port into which the wire will be plugged.
|
||||||
|
portspec_dst: The name of the destination port.
|
||||||
|
tool_port_names: The names of the ports on the generated pattern. It is unlikely
|
||||||
|
that you will need to change these. The first port is the input (to be
|
||||||
|
connected to `portspec`).
|
||||||
|
out_ptype: Passed to the pathing tool in order to specify the desired port type
|
||||||
|
to be generated at the destination end. If `None` (default), the destination
|
||||||
|
port's `ptype` will be used.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
PortError if either port does not have a specified rotation.
|
||||||
|
BuildError if and invalid port config is encountered:
|
||||||
|
- Non-manhattan ports
|
||||||
|
- U-bend
|
||||||
|
- Destination too close to (or behind) source
|
||||||
|
"""
|
||||||
|
if self._dead:
|
||||||
|
logger.error('Skipping path_into() since device is dead')
|
||||||
|
return self
|
||||||
|
|
||||||
|
port_src = self.pattern[portspec_src]
|
||||||
|
port_dst = self.pattern[portspec_dst]
|
||||||
|
|
||||||
|
if out_ptype is None:
|
||||||
|
out_ptype = port_dst.ptype
|
||||||
|
|
||||||
|
if port_src.rotation is None:
|
||||||
|
raise PortError(f'Port {portspec_src} has no rotation and cannot be used for path_into()')
|
||||||
|
if port_dst.rotation is None:
|
||||||
|
raise PortError(f'Port {portspec_dst} has no rotation and cannot be used for path_into()')
|
||||||
|
|
||||||
|
if not numpy.isclose(port_src.rotation % (pi / 2), 0):
|
||||||
|
raise BuildError('path_into was asked to route from non-manhattan port')
|
||||||
|
if not numpy.isclose(port_dst.rotation % (pi / 2), 0):
|
||||||
|
raise BuildError('path_into was asked to route to non-manhattan port')
|
||||||
|
|
||||||
|
src_is_horizontal = numpy.isclose(port_src.rotation % pi, 0)
|
||||||
|
dst_is_horizontal = numpy.isclose(port_dst.rotation % pi, 0)
|
||||||
|
xs, ys = port_src.offset
|
||||||
|
xd, yd = port_dst.offset
|
||||||
|
|
||||||
|
angle = (port_dst.rotation - port_src.rotation) % (2 * pi)
|
||||||
|
|
||||||
|
src_ne = port_src.rotation % (2 * pi) > (3 * pi / 4) # path from src will go north or east
|
||||||
|
|
||||||
|
def get_jog(ccw: SupportsBool, length: float) -> float:
|
||||||
|
tool = self.tools.get(portspec_src, self.tools[None])
|
||||||
|
in_ptype = 'unk' # Could use port_src.ptype, but we're assuming this is after one bend already...
|
||||||
|
tree2 = tool.path(ccw, length, in_ptype=in_ptype, port_names=('A', 'B'), out_ptype=out_ptype, **kwargs)
|
||||||
|
top2 = tree2.top_pattern()
|
||||||
|
jog = rotation_matrix_2d(top2['A'].rotation) @ (top2['B'].offset - top2['A'].offset)
|
||||||
|
return jog[1] * [-1, 1][int(bool(ccw))]
|
||||||
|
|
||||||
|
dst_extra_args = {'out_ptype': out_ptype}
|
||||||
|
if plug_destination:
|
||||||
|
dst_extra_args['plug_into'] = portspec_dst
|
||||||
|
|
||||||
|
src_args = {**kwargs, 'tool_port_names': tool_port_names}
|
||||||
|
dst_args = {**src_args, **dst_extra_args}
|
||||||
|
if src_is_horizontal and not dst_is_horizontal:
|
||||||
|
# single bend should suffice
|
||||||
|
self.path_to(portspec_src, angle > pi, x=xd, **src_args)
|
||||||
|
self.path_to(portspec_src, None, y=yd, **dst_args)
|
||||||
|
elif dst_is_horizontal and not src_is_horizontal:
|
||||||
|
# single bend should suffice
|
||||||
|
self.path_to(portspec_src, angle > pi, y=yd, **src_args)
|
||||||
|
self.path_to(portspec_src, None, x=xd, **dst_args)
|
||||||
|
elif numpy.isclose(angle, pi):
|
||||||
|
if src_is_horizontal and ys == yd:
|
||||||
|
# straight connector
|
||||||
|
self.path_to(portspec_src, None, x=xd, **dst_args)
|
||||||
|
elif not src_is_horizontal and xs == xd:
|
||||||
|
# straight connector
|
||||||
|
self.path_to(portspec_src, None, y=yd, **dst_args)
|
||||||
|
elif src_is_horizontal:
|
||||||
|
# figure out how much x our y-segment (2nd) takes up, then path based on that
|
||||||
|
y_len = numpy.abs(yd - ys)
|
||||||
|
ccw2 = src_ne != (yd > ys)
|
||||||
|
jog = get_jog(ccw2, y_len) * numpy.sign(xd - xs)
|
||||||
|
self.path_to(portspec_src, not ccw2, x=xd - jog, **src_args)
|
||||||
|
self.path_to(portspec_src, ccw2, y=yd, **dst_args)
|
||||||
|
else:
|
||||||
|
# figure out how much y our x-segment (2nd) takes up, then path based on that
|
||||||
|
x_len = numpy.abs(xd - xs)
|
||||||
|
ccw2 = src_ne != (xd < xs)
|
||||||
|
jog = get_jog(ccw2, x_len) * numpy.sign(yd - ys)
|
||||||
|
self.path_to(portspec_src, not ccw2, y=yd - jog, **src_args)
|
||||||
|
self.path_to(portspec_src, ccw2, x=xd, **dst_args)
|
||||||
|
elif numpy.isclose(angle, 0):
|
||||||
|
raise BuildError('Don\'t know how to route a U-bend yet (TODO)!')
|
||||||
|
else:
|
||||||
|
raise BuildError(f'Don\'t know how to route ports with relative angle {angle}')
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def mpath(
|
||||||
|
self,
|
||||||
|
portspec: str | Sequence[str],
|
||||||
|
ccw: SupportsBool | None,
|
||||||
|
*,
|
||||||
|
spacing: float | ArrayLike | None = None,
|
||||||
|
set_rotation: float | None = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> Self:
|
||||||
|
"""
|
||||||
|
`mpath` is a superset of `path` and `path_to` which can act on bundles or buses
|
||||||
|
of "wires or "waveguides".
|
||||||
|
|
||||||
|
The wires will travel so that the output ports will be placed at well-defined
|
||||||
|
locations along the axis of their input ports, but may have arbitrary (tool-
|
||||||
|
dependent) offsets in the perpendicular direction.
|
||||||
|
|
||||||
|
If `ccw` is not `None`, the wire bundle will turn 90 degres in either the
|
||||||
|
clockwise (`ccw=False`) or counter-clockwise (`ccw=True`) direction. Within the
|
||||||
|
bundle, the center-to-center wire spacings after the turn are set by `spacing`,
|
||||||
|
which is required when `ccw` is not `None`. The final position of bundle as a
|
||||||
|
whole can be set in a number of ways:
|
||||||
|
|
||||||
|
=A>---------------------------V turn direction: `ccw=False`
|
||||||
|
=B>-------------V |
|
||||||
|
=C>-----------------------V |
|
||||||
|
=D=>----------------V |
|
||||||
|
|
|
||||||
|
|
||||||
|
x---x---x---x `spacing` (can be scalar or array)
|
||||||
|
|
||||||
|
<--------------> `emin=`
|
||||||
|
<------> `bound_type='min_past_furthest', bound=`
|
||||||
|
<--------------------------------> `emax=`
|
||||||
|
x `pmin=`
|
||||||
|
x `pmax=`
|
||||||
|
|
||||||
|
- `emin=`, equivalent to `bound_type='min_extension', bound=`
|
||||||
|
The total extension value for the furthest-out port (B in the diagram).
|
||||||
|
- `emax=`, equivalent to `bound_type='max_extension', bound=`:
|
||||||
|
The total extension value for the closest-in port (C in the diagram).
|
||||||
|
- `pmin=`, equivalent to `xmin=`, `ymin=`, or `bound_type='min_position', bound=`:
|
||||||
|
The coordinate of the innermost bend (D's bend).
|
||||||
|
The x/y versions throw an error if they do not match the port axis (for debug)
|
||||||
|
- `pmax=`, `xmax=`, `ymax=`, or `bound_type='max_position', bound=`:
|
||||||
|
The coordinate of the outermost bend (A's bend).
|
||||||
|
The x/y versions throw an error if they do not match the port axis (for debug)
|
||||||
|
- `bound_type='min_past_furthest', bound=`:
|
||||||
|
The distance between furthest out-port (B) and the innermost bend (D's bend).
|
||||||
|
|
||||||
|
If `ccw=None`, final output positions (along the input axis) of all wires will be
|
||||||
|
identical (i.e. wires will all be cut off evenly). In this case, `spacing=None` is
|
||||||
|
required. In this case, `emin=` and `emax=` are equivalent to each other, and
|
||||||
|
`pmin=`, `pmax=`, `xmin=`, etc. are also equivalent to each other.
|
||||||
|
|
||||||
|
If using `RenderPather`, `RenderPather.render` must be called after all paths have been fully planned.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
portspec: The names of the ports which are to be routed.
|
||||||
|
ccw: If `None`, the outputs should be along the same axis as the inputs.
|
||||||
|
Otherwise, cast to bool and turn 90 degrees counterclockwise if `True`
|
||||||
|
and clockwise otherwise.
|
||||||
|
spacing: Center-to-center distance between output ports along the input port's axis.
|
||||||
|
Must be provided if (and only if) `ccw` is not `None`.
|
||||||
|
set_rotation: If the provided ports have `rotation=None`, this can be used
|
||||||
|
to set a rotation for them.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
BuildError if the implied length for any wire is too close to fit the bend
|
||||||
|
(if a bend is requested).
|
||||||
|
BuildError if `xmin`/`xmax` or `ymin`/`ymax` is specified but does not
|
||||||
|
match the axis of `portspec`.
|
||||||
|
BuildError if an incorrect bound type or spacing is specified.
|
||||||
|
"""
|
||||||
|
if self._dead:
|
||||||
|
logger.error('Skipping mpath() since device is dead')
|
||||||
|
return self
|
||||||
|
|
||||||
|
bound_types = set()
|
||||||
|
if 'bound_type' in kwargs:
|
||||||
|
bound_types.add(kwargs.pop('bound_type'))
|
||||||
|
bound = kwargs.pop('bound')
|
||||||
|
for bt in ('emin', 'emax', 'pmin', 'pmax', 'xmin', 'xmax', 'ymin', 'ymax', 'min_past_furthest'):
|
||||||
|
if bt in kwargs:
|
||||||
|
bound_types.add(bt)
|
||||||
|
bound = kwargs.pop('bt')
|
||||||
|
|
||||||
|
if not bound_types:
|
||||||
|
raise BuildError('No bound type specified for mpath')
|
||||||
|
if len(bound_types) > 1:
|
||||||
|
raise BuildError(f'Too many bound types specified for mpath: {bound_types}')
|
||||||
|
bound_type = tuple(bound_types)[0]
|
||||||
|
|
||||||
|
if isinstance(portspec, str):
|
||||||
|
portspec = [portspec]
|
||||||
|
ports = self.pattern[tuple(portspec)]
|
||||||
|
|
||||||
|
extensions = ell(ports, ccw, spacing=spacing, bound=bound, bound_type=bound_type, set_rotation=set_rotation)
|
||||||
|
|
||||||
|
#if container:
|
||||||
|
# assert not getattr(self, 'render'), 'Containers not implemented for RenderPather'
|
||||||
|
# bld = self.interface(source=ports, library=self.library, tools=self.tools)
|
||||||
|
# for port_name, length in extensions.items():
|
||||||
|
# bld.path(port_name, ccw, length, **kwargs)
|
||||||
|
# self.library[container] = bld.pattern
|
||||||
|
# self.plug(Abstract(container, bld.pattern.ports), {sp: 'in_' + sp for sp in ports}) # TODO safe to use 'in_'?
|
||||||
|
#else:
|
||||||
|
for port_name, length in extensions.items():
|
||||||
|
self.path(port_name, ccw, length, **kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
|
# TODO def bus_join()?
|
||||||
|
|
||||||
|
def flatten(self) -> Self:
|
||||||
|
"""
|
||||||
|
Flatten the contained pattern, using the contained library to resolve references.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
"""
|
||||||
|
self.pattern.flatten(self.library)
|
||||||
|
return self
|
||||||
@ -19,13 +19,13 @@ from ..ports import PortList, Port
|
|||||||
from ..abstract import Abstract
|
from ..abstract import Abstract
|
||||||
from ..utils import SupportsBool
|
from ..utils import SupportsBool
|
||||||
from .tools import Tool, RenderStep
|
from .tools import Tool, RenderStep
|
||||||
from .utils import ell
|
from .pather_mixin import PatherMixin
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class RenderPather(PortList):
|
class RenderPather(PortList, PatherMixin):
|
||||||
"""
|
"""
|
||||||
`RenderPather` is an alternative to `Pather` which uses the `path`/`path_to`/`mpath`
|
`RenderPather` is an alternative to `Pather` which uses the `path`/`path_to`/`mpath`
|
||||||
functions to plan out wire paths without incrementally generating the layout. Instead,
|
functions to plan out wire paths without incrementally generating the layout. Instead,
|
||||||
@ -108,15 +108,11 @@ class RenderPather(PortList):
|
|||||||
if self.pattern.ports:
|
if self.pattern.ports:
|
||||||
raise BuildError('Ports supplied for pattern with pre-existing ports!')
|
raise BuildError('Ports supplied for pattern with pre-existing ports!')
|
||||||
if isinstance(ports, str):
|
if isinstance(ports, str):
|
||||||
if library is None:
|
|
||||||
raise BuildError('Ports given as a string, but `library` was `None`!')
|
|
||||||
ports = library.abstract(ports).ports
|
ports = library.abstract(ports).ports
|
||||||
|
|
||||||
self.pattern.ports.update(copy.deepcopy(dict(ports)))
|
self.pattern.ports.update(copy.deepcopy(dict(ports)))
|
||||||
|
|
||||||
if name is not None:
|
if name is not None:
|
||||||
if library is None:
|
|
||||||
raise BuildError('Name was supplied, but no library was given!')
|
|
||||||
library[name] = self.pattern
|
library[name] = self.pattern
|
||||||
|
|
||||||
if tools is None:
|
if tools is None:
|
||||||
@ -186,6 +182,10 @@ class RenderPather(PortList):
|
|||||||
new = RenderPather(library=library, pattern=pat, name=name, tools=tools)
|
new = RenderPather(library=library, pattern=pat, name=name, tools=tools)
|
||||||
return new
|
return new
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
s = f'<RenderPather {self.pattern} L({len(self.library)}) {pformat(self.tools)}>'
|
||||||
|
return s
|
||||||
|
|
||||||
def plug(
|
def plug(
|
||||||
self,
|
self,
|
||||||
other: Abstract | str,
|
other: Abstract | str,
|
||||||
@ -345,35 +345,14 @@ class RenderPather(PortList):
|
|||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def retool(
|
|
||||||
self,
|
|
||||||
tool: Tool,
|
|
||||||
keys: str | Sequence[str | None] | None = None,
|
|
||||||
) -> Self:
|
|
||||||
"""
|
|
||||||
Update the `Tool` which will be used when generating `Pattern`s for the ports
|
|
||||||
given by `keys`.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
tool: The new `Tool` to use for the given ports.
|
|
||||||
keys: Which ports the tool should apply to. `None` indicates the default tool,
|
|
||||||
used when there is no matching entry in `self.tools` for the port in question.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
if keys is None or isinstance(keys, str):
|
|
||||||
self.tools[keys] = tool
|
|
||||||
else:
|
|
||||||
for key in keys:
|
|
||||||
self.tools[key] = tool
|
|
||||||
return self
|
|
||||||
|
|
||||||
def path(
|
def path(
|
||||||
self,
|
self,
|
||||||
portspec: str,
|
portspec: str,
|
||||||
ccw: SupportsBool | None,
|
ccw: SupportsBool | None,
|
||||||
length: float,
|
length: float,
|
||||||
|
*,
|
||||||
|
plug_into: str | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
"""
|
"""
|
||||||
@ -393,6 +372,8 @@ class RenderPather(PortList):
|
|||||||
and clockwise otherwise.
|
and clockwise otherwise.
|
||||||
length: The total distance from input to output, along the input's axis only.
|
length: The total distance from input to output, along the input's axis only.
|
||||||
(There may be a tool-dependent offset along the other axis.)
|
(There may be a tool-dependent offset along the other axis.)
|
||||||
|
plug_into: If not None, attempts to plug the wire's output port into the provided
|
||||||
|
port on `self`.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
@ -423,161 +404,9 @@ class RenderPather(PortList):
|
|||||||
|
|
||||||
self.pattern.ports[portspec] = out_port.copy()
|
self.pattern.ports[portspec] = out_port.copy()
|
||||||
|
|
||||||
return self
|
if plug_into is not None:
|
||||||
|
self.plugged({'portspec': plug_into})
|
||||||
|
|
||||||
def path_to(
|
|
||||||
self,
|
|
||||||
portspec: str,
|
|
||||||
ccw: SupportsBool | None,
|
|
||||||
position: float | None = None,
|
|
||||||
*,
|
|
||||||
x: float | None = None,
|
|
||||||
y: float | None = None,
|
|
||||||
**kwargs,
|
|
||||||
) -> Self:
|
|
||||||
"""
|
|
||||||
Plan a "wire"/"waveguide" extending from the port `portspec`, with the aim
|
|
||||||
of ending exactly at a target position.
|
|
||||||
|
|
||||||
The wire will travel so that the output port will be placed at exactly the target
|
|
||||||
position along the input port's axis. There can be an unspecified (tool-dependent)
|
|
||||||
offset in the perpendicular direction. The output port will be rotated (or not)
|
|
||||||
based on the `ccw` parameter.
|
|
||||||
|
|
||||||
`RenderPather.render` must be called after all paths have been fully planned.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
portspec: The name of the port into which the wire will be plugged.
|
|
||||||
ccw: If `None`, the output should be along the same axis as the input.
|
|
||||||
Otherwise, cast to bool and turn counterclockwise if True
|
|
||||||
and clockwise otherwise.
|
|
||||||
position: The final port position, along the input's axis only.
|
|
||||||
(There may be a tool-dependent offset along the other axis.)
|
|
||||||
Only one of `position`, `x`, and `y` may be specified.
|
|
||||||
x: The final port position along the x axis.
|
|
||||||
`portspec` must refer to a horizontal port if `x` is passed, otherwise a
|
|
||||||
BuildError will be raised.
|
|
||||||
y: The final port position along the y axis.
|
|
||||||
`portspec` must refer to a vertical port if `y` is passed, otherwise a
|
|
||||||
BuildError will be raised.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
BuildError if `position`, `x`, or `y` is too close to fit the bend (if a bend
|
|
||||||
is present).
|
|
||||||
BuildError if `x` or `y` is specified but does not match the axis of `portspec`.
|
|
||||||
BuildError if more than one of `x`, `y`, and `position` is specified.
|
|
||||||
"""
|
|
||||||
if self._dead:
|
|
||||||
logger.error('Skipping path_to() since device is dead')
|
|
||||||
return self
|
|
||||||
|
|
||||||
pos_count = sum(vv is not None for vv in (position, x, y))
|
|
||||||
if pos_count > 1:
|
|
||||||
raise BuildError('Only one of `position`, `x`, and `y` may be specified at once')
|
|
||||||
if pos_count < 1:
|
|
||||||
raise BuildError('One of `position`, `x`, and `y` must be specified')
|
|
||||||
|
|
||||||
port = self.pattern[portspec]
|
|
||||||
if port.rotation is None:
|
|
||||||
raise PortError(f'Port {portspec} has no rotation and cannot be used for path_to()')
|
|
||||||
|
|
||||||
if not numpy.isclose(port.rotation % (pi / 2), 0):
|
|
||||||
raise BuildError('path_to was asked to route from non-manhattan port')
|
|
||||||
|
|
||||||
is_horizontal = numpy.isclose(port.rotation % pi, 0)
|
|
||||||
if is_horizontal:
|
|
||||||
if y is not None:
|
|
||||||
raise BuildError('Asked to path to y-coordinate, but port is horizontal')
|
|
||||||
if position is None:
|
|
||||||
position = x
|
|
||||||
else:
|
|
||||||
if x is not None:
|
|
||||||
raise BuildError('Asked to path to x-coordinate, but port is vertical')
|
|
||||||
if position is None:
|
|
||||||
position = y
|
|
||||||
|
|
||||||
x0, y0 = port.offset
|
|
||||||
if is_horizontal:
|
|
||||||
if numpy.sign(numpy.cos(port.rotation)) == numpy.sign(position - x0):
|
|
||||||
raise BuildError(f'path_to routing to behind source port: x0={x0:g} to {position:g}')
|
|
||||||
length = numpy.abs(position - x0)
|
|
||||||
else:
|
|
||||||
if numpy.sign(numpy.sin(port.rotation)) == numpy.sign(position - y0):
|
|
||||||
raise BuildError(f'path_to routing to behind source port: y0={y0:g} to {position:g}')
|
|
||||||
length = numpy.abs(position - y0)
|
|
||||||
|
|
||||||
return self.path(portspec, ccw, length, **kwargs)
|
|
||||||
|
|
||||||
def mpath(
|
|
||||||
self,
|
|
||||||
portspec: str | Sequence[str],
|
|
||||||
ccw: SupportsBool | None,
|
|
||||||
*,
|
|
||||||
spacing: float | ArrayLike | None = None,
|
|
||||||
set_rotation: float | None = None,
|
|
||||||
**kwargs,
|
|
||||||
) -> Self:
|
|
||||||
"""
|
|
||||||
`mpath` is a superset of `path` and `path_to` which can act on bundles or buses
|
|
||||||
of "wires or "waveguides".
|
|
||||||
|
|
||||||
See `Pather.mpath` for details.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
portspec: The names of the ports which are to be routed.
|
|
||||||
ccw: If `None`, the outputs should be along the same axis as the inputs.
|
|
||||||
Otherwise, cast to bool and turn 90 degrees counterclockwise if `True`
|
|
||||||
and clockwise otherwise.
|
|
||||||
spacing: Center-to-center distance between output ports along the input port's axis.
|
|
||||||
Must be provided if (and only if) `ccw` is not `None`.
|
|
||||||
set_rotation: If the provided ports have `rotation=None`, this can be used
|
|
||||||
to set a rotation for them.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
BuildError if the implied length for any wire is too close to fit the bend
|
|
||||||
(if a bend is requested).
|
|
||||||
BuildError if `xmin`/`xmax` or `ymin`/`ymax` is specified but does not
|
|
||||||
match the axis of `portspec`.
|
|
||||||
BuildError if an incorrect bound type or spacing is specified.
|
|
||||||
"""
|
|
||||||
if self._dead:
|
|
||||||
logger.error('Skipping mpath() since device is dead')
|
|
||||||
return self
|
|
||||||
|
|
||||||
bound_types = set()
|
|
||||||
if 'bound_type' in kwargs:
|
|
||||||
bound_types.add(kwargs['bound_type'])
|
|
||||||
bound = kwargs['bound']
|
|
||||||
for bt in ('emin', 'emax', 'pmin', 'pmax', 'xmin', 'xmax', 'ymin', 'ymax', 'min_past_furthest'):
|
|
||||||
if bt in kwargs:
|
|
||||||
bound_types.add(bt)
|
|
||||||
bound = kwargs[bt]
|
|
||||||
|
|
||||||
if not bound_types:
|
|
||||||
raise BuildError('No bound type specified for mpath')
|
|
||||||
if len(bound_types) > 1:
|
|
||||||
raise BuildError(f'Too many bound types specified for mpath: {bound_types}')
|
|
||||||
bound_type = tuple(bound_types)[0]
|
|
||||||
|
|
||||||
if isinstance(portspec, str):
|
|
||||||
portspec = [portspec]
|
|
||||||
ports = self.pattern[tuple(portspec)]
|
|
||||||
|
|
||||||
extensions = ell(ports, ccw, spacing=spacing, bound=bound, bound_type=bound_type, set_rotation=set_rotation)
|
|
||||||
|
|
||||||
if len(ports) == 1:
|
|
||||||
# Not a bus, so having a container just adds noise to the layout
|
|
||||||
port_name = tuple(portspec)[0]
|
|
||||||
self.path(port_name, ccw, extensions[port_name])
|
|
||||||
else:
|
|
||||||
for port_name, length in extensions.items():
|
|
||||||
self.path(port_name, ccw, length)
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def render(
|
def render(
|
||||||
@ -696,8 +525,4 @@ class RenderPather(PortList):
|
|||||||
self._dead = True
|
self._dead = True
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
s = f'<Pather {self.pattern} L({len(self.library)}) {pformat(self.tools)}>'
|
|
||||||
return s
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user