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

@ -0,0 +1,9 @@
from .positionable import Positionable, PositionableImpl
from .layerable import Layerable, LayerableImpl
from .doseable import Doseable, DoseableImpl
from .rotatable import Rotatable, RotatableImpl, Pivotable, PivotableImpl
from .repeatable import Repeatable, RepeatableImpl
from .scalable import Scalable, ScalableImpl
from .mirrorable import Mirrorable
from .copyable import Copyable
from .lockable import Lockable, LockableImpl

34
masque/traits/copyable.py Normal file
View file

@ -0,0 +1,34 @@
from typing import List, Tuple, Callable, TypeVar, Optional
from abc import ABCMeta, abstractmethod
import copy
T = TypeVar('T', bound='Copyable')
class Copyable(metaclass=ABCMeta):
"""
Abstract class which adds .copy() and .deepcopy()
"""
__slots__ = ()
'''
---- Non-abstract methods
'''
def copy(self: T) -> T:
"""
Return a shallow copy of the object.
Returns:
`copy.copy(self)`
"""
return copy.copy(self)
def deepcopy(self: T) -> T:
"""
Return a deep copy of the object.
Returns:
`copy.deepcopy(self)`
"""
return copy.deepcopy(self)

82
masque/traits/doseable.py Normal file
View file

@ -0,0 +1,82 @@
from typing import List, Tuple, Callable, TypeVar, Optional
from abc import ABCMeta, abstractmethod
import copy
import numpy
from ..error import PatternError, PatternLockedError
from ..utils import is_scalar
T = TypeVar('T', bound='Doseable')
I = TypeVar('I', bound='DoseableImpl')
class Doseable(metaclass=ABCMeta):
"""
Abstract class for all doseable entities
"""
__slots__ = ()
'''
---- Properties
'''
@property
@abstractmethod
def dose(self) -> float:
"""
Dose (float >= 0)
"""
pass
@dose.setter
@abstractmethod
def dose(self, val: float):
pass
'''
---- Methods
'''
def set_dose(self: T, dose: float) -> T:
"""
Set the dose
Args:
dose: new value for dose
Returns:
self
"""
pass
class DoseableImpl(Doseable, metaclass=ABCMeta):
"""
Simple implementation of Doseable
"""
__slots__ = ()
_dose: float
""" Dose """
'''
---- Non-abstract properties
'''
@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
'''
---- Non-abstract methods
'''
def set_dose(self: I, dose: float) -> I:
self.dose = dose
return self

View file

@ -0,0 +1,76 @@
from typing import List, Tuple, Callable, TypeVar, Optional
from abc import ABCMeta, abstractmethod
import copy
import numpy
from ..error import PatternError, PatternLockedError
from ..utils import layer_t
T = TypeVar('T', bound='Layerable')
I = TypeVar('I', bound='LayerableImpl')
class Layerable(metaclass=ABCMeta):
"""
Abstract class for all layerable entities
"""
__slots__ = ()
'''
---- Properties
'''
@property
@abstractmethod
def layer(self) -> layer_t:
"""
Layer number or name (int, tuple of ints, or string)
"""
return self._layer
@layer.setter
@abstractmethod
def layer(self, val: layer_t):
self._layer = val
'''
---- Methods
'''
def set_layer(self: T, layer: layer_t) -> T:
"""
Set the layer
Args:
layer: new value for layer
Returns:
self
"""
pass
class LayerableImpl(Layerable, metaclass=ABCMeta):
"""
Simple implementation of Layerable
"""
__slots__ = ()
_layer: layer_t
""" Layer number, pair, or name """
'''
---- Non-abstract properties
'''
@property
def layer(self) -> layer_t:
return self._layer
@layer.setter
def layer(self, val: layer_t):
self._layer = val
'''
---- Non-abstract methods
'''
def set_layer(self: I, layer: layer_t) -> I:
self.layer = layer
return self

76
masque/traits/lockable.py Normal file
View file

@ -0,0 +1,76 @@
from typing import List, Tuple, Callable, TypeVar, Optional
from abc import ABCMeta, abstractmethod
import copy
import numpy
from ..error import PatternError, PatternLockedError
T = TypeVar('T', bound='Lockable')
I = TypeVar('I', bound='LockableImpl')
class Lockable(metaclass=ABCMeta):
"""
Abstract class for all lockable entities
"""
__slots__ = ()
'''
---- Methods
'''
def set_dose(self: T, dose: float) -> T:
"""
Set the dose
Args:
dose: new value for dose
Returns:
self
"""
pass
def lock(self: T) -> T:
"""
Lock the object, disallowing further changes
Returns:
self
"""
pass
def unlock(self: T) -> T:
"""
Unlock the object, reallowing changes
Returns:
self
"""
pass
class LockableImpl(Lockable, metaclass=ABCMeta):
"""
Simple implementation of Lockable
"""
__slots__ = ()
locked: bool
""" If `True`, disallows changes to the object """
'''
---- Non-abstract methods
'''
def __setattr__(self, name, value):
if self.locked and name != 'locked':
raise PatternLockedError()
object.__setattr__(self, name, value)
def lock(self: I) -> I:
object.__setattr__(self, 'locked', True)
return self
def unlock(self: I) -> I:
object.__setattr__(self, 'locked', False)
return self

