Add repetitions and split up code into traits

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

102
examples/test_rep.py Normal file
View File

@ -0,0 +1,102 @@
import numpy
from numpy import pi
import masque
import masque.file.gdsii
import masque.file.dxf
import masque.file.oasis
from masque import shapes, Pattern, SubPattern
from masque.repetition import Grid
from pprint import pprint
def main():
pat = masque.Pattern(name='ellip_grating')
for rmin in numpy.arange(10, 15, 0.5):
pat.shapes.append(shapes.Arc(
radii=(rmin, rmin),
width=0.1,
angles=(0*-numpy.pi/4, numpy.pi/4)
))
pat.scale_by(1000)
# pat.visualize()
pat2 = pat.copy()
pat2.name = 'grating2'
pat3 = Pattern('sref_test')
pat3.subpatterns = [
SubPattern(pat, offset=(1e5, 3e5)),
SubPattern(pat, offset=(2e5, 3e5), rotation=pi/3),
SubPattern(pat, offset=(3e5, 3e5), rotation=pi/2),
SubPattern(pat, offset=(4e5, 3e5), rotation=pi),
SubPattern(pat, offset=(5e5, 3e5), rotation=3*pi/2),
SubPattern(pat, mirrored=(True, False), offset=(1e5, 4e5)),
SubPattern(pat, mirrored=(True, False), offset=(2e5, 4e5), rotation=pi/3),
SubPattern(pat, mirrored=(True, False), offset=(3e5, 4e5), rotation=pi/2),
SubPattern(pat, mirrored=(True, False), offset=(4e5, 4e5), rotation=pi),
SubPattern(pat, mirrored=(True, False), offset=(5e5, 4e5), rotation=3*pi/2),
SubPattern(pat, mirrored=(False, True), offset=(1e5, 5e5)),
SubPattern(pat, mirrored=(False, True), offset=(2e5, 5e5), rotation=pi/3),
SubPattern(pat, mirrored=(False, True), offset=(3e5, 5e5), rotation=pi/2),
SubPattern(pat, mirrored=(False, True), offset=(4e5, 5e5), rotation=pi),
SubPattern(pat, mirrored=(False, True), offset=(5e5, 5e5), rotation=3*pi/2),
SubPattern(pat, mirrored=(True, True), offset=(1e5, 6e5)),
SubPattern(pat, mirrored=(True, True), offset=(2e5, 6e5), rotation=pi/3),
SubPattern(pat, mirrored=(True, True), offset=(3e5, 6e5), rotation=pi/2),
SubPattern(pat, mirrored=(True, True), offset=(4e5, 6e5), rotation=pi),
SubPattern(pat, mirrored=(True, True), offset=(5e5, 6e5), rotation=3*pi/2),
]
pprint(pat3)
pprint(pat3.subpatterns)
pprint(pat.shapes)
rep = Grid(a_vector=[1e4, 0],
b_vector=[0, 1.5e4],
a_count=3,
b_count=2,)
pat4 = Pattern('aref_test')
pat4.subpatterns = [
SubPattern(pat, repetition=rep, offset=(1e5, 3e5)),
SubPattern(pat, repetition=rep, offset=(2e5, 3e5), rotation=pi/3),
SubPattern(pat, repetition=rep, offset=(3e5, 3e5), rotation=pi/2),
SubPattern(pat, repetition=rep, offset=(4e5, 3e5), rotation=pi),
SubPattern(pat, repetition=rep, offset=(5e5, 3e5), rotation=3*pi/2),
SubPattern(pat, repetition=rep, mirrored=(True, False), offset=(1e5, 4e5)),
SubPattern(pat, repetition=rep, mirrored=(True, False), offset=(2e5, 4e5), rotation=pi/3),
SubPattern(pat, repetition=rep, mirrored=(True, False), offset=(3e5, 4e5), rotation=pi/2),
SubPattern(pat, repetition=rep, mirrored=(True, False), offset=(4e5, 4e5), rotation=pi),
SubPattern(pat, repetition=rep, mirrored=(True, False), offset=(5e5, 4e5), rotation=3*pi/2),
SubPattern(pat, repetition=rep, mirrored=(False, True), offset=(1e5, 5e5)),
SubPattern(pat, repetition=rep, mirrored=(False, True), offset=(2e5, 5e5), rotation=pi/3),
SubPattern(pat, repetition=rep, mirrored=(False, True), offset=(3e5, 5e5), rotation=pi/2),
SubPattern(pat, repetition=rep, mirrored=(False, True), offset=(4e5, 5e5), rotation=pi),
SubPattern(pat, repetition=rep, mirrored=(False, True), offset=(5e5, 5e5), rotation=3*pi/2),
SubPattern(pat, repetition=rep, mirrored=(True, True), offset=(1e5, 6e5)),
SubPattern(pat, repetition=rep, mirrored=(True, True), offset=(2e5, 6e5), rotation=pi/3),
SubPattern(pat, repetition=rep, mirrored=(True, True), offset=(3e5, 6e5), rotation=pi/2),
SubPattern(pat, repetition=rep, mirrored=(True, True), offset=(4e5, 6e5), rotation=pi),
SubPattern(pat, repetition=rep, mirrored=(True, True), offset=(5e5, 6e5), rotation=3*pi/2),
]
folder = 'layouts/'
masque.file.gdsii.writefile((pat, pat2, pat3, pat4), folder + 'rep.gds.gz', 1e-9, 1e-3)
cells = list(masque.file.gdsii.readfile(folder + 'rep.gds.gz')[0].values())
masque.file.gdsii.writefile(cells, folder + 'rerep.gds.gz', 1e-9, 1e-3)
masque.file.dxf.writefile(pat4, folder + 'rep.dxf.gz')
dxf, info = masque.file.dxf.readfile(folder + 'rep.dxf.gz')
masque.file.dxf.writefile(dxf, folder + 'rerep.dxf.gz')
layer_map = {'base': (0,0), 'mylabel': (1,2)}
masque.file.oasis.writefile((pat, pat2, pat3, pat4), folder + 'rep.oas.gz', 1000, layer_map=layer_map)
oas, info = masque.file.oasis.readfile(folder + 'rep.oas.gz')
masque.file.oasis.writefile(list(oas.values()), folder + 'rerep.oas.gz', 1000, layer_map=layer_map)
print(info)
if __name__ == '__main__':
main()

