Add repetitions and split up code into traits
This commit is contained in:
		
							parent
							
								
									d4fbdd8d27
								
							
						
					
					
						commit
						bab40474a0
					
				
							
								
								
									
										102
									
								
								examples/test_rep.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								examples/test_rep.py
									
									
									
									
									
										Normal 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() | ||||
| @ -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 | ||||
| 
 | ||||
| 
 | ||||
|  | ||||
| @ -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], | ||||
|  | ||||
| @ -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] | ||||
|     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], | ||||
|     subpat = SubPattern(pattern=None, | ||||
|                         offset=offset, | ||||
|                         rotation=rotation, | ||||
|                         scale=scale, | ||||
|                         mirrored=(mirror_across_x, False)) | ||||
|     gridrep.identifier = (element.struct_name,) | ||||
| 
 | ||||
|     return gridrep | ||||
|     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 | ||||
|         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 | ||||
| 
 | ||||
| 
 | ||||
|  | ||||
| @ -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, | ||||
|         mrep = Grid(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] | ||||
|                     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, | ||||
|  | ||||
| @ -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: | ||||
|  | ||||
| @ -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: | ||||
|  | ||||
| @ -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(), | ||||
|     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, | ||||
|                              offset=self.offset.copy(), | ||||
|                              rotation=self.rotation, | ||||
|                              dose=self.dose, | ||||
|                              scale=self.scale, | ||||
|                              mirrored=self.mirrored.copy(), | ||||
|                    locked=self.locked) | ||||
|         return new | ||||
| 
 | ||||
|     def  __deepcopy__(self, memo: Dict = None) -> '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) | ||||
|  | ||||
| @ -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 """ | ||||
| 
 | ||||
|  | ||||
| @ -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 """ | ||||
| 
 | ||||
|  | ||||
| @ -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 """ | ||||
| 
 | ||||
|  | ||||
| @ -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. | ||||
|  | ||||
| @ -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], ...]` """ | ||||
| 
 | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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] | ||||
|  | ||||
							
								
								
									
										9
									
								
								masque/traits/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								masque/traits/__init__.py
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										34
									
								
								masque/traits/copyable.py
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										82
									
								
								masque/traits/doseable.py
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										76
									
								
								masque/traits/layerable.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								masque/traits/layerable.py
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										76
									
								
								masque/traits/lockable.py
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										61
									
								
								masque/traits/mirrorable.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								masque/traits/mirrorable.py
									
									
									
									
									
										Normal 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 | ||||
| #    ''' | ||||
							
								
								
									
										135
									
								
								masque/traits/positionable.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								masque/traits/positionable.py
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										79
									
								
								masque/traits/repeatable.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								masque/traits/repeatable.py
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										119
									
								
								masque/traits/rotatable.py
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										79
									
								
								masque/traits/scalable.py
									
									
									
									
									
										Normal 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 | ||||
| @ -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) | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user