diff --git a/masque/builder/pather.py b/masque/builder/pather.py index a3c4dc5..68dfcd9 100644 --- a/masque/builder/pather.py +++ b/masque/builder/pather.py @@ -255,7 +255,7 @@ class Pather(Builder, PatherMixin): return s - def path( + def _path( self, portspec: str, ccw: SupportsBool | None, @@ -296,7 +296,7 @@ class Pather(Builder, PatherMixin): LibraryError if no valid name could be picked for the pattern. """ if self._dead: - logger.warning('Skipping geometry for path() since device is dead') + logger.warning('Skipping geometry for _path() since device is dead') tool_port_names = ('A', 'B') @@ -335,7 +335,7 @@ class Pather(Builder, PatherMixin): self.plug(tname, {portspec: tool_port_names[0], **output}) return self - def pathS( + def _pathS( self, portspec: str, length: float, @@ -346,20 +346,17 @@ class Pather(Builder, PatherMixin): ) -> Self: """ Create an S-shaped "wire"/"waveguide" and `plug` it into the port `portspec`, with the aim - of traveling exactly `length` distance with an offset `jog` along the other axis (+ve jog is - left of direction of travel). + of traveling exactly `length` distance. - The output port will have the same orientation as the source port (`portspec`). - - This function attempts to use `tool.planS()`, but falls back to `tool.planL()` if the former - raises a NotImplementedError. + The wire will travel `length` distance along the port's axis, and exactly `jog` + distance in the perpendicular direction. The output port will have an orientation + identical to the input port. Args: portspec: The name of the port into which the wire will be plugged. - jog: Total manhattan distance perpendicular to the direction of travel. - Positive values are to the left of the direction of travel. - length: The total manhattan distance from input to output, along the input's axis only. - (There may be a tool-dependent offset along the other axis.) + length: The total distance from input to output, along the input's axis only. + jog: Total distance perpendicular to the direction of travel. Positive values + are to the left of the direction of travel. plug_into: If not None, attempts to plug the wire's output port into the provided port on `self`. @@ -377,7 +374,7 @@ class Pather(Builder, PatherMixin): LibraryError if no valid name could be picked for the pattern. """ if self._dead: - logger.warning('Skipping geometry for pathS() since device is dead') + logger.warning('Skipping geometry for _pathS() since device is dead') tool_port_names = ('A', 'B') @@ -398,8 +395,8 @@ class Pather(Builder, PatherMixin): (_, jog1), _ = t_pat1[tool_port_names[0]].measure_travel(t_pat1[tool_port_names[1]]) kwargs_plug = kwargs | {'plug_into': plug_into} - self.path(portspec, ccw0, length - abs(jog1), **kwargs_no_out) - self.path(portspec, not ccw0, abs(jog - jog0), **kwargs_plug) + self._path(portspec, ccw0, length - abs(jog1), **kwargs_no_out) + self._path(portspec, not ccw0, abs(jog - jog0), **kwargs_plug) except (BuildError, NotImplementedError): if not self._dead: raise @@ -433,4 +430,3 @@ class Pather(Builder, PatherMixin): output = {} self.plug(tname, {portspec: tool_port_names[0], **output}) return self - diff --git a/masque/builder/pather_mixin.py b/masque/builder/pather_mixin.py index 1655329..0f5fa92 100644 --- a/masque/builder/pather_mixin.py +++ b/masque/builder/pather_mixin.py @@ -1,5 +1,5 @@ from typing import Self, overload -from collections.abc import Sequence, Iterator, Iterable +from collections.abc import Sequence, Iterator, Iterable, Mapping import logging from contextlib import contextmanager from abc import abstractmethod, ABCMeta @@ -37,8 +37,323 @@ class PatherMixin(PortList, metaclass=ABCMeta): (e.g wires or waveguides) to be plugged into this device. """ + def trace( + self, + portspec: str | Sequence[str], + ccw: SupportsBool | None, + length: float | None = None, + *, + spacing: float | ArrayLike | None = None, + **bounds, + ) -> Self: + """ + Create a "wire"/"waveguide" extending from the port(s) `portspec`. + + Args: + portspec: The name(s) of the port(s) into which the wire(s) 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. + length: The total distance from input to output, along the input's axis only. + Length is only allowed with a single port. + spacing: Center-to-center distance between output ports along the input port's axis. + Only used when routing multiple ports with a bend. + bounds: Boundary constraints for the trace. + - each: results in each port being extended by `each` distance. + - emin, emax, pmin, pmax, xmin, xmax, ymin, ymax: bundle routing via `ell()`. + - set_rotation: explicit rotation for ports without one. + + Returns: + self + """ + if isinstance(portspec, str): + portspec = [portspec] + + if length is not None: + if len(portspec) > 1: + raise BuildError('length is only allowed with a single port in trace()') + if bounds: + raise BuildError('length and bounds are mutually exclusive in trace()') + return self._path(portspec[0], ccw, length) + + if 'each' in bounds: + each = bounds.pop('each') + if bounds: + raise BuildError('each and other bounds are mutually exclusive in trace()') + for port in portspec: + self._path(port, ccw, each) + return self + + # Bundle routing (formerly mpath logic) + bound_types = set() + if 'bound_type' in bounds: + bound_types.add(bounds.pop('bound_type')) + bound = bounds.pop('bound') + for bt in ('emin', 'emax', 'pmin', 'pmax', 'xmin', 'xmax', 'ymin', 'ymax', 'min_past_furthest'): + if bt in bounds: + bound_types.add(bt) + bound = bounds.pop(bt) + + if not bound_types: + raise BuildError('No bound type specified for trace()') + if len(bound_types) > 1: + raise BuildError(f'Too many bound types specified: {bound_types}') + bound_type = tuple(bound_types)[0] + + ports = self.pattern[tuple(portspec)] + set_rotation = bounds.pop('set_rotation', None) + + extensions = ell(ports, ccw, spacing=spacing, bound=bound, bound_type=bound_type, set_rotation=set_rotation) + + for port_name, ext_len in extensions.items(): + self._path(port_name, ccw, ext_len, **bounds) + return self + + def trace_to( + self, + portspec: str | Sequence[str], + ccw: SupportsBool | None, + *, + spacing: float | ArrayLike | None = None, + **bounds, + ) -> Self: + """ + Create a "wire"/"waveguide" extending from the port(s) `portspec` to a target position. + + Args: + portspec: The name(s) of the port(s) into which the wire(s) 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. + spacing: Center-to-center distance between output ports along the input port's axis. + Only used when routing multiple ports with a bend. + bounds: Boundary constraints for the target position. + - p, x, y, pos, position: Coordinate of the target position. Error if used with multiple ports. + - pmin, pmax, xmin, xmax, ymin, ymax, emin, emax: bundle routing via `ell()`. + + Returns: + self + """ + if isinstance(portspec, str): + portspec = [portspec] + + pos_bounds = {kk: bounds[kk] for kk in ('p', 'x', 'y', 'pos', 'position') if kk in bounds} + if pos_bounds: + if len(portspec) > 1: + raise BuildError(f'{tuple(pos_bounds.keys())} bounds are only allowed with a single port in trace_to()') + if len(pos_bounds) > 1: + raise BuildError(f'Too many position bounds: {tuple(pos_bounds.keys())}') + + k, v = next(iter(pos_bounds.items())) + k = 'position' if k in ('p', 'pos') else k + + # Logic hoisted from path_to() + port_name = portspec[0] + port = self.pattern[port_name] + if port.rotation is None: + raise PortError(f'Port {port_name} has no rotation and cannot be used for trace_to()') + + if not numpy.isclose(port.rotation % (pi / 2), 0): + raise BuildError('trace_to was asked to route from non-manhattan port') + + is_horizontal = numpy.isclose(port.rotation % pi, 0) + if is_horizontal: + if k == 'y': + raise BuildError('Asked to trace to y-coordinate, but port is horizontal') + target = v + else: + if k == 'x': + raise BuildError('Asked to trace to x-coordinate, but port is vertical') + target = v + + x0, y0 = port.offset + if is_horizontal: + if numpy.sign(numpy.cos(port.rotation)) == numpy.sign(target - x0): + raise BuildError(f'trace_to routing to behind source port: x0={x0:g} to {target:g}') + length = numpy.abs(target - x0) + else: + if numpy.sign(numpy.sin(port.rotation)) == numpy.sign(target - y0): + raise BuildError(f'trace_to routing to behind source port: y0={y0:g} to {target:g}') + length = numpy.abs(target - y0) + + other_bounds = {bk: bv for bk, bv in bounds.items() if bk not in pos_bounds and bk != 'length'} + if 'length' in bounds and bounds['length'] is not None: + raise BuildError('Cannot specify both relative length and absolute position in trace_to()') + + return self._path(port_name, ccw, length, **other_bounds) + + # Bundle routing (delegate to trace which handles ell) + return self.trace(portspec, ccw, spacing=spacing, **bounds) + + def straight(self, portspec: str | Sequence[str], length: float | None = None, **bounds) -> Self: + """ Straight extension. Replaces `path(ccw=None)` and `path_to(ccw=None)` """ + return self.trace_to(portspec, None, length=length, **bounds) + + def bend(self, portspec: str | Sequence[str], ccw: SupportsBool, length: float | None = None, **bounds) -> Self: + """ Bend extension. Replaces `path(ccw=True/False)` and `path_to(ccw=True/False)` """ + return self.trace_to(portspec, ccw, length=length, **bounds) + + def ccw(self, portspec: str | Sequence[str], length: float | None = None, **bounds) -> Self: + """ Counter-clockwise bend extension. """ + return self.bend(portspec, True, length, **bounds) + + def cw(self, portspec: str | Sequence[str], length: float | None = None, **bounds) -> Self: + """ Clockwise bend extension. """ + return self.bend(portspec, False, length, **bounds) + + def jog(self, portspec: str | Sequence[str], offset: float, length: float | None = None, **bounds) -> Self: + """ Jog extension. Replaces `pathS`. """ + if isinstance(portspec, str): + portspec = [portspec] + + for port in portspec: + l_actual = length + if l_actual is None: + # TODO: use bounds to determine length? + raise BuildError('jog() currently requires a length') + self._pathS(port, l_actual, offset, **bounds) + return self + + def trace_into( + self, + portspec_src: str, + portspec_dst: str, + *, + out_ptype: str | None = None, + plug_destination: bool = True, + thru: str | None = None, + **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). + + Args: + portspec_src: The name of the starting port into which the wire will be plugged. + portspec_dst: The name of the destination port. + 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. + thru: If not `None`, the port by this name will be renamed to `portspec_src`. + This can be used when routing a signal through a pre-placed 2-port device. + + Returns: + self + + Raises: + PortError if either port does not have a specified rotation. + BuildError if an invalid port config is encountered: + - Non-manhattan ports + - U-bend + - Destination too close to (or behind) source + """ + if self._dead: + logger.error('Skipping trace_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 trace_into()') + if port_dst.rotation is None: + raise PortError(f'Port {portspec_dst} has no rotation and cannot be used for trace_into()') + + if not numpy.isclose(port_src.rotation % (pi / 2), 0): + raise BuildError('trace_into was asked to route from non-manhattan port') + if not numpy.isclose(port_dst.rotation % (pi / 2), 0): + raise BuildError('trace_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) + + dst_extra_args = {'out_ptype': out_ptype} + if plug_destination: + dst_extra_args['plug_into'] = portspec_dst + + src_args = {**kwargs} + dst_args = {**src_args, **dst_extra_args} + if src_is_horizontal and not dst_is_horizontal: + # single bend should suffice + self.trace_to(portspec_src, angle > pi, x=xd, **src_args) + self.trace_to(portspec_src, None, y=yd, **dst_args) + elif dst_is_horizontal and not src_is_horizontal: + # single bend should suffice + self.trace_to(portspec_src, angle > pi, y=yd, **src_args) + self.trace_to(portspec_src, None, x=xd, **dst_args) + elif numpy.isclose(angle, pi): + if src_is_horizontal and ys == yd: + # straight connector + self.trace_to(portspec_src, None, x=xd, **dst_args) + elif not src_is_horizontal and xs == xd: + # straight connector + self.trace_to(portspec_src, None, y=yd, **dst_args) + else: + # S-bend + (travel, jog), _ = port_src.measure_travel(port_dst) + self.jog(portspec_src, -jog, -travel, **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}") + + if thru is not None: + self.rename_ports({thru: portspec_src}) + + return self + + def _uturn_fallback( + self, + tool: Tool, + portspec: str, + jog: float, + length: float, + in_ptype: str | None, + plug_into: str | None, + **kwargs, + ) -> bool: + """ + Attempt to perform a U-turn using two L-bends. + Returns True if successful, False if planL failed. + """ + # Fall back to drawing two L-bends + ccw = jog > 0 + kwargs_no_out = kwargs | {'out_ptype': None} + try: + # First, find R by planning a minimal L-bend. + # Use a large length to ensure we don't hit tool-specific minimum length constraints. + dummy_port, _ = tool.planL(ccw, 1e9, in_ptype=in_ptype, **kwargs_no_out) + R = abs(dummy_port.y) + + L1 = length + R + L2 = abs(jog) - R + + kwargs_plug = kwargs | {'plug_into': plug_into} + self._path(portspec, ccw, L1, **kwargs_no_out) + self._path(portspec, ccw, L2, **kwargs_plug) + except (BuildError, NotImplementedError): + return False + else: + return True + @abstractmethod - def path( + def _path( self, portspec: str, ccw: SupportsBool | None, @@ -50,7 +365,7 @@ class PatherMixin(PortList, metaclass=ABCMeta): pass @abstractmethod - def pathS( + def _pathS( self, portspec: str, length: float, @@ -61,6 +376,16 @@ class PatherMixin(PortList, metaclass=ABCMeta): ) -> Self: pass + def path(self, *args, **kwargs) -> Self: + import warnings + warnings.warn("path() is deprecated; use trace(), straight(), or bend() instead", DeprecationWarning, stacklevel=2) + return self._path(*args, **kwargs) + + def pathS(self, *args, **kwargs) -> Self: + import warnings + warnings.warn("pathS() is deprecated; use jog() instead", DeprecationWarning, stacklevel=2) + return self._pathS(*args, **kwargs) + @abstractmethod def plug( self, @@ -76,6 +401,11 @@ class PatherMixin(PortList, metaclass=ABCMeta): ) -> Self: pass + @abstractmethod + def plugged(self, connections: dict[str, str]) -> Self: + """ Manual connection acknowledgment. """ + pass + def retool( self, tool: Tool, @@ -143,88 +473,13 @@ class PatherMixin(PortList, metaclass=ABCMeta): **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. + [DEPRECATED] use trace_to() instead. """ - if self._dead: - logger.error('Skipping path_to() since device is dead') - return self + import warnings + warnings.warn("path_to() is deprecated; use trace_to() instead", DeprecationWarning, stacklevel=2) - 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, - ) + bounds = {kk: vv for kk, vv in (('position', position), ('x', x), ('y', y)) if vv is not None} + return self.trace_to(portspec, ccw, plug_into=plug_into, **bounds, **kwargs) def path_into( self, @@ -237,100 +492,19 @@ class PatherMixin(PortList, metaclass=ABCMeta): **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. - 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. - thru: If not `None`, the port by this name will be rename to `portspec_src`. - This can be used when routing a signal through a pre-placed 2-port device. - - 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 + [DEPRECATED] use trace_into() instead. """ - if self._dead: - logger.error('Skipping path_into() since device is dead') - return self + import warnings + warnings.warn("path_into() is deprecated; use trace_into() instead", DeprecationWarning, stacklevel=2) - 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) - - dst_extra_args = {'out_ptype': out_ptype} - if plug_destination: - dst_extra_args['plug_into'] = portspec_dst - - src_args = {**kwargs} - 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) - else: - # S-bend, delegate to implementations - (travel, jog), _ = port_src.measure_travel(port_dst) - self.pathS(portspec_src, -travel, -jog, **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}') - - if thru is not None: - self.rename_ports({thru: portspec_src}) - - return self + return self.trace_into( + portspec_src, + portspec_dst, + out_ptype = out_ptype, + plug_destination = plug_destination, + thru = thru, + **kwargs, + ) def mpath( self, @@ -342,109 +516,12 @@ class PatherMixin(PortList, metaclass=ABCMeta): **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. + [DEPRECATED] use trace() or trace_to() instead. """ - if self._dead: - logger.error('Skipping mpath() since device is dead') - return self + import warnings + warnings.warn("mpath() is deprecated; use trace() or trace_to() instead", DeprecationWarning, stacklevel=2) - 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 + return self.trace(portspec, ccw, spacing=spacing, set_rotation=set_rotation, **kwargs) # TODO def bus_join()? @@ -488,61 +565,42 @@ class PortPather: with self.pather.toolctx(tool, keys=self.ports): yield self - def path(self, *args, **kwargs) -> Self: + def trace(self, ccw: SupportsBool | None, length: float | None = None, **kwargs) -> Self: + self.pather.trace(self.ports, ccw, length, **kwargs) + return self + + def trace_to(self, ccw: SupportsBool | None, **kwargs) -> Self: + self.pather.trace_to(self.ports, ccw, **kwargs) + return self + + def straight(self, length: float | None = None, **kwargs) -> Self: + self.pather.straight(self.ports, length, **kwargs) + return self + + def bend(self, ccw: SupportsBool, length: float | None = None, **kwargs) -> Self: + self.pather.bend(self.ports, ccw, length, **kwargs) + return self + + def ccw(self, length: float | None = None, **kwargs) -> Self: + self.pather.ccw(self.ports, length, **kwargs) + return self + + def cw(self, length: float | None = None, **kwargs) -> Self: + self.pather.cw(self.ports, length, **kwargs) + return self + + def jog(self, offset: float, length: float | None = None, **kwargs) -> Self: + self.pather.jog(self.ports, offset, length, **kwargs) + return self + + def uturn(self, offset: float, length: float | None = None, **kwargs) -> Self: + self.pather.uturn(self.ports, offset, length, **kwargs) + return self + + def trace_into(self, target_port: str, **kwargs) -> Self: if len(self.ports) > 1: - logger.warning('Use path_each() when pathing multiple ports independently') - for port in self.ports: - self.pather.path(port, *args, **kwargs) - return self - - def path_each(self, *args, **kwargs) -> Self: - for port in self.ports: - self.pather.path(port, *args, **kwargs) - return self - - def pathS(self, *args, **kwargs) -> Self: - if len(self.ports) > 1: - logger.warning('Use pathS_each() when pathing multiple ports independently') - for port in self.ports: - self.pather.pathS(port, *args, **kwargs) - return self - - def pathS_each(self, *args, **kwargs) -> Self: - for port in self.ports: - self.pather.pathS(port, *args, **kwargs) - return self - - def path_to(self, *args, **kwargs) -> Self: - if len(self.ports) > 1: - logger.warning('Use path_each_to() when pathing multiple ports independently') - for port in self.ports: - self.pather.path_to(port, *args, **kwargs) - return self - - def path_each_to(self, *args, **kwargs) -> Self: - for port in self.ports: - self.pather.path_to(port, *args, **kwargs) - return self - - def mpath(self, *args, **kwargs) -> Self: - self.pather.mpath(self.ports, *args, **kwargs) - return self - - def path_into(self, *args, **kwargs) -> Self: - """ Path_into, using the current port as the source """ - if len(self.ports) > 1: - raise BuildError(f'Unable use implicit path_into() with {len(self.ports)} (>1) ports.') - self.pather.path_into(self.ports[0], *args, **kwargs) - return self - - def path_from(self, *args, **kwargs) -> Self: - """ Path_into, using the current port as the destination """ - if len(self.ports) > 1: - raise BuildError(f'Unable use implicit path_from() with {len(self.ports)} (>1) ports.') - thru = kwargs.pop('thru', None) - self.pather.path_into(args[0], self.ports[0], *args[1:], **kwargs) - if thru is not None: - self.rename_from(thru) + raise BuildError(f'Unable use implicit trace_into() with {len(self.ports)} (>1) ports.') + self.pather.trace_into(self.ports[0], target_port, **kwargs) return self def plug( @@ -558,10 +616,13 @@ class PortPather: self.pather.plug(other, {self.ports[0]: other_port}, *args, **kwargs) return self - def plugged(self, other_port: str) -> Self: - if len(self.ports) > 1: + def plugged(self, other_port: str | Mapping[str, str]) -> Self: + if isinstance(other_port, Mapping): + self.pather.plugged(dict(other_port)) + elif len(self.ports) > 1: raise BuildError(f'Unable use implicit plugged() with {len(self.ports)} (>1) ports.') - self.pather.plugged({self.ports[0]: other_port}) + else: + self.pather.plugged({self.ports[0]: other_port}) return self # @@ -569,95 +630,91 @@ class PortPather: # def set_ptype(self, ptype: str) -> Self: for port in self.ports: - self.pather[port].set_ptype(ptype) + self.pather.pattern[port].set_ptype(ptype) return self def translate(self, *args, **kwargs) -> Self: for port in self.ports: - self.pather[port].translate(*args, **kwargs) + self.pather.pattern[port].translate(*args, **kwargs) return self def mirror(self, *args, **kwargs) -> Self: for port in self.ports: - self.pather[port].mirror(*args, **kwargs) + self.pather.pattern[port].mirror(*args, **kwargs) return self def rotate(self, rotation: float) -> Self: for port in self.ports: - self.pather[port].rotate(rotation) + self.pather.pattern[port].rotate(rotation) return self def set_rotation(self, rotation: float | None) -> Self: for port in self.ports: - self.pather[port].set_rotation(rotation) + self.pather.pattern[port].set_rotation(rotation) return self - def rename_to(self, new_name: str) -> Self: - if len(self.ports) > 1: - BuildError('Use rename_ports() for >1 port') - self.pather.rename_ports({self.ports[0]: new_name}) - self.ports[0] = new_name - return self - - def rename_from(self, old_name: str) -> Self: - if len(self.ports) > 1: - BuildError('Use rename_ports() for >1 port') - self.pather.rename_ports({old_name: self.ports[0]}) - return self - - def rename_ports(self, name_map: dict[str, str | None]) -> Self: + def rename(self, name: str | Mapping[str, str | None]) -> Self: + """ Rename active ports. Replaces `rename_to`. """ + name_map: dict[str, str | None] + if isinstance(name, str): + if len(self.ports) > 1: + raise BuildError('Use a mapping to rename >1 port') + name_map = {self.ports[0]: name} + else: + name_map = dict(name) self.pather.rename_ports(name_map) self.ports = [mm for mm in [name_map.get(pp, pp) for pp in self.ports] if mm is not None] return self - def add_ports(self, ports: Iterable[str]) -> Self: - ports = list(ports) - conflicts = set(ports) & set(self.ports) - if conflicts: - raise BuildError(f'ports {conflicts} already selected') - self.ports += ports + def select(self, ports: str | Iterable[str]) -> Self: + """ Add ports to the selection. Replaces `add_ports`. """ + if isinstance(ports, str): + ports = [ports] + for port in ports: + if port not in self.ports: + self.ports.append(port) return self - def add_port(self, port: str, index: int | None = None) -> Self: - if port in self.ports: - raise BuildError(f'{port=} already selected') - if index is not None: - self.ports.insert(index, port) + def deselect(self, ports: str | Iterable[str]) -> Self: + """ Remove ports from the selection. Replaces `drop_port`. """ + if isinstance(ports, str): + ports = [ports] + ports_set = set(ports) + self.ports = [pp for pp in self.ports if pp not in ports_set] + return self + + def mark(self, name: str | Mapping[str, str]) -> Self: + """ Bookmark current port(s). Replaces `save_copy`. """ + name_map: Mapping[str, str] + if isinstance(name, str): + if len(self.ports) > 1: + raise BuildError('Use a mapping to mark >1 port') + name_map = {self.ports[0]: name} else: - self.ports.append(port) + name_map = name + for src, dst in name_map.items(): + self.pather.pattern.ports[dst] = self.pather.pattern[src].copy() return self - def drop_port(self, port: str) -> Self: - if port not in self.ports: - raise BuildError(f'{port=} already not selected') - self.ports = [pp for pp in self.ports if pp != port] + def fork(self, name: str | Mapping[str, str]) -> Self: + """ Split and follow new name. Replaces `into_copy`. """ + name_map: Mapping[str, str] + if isinstance(name, str): + if len(self.ports) > 1: + raise BuildError('Use a mapping to fork >1 port') + name_map = {self.ports[0]: name} + else: + name_map = name + for src, dst in name_map.items(): + self.pather.pattern.ports[dst] = self.pather.pattern[src].copy() + self.ports = [(dst if pp == src else pp) for pp in self.ports] return self - def into_copy(self, new_name: str, src: str | None = None) -> Self: - """ Copy a port and replace it with the copy """ - if not self.ports: - raise BuildError('Have no ports to copy') - if len(self.ports) == 1: - src = self.ports[0] - elif src is None: - raise BuildError('Must specify src when >1 port is available') - if src not in self.ports: - raise BuildError(f'{src=} not available') - self.pather.ports[new_name] = self.pather[src].copy() - self.ports = [(new_name if pp == src else pp) for pp in self.ports] - return self - - def save_copy(self, new_name: str, src: str | None = None) -> Self: - """ Copy a port and but keep using the original """ - if not self.ports: - raise BuildError('Have no ports to copy') - if len(self.ports) == 1: - src = self.ports[0] - elif src is None: - raise BuildError('Must specify src when >1 port is available') - if src not in self.ports: - raise BuildError(f'{src=} not available') - self.pather.ports[new_name] = self.pather[src].copy() + def drop(self) -> Self: + """ Remove selected ports from the pattern and the PortPather. Replaces `delete(None)`. """ + for pp in self.ports: + del self.pather.pattern.ports[pp] + self.ports = [] return self @overload @@ -668,10 +725,9 @@ class PortPather: def delete(self, name: str | None = None) -> Self | None: if name is None: - for pp in self.ports: - del self.pather.ports[pp] + self.drop() return None - del self.pather.ports[name] + del self.pather.pattern.ports[name] self.ports = [pp for pp in self.ports if pp != name] return self diff --git a/masque/builder/renderpather.py b/masque/builder/renderpather.py index 8104a50..93fd2e3 100644 --- a/masque/builder/renderpather.py +++ b/masque/builder/renderpather.py @@ -28,7 +28,7 @@ logger = logging.getLogger(__name__) class RenderPather(PatherMixin): """ - `RenderPather` is an alternative to `Pather` which uses the `path`/`path_to`/`mpath` + `RenderPather` is an alternative to `Pather` which uses the `trace`/`trace_to` functions to plan out wire paths without incrementally generating the layout. Instead, it waits until `render` is called, at which point it draws all the planned segments simultaneously. This allows it to e.g. draw each wire using a single `Path` or @@ -97,7 +97,7 @@ class RenderPather(PatherMixin): in which case it is interpreted as a name in `library`. Default `None` (no ports). tools: A mapping of {port: tool} which specifies what `Tool` should be used - to generate waveguide or wire segments when `path`/`path_to`/`mpath` + to generate waveguide or wire segments when `trace`/`trace_to` are called. Relies on `Tool.planL` and `Tool.render` implementations. name: If specified, `library[name]` is set to `self.pattern`. """ @@ -150,7 +150,7 @@ class RenderPather(PatherMixin): and to which the new one should be added (if named). If not provided, `source.library` must exist and will be used. tools: `Tool`s which will be used by the pather for generating new wires - or waveguides (via `path`/`path_to`/`mpath`). + or waveguides (via `trace`/`trace_to`). in_prefix: Prepended to port names for newly-created ports with reversed directions compared to the current device. out_prefix: Prepended to port names for ports which are directly @@ -377,7 +377,7 @@ class RenderPather(PatherMixin): PortList.plugged(self, connections) return self - def path( + def _path( self, portspec: str, ccw: SupportsBool | None, @@ -420,7 +420,7 @@ class RenderPather(PatherMixin): LibraryError if no valid name could be picked for the pattern. """ if self._dead: - logger.warning('Skipping geometry for path() since device is dead') + logger.warning('Skipping geometry for _path() since device is dead') port = self.pattern[portspec] in_ptype = port.ptype @@ -460,7 +460,7 @@ class RenderPather(PatherMixin): return self - def pathS( + def _pathS( self, portspec: str, length: float, @@ -504,7 +504,7 @@ class RenderPather(PatherMixin): LibraryError if no valid name could be picked for the pattern. """ if self._dead: - logger.warning('Skipping geometry for pathS() since device is dead') + logger.warning('Skipping geometry for _pathS() since device is dead') port = self.pattern[portspec] in_ptype = port.ptype @@ -527,8 +527,8 @@ class RenderPather(PatherMixin): jog1 = Port((0, 0), 0).measure_travel(t_port1)[0][1] kwargs_plug = kwargs | {'plug_into': plug_into} - self.path(portspec, ccw0, length - abs(jog1), **kwargs_no_out) - self.path(portspec, not ccw0, abs(jog - jog0), **kwargs_plug) + self._path(portspec, ccw0, length - abs(jog1), **kwargs_no_out) + self._path(portspec, not ccw0, abs(jog - jog0), **kwargs_plug) except (BuildError, NotImplementedError): if not self._dead: raise @@ -564,7 +564,7 @@ class RenderPather(PatherMixin): append: bool = True, ) -> Self: """ - Generate the geometry which has been planned out with `path`/`path_to`/etc. + Generate the geometry which has been planned out with `trace`/`trace_to`/etc. Args: append: If `True`, the rendered geometry will be directly appended to diff --git a/masque/test/test_pather.py b/masque/test/test_pather.py index 35e9f53..47cae29 100644 --- a/masque/test/test_pather.py +++ b/masque/test/test_pather.py @@ -24,7 +24,7 @@ def pather_setup() -> tuple[Pather, PathTool, Library]: def test_pather_straight(pather_setup: tuple[Pather, PathTool, Library]) -> None: p, tool, lib = pather_setup # Route 10um "forward" - p.path("start", ccw=None, length=10) + p.straight("start", 10) # port rot pi/2 (North). Travel +pi relative to port -> South. assert_allclose(p.ports["start"].offset, [0, -10], atol=1e-10) @@ -37,7 +37,7 @@ def test_pather_bend(pather_setup: tuple[Pather, PathTool, Library]) -> None: # Start (0,0) rot pi/2 (North). # Path 10um "forward" (South), then turn Clockwise (ccw=False). # Facing South, turn Right -> West. - p.path("start", ccw=False, length=10) + p.cw("start", 10) # PathTool.planL(ccw=False, length=10) returns out_port at (10, -1) relative to (0,0) rot 0. # Transformed by port rot pi/2 (North) + pi (to move "forward" away from device): @@ -55,7 +55,7 @@ def test_pather_path_to(pather_setup: tuple[Pather, PathTool, Library]) -> None: p, tool, lib = pather_setup # start at (0,0) rot pi/2 (North) # path "forward" (South) to y=-50 - p.path_to("start", ccw=None, y=-50) + p.straight("start", y=-50) assert_equal(p.ports["start"].offset, [0, -50]) @@ -65,7 +65,7 @@ def test_pather_mpath(pather_setup: tuple[Pather, PathTool, Library]) -> None: p.ports["B"] = Port((10, 0), pi / 2, ptype="wire") # Path both "forward" (South) to y=-20 - p.mpath(["A", "B"], ccw=None, ymin=-20) + p.straight(["A", "B"], ymin=-20) assert_equal(p.ports["A"].offset, [0, -20]) assert_equal(p.ports["B"].offset, [10, -20]) @@ -73,7 +73,7 @@ def test_pather_mpath(pather_setup: tuple[Pather, PathTool, Library]) -> None: def test_pather_at_chaining(pather_setup: tuple[Pather, PathTool, Library]) -> None: p, tool, lib = pather_setup # Fluent API test - p.at("start").path(ccw=None, length=10).path(ccw=True, length=10) + p.at("start").straight(10).ccw(10) # 10um South -> (0, -10) rot pi/2 # then 10um South and turn CCW (Facing South, CCW is East) # PathTool.planL(ccw=True, length=10) -> out_port=(10, 1) rot -pi/2 relative to rot 0 @@ -93,14 +93,14 @@ def test_pather_dead_ports() -> None: p.set_dead() # Path with negative length (impossible for PathTool, would normally raise BuildError) - p.path("in", None, -10) + p.straight("in", -10) # Port 'in' should be updated by dummy extension despite tool failure # port_rot=0, forward is -x. path(-10) means moving -10 in -x direction -> +10 in x. assert_allclose(p.ports["in"].offset, [10, 0], atol=1e-10) # Downstream path should work correctly using the dummy port location - p.path("in", None, 20) + p.straight("in", 20) # 10 + (-20) = -10 assert_allclose(p.ports["in"].offset, [-10, 0], atol=1e-10) diff --git a/masque/test/test_renderpather.py b/masque/test/test_renderpather.py index 5d2c8c3..cbeef3a 100644 --- a/masque/test/test_renderpather.py +++ b/masque/test/test_renderpather.py @@ -24,7 +24,7 @@ def rpather_setup() -> tuple[RenderPather, PathTool, Library]: def test_renderpather_basic(rpather_setup: tuple[RenderPather, PathTool, Library]) -> None: rp, tool, lib = rpather_setup # Plan two segments - rp.at("start").path(ccw=None, length=10).path(ccw=None, length=10) + rp.at("start").straight(10).straight(10) # Before rendering, no shapes in pattern assert not rp.pattern.has_shapes() @@ -49,7 +49,7 @@ def test_renderpather_basic(rpather_setup: tuple[RenderPather, PathTool, Library def test_renderpather_bend(rpather_setup: tuple[RenderPather, PathTool, Library]) -> None: rp, tool, lib = rpather_setup # Plan straight then bend - rp.at("start").path(ccw=None, length=10).path(ccw=False, length=10) + rp.at("start").straight(10).cw(10) rp.render() path_shape = cast("Path", rp.pattern.shapes[(1, 0)][0]) @@ -69,9 +69,9 @@ def test_renderpather_retool(rpather_setup: tuple[RenderPather, PathTool, Librar rp, tool1, lib = rpather_setup tool2 = PathTool(layer=(2, 0), width=4, ptype="wire") - rp.at("start").path(ccw=None, length=10) + rp.at("start").straight(10) rp.retool(tool2, keys=["start"]) - rp.at("start").path(ccw=None, length=10) + rp.at("start").straight(10) rp.render() # Different tools should cause different batches/shapes @@ -86,7 +86,7 @@ def test_renderpather_dead_ports() -> None: rp.set_dead() # Impossible path - rp.path("in", None, -10) + rp.straight("in", -10) # port_rot=0, forward is -x. path(-10) means moving -10 in -x direction -> +10 in x. assert_allclose(rp.ports["in"].offset, [10, 0], atol=1e-10)