View file

@ -0,0 +1,61 @@
from typing import List, Tuple, Callable, TypeVar, Optional
from abc import ABCMeta, abstractmethod
import copy
import numpy
from ..error import PatternError, PatternLockedError
T = TypeVar('T', bound='Mirrorable')
T = TypeVar('T', bound='MirrorableImpl')
class Mirrorable(metaclass=ABCMeta):
"""
Abstract class for all mirrorable entities
"""
__slots__ = ()
'''
---- Abstract methods
'''
@abstractmethod
def mirror(self: T, axis: int) -> T:
"""
Mirror the entity across an axis.
Args:
axis: Axis to mirror across.
Returns:
self
"""
pass
#class MirrorableImpl(Mirrorable, metaclass=ABCMeta):
# """
# Simple implementation of `Mirrorable`
# """
# __slots__ = ()
#
# _mirrored: numpy.ndarray # ndarray[bool]
# """ Whether to mirror the instance across the x and/or y axes. """
#
# '''
# ---- Properties
# '''
# # Mirrored property
# @property
# def mirrored(self) -> numpy.ndarray: # ndarray[bool]
# """ Whether to mirror across the [x, y] axes, respectively """
# 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)
#
# '''
# ---- Methods
# '''

View file

@ -0,0 +1,135 @@
# TODO top-level comment about how traits should set __slots__ = (), and how to use AutoSlots
from typing import List, Tuple, Callable, TypeVar, Optional
from abc import ABCMeta, abstractmethod
import copy
import numpy
from ..error import PatternError, PatternLockedError
from ..utils import is_scalar, rotation_matrix_2d, vector2
T = TypeVar('T', bound='Positionable')
I = TypeVar('I', bound='PositionableImpl')
class Positionable(metaclass=ABCMeta):
"""
Abstract class for all positionable entities
"""
__slots__ = ()
'''
---- Abstract properties
'''
@property
@abstractmethod
def offset(self) -> numpy.ndarray:
"""
[x, y] offset
"""
pass
@offset.setter
@abstractmethod
def offset(self, val: vector2):
pass
'''
--- Abstract methods
'''
@abstractmethod
def get_bounds(self) -> numpy.ndarray:
"""
Returns `[[x_min, y_min], [x_max, y_max]]` which specify a minimal bounding box for the entity.
"""
pass
@abstractmethod
def set_offset(self: T, offset: vector2) -> T:
"""
Set the offset
Args:
offset: [x_offset, y,offset]
Returns:
self
"""
pass
@abstractmethod
def translate(self: T, offset: vector2) -> T:
"""
Translate the entity by the given offset
Args:
offset: [x_offset, y,offset]
Returns:
self
"""
pass
class PositionableImpl(Positionable, metaclass=ABCMeta):
"""
Simple implementation of Positionable
"""
__slots__ = ()
_offset: numpy.ndarray
""" `[x_offset, y_offset]` """
'''
---- Properties
'''
# offset property
@property
def offset(self) -> numpy.ndarray:
"""
[x, y] offset
"""
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()
'''
---- Methods
'''
def set_offset(self: I, offset: vector2) -> I:
self.offset = offset
return self
def translate(self: I, offset: vector2) -> I:
self._offset += offset
return self
def _lock(self: I) -> I:
"""
Lock the entity, disallowing further changes
Returns:
self
"""
self._offset.flags.writeable = False
return self
def _unlock(self: I) -> I:
"""
Unlock the entity
Returns:
self
"""
self._offset.flags.writeable = True
return self

View file

