""" SubPattern provides basic support for nesting Pattern objects within each other, by adding offset, rotation, scaling, and other such properties to the reference. """ from typing import Union, List, Dict, Tuple, Optional, Sequence, TYPE_CHECKING import copy import numpy from numpy import pi from .error import PatternError, PatternLockedError from .utils import is_scalar, rotation_matrix_2d, vector2 if TYPE_CHECKING: from . import Pattern class SubPattern: """ SubPattern provides basic support for nesting Pattern objects within each other, by adding offset, rotation, scaling, and associated methods. """ __slots__ = ('_pattern', '_offset', '_rotation', '_dose', '_scale', '_mirrored', 'identifier', 'locked') _pattern: Optional['Pattern'] """ The `Pattern` being instanced """ _offset: numpy.ndarray """ (x, y) offset for the instance """ _rotation: float """ rotation for the instance, radians counterclockwise """ _dose: float """ dose factor for the instance """ _scale: float """ scale factor for the instance """ _mirrored: numpy.ndarray # ndarray[bool] """ Whether to mirror the instanc across the x and/or y axes. """ identifier: Tuple """ An arbitrary identifier """ locked: bool """ If `True`, disallows changes to the GridRepetition """ #TODO more documentation? def __init__(self, pattern: Optional['Pattern'], offset: vector2 = (0.0, 0.0), rotation: float = 0.0, mirrored: Optional[Sequence[bool]] = None, dose: float = 1.0, scale: float = 1.0, locked: bool = False): object.__setattr__(self, 'locked', False) self.identifier = () self.pattern = pattern self.offset = offset self.rotation = rotation self.dose = dose self.scale = scale if mirrored is None: mirrored = [False, False] self.mirrored = mirrored self.locked = locked def __setattr__(self, name, value): if self.locked and name != 'locked': raise PatternLockedError() object.__setattr__(self, name, value) def __copy__(self) -> 'SubPattern': new = SubPattern(pattern=self.pattern, offset=self.offset.copy(), rotation=self.rotation, dose=self.dose, scale=self.scale, mirrored=self.mirrored.copy(), locked=self.locked) return new def __deepcopy__(self, memo: Dict = None) -> 'SubPattern': memo = {} if memo is None else memo new = copy.copy(self).unlock() new.pattern = copy.deepcopy(self.pattern, memo) new.locked = self.locked return new # pattern property @property def pattern(self) -> Optional['Pattern']: return self._pattern @pattern.setter def pattern(self, val: Optional['Pattern']): from .pattern import Pattern if val is not None and not isinstance(val, Pattern): raise PatternError('Provided pattern {} is not a Pattern object or None!'.format(val)) self._pattern = val # offset property @property def offset(self) -> numpy.ndarray: return self._offset @offset.setter def offset(self, val: vector2): if not isinstance(val, numpy.ndarray): val = numpy.array(val, dtype=float) if val.size != 2: raise PatternError('Offset must be convertible to size-2 ndarray') self._offset = val.flatten().astype(float) # dose property @property def dose(self) -> float: return self._dose @dose.setter def dose(self, val: float): if not is_scalar(val): raise PatternError('Dose must be a scalar') if not val >= 0: raise PatternError('Dose must be non-negative') self._dose = val # scale property @property def scale(self) -> float: return self._scale @scale.setter def scale(self, val: float): if not is_scalar(val): raise PatternError('Scale must be a scalar') if not val > 0: raise PatternError('Scale must be positive') self._scale = val # Rotation property [ccw] @property def rotation(self) -> float: return self._rotation @rotation.setter def rotation(self, val: float): if not is_scalar(val): raise PatternError('Rotation must be a scalar') self._rotation = val % (2 * pi) # Mirrored property @property def mirrored(self) -> numpy.ndarray: # ndarray[bool] return self._mirrored @mirrored.setter def mirrored(self, val: Sequence[bool]): if is_scalar(val): raise PatternError('Mirrored must be a 2-element list of booleans') self._mirrored = numpy.array(val, dtype=bool, copy=True) def as_pattern(self) -> 'Pattern': """ Returns: A copy of self.pattern which has been scaled, rotated, etc. according to this `SubPattern`'s properties. """ assert(self.pattern is not None) pattern = self.pattern.deepcopy().deepunlock() pattern.scale_by(self.scale) [pattern.mirror(ax) for ax, do in enumerate(self.mirrored) if do] pattern.rotate_around((0.0, 0.0), self.rotation) pattern.translate_elements(self.offset) pattern.scale_element_doses(self.dose) return pattern def translate(self, offset: vector2) -> 'SubPattern': """ Translate by the given offset Args: offset: Offset `[x, y]` to translate by Returns: self """ self.offset += offset return self def rotate_around(self, pivot: vector2, rotation: float) -> 'SubPattern': """ Rotate around a point Args: pivot: Point `[x, y]` to rotate around rotation: Angle to rotate by (counterclockwise, radians) Returns: self """ pivot = numpy.array(pivot, dtype=float) self.translate(-pivot) self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) self.rotate(rotation) self.translate(+pivot) return self def rotate(self, rotation: float) -> 'SubPattern': """ Rotate the instance around it's origin Args: rotation: Angle to rotate by (counterclockwise, radians) Returns: self """ self.rotation += rotation return self def mirror(self, axis: int) -> 'SubPattern': """ Mirror the subpattern across an axis. Args: axis: Axis to mirror across. Returns: self """ self.mirrored[axis] = not self.mirrored[axis] self.rotation *= -1 return self def get_bounds(self) -> Optional[numpy.ndarray]: """ Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the extent of the `SubPattern` in each dimension. Returns `None` if the contained `Pattern` is empty. Returns: `[[x_min, y_min], [x_max, y_max]]` or `None` """ if self.pattern is None: return None return self.as_pattern().get_bounds() def scale_by(self, c: float) -> 'SubPattern': """ Scale the subpattern by a factor Args: c: scaling factor Returns: self """ self.scale *= c return self def copy(self) -> 'SubPattern': """ Return a shallow copy of the subpattern. Returns: `copy.copy(self)` """ return copy.copy(self) def deepcopy(self) -> 'SubPattern': """ Return a deep copy of the subpattern. Returns: `copy.deepcopy(self)` """ return copy.deepcopy(self) def lock(self) -> 'SubPattern': """ Lock the SubPattern, disallowing changes Returns: self """ self.offset.flags.writeable = False self.mirrored.flags.writeable = False object.__setattr__(self, 'locked', True) return self def unlock(self) -> 'SubPattern': """ Unlock the SubPattern Returns: self """ self.offset.flags.writeable = True self.mirrored.flags.writeable = True object.__setattr__(self, 'locked', False) return self def deeplock(self) -> 'SubPattern': """ Recursively lock the SubPattern and its contained pattern Returns: self """ assert(self.pattern is not None) self.lock() self.pattern.deeplock() return self def deepunlock(self) -> 'SubPattern': """ Recursively unlock the SubPattern and its contained pattern This is dangerous unless you have just performed a deepcopy, since the subpattern and its components may be used in more than one once! Returns: self """ assert(self.pattern is not None) self.unlock() self.pattern.deepunlock() return self def __repr__(self) -> str: name = self.pattern.name if self.pattern is not None else None rotation = f' r{self.rotation*180/pi:g}' if self.rotation != 0 else '' scale = f' d{self.scale:g}' if self.scale != 1 else '' mirrored = ' m{:d}{:d}'.format(*self.mirrored) if self.mirrored.any() else '' dose = f' d{self.dose:g}' if self.dose != 1 else '' locked = ' L' if self.locked else '' return f''