You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
masque/masque/builder/utils.py

195 lines
8.2 KiB
Python

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()
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