Add repetitions and split up code into traits

This commit is contained in:
Jan Petykiewicz 2020-07-22 02:45:16 -07:00
commit bab40474a0
27 changed files with 1183 additions and 929 deletions

View file

@ -1,78 +1,47 @@
"""
Repetitions provides support for efficiently nesting multiple identical
instances of a Pattern in the same parent Pattern.
Repetitions provide support for efficiently representing multiple identical
instances of an object .
"""
from typing import Union, List, Dict, Tuple, Optional, Sequence, TYPE_CHECKING, Any
import copy
from abc import ABCMeta, abstractmethod
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
from .utils import rotation_matrix_2d, vector2, AutoSlots
from .traits import LockableImpl, Copyable, Scalable, Rotatable, Mirrorable
# TODO need top-level comment about what order rotation/scale/offset/mirror/array are applied
class GridRepetition:
class Repetition(Copyable, Rotatable, Mirrorable, Scalable, metaclass=ABCMeta):
"""
GridRepetition provides support for efficiently embedding multiple copies of a `Pattern`
into another `Pattern` at regularly-spaced offsets.
Note that rotation, scaling, and mirroring are applied to individual instances of the
pattern, not to the grid vectors.
The order of operations is
1. A single refernce instance to the target pattern is mirrored
2. The single instance is rotated.
3. The instance is scaled by the scaling factor.
4. The instance is shifted by the provided offset
(no mirroring/scaling/rotation is applied to the offset).
5. Additional copies of the instance will appear at coordinates specified by
`(offset + aa * a_vector + bb * b_vector)`, with `aa in range(0, a_count)`
and `bb in range(0, b_count)`. All instance locations remain unaffected by
mirroring/scaling/rotation, though each instance's data will be transformed
relative to the instance's location (i.e. relative to the contained pattern's
(0, 0) point).
Interface common to all objects which specify repetitions
"""
__slots__ = ('_pattern',
'_offset',
'_rotation',
'_dose',
'_scale',
'_mirrored',
'_a_vector',
__slots__ = ()
@property
@abstractmethod
def displacements(self) -> numpy.ndarray:
"""
An Nx2 ndarray specifying all offsets generated by this repetition
"""
pass
class Grid(LockableImpl, Repetition, metaclass=AutoSlots):
"""
`Grid` describes a 2D grid formed by two basis vectors and two 'counts' (sizes).
The second basis vector and count (`b_vector` and `b_count`) may be omitted,
which makes the grid describe a 1D array.
Note that the offsets in either the 2D or 1D grids do not have to be axis-aligned.
"""
__slots__ = ('_a_vector',
'_b_vector',
'_a_count',
'_b_count',
'identifier',
'locked')
_pattern: Optional['Pattern']
""" The `Pattern` being instanced """
_offset: numpy.ndarray
""" (x, y) offset for the base instance """
_dose: float
""" Scaling factor applied to the dose """
_rotation: float
""" Rotation of the individual instances in the grid (not the grid vectors).
Radians, counterclockwise.
"""
_scale: float
""" Scaling factor applied to individual instances in the grid (not the grid vectors) """
_mirrored: numpy.ndarray # ndarray[bool]
""" Whether to mirror individual instances across the x and y axes
(Applies to individual instances in the grid, not the grid vectors)
"""
'_b_count')
_a_vector: numpy.ndarray
""" Vector `[x, y]` specifying the first lattice vector of the grid.
@ -91,28 +60,14 @@ class GridRepetition:
_b_count: int
""" Number of instances along the direction specified by the `b_vector` """
identifier: Tuple[Any, ...]
""" Arbitrary identifier, used internally by some `masque` functions. """
locked: bool
""" If `True`, disallows changes to the GridRepetition """
def __init__(self,
pattern: Optional['Pattern'],
a_vector: numpy.ndarray,
a_count: int,
b_vector: Optional[numpy.ndarray] = None,
b_count: Optional[int] = 1,
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,
identifier: Tuple[Any, ...] = ()):
locked: bool = False,):
"""
Args:
pattern: Pattern to reference.
a_vector: First lattice vector, of the form `[x, y]`.
Specifies center-to-center spacing between adjacent instances.
a_count: Number of elements in the a_vector direction.
@ -121,14 +76,7 @@ class GridRepetition:
Can be omitted when specifying a 1D array.
b_count: Number of elements in the `b_vector` direction.
Should be omitted if `b_vector` was omitted.
offset: (x, y) offset applied to all instances.
rotation: Rotation (radians, counterclockwise) applied to each instance.
Relative to each instance's (0, 0).
mirrored: Whether to mirror individual instances across the x and y axes.
dose: Scaling factor applied to the dose.
scale: Scaling factor applied to the instances' geometry.
locked: Whether the `GridRepetition` is locked after initialization.
identifier: Arbitrary tuple, used internally by some `masque` functions.
locked: Whether the `Grid` is locked after initialization.
Raises:
PatternError if `b_*` inputs conflict with each other
@ -144,132 +92,31 @@ class GridRepetition:
b_vector = numpy.array([0.0, 0.0])
if a_count < 1:
raise PatternError('Repetition has too-small a_count: '
'{}'.format(a_count))
raise PatternError(f'Repetition has too-small a_count: {a_count}')
if b_count < 1:
raise PatternError('Repetition has too-small b_count: '
'{}'.format(b_count))
raise PatternError(f'Repetition has too-small b_count: {b_count}')
object.__setattr__(self, 'locked', False)
self.a_vector = a_vector
self.b_vector = b_vector
self.a_count = a_count
self.b_count = b_count
self.identifier = 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) -> 'GridRepetition':
new = GridRepetition(pattern=self.pattern,
a_vector=self.a_vector.copy(),
b_vector=copy.copy(self.b_vector),
a_count=self.a_count,
b_count=self.b_count,
offset=self.offset.copy(),
rotation=self.rotation,
dose=self.dose,
scale=self.scale,
mirrored=self.mirrored.copy(),
locked=self.locked)
def __copy__(self) -> 'Grid':
new = Grid(a_vector=self.a_vector.copy(),
b_vector=copy.copy(self.b_vector),
a_count=self.a_count,
b_count=self.b_count,
locked=self.locked)
return new
def __deepcopy__(self, memo: Dict = None) -> 'GridRepetition':
def __deepcopy__(self, memo: Dict = None) -> 'Grid':
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 self.locked:
raise PatternLockedError()
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)
# a_vector property
@property
def a_vector(self) -> numpy.ndarray:
@ -320,69 +167,15 @@ class GridRepetition:
raise PatternError('b_count must be convertable to an int!')
self._b_count = int(val)
def as_pattern(self) -> 'Pattern':
@property
def displacements(self) -> numpy.ndarray:
aa, bb = numpy.meshgrid(numpy.arange(self.a_count), numpy.arange(self.b_count), indexing='ij')
return (aa.flat[:, None] * self.a_vector[None, :] +
bb.flat[:, None] * self.b_vector[None, :])
def rotate(self, rotation: float) -> 'Grid':
"""
Returns a copy of self.pattern which has been scaled, rotated, repeated, etc.
etc. according to this `GridRepetition`'s properties.
Returns:
A copy of self.pattern which has been scaled, rotated, repeated, etc.
etc. according to this `GridRepetition`'s properties.
"""
assert(self.pattern is not None)
patterns = []
pat = self.pattern.deepcopy().deepunlock()
pat.scale_by(self.scale)
[pat.mirror(ax) for ax, do in enumerate(self.mirrored) if do]
pat.rotate_around((0.0, 0.0), self.rotation)
pat.translate_elements(self.offset)
pat.scale_element_doses(self.dose)
combined = type(pat)(name='__GridRepetition__')
for a in range(self.a_count):
for b in range(self.b_count):
offset = a * self.a_vector + b * self.b_vector
newPat = pat.deepcopy()
newPat.translate_elements(offset)
combined.append(newPat)
return combined
def translate(self, offset: vector2) -> 'GridRepetition':
"""
Translate by the given offset
Args:
offset: `[x, y]` to translate by
Returns:
self
"""
self.offset += offset
return self
def rotate_around(self, pivot: vector2, rotation: float) -> 'GridRepetition':
"""
Rotate the array 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) -> 'GridRepetition':
"""
Rotate around (0, 0)
Rotate lattice vectors (around (0, 0))
Args:
rotation: Angle to rotate by (counterclockwise, radians)
@ -390,28 +183,14 @@ class GridRepetition:
Returns:
self
"""
self.rotate_elements(rotation)
self.a_vector = numpy.dot(rotation_matrix_2d(rotation), self.a_vector)
if self.b_vector is not None:
self.b_vector = numpy.dot(rotation_matrix_2d(rotation), self.b_vector)
return self
def rotate_elements(self, rotation: float) -> 'GridRepetition':
def mirror(self, axis: int) -> 'Grid':
"""
Rotate each element around its origin
Args:
rotation: Angle to rotate by (counterclockwise, radians)
Returns:
self
"""
self.rotation += rotation
return self
def mirror(self, axis: int) -> 'GridRepetition':
"""
Mirror the GridRepetition across an axis.
Mirror the Grid across an axis.
Args:
axis: Axis to mirror across.
@ -420,43 +199,30 @@ class GridRepetition:
Returns:
self
"""
self.mirror_elements(axis)
self.a_vector[1-axis] *= -1
if self.b_vector is not None:
self.b_vector[1-axis] *= -1
return self
def mirror_elements(self, axis: int) -> 'GridRepetition':
"""
Mirror each element across an axis relative to its origin.
Args:
axis: Axis to mirror across.
(0: mirror across x-axis, 1: mirror across y-axis)
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 `GridRepetition` in each dimension.
Returns `None` if the contained `Pattern` is empty.
extent of the `Grid` in each dimension.
Returns:
`[[x_min, y_min], [x_max, y_max]]` or `None`
"""
if self.pattern is None:
return None
return self.as_pattern().get_bounds()
a_extent = self.a_vector * self.a_count
b_extent = self.b_vector * self.b_count if self.b_count != 0 else 0
def scale_by(self, c: float) -> 'GridRepetition':
corners = ((0, 0), a_extent, b_extent, a_extent + b_extent)
xy_min = numpy.min(corners, axis=0)
xy_max = numpy.min(corners, axis=0)
return numpy.array((xy_min, xy_max))
def scale_by(self, c: float) -> 'Grid':
"""
Scale the GridRepetition by a factor
Scale the Grid by a factor
Args:
c: scaling factor
@ -464,107 +230,116 @@ class GridRepetition:
Returns:
self
"""
self.scale_elements_by(c)
self.a_vector *= c
if self.b_vector is not None:
self.b_vector *= c
return self
def scale_elements_by(self, c: float) -> 'GridRepetition':
def lock(self) -> 'Grid':
"""
Scale each element by a factor
Args:
c: scaling factor
Lock the `Grid`, disallowing changes.
Returns:
self
"""
self.scale *= c
return self
def copy(self) -> 'GridRepetition':
"""
Return a shallow copy of the repetition.
Returns:
`copy.copy(self)`
"""
return copy.copy(self)
def deepcopy(self) -> 'GridRepetition':
"""
Return a deep copy of the repetition.
Returns:
`copy.deepcopy(self)`
"""
return copy.deepcopy(self)
def lock(self) -> 'GridRepetition':
"""
Lock the `GridRepetition`, disallowing changes.
Returns:
self
"""
self.offset.flags.writeable = False
self.a_vector.flags.writeable = False
self.mirrored.flags.writeable = False
if self.b_vector is not None:
self.b_vector.flags.writeable = False
object.__setattr__(self, 'locked', True)
LockableImpl.lock(self)
return self
def unlock(self) -> 'GridRepetition':
def unlock(self) -> 'Grid':
"""
Unlock the `GridRepetition`
Unlock the `Grid`
Returns:
self
"""
self.offset.flags.writeable = True
self.a_vector.flags.writeable = True
self.mirrored.flags.writeable = True
if self.b_vector is not None:
self.b_vector.flags.writeable = True
object.__setattr__(self, 'locked', False)
return self
def deeplock(self) -> 'GridRepetition':
"""
Recursively lock the `GridRepetition` and its contained pattern
Returns:
self
"""
assert(self.pattern is not None)
self.lock()
self.pattern.deeplock()
return self
def deepunlock(self) -> 'GridRepetition':
"""
Recursively unlock the `GridRepetition` and its contained pattern
This is dangerous unless you have just performed a deepcopy, since
the component parts may be reused elsewhere.
Returns:
self
"""
assert(self.pattern is not None)
self.unlock()
self.pattern.deepunlock()
LockableImpl.unlock(self)
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 ''
bv = f', {self.b_vector}' if self.b_vector is not None else ''
return (f'<GridRepetition "{name}" at {self.offset} {rotation}{scale}{mirrored}{dose}'
f' {self.a_count}x{self.b_count} ({self.a_vector}{bv}){locked}>')
return (f'<Grid {self.a_count}x{self.b_count} ({self.a_vector}{bv}){locked}>')
def __eq__(self, other: Any) -> bool:
if not isinstance(other, type(self)):
return False
if self.a_count != other.a_count or self.b_count != other.b_count:
return False
if any(self.a_vector[ii] != other.a_vector[ii] for ii in range(2)):
return False
if self.b_vector is None and other.b_vector is None:
return True
if self.b_vector is None or other.b_vector is None:
return False
if any(self.b_vector[ii] != other.b_vector[ii] for ii in range(2)):
return False
if self.locked != other.locked:
return False
return True
class Arbitrary(LockableImpl, Repetition, metaclass=AutoSlots):
"""
`Arbitrary` is a simple list of (absolute) displacements for instances.
Attributes:
displacements (numpy.ndarray): absolute displacements of all elements
`[[x0, y0], [x1, y1], ...]`
"""
_displacements: numpy.ndarray
""" List of vectors `[[x0, y0], [x1, y1], ...]` specifying the offsets
of the instances.
"""
locked: bool
""" If `True`, disallows changes to the object. """
@property
def displacements(self) -> numpy.ndarray:
return self._displacements
@displacements.setter
def displacements(self, val: Union[Sequence[Sequence[float]], numpy.ndarray]):
val = numpy.array(val, float)
val = numpy.sort(val.view([('', val.dtype)] * val.shape[1]), 0).view(val.dtype) # sort rows
self._displacements = val
def lock(self) -> 'Arbitrary':
"""
Lock the object, disallowing changes.
Returns:
self
"""
self._displacements.flags.writeable = False
LockableImpl.lock(self)
return self
def unlock(self) -> 'Arbitrary':
"""
Unlock the object
Returns:
self
"""
self._displacements.flags.writeable = True
LockableImpl.unlock(self)
return self
def __repr__(self) -> str:
locked = ' L' if self.locked else ''
return (f'<Arbitrary {len(self.displacements)}pts {locked}>')
def __eq__(self, other: Any) -> bool:
if not isinstance(other, type(self)):
return False
if self.locked != other.locked:
return False
return numpy.array_equal(self.displacements, other.displacements)