View File

@ -8,12 +8,10 @@
`Pattern` is a basic object containing a 2D lithography mask, composed of a list of `Shape`
objects, a list of `Label` objects, and a list of references to other `Patterns` (using
`SubPattern` and `GridRepetition`).
`SubPattern`).
`SubPattern` provides basic support for nesting `Pattern` objects within each other, by adding
offset, rotation, scaling, and other such properties to a Pattern reference.
`GridRepetition` provides support for nesting regular arrays of `Pattern` objects.
offset, rotation, scaling, repetition, and other such properties to a Pattern reference.
Note that the methods for these classes try to avoid copying wherever possible, so unless
otherwise noted, assume that arguments are stored by-reference.
@ -31,8 +29,7 @@ import pathlib
from .error import PatternError, PatternLockedError
from .shapes import Shape
from .label import Label
from .subpattern import SubPattern, subpattern_t
from .repetition import GridRepetition
from .subpattern import SubPattern
from .pattern import Pattern

View File

@ -16,8 +16,9 @@ from numpy import pi
import ezdxf
from .utils import mangle_name, make_dose_table
from .. import Pattern, SubPattern, GridRepetition, PatternError, Label, Shape, subpattern_t
from .. import Pattern, SubPattern, PatternError, Label, Shape
from ..shapes import Polygon, Path
from ..repetition import Grid
from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar, layer_t
from ..utils import remove_colinear_vertices, normalize_mirror
@ -55,7 +56,7 @@ def write(pattern: Pattern,
If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()`
prior to calling this function.
Only `GridRepetition` objects with manhattan basis vectors are preserved as arrays. Since DXF
Only `Grid` repetition objects with manhattan basis vectors are preserved as arrays. Since DXF
rotations apply to basis vectors while `masque`'s rotations do not, the basis vectors of an
array with rotated instances must be manhattan _after_ having a compensating rotation applied.
@ -276,7 +277,7 @@ def _read_block(block, clean_vertices):
def _subpatterns_to_refs(block: Union[ezdxf.layouts.BlockLayout, ezdxf.layouts.Modelspace],
subpatterns: List[subpattern_t]):
subpatterns: List[SubPattern]):
for subpat in subpatterns:
if subpat.pattern is None:
continue
@ -289,9 +290,12 @@ def _subpatterns_to_refs(block: Union[ezdxf.layouts.BlockLayout, ezdxf.layouts.M
'rotation': rotation,
}
if isinstance(subpat, GridRepetition):
a = subpat.a_vector
b = subpat.b_vector if subpat.b_vector is not None else numpy.zeros(2)
rep = subpat.repetition
if rep is None:
block.add_blockref(encoded_name, subpat.offset, dxfattribs=attribs)
elif isinstance(rep, Grid):
a = rep.a_vector
b = rep.b_vector if rep.b_vector is not None else numpy.zeros(2)
rotated_a = rotation_matrix_2d(-subpat.rotation) @ a
rotated_b = rotation_matrix_2d(-subpat.rotation) @ b
if rotated_a[1] == 0 and rotated_b[0] == 0:
@ -310,11 +314,11 @@ def _subpatterns_to_refs(block: Union[ezdxf.layouts.BlockLayout, ezdxf.layouts.M
#NOTE: We could still do non-manhattan (but still orthogonal) grids by getting
# creative with counter-rotated nested patterns, but probably not worth it.
# Instead, just break appart the grid into individual elements:
for aa in numpy.arange(subpat.a_count):
for bb in numpy.arange(subpat.b_count):
block.add_blockref(encoded_name, subpat.offset + aa * a + bb * b, dxfattribs=attribs)
for dd in rep.displacements:
block.add_blockref(encoded_name, subpat.offset + dd, dxfattribs=attribs)
else:
block.add_blockref(encoded_name, subpat.offset, dxfattribs=attribs)
for dd in rep.displacements:
block.add_blockref(encoded_name, subpat.offset + dd, dxfattribs=attribs)
def _shapes_to_elements(block: Union[ezdxf.layouts.BlockLayout, ezdxf.layouts.Modelspace],

View File

@ -28,8 +28,9 @@ import gdsii.structure
import gdsii.elements
from .utils import mangle_name, make_dose_table, dose2dtype, dtype2dose
from .. import Pattern, SubPattern, GridRepetition, PatternError, Label, Shape, subpattern_t
from .. import Pattern, SubPattern, PatternError, Label, Shape
from ..shapes import Polygon, Path
from ..repetition import Grid
from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar, layer_t
from ..utils import remove_colinear_vertices, normalize_mirror
@ -291,11 +292,9 @@ def read(stream: io.BufferedIOBase,
string=element.string.decode('ASCII'))
pat.labels.append(label)
elif isinstance(element, gdsii.elements.SRef):
pat.subpatterns.append(_sref_to_subpat(element))
elif isinstance(element, gdsii.elements.ARef):
pat.subpatterns.append(_aref_to_gridrep(element))
elif (isinstance(element, gdsii.elements.SRef) or
isinstance(element, gdsii.elements.ARef)):
pat.subpatterns.append(_ref_to_subpat(element))
if use_dtype_as_dose:
logger.warning('use_dtype_as_dose will be removed in the future!')
@ -330,40 +329,11 @@ def _mlayer2gds(mlayer: layer_t) -> Tuple[int, int]:
return layer, data_type
def _sref_to_subpat(element: gdsii.elements.SRef) -> SubPattern:
def _ref_to_subpat(element: Union[gdsii.elements.SRef,
gdsii.elements.ARef]
) -> SubPattern:
"""
Helper function to create a SubPattern from an SREF. Sets subpat.pattern to None
and sets the instance .identifier to (struct_name,).
BUG:
"Absolute" means not affected by parent elements.
That's not currently supported by masque at all, so need to either tag it and
undo the parent transformations, or implement it in masque.
"""
subpat = SubPattern(pattern=None, offset=element.xy)
subpat.identifier = (element.struct_name,)
if element.strans is not None:
if element.mag is not None:
subpat.scale = element.mag
# Bit 13 means absolute scale
if get_bit(element.strans, 15 - 13):
#subpat.offset *= subpat.scale
raise PatternError('Absolute scale is not implemented yet!')
if element.angle is not None:
subpat.rotation = element.angle * numpy.pi / 180
# Bit 14 means absolute rotation
if get_bit(element.strans, 15 - 14):
#subpat.offset = numpy.dot(rotation_matrix_2d(subpat.rotation), subpat.offset)
raise PatternError('Absolute rotation is not implemented yet!')
# Bit 0 means mirror x-axis
if get_bit(element.strans, 15 - 0):
subpat.mirrored[0] = 1
return subpat
def _aref_to_gridrep(element: gdsii.elements.ARef) -> GridRepetition:
"""
Helper function to create a GridRepetition from an AREF. Sets gridrep.pattern to None
Helper function to create a SubPattern from an SREF or AREF. Sets subpat.pattern to None
and sets the instance .identifier to (struct_name,).
BUG:
@ -375,6 +345,7 @@ def _aref_to_gridrep(element: gdsii.elements.ARef) -> GridRepetition:
offset = numpy.array(element.xy[0])
scale = 1
mirror_across_x = False
repetition = None
if element.strans is not None:
if element.mag is not None:
@ -383,7 +354,7 @@ def _aref_to_gridrep(element: gdsii.elements.ARef) -> GridRepetition:
if get_bit(element.strans, 15 - 13):
raise PatternError('Absolute scale is not implemented yet!')
if element.angle is not None:
rotation = element.angle * numpy.pi / 180
rotation = numpy.deg2rad(element.angle)
# Bit 14 means absolute rotation
if get_bit(element.strans, 15 - 14):
raise PatternError('Absolute rotation is not implemented yet!')
@ -391,25 +362,24 @@ def _aref_to_gridrep(element: gdsii.elements.ARef) -> GridRepetition:
if get_bit(element.strans, 15 - 0):
mirror_across_x = True
counts = [element.cols, element.rows]
a_vector = (element.xy[1] - offset) / counts[0]
b_vector = (element.xy[2] - offset) / counts[1]
if isinstance(element, gdsii.elements.ARef):
a_count = element.cols
b_count = element.rows
a_vector = (element.xy[1] - offset) / counts[0]
b_vector = (element.xy[2] - offset) / counts[1]
repetition = Grid(a_vector=a_vector, b_vector=b_vector,
a_count=a_count, b_count=b_count)
gridrep = GridRepetition(pattern=None,
a_vector=a_vector,
b_vector=b_vector,
a_count=counts[0],
b_count=counts[1],
offset=offset,
rotation=rotation,
scale=scale,
mirrored=(mirror_across_x, False))
gridrep.identifier = (element.struct_name,)
return gridrep
subpat = SubPattern(pattern=None,
offset=offset,
rotation=rotation,
scale=scale,
mirrored=(mirror_across_x, False))
subpat.identifier = (element.struct_name,)
return subpat
def _subpatterns_to_refs(subpatterns: List[subpattern_t]
def _subpatterns_to_refs(subpatterns: List[SubPattern]
) -> List[Union[gdsii.elements.ARef, gdsii.elements.SRef]]:
refs = []
for subpat in subpatterns:
@ -420,26 +390,35 @@ def _subpatterns_to_refs(subpatterns: List[subpattern_t]
# Note: GDS mirrors first and rotates second
mirror_across_x, extra_angle = normalize_mirror(subpat.mirrored)
ref: Union[gdsii.elements.SRef, gdsii.elements.ARef]
if isinstance(subpat, GridRepetition):
rep = subpat.repetition
if isinstance(rep, Grid):
xy = numpy.array(subpat.offset) + [
[0, 0],
subpat.a_vector * subpat.a_count,
subpat.b_vector * subpat.b_count,
rep.a_vector * rep.a_count,
rep.b_vector * rep.b_count,
]
ref = gdsii.elements.ARef(struct_name=encoded_name,
xy=numpy.round(xy).astype(int),
cols=numpy.round(subpat.a_count).astype(int),
rows=numpy.round(subpat.b_count).astype(int))
else:
cols=numpy.round(rep.a_count).astype(int),
rows=numpy.round(rep.b_count).astype(int))
new_refs = [ref]
elif rep is None:
ref = gdsii.elements.SRef(struct_name=encoded_name,
xy=numpy.round([subpat.offset]).astype(int))
new_refs = [ref]
else:
new_refs = [gdsii.elements.SRef(struct_name=encoded_name,
xy=numpy.round([subpat.offset + dd]).astype(int))
for dd in rep.displacements]
ref.angle = ((subpat.rotation + extra_angle) * 180 / numpy.pi) % 360
# strans must be non-None for angle and mag to take effect
ref.strans = set_bit(0, 15 - 0, mirror_across_x)
ref.mag = subpat.scale
for ref in new_refs:
ref.angle = numpy.rad2deg(subpat.rotation + extra_angle) % 360
# strans must be non-None for angle and mag to take effect
ref.strans = set_bit(0, 15 - 0, mirror_across_x)
ref.mag = subpat.scale
refs.append(ref)
refs += new_refs
return refs

View File

@ -28,8 +28,9 @@ import fatamorgana.records as fatrec
from fatamorgana.basic import PathExtensionScheme
from .utils import mangle_name, make_dose_table
from .. import Pattern, SubPattern, GridRepetition, PatternError, Label, Shape, subpattern_t
from .. import Pattern, SubPattern, PatternError, Label, Shape
from ..shapes import Polygon, Path, Circle
from ..repetition import Grid, Arbitrary, Repetition
from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar, layer_t
from ..utils import remove_colinear_vertices, normalize_mirror
@ -221,7 +222,7 @@ def read(stream: io.BufferedIOBase,
"""
Read a OASIS file and translate it into a dict of Pattern objects. OASIS cells are
translated into Pattern objects; Polygons are translated into polygons, and Placements
are translated into SubPattern or GridRepetition objects.
are translated into SubPattern objects.
Additional library info is returned in a dict, containing:
'units_per_micrometer': number of database units per micrometer (all values are in database units)
@ -417,7 +418,7 @@ def read(stream: io.BufferedIOBase,
continue
for placement in cell.placements:
pat.subpatterns += _placement_to_subpats(placement)
pat.subpatterns.append(_placement_to_subpat(placement))
patterns.append(pat)
@ -451,7 +452,7 @@ def _mlayer2oas(mlayer: layer_t) -> Tuple[int, int]:
return layer, data_type
def _placement_to_subpats(placement: fatrec.Placement) -> List[subpattern_t]:
def _placement_to_subpat(placement: fatrec.Placement) -> SubPattern:
"""
Helper function to create a SubPattern from a placment. Sets subpat.pattern to None
and sets the instance .identifier to (struct_name,).
@ -468,27 +469,24 @@ def _placement_to_subpats(placement: fatrec.Placement) -> List[subpattern_t]:
'identifier': (name,),
}
subpats: List[subpattern_t]
mrep: Repetition
rep = placement.repetition
if isinstance(rep, fatamorgana.GridRepetition):
subpat = GridRepetition(a_vector=rep.a_vector,
b_vector=rep.b_vector,
a_count=rep.a_count,
b_count=rep.b_count,
offset=xy,
**args)
subpats = [subpat]
mrep = Grid(a_vector=rep.a_vector,
b_vector=rep.b_vector,
a_count=rep.a_count,
b_count=rep.b_count)
elif isinstance(rep, fatamorgana.ArbitraryRepetition):
subpats = []
for rep_offset in numpy.cumsum(numpy.column_stack((rep.x_displacements,
rep.y_displacements))):
subpats.append(SubPattern(offset=xy + rep_offset, **args))
mrep = Arbitrary(numpy.cumsum(numpy.column_stack((rep.x_displacements,
rep.y_displacements))))
elif rep is None:
subpats = [SubPattern(offset=xy, **args)]
return subpats
mrep = None
subpat = SubPattern(offset=xy, repetition=mrep, **args)
return subpat
def _subpatterns_to_refs(subpatterns: List[subpattern_t]
def _subpatterns_to_refs(subpatterns: List[SubPattern]
) -> List[fatrec.Placement]:
refs = []
for subpat in subpatterns:
@ -503,14 +501,21 @@ def _subpatterns_to_refs(subpatterns: List[subpattern_t]
'y': xy[1],
}
if isinstance(subpat, GridRepetition):
rep = subpat.repetition
if isinstance(rep, Grid):
args['repetition'] = fatamorgana.GridRepetition(
a_vector=numpy.round(subpat.a_vector).astype(int),
b_vector=numpy.round(subpat.b_vector).astype(int),
a_count=numpy.round(subpat.a_count).astype(int),
b_count=numpy.round(subpat.b_count).astype(int))
a_vector=numpy.round(rep.a_vector).astype(int),
b_vector=numpy.round(rep.b_vector).astype(int),
a_count=numpy.round(rep.a_count).astype(int),
b_count=numpy.round(rep.b_count).astype(int))
elif isinstance(rep, Arbitrary):
diffs = numpy.diff(rep.displacements, axis=0)
args['repetition'] = fatamorgana.ArbitraryRepetition(
numpy.round(diffs).astype(int))
else:
assert(rep is None)
angle = ((subpat.rotation + extra_angle) * 180 / numpy.pi) % 360
angle = numpy.rad2deg(subpat.rotation + extra_angle) % 360
ref = fatrec.Placement(
name=subpat.pattern.name,
flip=mirror_across_x,

View File

@ -4,20 +4,15 @@ import numpy
from numpy import pi
from .error import PatternError, PatternLockedError
from .utils import is_scalar, vector2, rotation_matrix_2d, layer_t
from .utils import is_scalar, vector2, rotation_matrix_2d, layer_t, AutoSlots
from .traits import PositionableImpl, LayerableImpl, Copyable, Pivotable, LockableImpl
class Label:
class Label(PositionableImpl, LayerableImpl, LockableImpl, Pivotable, Copyable, metaclass=AutoSlots):
"""
A text annotation with a position and layer (but no size; it is not drawn)
"""
__slots__ = ('_offset', '_layer', '_string', 'identifier', 'locked')
_offset: numpy.ndarray
""" [x_offset, y_offset] """
_layer: layer_t
""" Layer (integer >= 0, or 2-Tuple of integers) """
__slots__ = ( '_string', 'identifier')
_string: str
""" Label string """
@ -25,44 +20,9 @@ class Label:
identifier: Tuple
""" Arbitrary identifier tuple, useful for keeping track of history when flattening """
locked: bool
""" If `True`, any changes to the label will raise a `PatternLockedError` """
def __setattr__(self, name, value):
if self.locked and name != 'locked':
raise PatternLockedError()
object.__setattr__(self, name, value)
# ---- 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().astype(float)
# layer property
@property
def layer(self) -> layer_t:
"""
Layer number or name (int, tuple of ints, or string)
"""
return self._layer
@layer.setter
def layer(self, val: layer_t):
self._layer = val
'''
---- Properties
'''
# string property
@property
def string(self) -> str:
@ -100,25 +60,6 @@ class Label:
new.locked = self.locked
return new
def copy(self) -> 'Label':
"""
Returns a deep copy of the label.
"""
return copy.deepcopy(self)
def translate(self, offset: vector2) -> 'Label':
"""
Translate the label by the given offset
Args:
offset: [x_offset, y,offset]
Returns:
self
"""
self.offset += offset
return self
def rotate_around(self, pivot: vector2, rotation: float) -> 'Label':
"""
Rotate the label around a point.
@ -150,25 +91,13 @@ class Label:
return numpy.array([self.offset, self.offset])
def lock(self) -> 'Label':
"""
Lock the Label, causing any modifications to raise an exception.
Return:
self
"""
self.offset.flags.writeable = False
object.__setattr__(self, 'locked', True)
PositionableImpl._lock(self)
LockableImpl.lock(self)
return self
def unlock(self) -> 'Label':
"""
Unlock the Label, re-allowing changes.
Return:
self
"""
object.__setattr__(self, 'locked', False)
self.offset.flags.writeable = True
LockableImpl.unlock(self)
PositionableImpl._unlock(self)
return self
def __repr__(self) -> str:

View File

@ -13,8 +13,7 @@ import numpy
from numpy import inf
# .visualize imports matplotlib and matplotlib.collections
from .subpattern import SubPattern, subpattern_t
from .repetition import GridRepetition
from .subpattern import SubPattern
from .shapes import Shape, Polygon
from .label import Label
from .utils import rotation_matrix_2d, vector2, normalize_mirror
@ -27,8 +26,7 @@ visitor_function_t = Callable[['Pattern', Tuple['Pattern'], Dict, numpy.ndarray]
class Pattern:
"""
2D layout consisting of some set of shapes, labels, and references to other Pattern objects
(via SubPattern and GridRepetition). Shapes are assumed to inherit from
masque.shapes.Shape or provide equivalent functions.
(via SubPattern). Shapes are assumed to inherit from masque.shapes.Shape or provide equivalent functions.
"""
__slots__ = ('shapes', 'labels', 'subpatterns', 'name', 'locked')
@ -40,11 +38,10 @@ class Pattern:
labels: List[Label]
""" List of all labels in this Pattern. """
subpatterns: List[subpattern_t]
""" List of all objects referencing other patterns in this Pattern.
Examples are SubPattern (gdsii "instances") or GridRepetition (gdsii "arrays")
subpatterns: List[SubPattern]
""" List of all references to other patterns (`SubPattern`s) in this `Pattern`.
Multiple objects in this list may reference the same Pattern object
(multiple instances of the same object).
(i.e. multiple instances of the same object).
"""
name: str
@ -57,7 +54,7 @@ class Pattern:
name: str = '',
shapes: Sequence[Shape] = (),
labels: Sequence[Label] = (),
subpatterns: Sequence[subpattern_t] = (),
subpatterns: Sequence[SubPattern] = (),
locked: bool = False,
):
"""
@ -134,7 +131,7 @@ class Pattern:
def subset(self,
shapes_func: Callable[[Shape], bool] = None,
labels_func: Callable[[Label], bool] = None,
subpatterns_func: Callable[[subpattern_t], bool] = None,
subpatterns_func: Callable[[SubPattern], bool] = None,
recursive: bool = False,
) -> 'Pattern':
"""
@ -493,7 +490,7 @@ class Pattern:
def subpatterns_by_id(self,
include_none: bool = False,
recursive: bool = True,
) -> Dict[int, List[subpattern_t]]:
) -> Dict[int, List[SubPattern]]:
"""
Create a dictionary which maps `{id(referenced_pattern): [subpattern0, ...]}`
for all SubPattern objects referenced by this Pattern (by default, operates
@ -506,7 +503,7 @@ class Pattern:
Returns:
Dictionary mapping each pattern id to a list of subpattern objects referencing the pattern.
"""
ids: Dict[int, List[subpattern_t]] = defaultdict(list)
ids: Dict[int, List[SubPattern]] = defaultdict(list)
for subpat in self.subpatterns:
pat = subpat.pattern
if include_none or pat is not None:

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)

View File

@ -6,10 +6,10 @@ from numpy import pi
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS
from .. import PatternError
from ..utils import is_scalar, vector2, layer_t
from ..utils import is_scalar, vector2, layer_t, AutoSlots
class Arc(Shape):
class Arc(Shape, metaclass=AutoSlots):
"""
An elliptical arc, formed by cutting off an elliptical ring with two rays which exit from its
center. It has a position, two radii, a start and stop angle, a rotation, and a width.
@ -20,6 +20,7 @@ class Arc(Shape):
"""
__slots__ = ('_radii', '_angles', '_width', '_rotation',
'poly_num_points', 'poly_max_arclen')
_radii: numpy.ndarray
""" Two radii for defining an ellipse """

View File

@ -5,14 +5,15 @@ from numpy import pi
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS
from .. import PatternError
from ..utils import is_scalar, vector2, layer_t
from ..utils import is_scalar, vector2, layer_t, AutoSlots
class Circle(Shape):
class Circle(Shape, metaclass=AutoSlots):
"""
A circle, which has a position and radius.
"""
__slots__ = ('_radius', 'poly_num_points', 'poly_max_arclen')
_radius: float
""" Circle radius """

View File

@ -6,16 +6,17 @@ from numpy import pi
from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS
from .. import PatternError
from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t
from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t, AutoSlots
class Ellipse(Shape):
class Ellipse(Shape, metaclass=AutoSlots):
"""
An ellipse, which has a position, two radii, and a rotation.
The rotation gives the angle from x-axis, counterclockwise, to the first (x) radius.
"""
__slots__ = ('_radii', '_rotation',
'poly_num_points', 'poly_max_arclen')
_radii: numpy.ndarray
""" Ellipse radii """

View File

@ -6,7 +6,7 @@ from numpy import pi, inf
from . import Shape, normalized_shape_tuple, Polygon, Circle
from .. import PatternError
from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t
from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t, AutoSlots
from ..utils import remove_colinear_vertices, remove_duplicate_vertices
@ -18,7 +18,7 @@ class PathCap(Enum):
# defined by path.cap_extensions
class Path(Shape):
class Path(Shape, metaclass=AutoSlots):
"""
A path, consisting of a bunch of vertices (Nx2 ndarray), a width, an end-cap shape,
and an offset.

View File

@ -5,11 +5,11 @@ from numpy import pi
from . import Shape, normalized_shape_tuple
from .. import PatternError
from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t
from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t, AutoSlots
from ..utils import remove_colinear_vertices, remove_duplicate_vertices
class Polygon(Shape):
class Polygon(Shape, metaclass=AutoSlots):
"""
A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an
implicitly-closed boundary, and an offset.
@ -17,6 +17,7 @@ class Polygon(Shape):
A `normalized_form(...)` is available, but can be quite slow with lots of vertices.
"""
__slots__ = ('_vertices',)
_vertices: numpy.ndarray
""" Nx2 ndarray of vertices `[[x0, y0], [x1, y1], ...]` """

View File

@ -5,6 +5,9 @@ import numpy
from ..error import PatternError, PatternLockedError
from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t
from ..traits import (PositionableImpl, LayerableImpl, DoseableImpl,
Rotatable, Mirrorable, Copyable, Scalable,
PivotableImpl, LockableImpl)
if TYPE_CHECKING:
from . import Polygon
@ -23,38 +26,20 @@ DEFAULT_POLY_NUM_POINTS = 24
T = TypeVar('T', bound='Shape')
class Shape(metaclass=ABCMeta):
class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable, Copyable, Scalable, PivotableImpl, LockableImpl, metaclass=ABCMeta):
"""
Abstract class specifying functions common to all shapes.
"""
__slots__ = ('_offset', '_layer', '_dose', 'identifier', 'locked')
_offset: numpy.ndarray
""" `[x_offset, y_offset]` """
_layer: layer_t
""" Layer (integer >= 0 or tuple) """
_dose: float
""" Dose """
identifier: Tuple
""" An arbitrary identifier for the shape, usually empty but used by `Pattern.flatten()` """
locked: bool
""" If `True`, any changes to the shape will raise a `PatternLockedError` """
def __setattr__(self, name, value):
if self.locked and name != 'locked':
raise PatternLockedError()
object.__setattr__(self, name, value)
def __copy__(self) -> 'Shape':
cls = self.__class__
new = cls.__new__(cls)
for name in Shape.__slots__ + self.__slots__:
object.__setattr__(new, name, getattr(self, name))
return new
# def __copy__(self) -> 'Shape':
# cls = self.__class__
# new = cls.__new__(cls)
# for name in Shape.__slots__ + self.__slots__:
# object.__setattr__(new, name, getattr(self, name))
# return new
'''
--- Abstract methods
@ -79,53 +64,6 @@ class Shape(metaclass=ABCMeta):
"""
pass
@abstractmethod
def get_bounds(self) -> numpy.ndarray:
"""
Returns `[[x_min, y_min], [x_max, y_max]]` which specify a minimal bounding box for the shape.
"""
pass
@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
@abstractmethod
def mirror(self: T, axis: int) -> T:
"""
Mirror the shape across an axis.
Args:
axis: Axis to mirror across.
(0: mirror across x axis, 1: mirror across y axis)
Returns:
self
"""
pass
@abstractmethod
def scale_by(self: T, c: float) -> T:
"""
Scale the shape's size (eg. radius, for a circle) by a constant factor.
Args:
c: Factor to scale by
Returns:
self
"""
pass
@abstractmethod
def normalized_form(self: T, norm_value: int) -> normalized_shape_tuple:
"""
@ -150,97 +88,9 @@ class Shape(metaclass=ABCMeta):
"""
pass
'''
---- Non-abstract 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()
# layer property
@property
def layer(self) -> layer_t:
"""
Layer number or name (int, tuple of ints, or string)
"""
return self._layer
@layer.setter
def layer(self, val: layer_t):
self._layer = val
# dose property
@property
def dose(self) -> float:
"""
Dose (float >= 0)
"""
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 copy(self: T) -> T:
"""
Returns a deep copy of the shape.
Returns:
copy.deepcopy(self)
"""
return copy.deepcopy(self)
def translate(self: T, offset: vector2) -> T:
"""
Translate the shape by the given offset
Args:
offset: [x_offset, y,offset]
Returns:
self
"""
self.offset += offset
return self
def rotate_around(self: T, pivot: vector2, rotation: float) -> T:
"""
Rotate the shape 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.rotate(rotation)
self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset)
self.translate(+pivot)
return self
def manhattanize_fast(self,
grid_x: numpy.ndarray,
grid_y: numpy.ndarray,
@ -442,37 +292,12 @@ class Shape(metaclass=ABCMeta):
return manhattan_polygons
def set_layer(self: T, layer: layer_t) -> T:
"""
Chainable method for changing the layer.
Args:
layer: new value for self.layer
Returns:
self
"""
self.layer = layer
return self
def lock(self: T) -> T:
"""
Lock the Shape, disallowing further changes
Returns:
self
"""
self.offset.flags.writeable = False
object.__setattr__(self, 'locked', True)
PositionableImpl._lock(self)
LockableImpl.lock(self)
return self
def unlock(self: T) -> T:
"""
Unlock the Shape
Returns:
self
"""
object.__setattr__(self, 'locked', False)
self.offset.flags.writeable = True
LockableImpl.unlock(self)
PositionableImpl._unlock(self)
return self

View File

@ -5,22 +5,23 @@ from numpy import pi, inf
from . import Shape, Polygon, normalized_shape_tuple
from .. import PatternError
from ..utils import is_scalar, vector2, get_bit, normalize_mirror, layer_t
from ..traits import RotatableImpl
from ..utils import is_scalar, vector2, get_bit, normalize_mirror, layer_t, AutoSlots
# Loaded on use:
# from freetype import Face
# from matplotlib.path import Path
class Text(Shape):
class Text(RotatableImpl, Shape, metaclass=AutoSlots):
"""
Text (to be printed e.g. as a set of polygons).
This is distinct from non-printed Label objects.
"""
__slots__ = ('_string', '_height', '_rotation', '_mirrored', 'font_path')
__slots__ = ('_string', '_height', '_mirrored', 'font_path')
_string: str
_height: float
_rotation: float
_mirrored: numpy.ndarray #ndarray[bool]
font_path: str
@ -33,17 +34,6 @@ class Text(Shape):
def string(self, val: str):
self._string = val
# Rotation property
@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)
# Height property
@property
def height(self) -> float:
@ -120,10 +110,6 @@ class Text(Shape):
return all_polygons
def rotate(self, theta: float) -> 'Text':
self.rotation += theta
return self
def mirror(self, axis: int) -> 'Text':
self.mirrored[axis] = not self.mirrored[axis]
return self

View File

@ -11,51 +11,50 @@ import numpy
from numpy import pi
from .error import PatternError, PatternLockedError
from .utils import is_scalar, rotation_matrix_2d, vector2
from .repetition import GridRepetition
from .utils import is_scalar, rotation_matrix_2d, vector2, AutoSlots
from .repetition import Repetition
from .traits import (PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl,
Mirrorable, Pivotable, Copyable, LockableImpl, RepeatableImpl)
if TYPE_CHECKING:
from . import Pattern
class SubPattern:
class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mirrorable,
Pivotable, Copyable, RepeatableImpl, LockableImpl, metaclass=AutoSlots):
"""
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 """
# _offset: numpy.ndarray
# """ (x, y) offset for the instance """
_rotation: float
""" rotation for the instance, radians counterclockwise """
# _rotation: float
# """ rotation for the instance, radians counterclockwise """
_dose: float
""" dose factor for the instance """
# _dose: float
# """ dose factor for the instance """
_scale: float
""" scale 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. """
""" Whether to mirror the instance across the x and/or y axes. """
identifier: Tuple[Any, ...]
""" Arbitrary identifier, used internally by some `masque` functions. """
locked: bool
""" If `True`, disallows changes to the GridRepetition """
# locked: bool
# """ If `True`, disallows changes to the SubPattern"""
def __init__(self,
pattern: Optional['Pattern'],
@ -64,6 +63,7 @@ class SubPattern:
mirrored: Optional[Sequence[bool]] = None,
dose: float = 1.0,
scale: float = 1.0,
repetition: Optional[Repetition] = None,
locked: bool = False,
identifier: Tuple[Any, ...] = ()):
"""
@ -74,10 +74,12 @@ class SubPattern:
mirrored: Whether to mirror the referenced pattern across its x and y axes.
dose: Scaling factor applied to the dose.
scale: Scaling factor applied to the pattern's geometry.
repetition: TODO
locked: Whether the `SubPattern` is locked after initialization.
identifier: Arbitrary tuple, used internally by some `masque` functions.
"""
object.__setattr__(self, 'locked', False)
LockableImpl.unlock(self)
# object.__setattr__(self, 'locked', False)
self.identifier = identifier
self.pattern = pattern
self.offset = offset
@ -87,13 +89,9 @@ class SubPattern:
if mirrored is None:
mirrored = [False, False]
self.mirrored = mirrored
self.repetition = repetition
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(),
@ -123,57 +121,6 @@ class SubPattern:
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]
@ -198,52 +145,17 @@ class SubPattern:
pattern.rotate_around((0.0, 0.0), self.rotation)
pattern.translate_elements(self.offset)
pattern.scale_element_doses(self.dose)
if pattern.repetition is not None:
combined = type(pat)(name='__repetition__')
for dd in pattern.repetition.displacements:
temp_pat = pattern.deepcopy()
temp_pat.translate_elements(dd)
combined.append(temp_pat)
pattern = combined
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.
@ -271,37 +183,6 @@ class SubPattern:
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
@ -309,9 +190,9 @@ class SubPattern:
Returns:
self
"""
self.offset.flags.writeable = False
self.mirrored.flags.writeable = False
object.__setattr__(self, 'locked', True)
PositionableImpl._lock(self)
LockableImpl.lock(self)
return self
def unlock(self) -> 'SubPattern':
@ -321,9 +202,9 @@ class SubPattern:
Returns:
self
"""
self.offset.flags.writeable = True
LockableImpl.unlock(self)
PositionableImpl._unlock(self)
self.mirrored.flags.writeable = True
object.__setattr__(self, 'locked', False)
return self
def deeplock(self) -> 'SubPattern':
@ -361,6 +242,3 @@ class SubPattern:
dose = f' d{self.dose:g}' if self.dose != 1 else ''
locked = ' L' if self.locked else ''
return f'<SubPattern "{name}" at {self.offset}{rotation}{scale}{mirrored}{dose}{locked}>'
subpattern_t = Union[SubPattern, GridRepetition]

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

View File

@ -3,6 +3,7 @@ Various helper functions
"""
from typing import Any, Union, Tuple, Sequence
from abc import ABCMeta
import numpy
@ -133,3 +134,25 @@ def remove_colinear_vertices(vertices: numpy.ndarray, closed_path: bool = True)
slopes_equal[[0, -1]] = False
return vertices[~slopes_equal]
class AutoSlots(ABCMeta):
"""
Metaclass for automatically generating __slots__ based on superclass type annotations.
Superclasses must set `__slots__ = ()` to make this work properly.
This is a workaround for the fact that non-empty `__slots__` can't be used
with multiple inheritance. Since we only use multiple inheritance with abstract
classes, they can have empty `__slots__` and their attribute type annotations
can be used to generate a full `__slots__` for the concrete class.
"""
def __new__(cls, name, bases, dctn):
slots = tuple(dctn.get('__slots__', tuple()))
for base in bases:
if not hasattr(base, '__annotations__'):
continue
slots += tuple(getattr(base, '__annotations__').keys())
dctn['__slots__'] = slots
return super().__new__(cls,name,bases,dctn)