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` `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 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 `SubPattern` provides basic support for nesting `Pattern` objects within each other, by adding
offset, rotation, scaling, and other such properties to a Pattern reference. offset, rotation, scaling, repetition, and other such properties to a Pattern reference.
`GridRepetition` provides support for nesting regular arrays of `Pattern` objects.
Note that the methods for these classes try to avoid copying wherever possible, so unless Note that the methods for these classes try to avoid copying wherever possible, so unless
otherwise noted, assume that arguments are stored by-reference. otherwise noted, assume that arguments are stored by-reference.
@ -31,8 +29,7 @@ import pathlib
from .error import PatternError, PatternLockedError from .error import PatternError, PatternLockedError
from .shapes import Shape from .shapes import Shape
from .label import Label from .label import Label
from .subpattern import SubPattern, subpattern_t from .subpattern import SubPattern
from .repetition import GridRepetition
from .pattern import Pattern from .pattern import Pattern

View File

@ -16,8 +16,9 @@ from numpy import pi
import ezdxf import ezdxf
from .utils import mangle_name, make_dose_table 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 ..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 rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar, layer_t
from ..utils import remove_colinear_vertices, normalize_mirror 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()` If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()`
prior to calling this function. 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 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. 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], def _subpatterns_to_refs(block: Union[ezdxf.layouts.BlockLayout, ezdxf.layouts.Modelspace],
subpatterns: List[subpattern_t]): subpatterns: List[SubPattern]):
for subpat in subpatterns: for subpat in subpatterns:
if subpat.pattern is None: if subpat.pattern is None:
continue continue
@ -289,9 +290,12 @@ def _subpatterns_to_refs(block: Union[ezdxf.layouts.BlockLayout, ezdxf.layouts.M
'rotation': rotation, 'rotation': rotation,
} }
if isinstance(subpat, GridRepetition): rep = subpat.repetition
a = subpat.a_vector if rep is None:
b = subpat.b_vector if subpat.b_vector is not None else numpy.zeros(2) 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_a = rotation_matrix_2d(-subpat.rotation) @ a
rotated_b = rotation_matrix_2d(-subpat.rotation) @ b rotated_b = rotation_matrix_2d(-subpat.rotation) @ b
if rotated_a[1] == 0 and rotated_b[0] == 0: 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 #NOTE: We could still do non-manhattan (but still orthogonal) grids by getting
# creative with counter-rotated nested patterns, but probably not worth it. # creative with counter-rotated nested patterns, but probably not worth it.
# Instead, just break appart the grid into individual elements: # Instead, just break appart the grid into individual elements:
for aa in numpy.arange(subpat.a_count): for dd in rep.displacements:
for bb in numpy.arange(subpat.b_count): block.add_blockref(encoded_name, subpat.offset + dd, dxfattribs=attribs)
block.add_blockref(encoded_name, subpat.offset + aa * a + bb * b, dxfattribs=attribs)
else: 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], def _shapes_to_elements(block: Union[ezdxf.layouts.BlockLayout, ezdxf.layouts.Modelspace],

View File

@ -28,8 +28,9 @@ import gdsii.structure
import gdsii.elements import gdsii.elements
from .utils import mangle_name, make_dose_table, dose2dtype, dtype2dose 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 ..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 rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar, layer_t
from ..utils import remove_colinear_vertices, normalize_mirror from ..utils import remove_colinear_vertices, normalize_mirror
@ -291,11 +292,9 @@ def read(stream: io.BufferedIOBase,
string=element.string.decode('ASCII')) string=element.string.decode('ASCII'))
pat.labels.append(label) pat.labels.append(label)
elif isinstance(element, gdsii.elements.SRef): elif (isinstance(element, gdsii.elements.SRef) or
pat.subpatterns.append(_sref_to_subpat(element)) isinstance(element, gdsii.elements.ARef)):
pat.subpatterns.append(_ref_to_subpat(element))
elif isinstance(element, gdsii.elements.ARef):
pat.subpatterns.append(_aref_to_gridrep(element))
if use_dtype_as_dose: if use_dtype_as_dose:
logger.warning('use_dtype_as_dose will be removed in the future!') 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 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 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:
"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
and sets the instance .identifier to (struct_name,). and sets the instance .identifier to (struct_name,).
BUG: BUG:
@ -375,6 +345,7 @@ def _aref_to_gridrep(element: gdsii.elements.ARef) -> GridRepetition:
offset = numpy.array(element.xy[0]) offset = numpy.array(element.xy[0])
scale = 1 scale = 1
mirror_across_x = False mirror_across_x = False
repetition = None
if element.strans is not None: if element.strans is not None:
if element.mag 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): if get_bit(element.strans, 15 - 13):
raise PatternError('Absolute scale is not implemented yet!') raise PatternError('Absolute scale is not implemented yet!')
if element.angle is not None: if element.angle is not None:
rotation = element.angle * numpy.pi / 180 rotation = numpy.deg2rad(element.angle)
# Bit 14 means absolute rotation # Bit 14 means absolute rotation
if get_bit(element.strans, 15 - 14): if get_bit(element.strans, 15 - 14):
raise PatternError('Absolute rotation is not implemented yet!') 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): if get_bit(element.strans, 15 - 0):
mirror_across_x = True mirror_across_x = True
counts = [element.cols, element.rows] if isinstance(element, gdsii.elements.ARef):
a_count = element.cols
b_count = element.rows
a_vector = (element.xy[1] - offset) / counts[0] a_vector = (element.xy[1] - offset) / counts[0]
b_vector = (element.xy[2] - offset) / counts[1] 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, subpat = SubPattern(pattern=None,
a_vector=a_vector,
b_vector=b_vector,
a_count=counts[0],
b_count=counts[1],
offset=offset, offset=offset,
rotation=rotation, rotation=rotation,
scale=scale, scale=scale,
mirrored=(mirror_across_x, False)) mirrored=(mirror_across_x, False))
gridrep.identifier = (element.struct_name,) subpat.identifier = (element.struct_name,)
return subpat
return gridrep
def _subpatterns_to_refs(subpatterns: List[subpattern_t] def _subpatterns_to_refs(subpatterns: List[SubPattern]
) -> List[Union[gdsii.elements.ARef, gdsii.elements.SRef]]: ) -> List[Union[gdsii.elements.ARef, gdsii.elements.SRef]]:
refs = [] refs = []
for subpat in subpatterns: for subpat in subpatterns:
@ -420,26 +390,35 @@ def _subpatterns_to_refs(subpatterns: List[subpattern_t]
# Note: GDS mirrors first and rotates second # Note: GDS mirrors first and rotates second
mirror_across_x, extra_angle = normalize_mirror(subpat.mirrored) mirror_across_x, extra_angle = normalize_mirror(subpat.mirrored)
ref: Union[gdsii.elements.SRef, gdsii.elements.ARef] ref: Union[gdsii.elements.SRef, gdsii.elements.ARef]
if isinstance(subpat, GridRepetition):
rep = subpat.repetition
if isinstance(rep, Grid):
xy = numpy.array(subpat.offset) + [ xy = numpy.array(subpat.offset) + [
[0, 0], [0, 0],
subpat.a_vector * subpat.a_count, rep.a_vector * rep.a_count,
subpat.b_vector * subpat.b_count, rep.b_vector * rep.b_count,
] ]
ref = gdsii.elements.ARef(struct_name=encoded_name, ref = gdsii.elements.ARef(struct_name=encoded_name,
xy=numpy.round(xy).astype(int), xy=numpy.round(xy).astype(int),
cols=numpy.round(subpat.a_count).astype(int), cols=numpy.round(rep.a_count).astype(int),
rows=numpy.round(subpat.b_count).astype(int)) rows=numpy.round(rep.b_count).astype(int))
else: new_refs = [ref]
elif rep is None:
ref = gdsii.elements.SRef(struct_name=encoded_name, ref = gdsii.elements.SRef(struct_name=encoded_name,
xy=numpy.round([subpat.offset]).astype(int)) 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 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 # strans must be non-None for angle and mag to take effect
ref.strans = set_bit(0, 15 - 0, mirror_across_x) ref.strans = set_bit(0, 15 - 0, mirror_across_x)
ref.mag = subpat.scale ref.mag = subpat.scale
refs.append(ref) refs += new_refs
return refs return refs

View File

@ -28,8 +28,9 @@ import fatamorgana.records as fatrec
from fatamorgana.basic import PathExtensionScheme from fatamorgana.basic import PathExtensionScheme
from .utils import mangle_name, make_dose_table 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 ..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 rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar, layer_t
from ..utils import remove_colinear_vertices, normalize_mirror 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 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 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: Additional library info is returned in a dict, containing:
'units_per_micrometer': number of database units per micrometer (all values are in database units) '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 continue
for placement in cell.placements: for placement in cell.placements:
pat.subpatterns += _placement_to_subpats(placement) pat.subpatterns.append(_placement_to_subpat(placement))
patterns.append(pat) patterns.append(pat)
@ -451,7 +452,7 @@ def _mlayer2oas(mlayer: layer_t) -> Tuple[int, int]:
return layer, data_type 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 Helper function to create a SubPattern from a placment. Sets subpat.pattern to None
and sets the instance .identifier to (struct_name,). and sets the instance .identifier to (struct_name,).
@ -468,27 +469,24 @@ def _placement_to_subpats(placement: fatrec.Placement) -> List[subpattern_t]:
'identifier': (name,), 'identifier': (name,),
} }
subpats: List[subpattern_t] mrep: Repetition
rep = placement.repetition rep = placement.repetition
if isinstance(rep, fatamorgana.GridRepetition): if isinstance(rep, fatamorgana.GridRepetition):
subpat = GridRepetition(a_vector=rep.a_vector, mrep = Grid(a_vector=rep.a_vector,
b_vector=rep.b_vector, b_vector=rep.b_vector,
a_count=rep.a_count, a_count=rep.a_count,
b_count=rep.b_count, b_count=rep.b_count)
offset=xy,
**args)
subpats = [subpat]
elif isinstance(rep, fatamorgana.ArbitraryRepetition): elif isinstance(rep, fatamorgana.ArbitraryRepetition):
subpats = [] mrep = Arbitrary(numpy.cumsum(numpy.column_stack((rep.x_displacements,
for rep_offset in numpy.cumsum(numpy.column_stack((rep.x_displacements, rep.y_displacements))))
rep.y_displacements))):
subpats.append(SubPattern(offset=xy + rep_offset, **args))
elif rep is None: elif rep is None:
subpats = [SubPattern(offset=xy, **args)] mrep = None
return subpats
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]: ) -> List[fatrec.Placement]:
refs = [] refs = []
for subpat in subpatterns: for subpat in subpatterns:
@ -503,14 +501,21 @@ def _subpatterns_to_refs(subpatterns: List[subpattern_t]
'y': xy[1], 'y': xy[1],
} }
if isinstance(subpat, GridRepetition): rep = subpat.repetition
if isinstance(rep, Grid):
args['repetition'] = fatamorgana.GridRepetition( args['repetition'] = fatamorgana.GridRepetition(
a_vector=numpy.round(subpat.a_vector).astype(int), a_vector=numpy.round(rep.a_vector).astype(int),
b_vector=numpy.round(subpat.b_vector).astype(int), b_vector=numpy.round(rep.b_vector).astype(int),
a_count=numpy.round(subpat.a_count).astype(int), a_count=numpy.round(rep.a_count).astype(int),
b_count=numpy.round(subpat.b_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( ref = fatrec.Placement(
name=subpat.pattern.name, name=subpat.pattern.name,
flip=mirror_across_x, flip=mirror_across_x,

View File

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

View File

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

View File

@ -1,78 +1,47 @@
""" """
Repetitions provides support for efficiently nesting multiple identical Repetitions provide support for efficiently representing multiple identical
instances of a Pattern in the same parent Pattern. instances of an object .
""" """
from typing import Union, List, Dict, Tuple, Optional, Sequence, TYPE_CHECKING, Any from typing import Union, List, Dict, Tuple, Optional, Sequence, TYPE_CHECKING, Any
import copy import copy
from abc import ABCMeta, abstractmethod
import numpy import numpy
from numpy import pi
from .error import PatternError, PatternLockedError from .error import PatternError, PatternLockedError
from .utils import is_scalar, rotation_matrix_2d, vector2 from .utils import rotation_matrix_2d, vector2, AutoSlots
from .traits import LockableImpl, Copyable, Scalable, Rotatable, Mirrorable
if TYPE_CHECKING:
from . import Pattern
# TODO need top-level comment about what order rotation/scale/offset/mirror/array are applied class Repetition(Copyable, Rotatable, Mirrorable, Scalable, metaclass=ABCMeta):
class GridRepetition:
""" """
GridRepetition provides support for efficiently embedding multiple copies of a `Pattern` Interface common to all objects which specify repetitions
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).
""" """
__slots__ = ('_pattern', __slots__ = ()
'_offset',
'_rotation', @property
'_dose', @abstractmethod
'_scale', def displacements(self) -> numpy.ndarray:
'_mirrored', """
'_a_vector', 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', '_b_vector',
'_a_count', '_a_count',
'_b_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)
"""
_a_vector: numpy.ndarray _a_vector: numpy.ndarray
""" Vector `[x, y]` specifying the first lattice vector of the grid. """ Vector `[x, y]` specifying the first lattice vector of the grid.
@ -91,28 +60,14 @@ class GridRepetition:
_b_count: int _b_count: int
""" Number of instances along the direction specified by the `b_vector` """ """ 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, def __init__(self,
pattern: Optional['Pattern'],
a_vector: numpy.ndarray, a_vector: numpy.ndarray,
a_count: int, a_count: int,
b_vector: Optional[numpy.ndarray] = None, b_vector: Optional[numpy.ndarray] = None,
b_count: Optional[int] = 1, b_count: Optional[int] = 1,
offset: vector2 = (0.0, 0.0), locked: bool = False,):
rotation: float = 0.0,
mirrored: Optional[Sequence[bool]] = None,
dose: float = 1.0,
scale: float = 1.0,
locked: bool = False,
identifier: Tuple[Any, ...] = ()):
""" """
Args: Args:
pattern: Pattern to reference.
a_vector: First lattice vector, of the form `[x, y]`. a_vector: First lattice vector, of the form `[x, y]`.
Specifies center-to-center spacing between adjacent instances. Specifies center-to-center spacing between adjacent instances.
a_count: Number of elements in the a_vector direction. a_count: Number of elements in the a_vector direction.
@ -121,14 +76,7 @@ class GridRepetition:
Can be omitted when specifying a 1D array. Can be omitted when specifying a 1D array.
b_count: Number of elements in the `b_vector` direction. b_count: Number of elements in the `b_vector` direction.
Should be omitted if `b_vector` was omitted. Should be omitted if `b_vector` was omitted.
offset: (x, y) offset applied to all instances. locked: Whether the `Grid` is locked after initialization.
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.
Raises: Raises:
PatternError if `b_*` inputs conflict with each other PatternError if `b_*` inputs conflict with each other
@ -144,132 +92,31 @@ class GridRepetition:
b_vector = numpy.array([0.0, 0.0]) b_vector = numpy.array([0.0, 0.0])
if a_count < 1: if a_count < 1:
raise PatternError('Repetition has too-small a_count: ' raise PatternError(f'Repetition has too-small a_count: {a_count}')
'{}'.format(a_count))
if b_count < 1: if b_count < 1:
raise PatternError('Repetition has too-small b_count: ' raise PatternError(f'Repetition has too-small b_count: {b_count}')
'{}'.format(b_count))
object.__setattr__(self, 'locked', False) object.__setattr__(self, 'locked', False)
self.a_vector = a_vector self.a_vector = a_vector
self.b_vector = b_vector self.b_vector = b_vector
self.a_count = a_count self.a_count = a_count
self.b_count = b_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 self.locked = locked
def __setattr__(self, name, value): def __copy__(self) -> 'Grid':
if self.locked and name != 'locked': new = Grid(a_vector=self.a_vector.copy(),
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), b_vector=copy.copy(self.b_vector),
a_count=self.a_count, a_count=self.a_count,
b_count=self.b_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) locked=self.locked)
return new return new
def __deepcopy__(self, memo: Dict = None) -> 'GridRepetition': def __deepcopy__(self, memo: Dict = None) -> 'Grid':
memo = {} if memo is None else memo memo = {} if memo is None else memo
new = copy.copy(self).unlock() new = copy.copy(self).unlock()
new.pattern = copy.deepcopy(self.pattern, memo)
new.locked = self.locked new.locked = self.locked
return new 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 # a_vector property
@property @property
def a_vector(self) -> numpy.ndarray: def a_vector(self) -> numpy.ndarray:
@ -320,69 +167,15 @@ class GridRepetition:
raise PatternError('b_count must be convertable to an int!') raise PatternError('b_count must be convertable to an int!')
self._b_count = int(val) 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. Rotate lattice vectors (around (0, 0))
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)
Args: Args:
rotation: Angle to rotate by (counterclockwise, radians) rotation: Angle to rotate by (counterclockwise, radians)
@ -390,28 +183,14 @@ class GridRepetition:
Returns: Returns:
self self
""" """
self.rotate_elements(rotation)
self.a_vector = numpy.dot(rotation_matrix_2d(rotation), self.a_vector) self.a_vector = numpy.dot(rotation_matrix_2d(rotation), self.a_vector)
if self.b_vector is not None: if self.b_vector is not None:
self.b_vector = numpy.dot(rotation_matrix_2d(rotation), self.b_vector) self.b_vector = numpy.dot(rotation_matrix_2d(rotation), self.b_vector)
return self return self
def rotate_elements(self, rotation: float) -> 'GridRepetition': def mirror(self, axis: int) -> 'Grid':
""" """
Rotate each element around its origin Mirror the Grid across an axis.
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.
Args: Args:
axis: Axis to mirror across. axis: Axis to mirror across.
@ -420,43 +199,30 @@ class GridRepetition:
Returns: Returns:
self self
""" """
self.mirror_elements(axis)
self.a_vector[1-axis] *= -1 self.a_vector[1-axis] *= -1
if self.b_vector is not None: if self.b_vector is not None:
self.b_vector[1-axis] *= -1 self.b_vector[1-axis] *= -1
return self 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]: def get_bounds(self) -> Optional[numpy.ndarray]:
""" """
Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the
extent of the `GridRepetition` in each dimension. extent of the `Grid` in each dimension.
Returns `None` if the contained `Pattern` is empty.
Returns: Returns:
`[[x_min, y_min], [x_max, y_max]]` or `None` `[[x_min, y_min], [x_max, y_max]]` or `None`
""" """
if self.pattern is None: a_extent = self.a_vector * self.a_count
return None b_extent = self.b_vector * self.b_count if self.b_count != 0 else 0
return self.as_pattern().get_bounds()
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: Args:
c: scaling factor c: scaling factor
@ -464,107 +230,116 @@ class GridRepetition:
Returns: Returns:
self self
""" """
self.scale_elements_by(c)
self.a_vector *= c self.a_vector *= c
if self.b_vector is not None: if self.b_vector is not None:
self.b_vector *= c self.b_vector *= c
return self return self
def scale_elements_by(self, c: float) -> 'GridRepetition': def lock(self) -> 'Grid':
""" """
Scale each element by a factor Lock the `Grid`, disallowing changes.
Args:
c: scaling factor
Returns: Returns:
self 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.a_vector.flags.writeable = False
self.mirrored.flags.writeable = False
if self.b_vector is not None: if self.b_vector is not None:
self.b_vector.flags.writeable = False self.b_vector.flags.writeable = False
object.__setattr__(self, 'locked', True) LockableImpl.lock(self)
return self return self
def unlock(self) -> 'GridRepetition': def unlock(self) -> 'Grid':
""" """
Unlock the `GridRepetition` Unlock the `Grid`
Returns: Returns:
self self
""" """
self.offset.flags.writeable = True
self.a_vector.flags.writeable = True self.a_vector.flags.writeable = True
self.mirrored.flags.writeable = True
if self.b_vector is not None: if self.b_vector is not None:
self.b_vector.flags.writeable = True self.b_vector.flags.writeable = True
object.__setattr__(self, 'locked', False) LockableImpl.unlock(self)
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()
return self return self
def __repr__(self) -> str: 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 '' locked = ' L' if self.locked else ''
bv = f', {self.b_vector}' if self.b_vector is not None 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}' return (f'<Grid {self.a_count}x{self.b_count} ({self.a_vector}{bv}){locked}>')
f' {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 Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS
from .. import PatternError 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 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. 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', __slots__ = ('_radii', '_angles', '_width', '_rotation',
'poly_num_points', 'poly_max_arclen') 'poly_num_points', 'poly_max_arclen')
_radii: numpy.ndarray _radii: numpy.ndarray
""" Two radii for defining an ellipse """ """ 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 Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS
from .. import PatternError 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. A circle, which has a position and radius.
""" """
__slots__ = ('_radius', 'poly_num_points', 'poly_max_arclen') __slots__ = ('_radius', 'poly_num_points', 'poly_max_arclen')
_radius: float _radius: float
""" Circle radius """ """ 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 Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS
from .. import PatternError 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. 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. The rotation gives the angle from x-axis, counterclockwise, to the first (x) radius.
""" """
__slots__ = ('_radii', '_rotation', __slots__ = ('_radii', '_rotation',
'poly_num_points', 'poly_max_arclen') 'poly_num_points', 'poly_max_arclen')
_radii: numpy.ndarray _radii: numpy.ndarray
""" Ellipse radii """ """ Ellipse radii """

View File

@ -6,7 +6,7 @@ from numpy import pi, inf
from . import Shape, normalized_shape_tuple, Polygon, Circle from . import Shape, normalized_shape_tuple, Polygon, Circle
from .. import PatternError 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 from ..utils import remove_colinear_vertices, remove_duplicate_vertices
@ -18,7 +18,7 @@ class PathCap(Enum):
# defined by path.cap_extensions # 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, A path, consisting of a bunch of vertices (Nx2 ndarray), a width, an end-cap shape,
and an offset. and an offset.

View File

@ -5,11 +5,11 @@ from numpy import pi
from . import Shape, normalized_shape_tuple from . import Shape, normalized_shape_tuple
from .. import PatternError 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 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 A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an
implicitly-closed boundary, and an offset. 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. A `normalized_form(...)` is available, but can be quite slow with lots of vertices.
""" """
__slots__ = ('_vertices',) __slots__ = ('_vertices',)
_vertices: numpy.ndarray _vertices: numpy.ndarray
""" Nx2 ndarray of vertices `[[x0, y0], [x1, y1], ...]` """ """ Nx2 ndarray of vertices `[[x0, y0], [x1, y1], ...]` """

View File

@ -5,6 +5,9 @@ import numpy
from ..error import PatternError, PatternLockedError from ..error import PatternError, PatternLockedError
from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t 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: if TYPE_CHECKING:
from . import Polygon from . import Polygon
@ -23,38 +26,20 @@ DEFAULT_POLY_NUM_POINTS = 24
T = TypeVar('T', bound='Shape') 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. 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 identifier: Tuple
""" An arbitrary identifier for the shape, usually empty but used by `Pattern.flatten()` """ """ An arbitrary identifier for the shape, usually empty but used by `Pattern.flatten()` """
locked: bool # def __copy__(self) -> 'Shape':
""" If `True`, any changes to the shape will raise a `PatternLockedError` """ # cls = self.__class__
# new = cls.__new__(cls)
def __setattr__(self, name, value): # for name in Shape.__slots__ + self.__slots__:
if self.locked and name != 'locked': # object.__setattr__(new, name, getattr(self, name))
raise PatternLockedError() # return new
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
''' '''
--- Abstract methods --- Abstract methods
@ -79,53 +64,6 @@ class Shape(metaclass=ABCMeta):
""" """
pass 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 @abstractmethod
def normalized_form(self: T, norm_value: int) -> normalized_shape_tuple: def normalized_form(self: T, norm_value: int) -> normalized_shape_tuple:
""" """
@ -150,97 +88,9 @@ class Shape(metaclass=ABCMeta):
""" """
pass 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 ---- 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, def manhattanize_fast(self,
grid_x: numpy.ndarray, grid_x: numpy.ndarray,
grid_y: numpy.ndarray, grid_y: numpy.ndarray,
@ -442,37 +292,12 @@ class Shape(metaclass=ABCMeta):
return manhattan_polygons 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: def lock(self: T) -> T:
""" PositionableImpl._lock(self)
Lock the Shape, disallowing further changes LockableImpl.lock(self)
Returns:
self
"""
self.offset.flags.writeable = False
object.__setattr__(self, 'locked', True)
return self return self
def unlock(self: T) -> T: def unlock(self: T) -> T:
""" LockableImpl.unlock(self)
Unlock the Shape PositionableImpl._unlock(self)
Returns:
self
"""
object.__setattr__(self, 'locked', False)
self.offset.flags.writeable = True
return self return self

View File

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

View File

@ -11,51 +11,50 @@ import numpy
from numpy import pi from numpy import pi
from .error import PatternError, PatternLockedError from .error import PatternError, PatternLockedError
from .utils import is_scalar, rotation_matrix_2d, vector2 from .utils import is_scalar, rotation_matrix_2d, vector2, AutoSlots
from .repetition import GridRepetition from .repetition import Repetition
from .traits import (PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl,
Mirrorable, Pivotable, Copyable, LockableImpl, RepeatableImpl)
if TYPE_CHECKING: if TYPE_CHECKING:
from . import Pattern 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 SubPattern provides basic support for nesting Pattern objects within each other, by adding
offset, rotation, scaling, and associated methods. offset, rotation, scaling, and associated methods.
""" """
__slots__ = ('_pattern', __slots__ = ('_pattern',
'_offset',
'_rotation',
'_dose',
'_scale',
'_mirrored', '_mirrored',
'identifier', 'identifier',
'locked') )
_pattern: Optional['Pattern'] _pattern: Optional['Pattern']
""" The `Pattern` being instanced """ """ The `Pattern` being instanced """
_offset: numpy.ndarray # _offset: numpy.ndarray
""" (x, y) offset for the instance """ # """ (x, y) offset for the instance """
_rotation: float # _rotation: float
""" rotation for the instance, radians counterclockwise """ # """ rotation for the instance, radians counterclockwise """
_dose: float # _dose: float
""" dose factor for the instance """ # """ dose factor for the instance """
_scale: float # _scale: float
""" scale factor for the instance """ # """ scale factor for the instance """
_mirrored: numpy.ndarray # ndarray[bool] _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, ...] identifier: Tuple[Any, ...]
""" Arbitrary identifier, used internally by some `masque` functions. """ """ Arbitrary identifier, used internally by some `masque` functions. """
locked: bool # locked: bool
""" If `True`, disallows changes to the GridRepetition """ # """ If `True`, disallows changes to the SubPattern"""
def __init__(self, def __init__(self,
pattern: Optional['Pattern'], pattern: Optional['Pattern'],
@ -64,6 +63,7 @@ class SubPattern:
mirrored: Optional[Sequence[bool]] = None, mirrored: Optional[Sequence[bool]] = None,
dose: float = 1.0, dose: float = 1.0,
scale: float = 1.0, scale: float = 1.0,
repetition: Optional[Repetition] = None,
locked: bool = False, locked: bool = False,
identifier: Tuple[Any, ...] = ()): identifier: Tuple[Any, ...] = ()):
""" """
@ -74,10 +74,12 @@ class SubPattern:
mirrored: Whether to mirror the referenced pattern across its x and y axes. mirrored: Whether to mirror the referenced pattern across its x and y axes.
dose: Scaling factor applied to the dose. dose: Scaling factor applied to the dose.
scale: Scaling factor applied to the pattern's geometry. scale: Scaling factor applied to the pattern's geometry.
repetition: TODO
locked: Whether the `SubPattern` is locked after initialization. locked: Whether the `SubPattern` is locked after initialization.
identifier: Arbitrary tuple, used internally by some `masque` functions. 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.identifier = identifier
self.pattern = pattern self.pattern = pattern
self.offset = offset self.offset = offset
@ -87,13 +89,9 @@ class SubPattern:
if mirrored is None: if mirrored is None:
mirrored = [False, False] mirrored = [False, False]
self.mirrored = mirrored self.mirrored = mirrored
self.repetition = repetition
self.locked = locked 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': def __copy__(self) -> 'SubPattern':
new = SubPattern(pattern=self.pattern, new = SubPattern(pattern=self.pattern,
offset=self.offset.copy(), offset=self.offset.copy(),
@ -123,57 +121,6 @@ class SubPattern:
raise PatternError('Provided pattern {} is not a Pattern object or None!'.format(val)) raise PatternError('Provided pattern {} is not a Pattern object or None!'.format(val))
self._pattern = 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 # Mirrored property
@property @property
def mirrored(self) -> numpy.ndarray: # ndarray[bool] def mirrored(self) -> numpy.ndarray: # ndarray[bool]
@ -198,52 +145,17 @@ class SubPattern:
pattern.rotate_around((0.0, 0.0), self.rotation) pattern.rotate_around((0.0, 0.0), self.rotation)
pattern.translate_elements(self.offset) pattern.translate_elements(self.offset)
pattern.scale_element_doses(self.dose) 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 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': def mirror(self, axis: int) -> 'SubPattern':
""" """
Mirror the subpattern across an axis. Mirror the subpattern across an axis.
@ -271,37 +183,6 @@ class SubPattern:
return None return None
return self.as_pattern().get_bounds() 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': def lock(self) -> 'SubPattern':
""" """
Lock the SubPattern, disallowing changes Lock the SubPattern, disallowing changes
@ -309,9 +190,9 @@ class SubPattern:
Returns: Returns:
self self
""" """
self.offset.flags.writeable = False
self.mirrored.flags.writeable = False self.mirrored.flags.writeable = False
object.__setattr__(self, 'locked', True) PositionableImpl._lock(self)
LockableImpl.lock(self)
return self return self
def unlock(self) -> 'SubPattern': def unlock(self) -> 'SubPattern':
@ -321,9 +202,9 @@ class SubPattern:
Returns: Returns:
self self
""" """
self.offset.flags.writeable = True LockableImpl.unlock(self)
PositionableImpl._unlock(self)
self.mirrored.flags.writeable = True self.mirrored.flags.writeable = True
object.__setattr__(self, 'locked', False)
return self return self
def deeplock(self) -> 'SubPattern': def deeplock(self) -> 'SubPattern':
@ -361,6 +242,3 @@ class SubPattern:
dose = f' d{self.dose:g}' if self.dose != 1 else '' dose = f' d{self.dose:g}' if self.dose != 1 else ''
locked = ' L' if self.locked else '' locked = ' L' if self.locked else ''
return f'<SubPattern "{name}" at {self.offset}{rotation}{scale}{mirrored}{dose}{locked}>' 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 typing import Any, Union, Tuple, Sequence
from abc import ABCMeta
import numpy import numpy
@ -133,3 +134,25 @@ def remove_colinear_vertices(vertices: numpy.ndarray, closed_path: bool = True)
slopes_equal[[0, -1]] = False slopes_equal[[0, -1]] = False
return vertices[~slopes_equal] 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)