@ -0,0 +1,79 @@
from typing import List, Tuple, Callable, TypeVar, Optional
from abc import ABCMeta, abstractmethod
import copy
import numpy
from ..error import PatternError, PatternLockedError
T = TypeVar('T', bound='Repeatable')
I = TypeVar('I', bound='RepeatableImpl')
class Repeatable(metaclass=ABCMeta):
"""
Abstract class for all repeatable entities
"""
__slots__ = ()
'''
---- Properties
'''
@property
@abstractmethod
def repetition(self) -> Optional['Repetition']:
"""
Repetition object, or None (single instance only)
"""
pass
@repetition.setter
@abstractmethod
def repetition(self, repetition: Optional['Repetition']):
pass
'''
---- Methods
'''
def set_repetition(self: T, repetition: Optional['Repetition']) -> T:
"""
Set the repetition
Args:
repetition: new value for repetition, or None (single instance)
Returns:
self
"""
pass
class RepeatableImpl(Repeatable, metaclass=ABCMeta):
"""
Simple implementation of `Repeatable`
"""
__slots__ = ()
_repetition: Optional['Repetition']
""" Repetition object, or None (single instance only) """
'''
---- Non-abstract properties
'''
@property
def repetition(self) -> Optional['Repetition']:
return self._repetition
@repetition.setter
def repetition(self, repetition: Optional['Repetition']):
from ..repetition import Repetition
if repetition is not None and not isinstance(repetition, Repetition):
raise PatternError(f'{repetition} is not a valid Repetition object!')
self._repetition = repetition
'''
---- Non-abstract methods
'''
def set_repetition(self: I, repetition: 'Repetition') -> I:
self.repetition = repetition
return self

119
masque/traits/rotatable.py Normal file
View file

@ -0,0 +1,119 @@
from typing import List, Tuple, Callable, TypeVar, Optional
from abc import ABCMeta, abstractmethod
import copy
import numpy
from numpy import pi
from .positionable import Positionable
from ..error import PatternError, PatternLockedError
from ..utils import is_scalar, rotation_matrix_2d, vector2
T = TypeVar('T', bound='Rotatable')
I = TypeVar('I', bound='RotatableImpl')
P = TypeVar('P', bound='Pivotable')
J = TypeVar('J', bound='PivotableImpl')
class Rotatable(metaclass=ABCMeta):
"""
Abstract class for all rotatable entities
"""
__slots__ = ()
'''
---- Abstract methods
'''
@abstractmethod
def rotate(self: T, theta: float) -> T:
"""
Rotate the shape around its origin (0, 0), ignoring its offset.
Args:
theta: Angle to rotate by (counterclockwise, radians)
Returns:
self
"""
pass
class RotatableImpl(Rotatable, metaclass=ABCMeta):
"""
Simple implementation of `Rotatable`
"""
__slots__ = ()
_rotation: float
""" rotation for the object, radians counterclockwise """
'''
---- Properties
'''
@property
def rotation(self) -> float:
""" Rotation, radians counterclockwise """
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)
'''
---- Methods
'''
def rotate(self: I, rotation: float) -> I:
self.rotation += rotation
return self
def set_rotation(self: I, rotation: float) -> I:
"""
Set the rotation to a value
Args:
rotation: radians ccw
Returns:
self
"""
self.rotation = rotation
return self
class Pivotable(metaclass=ABCMeta):
"""
Abstract class for entites which can be rotated around a point.
This requires that they are `Positionable` but not necessarily `Rotatable` themselves.
"""
__slots__ = ()
def rotate_around(self: P, pivot: vector2, rotation: float) -> P:
"""
Rotate the object around a point.
Args:
pivot: Point (x, y) to rotate around
rotation: Angle to rotate by (counterclockwise, radians)
Returns:
self
"""
pass
class PivotableImpl(Pivotable, metaclass=ABCMeta):
"""
Implementation of `Pivotable` for objects which are `Rotatable`
"""
__slots__ = ()
def rotate_around(self: J, pivot: vector2, rotation: float) -> J:
pivot = numpy.array(pivot, dtype=float)
self.translate(-pivot)
self.rotate(rotation)
self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset)
self.translate(+pivot)
return self

79
masque/traits/scalable.py Normal file
View file

@ -0,0 +1,79 @@
from typing import List, Tuple, Callable, TypeVar, Optional
from abc import ABCMeta, abstractmethod
import copy
import numpy
from ..error import PatternError, PatternLockedError
from ..utils import is_scalar
T = TypeVar('T', bound='Scalable')
I = TypeVar('I', bound='ScalableImpl')
class Scalable(metaclass=ABCMeta):
"""
Abstract class for all scalable entities
"""
__slots__ = ()
'''
---- Abstract methods
'''
@abstractmethod
def scale_by(self: T, c: float) -> T:
"""
Scale the entity by a factor
Args:
c: scaling factor
Returns:
self
"""
pass
class ScalableImpl(Scalable, metaclass=ABCMeta):
"""
Simple implementation of Scalable
"""
__slots__ = ()
_scale: float
""" scale factor for the entity """
'''
---- Properties
'''
@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
'''
---- Methods
'''
def scale_by(self: I, c: float) -> I:
self.scale *= c
return self
def set_scale(self: I, scale: float) -> I:
"""
Set the sclae to a value
Args:
scale: absolute scale factor
Returns:
self
"""
self.scale = scale
return self