from typing import Dict, Tuple, List, Optional, Union, Any, cast, Sequence from pprint import pformat import numpy from numpy import pi from numpy.typing import ArrayLike from .devices import Port from ..utils import rotation_matrix_2d from ..error import BuildError def ell( ports: Dict[str, Port], ccw: Optional[bool], bound_type: str, bound: Union[float, ArrayLike], *, spacing: Optional[Union[float, ArrayLike]] = None, set_rotation: Optional[float] = None, ) -> Dict[str, float]: """ Calculate extension for each port in order to build a 90-degree bend with the provided channel spacing: =A>---------------------------V turn direction: `ccw=False` =B>-------------V | =C>-----------------------V | | =D=>----------------V | | | x---x---x---x `spacing` (can be scalar or array) <--------------> `bound_type='min_extension'` <------> `'min_past_furthest'` <--------------------------------> `'max_extension'` x `'min_position'` x `'max_position'` Args: ports: `name: port` mapping. All ports should have the same rotation (or `None`). If no port has a rotation specified, `set_rotation` must be provided. ccw: Turn direction. `True` means counterclockwise, `False` means clockwise, and `None` means no bend. If `None`, spacing must remain `None` or `0` (default), Otherwise, spacing must be set to a non-`None` value. bound_method: Method used for determining the travel distance; see diagram above. Valid values are: - 'min_extension' or 'emin': The total extension value for the furthest-out port (B in the diagram). - 'min_past_furthest': The distance between furthest out-port (B) and the innermost bend (D's bend). - 'max_extension' or 'emax': The total extension value for the closest-in port (C in the diagram). - 'min_position' or 'pmin': The coordinate of the innermost bend (D's bend). - 'max_position' or 'pmax': The coordinate of the outermost bend (A's bend). `bound` can also be a vector. If specifying an extension (e.g. 'min_extension', 'max_extension', 'min_past_furthest'), it sets independent limits along the x- and y- axes. If specifying a position, it is projected onto the extension direction. bound_value: Value associated with `bound_type`, see above. spacing: Distance between adjacent channels. Can be scalar, resulting in evenly spaced channels, or a vector with length one less than `ports`, allowing non-uniform spacing. The ordering of the vector corresponds to the output order (DCBA in the diagram above), *not* the order of `ports`. set_rotation: If all ports have no specified rotation, this value is used to set the extension direction. Otherwise it must remain `None`. Returns: Dict of {port_name: distance_to_bend} Raises: `BuildError` on bad inputs `BuildError` if the requested bound is impossible """ if not ports: raise BuildError('Empty port list passed to `ell()`') if ccw is None: if spacing is not None and not numpy.isclose(spacing, 0): raise BuildError('Spacing must be 0 or None when ccw=None') spacing = 0 elif spacing is None: raise BuildError('Must provide spacing if a bend direction is specified') has_rotation = numpy.array([p.rotation is not None for p in ports.values()], dtype=bool) if has_rotation.any(): if set_rotation is not None: raise BuildError('set_rotation must be None when ports have rotations!') rotations = numpy.array([p.rotation if p.rotation is not None else 0 for p in ports.values()]) rotations[~has_rotation] = rotations[has_rotation][0] if not numpy.allclose(rotations[0], rotations): port_rotations = {k: numpy.rad2deg(p.rotation) if p.rotation is not None else None for k, p in ports.items()} raise BuildError('Asked to find aggregation for ports that face in different directions:\n' + pformat(port_rotations)) else: if set_rotation is not None: raise BuildError('set_rotation must be specified if no ports have rotations!') rotations = numpy.full_like(has_rotation, set_rotation, dtype=float) direction = rotations[0] + pi # direction we want to travel in (+pi relative to port) rot_matrix = rotation_matrix_2d(-direction) # Rotate so are traveling in +x orig_offsets = numpy.array([p.offset for p in ports.values()]) rot_offsets = (rot_matrix @ orig_offsets.T).T y_order = ((-1 if ccw else 1) * rot_offsets[:, 1]).argsort(kind='stable') y_ind = numpy.empty_like(y_order, dtype=int) y_ind[y_order] = numpy.arange(y_ind.shape[0]) if spacing is None: ch_offsets = numpy.zeros_like(y_order) else: steps = numpy.zeros_like(y_order) steps[1:] = spacing ch_offsets = numpy.cumsum(steps)[y_ind] x_start = rot_offsets[:, 0] # A---------| `d_to_align[0]` # B `d_to_align[1]` # C-------------| `d_to_align[2]` # D-----------| `d_to_align[3]` # d_to_align = x_start.max() - x_start # distance to travel to align all if bound_type == 'min_past_furthest': # A------------------V `d_to_exit[0]` # B-----V `d_to_exit[1]` # C----------------V `d_to_exit[2]` # D-----------V `d_to_exit[3]` offsets = d_to_align + ch_offsets else: # A---------V `travel[0]` <-- Outermost port aligned to furthest-x port # V--B `travel[1]` <-- Remaining ports follow spacing # C-------V `travel[2]` # D--V `travel[3]` # # A------------V `offsets[0]` # B `offsets[1]` <-- Travels adjusted to be non-negative # C----------V `offsets[2]` # D-----V `offsets[3]` travel = d_to_align - (ch_offsets.max() - ch_offsets) offsets = travel - travel.min().clip(max=0) if bound_type in ('emin', 'min_extension', 'emax', 'max_extension', 'min_past_furthest',): if numpy.size(bound) == 2: bound = cast(Sequence[float], bound) rot_bound = (rot_matrix @ ((bound[0], 0), (0, bound[1])))[0, :] else: bound = cast(float, bound) rot_bound = numpy.array(bound) if rot_bound < 0: raise BuildError(f'Got negative bound for extension: {rot_bound}') if bound_type in ('emin', 'min_extension', 'min_past_furthest'): offsets += rot_bound.max() elif bound_type in('emax', 'max_extension'): offsets += rot_bound.min() - offsets.max() else: if numpy.size(bound) == 2: bound = cast(Sequence[float], bound) rot_bound = (rot_matrix @ bound)[0] else: bound = cast(float, bound) neg = (direction + pi / 4) % (2 * pi) > pi rot_bound = -bound if neg else bound min_possible = x_start + offsets if bound_type in ('pmax', 'max_position'): extension = rot_bound - min_possible.max() elif bound_type in ('pmin', 'min_position'): extension = rot_bound - min_possible.min() offsets += extension if extension < 0: raise BuildError(f'Position is too close by at least {-numpy.floor(extension)}. Total extensions would be' + '\n\t'.join(f'{key}: {off}' for key, off in zip(ports.keys(), offsets))) result = dict(zip(ports.keys(), offsets)) return result