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` |  `Pattern` is a basic object containing a 2D lithography mask, composed of a list of `Shape` | ||||||
|   objects, a list of `Label` objects, and a list of references to other `Patterns` (using |   objects, a list of `Label` objects, and a list of references to other `Patterns` (using | ||||||
|   `SubPattern` and `GridRepetition`). |   `SubPattern`). | ||||||
| 
 | 
 | ||||||
|  `SubPattern` provides basic support for nesting `Pattern` objects within each other, by adding |  `SubPattern` provides basic support for nesting `Pattern` objects within each other, by adding | ||||||
|   offset, rotation, scaling, and other such properties to a Pattern reference. |   offset, rotation, scaling, repetition, and other such properties to a Pattern reference. | ||||||
| 
 |  | ||||||
|  `GridRepetition` provides support for nesting regular arrays of `Pattern` objects. |  | ||||||
| 
 | 
 | ||||||
|  Note that the methods for these classes try to avoid copying wherever possible, so unless |  Note that the methods for these classes try to avoid copying wherever possible, so unless | ||||||
|   otherwise noted, assume that arguments are stored by-reference. |   otherwise noted, assume that arguments are stored by-reference. | ||||||
| @ -31,8 +29,7 @@ import pathlib | |||||||
| from .error import PatternError, PatternLockedError | from .error import PatternError, PatternLockedError | ||||||
| from .shapes import Shape | from .shapes import Shape | ||||||
| from .label import Label | from .label import Label | ||||||
| from .subpattern import SubPattern, subpattern_t | from .subpattern import SubPattern | ||||||
| from .repetition import GridRepetition |  | ||||||
| from .pattern import Pattern | from .pattern import Pattern | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -16,8 +16,9 @@ from numpy import pi | |||||||
| import ezdxf | import ezdxf | ||||||
| 
 | 
 | ||||||
| from .utils import mangle_name, make_dose_table | from .utils import mangle_name, make_dose_table | ||||||
| from .. import Pattern, SubPattern, GridRepetition, PatternError, Label, Shape, subpattern_t | from .. import Pattern, SubPattern, PatternError, Label, Shape | ||||||
| from ..shapes import Polygon, Path | from ..shapes import Polygon, Path | ||||||
|  | from ..repetition import Grid | ||||||
| from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar, layer_t | from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar, layer_t | ||||||
| from ..utils import remove_colinear_vertices, normalize_mirror | from ..utils import remove_colinear_vertices, normalize_mirror | ||||||
| 
 | 
 | ||||||
| @ -55,7 +56,7 @@ def write(pattern: Pattern, | |||||||
|     If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()` |     If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()` | ||||||
|      prior to calling this function. |      prior to calling this function. | ||||||
| 
 | 
 | ||||||
|     Only `GridRepetition` objects with manhattan basis vectors are preserved as arrays. Since DXF |     Only `Grid` repetition objects with manhattan basis vectors are preserved as arrays. Since DXF | ||||||
|      rotations apply to basis vectors while `masque`'s rotations do not, the basis vectors of an |      rotations apply to basis vectors while `masque`'s rotations do not, the basis vectors of an | ||||||
|      array with rotated instances must be manhattan _after_ having a compensating rotation applied. |      array with rotated instances must be manhattan _after_ having a compensating rotation applied. | ||||||
| 
 | 
 | ||||||
| @ -276,7 +277,7 @@ def _read_block(block, clean_vertices): | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def _subpatterns_to_refs(block: Union[ezdxf.layouts.BlockLayout, ezdxf.layouts.Modelspace], | def _subpatterns_to_refs(block: Union[ezdxf.layouts.BlockLayout, ezdxf.layouts.Modelspace], | ||||||
|                          subpatterns: List[subpattern_t]): |                          subpatterns: List[SubPattern]): | ||||||
|     for subpat in subpatterns: |     for subpat in subpatterns: | ||||||
|         if subpat.pattern is None: |         if subpat.pattern is None: | ||||||
|             continue |             continue | ||||||
| @ -289,9 +290,12 @@ def _subpatterns_to_refs(block: Union[ezdxf.layouts.BlockLayout, ezdxf.layouts.M | |||||||
|             'rotation': rotation, |             'rotation': rotation, | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|         if isinstance(subpat, GridRepetition): |         rep = subpat.repetition | ||||||
|             a = subpat.a_vector |         if rep is None: | ||||||
|             b = subpat.b_vector if subpat.b_vector is not None else numpy.zeros(2) |             block.add_blockref(encoded_name, subpat.offset, dxfattribs=attribs) | ||||||
|  |         elif isinstance(rep, Grid): | ||||||
|  |             a = rep.a_vector | ||||||
|  |             b = rep.b_vector if rep.b_vector is not None else numpy.zeros(2) | ||||||
|             rotated_a = rotation_matrix_2d(-subpat.rotation) @ a |             rotated_a = rotation_matrix_2d(-subpat.rotation) @ a | ||||||
|             rotated_b = rotation_matrix_2d(-subpat.rotation) @ b |             rotated_b = rotation_matrix_2d(-subpat.rotation) @ b | ||||||
|             if rotated_a[1] == 0 and rotated_b[0] == 0: |             if rotated_a[1] == 0 and rotated_b[0] == 0: | ||||||
| @ -310,11 +314,11 @@ def _subpatterns_to_refs(block: Union[ezdxf.layouts.BlockLayout, ezdxf.layouts.M | |||||||
|                 #NOTE: We could still do non-manhattan (but still orthogonal) grids by getting |                 #NOTE: We could still do non-manhattan (but still orthogonal) grids by getting | ||||||
|                 #       creative with counter-rotated nested patterns, but probably not worth it. |                 #       creative with counter-rotated nested patterns, but probably not worth it. | ||||||
|                 # Instead, just break appart the grid into individual elements: |                 # Instead, just break appart the grid into individual elements: | ||||||
|                 for aa in numpy.arange(subpat.a_count): |                 for dd in rep.displacements: | ||||||
|                     for bb in numpy.arange(subpat.b_count): |                     block.add_blockref(encoded_name, subpat.offset + dd, dxfattribs=attribs) | ||||||
|                         block.add_blockref(encoded_name, subpat.offset + aa * a + bb * b, dxfattribs=attribs) |  | ||||||
|         else: |         else: | ||||||
|             block.add_blockref(encoded_name, subpat.offset, dxfattribs=attribs) |             for dd in rep.displacements: | ||||||
|  |                 block.add_blockref(encoded_name, subpat.offset + dd, dxfattribs=attribs) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def _shapes_to_elements(block: Union[ezdxf.layouts.BlockLayout, ezdxf.layouts.Modelspace], | def _shapes_to_elements(block: Union[ezdxf.layouts.BlockLayout, ezdxf.layouts.Modelspace], | ||||||
|  | |||||||
| @ -28,8 +28,9 @@ import gdsii.structure | |||||||
| import gdsii.elements | import gdsii.elements | ||||||
| 
 | 
 | ||||||
| from .utils import mangle_name, make_dose_table, dose2dtype, dtype2dose | from .utils import mangle_name, make_dose_table, dose2dtype, dtype2dose | ||||||
| from .. import Pattern, SubPattern, GridRepetition, PatternError, Label, Shape, subpattern_t | from .. import Pattern, SubPattern, PatternError, Label, Shape | ||||||
| from ..shapes import Polygon, Path | from ..shapes import Polygon, Path | ||||||
|  | from ..repetition import Grid | ||||||
| from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar, layer_t | from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar, layer_t | ||||||
| from ..utils import remove_colinear_vertices, normalize_mirror | from ..utils import remove_colinear_vertices, normalize_mirror | ||||||
| 
 | 
 | ||||||
| @ -291,11 +292,9 @@ def read(stream: io.BufferedIOBase, | |||||||
|                               string=element.string.decode('ASCII')) |                               string=element.string.decode('ASCII')) | ||||||
|                 pat.labels.append(label) |                 pat.labels.append(label) | ||||||
| 
 | 
 | ||||||
|             elif isinstance(element, gdsii.elements.SRef): |             elif (isinstance(element, gdsii.elements.SRef) or | ||||||
|                 pat.subpatterns.append(_sref_to_subpat(element)) |                   isinstance(element, gdsii.elements.ARef)): | ||||||
| 
 |                 pat.subpatterns.append(_ref_to_subpat(element)) | ||||||
|             elif isinstance(element, gdsii.elements.ARef): |  | ||||||
|                 pat.subpatterns.append(_aref_to_gridrep(element)) |  | ||||||
| 
 | 
 | ||||||
|         if use_dtype_as_dose: |         if use_dtype_as_dose: | ||||||
|             logger.warning('use_dtype_as_dose will be removed in the future!') |             logger.warning('use_dtype_as_dose will be removed in the future!') | ||||||
| @ -330,40 +329,11 @@ def _mlayer2gds(mlayer: layer_t) -> Tuple[int, int]: | |||||||
|     return layer, data_type |     return layer, data_type | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def _sref_to_subpat(element: gdsii.elements.SRef) -> SubPattern: | def _ref_to_subpat(element: Union[gdsii.elements.SRef, | ||||||
|  |                                   gdsii.elements.ARef] | ||||||
|  |                    ) -> SubPattern: | ||||||
|     """ |     """ | ||||||
|     Helper function to create a SubPattern from an SREF. Sets subpat.pattern to None |     Helper function to create a SubPattern from an SREF or AREF. Sets subpat.pattern to None | ||||||
|      and sets the instance .identifier to (struct_name,). |  | ||||||
| 
 |  | ||||||
|     BUG: |  | ||||||
|         "Absolute" means not affected by parent elements. |  | ||||||
|           That's not currently supported by masque at all, so need to either tag it and |  | ||||||
|           undo the parent transformations, or implement it in masque. |  | ||||||
|     """ |  | ||||||
|     subpat = SubPattern(pattern=None, offset=element.xy) |  | ||||||
|     subpat.identifier = (element.struct_name,) |  | ||||||
|     if element.strans is not None: |  | ||||||
|         if element.mag is not None: |  | ||||||
|             subpat.scale = element.mag |  | ||||||
|             # Bit 13 means absolute scale |  | ||||||
|             if get_bit(element.strans, 15 - 13): |  | ||||||
|                 #subpat.offset *= subpat.scale |  | ||||||
|                 raise PatternError('Absolute scale is not implemented yet!') |  | ||||||
|         if element.angle is not None: |  | ||||||
|             subpat.rotation = element.angle * numpy.pi / 180 |  | ||||||
|             # Bit 14 means absolute rotation |  | ||||||
|             if get_bit(element.strans, 15 - 14): |  | ||||||
|                 #subpat.offset = numpy.dot(rotation_matrix_2d(subpat.rotation), subpat.offset) |  | ||||||
|                 raise PatternError('Absolute rotation is not implemented yet!') |  | ||||||
|         # Bit 0 means mirror x-axis |  | ||||||
|         if get_bit(element.strans, 15 - 0): |  | ||||||
|             subpat.mirrored[0] = 1 |  | ||||||
|     return subpat |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def _aref_to_gridrep(element: gdsii.elements.ARef) -> GridRepetition: |  | ||||||
|     """ |  | ||||||
|     Helper function to create a GridRepetition from an AREF. Sets gridrep.pattern to None |  | ||||||
|      and sets the instance .identifier to (struct_name,). |      and sets the instance .identifier to (struct_name,). | ||||||
| 
 | 
 | ||||||
|     BUG: |     BUG: | ||||||
| @ -375,6 +345,7 @@ def _aref_to_gridrep(element: gdsii.elements.ARef) -> GridRepetition: | |||||||
|     offset = numpy.array(element.xy[0]) |     offset = numpy.array(element.xy[0]) | ||||||
|     scale = 1 |     scale = 1 | ||||||
|     mirror_across_x = False |     mirror_across_x = False | ||||||
|  |     repetition = None | ||||||
| 
 | 
 | ||||||
|     if element.strans is not None: |     if element.strans is not None: | ||||||
|         if element.mag is not None: |         if element.mag is not None: | ||||||
| @ -383,7 +354,7 @@ def _aref_to_gridrep(element: gdsii.elements.ARef) -> GridRepetition: | |||||||
|             if get_bit(element.strans, 15 - 13): |             if get_bit(element.strans, 15 - 13): | ||||||
|                 raise PatternError('Absolute scale is not implemented yet!') |                 raise PatternError('Absolute scale is not implemented yet!') | ||||||
|         if element.angle is not None: |         if element.angle is not None: | ||||||
|             rotation = element.angle * numpy.pi / 180 |             rotation = numpy.deg2rad(element.angle) | ||||||
|             # Bit 14 means absolute rotation |             # Bit 14 means absolute rotation | ||||||
|             if get_bit(element.strans, 15 - 14): |             if get_bit(element.strans, 15 - 14): | ||||||
|                 raise PatternError('Absolute rotation is not implemented yet!') |                 raise PatternError('Absolute rotation is not implemented yet!') | ||||||
| @ -391,25 +362,24 @@ def _aref_to_gridrep(element: gdsii.elements.ARef) -> GridRepetition: | |||||||
|         if get_bit(element.strans, 15 - 0): |         if get_bit(element.strans, 15 - 0): | ||||||
|             mirror_across_x = True |             mirror_across_x = True | ||||||
| 
 | 
 | ||||||
|     counts = [element.cols, element.rows] |     if isinstance(element, gdsii.elements.ARef): | ||||||
|     a_vector = (element.xy[1] - offset) / counts[0] |         a_count = element.cols | ||||||
|     b_vector = (element.xy[2] - offset) / counts[1] |         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, |     subpat = SubPattern(pattern=None, | ||||||
|                             a_vector=a_vector, |                         offset=offset, | ||||||
|                             b_vector=b_vector, |                         rotation=rotation, | ||||||
|                             a_count=counts[0], |                         scale=scale, | ||||||
|                             b_count=counts[1], |                         mirrored=(mirror_across_x, False)) | ||||||
|                             offset=offset, |     subpat.identifier = (element.struct_name,) | ||||||
|                             rotation=rotation, |     return subpat | ||||||
|                             scale=scale, |  | ||||||
|                             mirrored=(mirror_across_x, False)) |  | ||||||
|     gridrep.identifier = (element.struct_name,) |  | ||||||
| 
 |  | ||||||
|     return gridrep |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def _subpatterns_to_refs(subpatterns: List[subpattern_t] | def _subpatterns_to_refs(subpatterns: List[SubPattern] | ||||||
|                         ) -> List[Union[gdsii.elements.ARef, gdsii.elements.SRef]]: |                         ) -> List[Union[gdsii.elements.ARef, gdsii.elements.SRef]]: | ||||||
|     refs = [] |     refs = [] | ||||||
|     for subpat in subpatterns: |     for subpat in subpatterns: | ||||||
| @ -420,26 +390,35 @@ def _subpatterns_to_refs(subpatterns: List[subpattern_t] | |||||||
|         # Note: GDS mirrors first and rotates second |         # Note: GDS mirrors first and rotates second | ||||||
|         mirror_across_x, extra_angle = normalize_mirror(subpat.mirrored) |         mirror_across_x, extra_angle = normalize_mirror(subpat.mirrored) | ||||||
|         ref: Union[gdsii.elements.SRef, gdsii.elements.ARef] |         ref: Union[gdsii.elements.SRef, gdsii.elements.ARef] | ||||||
|         if isinstance(subpat, GridRepetition): | 
 | ||||||
|  |         rep = subpat.repetition | ||||||
|  |         if isinstance(rep, Grid): | ||||||
|             xy = numpy.array(subpat.offset) + [ |             xy = numpy.array(subpat.offset) + [ | ||||||
|                   [0, 0], |                   [0, 0], | ||||||
|                   subpat.a_vector * subpat.a_count, |                   rep.a_vector * rep.a_count, | ||||||
|                   subpat.b_vector * subpat.b_count, |                   rep.b_vector * rep.b_count, | ||||||
|                  ] |                  ] | ||||||
|             ref = gdsii.elements.ARef(struct_name=encoded_name, |             ref = gdsii.elements.ARef(struct_name=encoded_name, | ||||||
|                                        xy=numpy.round(xy).astype(int), |                                        xy=numpy.round(xy).astype(int), | ||||||
|                                        cols=numpy.round(subpat.a_count).astype(int), |                                        cols=numpy.round(rep.a_count).astype(int), | ||||||
|                                        rows=numpy.round(subpat.b_count).astype(int)) |                                        rows=numpy.round(rep.b_count).astype(int)) | ||||||
|         else: |             new_refs = [ref] | ||||||
|  |         elif rep is None: | ||||||
|             ref = gdsii.elements.SRef(struct_name=encoded_name, |             ref = gdsii.elements.SRef(struct_name=encoded_name, | ||||||
|                                       xy=numpy.round([subpat.offset]).astype(int)) |                                       xy=numpy.round([subpat.offset]).astype(int)) | ||||||
|  |             new_refs = [ref] | ||||||
|  |         else: | ||||||
|  |             new_refs = [gdsii.elements.SRef(struct_name=encoded_name, | ||||||
|  |                                             xy=numpy.round([subpat.offset + dd]).astype(int)) | ||||||
|  |                         for dd in rep.displacements] | ||||||
| 
 | 
 | ||||||
|         ref.angle = ((subpat.rotation + extra_angle) * 180 / numpy.pi) % 360 |         for ref in new_refs: | ||||||
|         #  strans must be non-None for angle and mag to take effect |             ref.angle = numpy.rad2deg(subpat.rotation + extra_angle) % 360 | ||||||
|         ref.strans = set_bit(0, 15 - 0, mirror_across_x) |             #  strans must be non-None for angle and mag to take effect | ||||||
|         ref.mag = subpat.scale |             ref.strans = set_bit(0, 15 - 0, mirror_across_x) | ||||||
|  |             ref.mag = subpat.scale | ||||||
| 
 | 
 | ||||||
|         refs.append(ref) |         refs += new_refs | ||||||
|     return refs |     return refs | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -28,8 +28,9 @@ import fatamorgana.records as fatrec | |||||||
| from fatamorgana.basic import PathExtensionScheme | from fatamorgana.basic import PathExtensionScheme | ||||||
| 
 | 
 | ||||||
| from .utils import mangle_name, make_dose_table | from .utils import mangle_name, make_dose_table | ||||||
| from .. import Pattern, SubPattern, GridRepetition, PatternError, Label, Shape, subpattern_t | from .. import Pattern, SubPattern, PatternError, Label, Shape | ||||||
| from ..shapes import Polygon, Path, Circle | from ..shapes import Polygon, Path, Circle | ||||||
|  | from ..repetition import Grid, Arbitrary, Repetition | ||||||
| from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar, layer_t | from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar, layer_t | ||||||
| from ..utils import remove_colinear_vertices, normalize_mirror | from ..utils import remove_colinear_vertices, normalize_mirror | ||||||
| 
 | 
 | ||||||
| @ -221,7 +222,7 @@ def read(stream: io.BufferedIOBase, | |||||||
|     """ |     """ | ||||||
|     Read a OASIS file and translate it into a dict of Pattern objects. OASIS cells are |     Read a OASIS file and translate it into a dict of Pattern objects. OASIS cells are | ||||||
|      translated into Pattern objects; Polygons are translated into polygons, and Placements |      translated into Pattern objects; Polygons are translated into polygons, and Placements | ||||||
|      are translated into SubPattern or GridRepetition objects. |      are translated into SubPattern objects. | ||||||
| 
 | 
 | ||||||
|     Additional library info is returned in a dict, containing: |     Additional library info is returned in a dict, containing: | ||||||
|       'units_per_micrometer': number of database units per micrometer (all values are in database units) |       'units_per_micrometer': number of database units per micrometer (all values are in database units) | ||||||
| @ -417,7 +418,7 @@ def read(stream: io.BufferedIOBase, | |||||||
|                 continue |                 continue | ||||||
| 
 | 
 | ||||||
|         for placement in cell.placements: |         for placement in cell.placements: | ||||||
|             pat.subpatterns += _placement_to_subpats(placement) |             pat.subpatterns.append(_placement_to_subpat(placement)) | ||||||
| 
 | 
 | ||||||
|         patterns.append(pat) |         patterns.append(pat) | ||||||
| 
 | 
 | ||||||
| @ -451,7 +452,7 @@ def _mlayer2oas(mlayer: layer_t) -> Tuple[int, int]: | |||||||
|     return layer, data_type |     return layer, data_type | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def _placement_to_subpats(placement: fatrec.Placement) -> List[subpattern_t]: | def _placement_to_subpat(placement: fatrec.Placement) -> SubPattern: | ||||||
|     """ |     """ | ||||||
|     Helper function to create a SubPattern from a placment. Sets subpat.pattern to None |     Helper function to create a SubPattern from a placment. Sets subpat.pattern to None | ||||||
|      and sets the instance .identifier to (struct_name,). |      and sets the instance .identifier to (struct_name,). | ||||||
| @ -468,27 +469,24 @@ def _placement_to_subpats(placement: fatrec.Placement) -> List[subpattern_t]: | |||||||
|        'identifier': (name,), |        'identifier': (name,), | ||||||
|        } |        } | ||||||
| 
 | 
 | ||||||
|     subpats: List[subpattern_t] |     mrep: Repetition | ||||||
|     rep = placement.repetition |     rep = placement.repetition | ||||||
|     if isinstance(rep, fatamorgana.GridRepetition): |     if isinstance(rep, fatamorgana.GridRepetition): | ||||||
|         subpat = GridRepetition(a_vector=rep.a_vector, |         mrep = Grid(a_vector=rep.a_vector, | ||||||
|                                 b_vector=rep.b_vector, |                     b_vector=rep.b_vector, | ||||||
|                                 a_count=rep.a_count, |                     a_count=rep.a_count, | ||||||
|                                 b_count=rep.b_count, |                     b_count=rep.b_count) | ||||||
|                                 offset=xy, |  | ||||||
|                                 **args) |  | ||||||
|         subpats = [subpat] |  | ||||||
|     elif isinstance(rep, fatamorgana.ArbitraryRepetition): |     elif isinstance(rep, fatamorgana.ArbitraryRepetition): | ||||||
|         subpats = [] |         mrep = Arbitrary(numpy.cumsum(numpy.column_stack((rep.x_displacements, | ||||||
|         for rep_offset in numpy.cumsum(numpy.column_stack((rep.x_displacements, |                                                           rep.y_displacements)))) | ||||||
|                                                            rep.y_displacements))): |  | ||||||
|             subpats.append(SubPattern(offset=xy + rep_offset, **args)) |  | ||||||
|     elif rep is None: |     elif rep is None: | ||||||
|         subpats = [SubPattern(offset=xy, **args)] |         mrep = None | ||||||
|     return subpats | 
 | ||||||
|  |     subpat = SubPattern(offset=xy, repetition=mrep, **args) | ||||||
|  |     return subpat | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def _subpatterns_to_refs(subpatterns: List[subpattern_t] | def _subpatterns_to_refs(subpatterns: List[SubPattern] | ||||||
|                         ) -> List[fatrec.Placement]: |                         ) -> List[fatrec.Placement]: | ||||||
|     refs = [] |     refs = [] | ||||||
|     for subpat in subpatterns: |     for subpat in subpatterns: | ||||||
| @ -503,14 +501,21 @@ def _subpatterns_to_refs(subpatterns: List[subpattern_t] | |||||||
|             'y': xy[1], |             'y': xy[1], | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|         if isinstance(subpat, GridRepetition): |         rep = subpat.repetition | ||||||
|  |         if isinstance(rep, Grid): | ||||||
|             args['repetition'] = fatamorgana.GridRepetition( |             args['repetition'] = fatamorgana.GridRepetition( | ||||||
|                               a_vector=numpy.round(subpat.a_vector).astype(int), |                               a_vector=numpy.round(rep.a_vector).astype(int), | ||||||
|                               b_vector=numpy.round(subpat.b_vector).astype(int), |                               b_vector=numpy.round(rep.b_vector).astype(int), | ||||||
|                               a_count=numpy.round(subpat.a_count).astype(int), |                               a_count=numpy.round(rep.a_count).astype(int), | ||||||
|                               b_count=numpy.round(subpat.b_count).astype(int)) |                               b_count=numpy.round(rep.b_count).astype(int)) | ||||||
|  |         elif isinstance(rep, Arbitrary): | ||||||
|  |             diffs = numpy.diff(rep.displacements, axis=0) | ||||||
|  |             args['repetition'] = fatamorgana.ArbitraryRepetition( | ||||||
|  |                                     numpy.round(diffs).astype(int)) | ||||||
|  |         else: | ||||||
|  |             assert(rep is None) | ||||||
| 
 | 
 | ||||||
|         angle = ((subpat.rotation + extra_angle) * 180 / numpy.pi) % 360 |         angle = numpy.rad2deg(subpat.rotation + extra_angle) % 360 | ||||||
|         ref = fatrec.Placement( |         ref = fatrec.Placement( | ||||||
|                 name=subpat.pattern.name, |                 name=subpat.pattern.name, | ||||||
|                 flip=mirror_across_x, |                 flip=mirror_across_x, | ||||||
|  | |||||||
| @ -4,20 +4,15 @@ import numpy | |||||||
| from numpy import pi | from numpy import pi | ||||||
| 
 | 
 | ||||||
| from .error import PatternError, PatternLockedError | from .error import PatternError, PatternLockedError | ||||||
| from .utils import is_scalar, vector2, rotation_matrix_2d, layer_t | from .utils import is_scalar, vector2, rotation_matrix_2d, layer_t, AutoSlots | ||||||
|  | from .traits import PositionableImpl, LayerableImpl, Copyable, Pivotable, LockableImpl | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Label: | class Label(PositionableImpl, LayerableImpl, LockableImpl, Pivotable, Copyable, metaclass=AutoSlots): | ||||||
|     """ |     """ | ||||||
|     A text annotation with a position and layer (but no size; it is not drawn) |     A text annotation with a position and layer (but no size; it is not drawn) | ||||||
|     """ |     """ | ||||||
|     __slots__ = ('_offset', '_layer', '_string', 'identifier', 'locked') |     __slots__ = ( '_string', 'identifier') | ||||||
| 
 |  | ||||||
|     _offset: numpy.ndarray |  | ||||||
|     """ [x_offset, y_offset] """ |  | ||||||
| 
 |  | ||||||
|     _layer: layer_t |  | ||||||
|     """ Layer (integer >= 0, or 2-Tuple of integers) """ |  | ||||||
| 
 | 
 | ||||||
|     _string: str |     _string: str | ||||||
|     """ Label string """ |     """ Label string """ | ||||||
| @ -25,44 +20,9 @@ class Label: | |||||||
|     identifier: Tuple |     identifier: Tuple | ||||||
|     """ Arbitrary identifier tuple, useful for keeping track of history when flattening """ |     """ Arbitrary identifier tuple, useful for keeping track of history when flattening """ | ||||||
| 
 | 
 | ||||||
|     locked: bool |     ''' | ||||||
|     """ If `True`, any changes to the label will raise a `PatternLockedError` """ |     ---- Properties | ||||||
| 
 |     ''' | ||||||
|     def __setattr__(self, name, value): |  | ||||||
|         if self.locked and name != 'locked': |  | ||||||
|             raise PatternLockedError() |  | ||||||
|         object.__setattr__(self, name, value) |  | ||||||
| 
 |  | ||||||
|     # ---- Properties |  | ||||||
|     # offset property |  | ||||||
|     @property |  | ||||||
|     def offset(self) -> numpy.ndarray: |  | ||||||
|         """ |  | ||||||
|         [x, y] offset |  | ||||||
|         """ |  | ||||||
|         return self._offset |  | ||||||
| 
 |  | ||||||
|     @offset.setter |  | ||||||
|     def offset(self, val: vector2): |  | ||||||
|         if not isinstance(val, numpy.ndarray): |  | ||||||
|             val = numpy.array(val, dtype=float) |  | ||||||
| 
 |  | ||||||
|         if val.size != 2: |  | ||||||
|             raise PatternError('Offset must be convertible to size-2 ndarray') |  | ||||||
|         self._offset = val.flatten().astype(float) |  | ||||||
| 
 |  | ||||||
|     # layer property |  | ||||||
|     @property |  | ||||||
|     def layer(self) -> layer_t: |  | ||||||
|         """ |  | ||||||
|         Layer number or name (int, tuple of ints, or string) |  | ||||||
|         """ |  | ||||||
|         return self._layer |  | ||||||
| 
 |  | ||||||
|     @layer.setter |  | ||||||
|     def layer(self, val: layer_t): |  | ||||||
|         self._layer = val |  | ||||||
| 
 |  | ||||||
|     # string property |     # string property | ||||||
|     @property |     @property | ||||||
|     def string(self) -> str: |     def string(self) -> str: | ||||||
| @ -100,25 +60,6 @@ class Label: | |||||||
|         new.locked = self.locked |         new.locked = self.locked | ||||||
|         return new |         return new | ||||||
| 
 | 
 | ||||||
|     def copy(self) -> 'Label': |  | ||||||
|         """ |  | ||||||
|         Returns a deep copy of the label. |  | ||||||
|         """ |  | ||||||
|         return copy.deepcopy(self) |  | ||||||
| 
 |  | ||||||
|     def translate(self, offset: vector2) -> 'Label': |  | ||||||
|         """ |  | ||||||
|         Translate the label by the given offset |  | ||||||
| 
 |  | ||||||
|         Args: |  | ||||||
|             offset: [x_offset, y,offset] |  | ||||||
| 
 |  | ||||||
|         Returns: |  | ||||||
|             self |  | ||||||
|         """ |  | ||||||
|         self.offset += offset |  | ||||||
|         return self |  | ||||||
| 
 |  | ||||||
|     def rotate_around(self, pivot: vector2, rotation: float) -> 'Label': |     def rotate_around(self, pivot: vector2, rotation: float) -> 'Label': | ||||||
|         """ |         """ | ||||||
|         Rotate the label around a point. |         Rotate the label around a point. | ||||||
| @ -150,25 +91,13 @@ class Label: | |||||||
|         return numpy.array([self.offset, self.offset]) |         return numpy.array([self.offset, self.offset]) | ||||||
| 
 | 
 | ||||||
|     def lock(self) -> 'Label': |     def lock(self) -> 'Label': | ||||||
|         """ |         PositionableImpl._lock(self) | ||||||
|         Lock the Label, causing any modifications to raise an exception. |         LockableImpl.lock(self) | ||||||
| 
 |  | ||||||
|         Return: |  | ||||||
|             self |  | ||||||
|         """ |  | ||||||
|         self.offset.flags.writeable = False |  | ||||||
|         object.__setattr__(self, 'locked', True) |  | ||||||
|         return self |         return self | ||||||
| 
 | 
 | ||||||
|     def unlock(self) -> 'Label': |     def unlock(self) -> 'Label': | ||||||
|         """ |         LockableImpl.unlock(self) | ||||||
|         Unlock the Label, re-allowing changes. |         PositionableImpl._unlock(self) | ||||||
| 
 |  | ||||||
|         Return: |  | ||||||
|             self |  | ||||||
|         """ |  | ||||||
|         object.__setattr__(self, 'locked', False) |  | ||||||
|         self.offset.flags.writeable = True |  | ||||||
|         return self |         return self | ||||||
| 
 | 
 | ||||||
|     def __repr__(self) -> str: |     def __repr__(self) -> str: | ||||||
|  | |||||||
| @ -13,8 +13,7 @@ import numpy | |||||||
| from numpy import inf | from numpy import inf | ||||||
| # .visualize imports matplotlib and matplotlib.collections | # .visualize imports matplotlib and matplotlib.collections | ||||||
| 
 | 
 | ||||||
| from .subpattern import SubPattern, subpattern_t | from .subpattern import SubPattern | ||||||
| from .repetition import GridRepetition |  | ||||||
| from .shapes import Shape, Polygon | from .shapes import Shape, Polygon | ||||||
| from .label import Label | from .label import Label | ||||||
| from .utils import rotation_matrix_2d, vector2, normalize_mirror | from .utils import rotation_matrix_2d, vector2, normalize_mirror | ||||||
| @ -27,8 +26,7 @@ visitor_function_t = Callable[['Pattern', Tuple['Pattern'], Dict, numpy.ndarray] | |||||||
| class Pattern: | class Pattern: | ||||||
|     """ |     """ | ||||||
|     2D layout consisting of some set of shapes, labels, and references to other Pattern objects |     2D layout consisting of some set of shapes, labels, and references to other Pattern objects | ||||||
|      (via SubPattern and GridRepetition). Shapes are assumed to inherit from |      (via SubPattern). Shapes are assumed to inherit from masque.shapes.Shape or provide equivalent functions. | ||||||
|      masque.shapes.Shape or provide equivalent functions. |  | ||||||
|     """ |     """ | ||||||
|     __slots__ = ('shapes', 'labels', 'subpatterns', 'name', 'locked') |     __slots__ = ('shapes', 'labels', 'subpatterns', 'name', 'locked') | ||||||
| 
 | 
 | ||||||
| @ -40,11 +38,10 @@ class Pattern: | |||||||
|     labels: List[Label] |     labels: List[Label] | ||||||
|     """ List of all labels in this Pattern. """ |     """ List of all labels in this Pattern. """ | ||||||
| 
 | 
 | ||||||
|     subpatterns: List[subpattern_t] |     subpatterns: List[SubPattern] | ||||||
|     """ List of all objects referencing other patterns in this Pattern. |     """ List of all references to other patterns (`SubPattern`s) in this `Pattern`. | ||||||
|     Examples are SubPattern (gdsii "instances") or GridRepetition (gdsii "arrays") |  | ||||||
|     Multiple objects in this list may reference the same Pattern object |     Multiple objects in this list may reference the same Pattern object | ||||||
|     (multiple instances of the same object). |       (i.e. multiple instances of the same object). | ||||||
|     """ |     """ | ||||||
| 
 | 
 | ||||||
|     name: str |     name: str | ||||||
| @ -57,7 +54,7 @@ class Pattern: | |||||||
|                  name: str = '', |                  name: str = '', | ||||||
|                  shapes: Sequence[Shape] = (), |                  shapes: Sequence[Shape] = (), | ||||||
|                  labels: Sequence[Label] = (), |                  labels: Sequence[Label] = (), | ||||||
|                  subpatterns: Sequence[subpattern_t] = (), |                  subpatterns: Sequence[SubPattern] = (), | ||||||
|                  locked: bool = False, |                  locked: bool = False, | ||||||
|                  ): |                  ): | ||||||
|         """ |         """ | ||||||
| @ -134,7 +131,7 @@ class Pattern: | |||||||
|     def subset(self, |     def subset(self, | ||||||
|                shapes_func: Callable[[Shape], bool] = None, |                shapes_func: Callable[[Shape], bool] = None, | ||||||
|                labels_func: Callable[[Label], bool] = None, |                labels_func: Callable[[Label], bool] = None, | ||||||
|                subpatterns_func: Callable[[subpattern_t], bool] = None, |                subpatterns_func: Callable[[SubPattern], bool] = None, | ||||||
|                recursive: bool = False, |                recursive: bool = False, | ||||||
|                ) -> 'Pattern': |                ) -> 'Pattern': | ||||||
|         """ |         """ | ||||||
| @ -493,7 +490,7 @@ class Pattern: | |||||||
|     def subpatterns_by_id(self, |     def subpatterns_by_id(self, | ||||||
|                           include_none: bool = False, |                           include_none: bool = False, | ||||||
|                           recursive: bool = True, |                           recursive: bool = True, | ||||||
|                           ) -> Dict[int, List[subpattern_t]]: |                           ) -> Dict[int, List[SubPattern]]: | ||||||
|         """ |         """ | ||||||
|         Create a dictionary which maps `{id(referenced_pattern): [subpattern0, ...]}` |         Create a dictionary which maps `{id(referenced_pattern): [subpattern0, ...]}` | ||||||
|          for all SubPattern objects referenced by this Pattern (by default, operates |          for all SubPattern objects referenced by this Pattern (by default, operates | ||||||
| @ -506,7 +503,7 @@ class Pattern: | |||||||
|         Returns: |         Returns: | ||||||
|             Dictionary mapping each pattern id to a list of subpattern objects referencing the pattern. |             Dictionary mapping each pattern id to a list of subpattern objects referencing the pattern. | ||||||
|         """ |         """ | ||||||
|         ids: Dict[int, List[subpattern_t]] = defaultdict(list) |         ids: Dict[int, List[SubPattern]] = defaultdict(list) | ||||||
|         for subpat in self.subpatterns: |         for subpat in self.subpatterns: | ||||||
|             pat = subpat.pattern |             pat = subpat.pattern | ||||||
|             if include_none or pat is not None: |             if include_none or pat is not None: | ||||||
|  | |||||||
| @ -1,78 +1,47 @@ | |||||||
| """ | """ | ||||||
|     Repetitions provides support for efficiently nesting multiple identical |     Repetitions provide support for efficiently representing multiple identical | ||||||
|      instances of a Pattern in the same parent Pattern. |      instances of an object . | ||||||
| """ | """ | ||||||
| 
 | 
 | ||||||
| from typing import Union, List, Dict, Tuple, Optional, Sequence, TYPE_CHECKING, Any | from typing import Union, List, Dict, Tuple, Optional, Sequence, TYPE_CHECKING, Any | ||||||
| import copy | import copy | ||||||
|  | from abc import ABCMeta, abstractmethod | ||||||
| 
 | 
 | ||||||
| import numpy | import numpy | ||||||
| from numpy import pi |  | ||||||
| 
 | 
 | ||||||
| from .error import PatternError, PatternLockedError | from .error import PatternError, PatternLockedError | ||||||
| from .utils import is_scalar, rotation_matrix_2d, vector2 | from .utils import rotation_matrix_2d, vector2, AutoSlots | ||||||
| 
 | from .traits import LockableImpl, Copyable, Scalable, Rotatable, Mirrorable | ||||||
| if TYPE_CHECKING: |  | ||||||
|     from . import Pattern |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| # TODO need top-level comment about what order rotation/scale/offset/mirror/array are applied | class Repetition(Copyable, Rotatable, Mirrorable, Scalable, metaclass=ABCMeta): | ||||||
| 
 |  | ||||||
| class GridRepetition: |  | ||||||
|     """ |     """ | ||||||
|     GridRepetition provides support for efficiently embedding multiple copies of a `Pattern` |     Interface common to all objects which specify repetitions | ||||||
|      into another `Pattern` at regularly-spaced offsets. |  | ||||||
| 
 |  | ||||||
|     Note that rotation, scaling, and mirroring are applied to individual instances of the |  | ||||||
|      pattern, not to the grid vectors. |  | ||||||
| 
 |  | ||||||
|     The order of operations is |  | ||||||
|         1. A single refernce instance to the target pattern is mirrored |  | ||||||
|         2. The single instance is rotated. |  | ||||||
|         3. The instance is scaled by the scaling factor. |  | ||||||
|         4. The instance is shifted by the provided offset |  | ||||||
|             (no mirroring/scaling/rotation is applied to the offset). |  | ||||||
|         5. Additional copies of the instance will appear at coordinates specified by |  | ||||||
|             `(offset + aa * a_vector + bb * b_vector)`, with `aa in range(0, a_count)` |  | ||||||
|             and `bb in range(0, b_count)`. All instance locations remain unaffected by |  | ||||||
|             mirroring/scaling/rotation, though each instance's data will be transformed |  | ||||||
|             relative to the instance's location (i.e. relative to the contained pattern's |  | ||||||
|             (0, 0) point). |  | ||||||
|     """ |     """ | ||||||
|     __slots__ = ('_pattern', |     __slots__ = () | ||||||
|                  '_offset', | 
 | ||||||
|                  '_rotation', |     @property | ||||||
|                  '_dose', |     @abstractmethod | ||||||
|                  '_scale', |     def displacements(self) -> numpy.ndarray: | ||||||
|                  '_mirrored', |         """ | ||||||
|                  '_a_vector', |         An Nx2 ndarray specifying all offsets generated by this repetition | ||||||
|  |         """ | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Grid(LockableImpl, Repetition, metaclass=AutoSlots): | ||||||
|  |     """ | ||||||
|  |     `Grid` describes a 2D grid formed by two basis vectors and two 'counts' (sizes). | ||||||
|  | 
 | ||||||
|  |     The second basis vector and count (`b_vector` and `b_count`) may be omitted, | ||||||
|  |       which makes the grid describe a 1D array. | ||||||
|  | 
 | ||||||
|  |     Note that the offsets in either the 2D or 1D grids do not have to be axis-aligned. | ||||||
|  |     """ | ||||||
|  |     __slots__ = ('_a_vector', | ||||||
|                  '_b_vector', |                  '_b_vector', | ||||||
|                  '_a_count', |                  '_a_count', | ||||||
|                  '_b_count', |                  '_b_count') | ||||||
|                  'identifier', |  | ||||||
|                  'locked') |  | ||||||
| 
 |  | ||||||
|     _pattern: Optional['Pattern'] |  | ||||||
|     """ The `Pattern` being instanced """ |  | ||||||
| 
 |  | ||||||
|     _offset: numpy.ndarray |  | ||||||
|     """ (x, y) offset for the base instance """ |  | ||||||
| 
 |  | ||||||
|     _dose: float |  | ||||||
|     """ Scaling factor applied to the dose """ |  | ||||||
| 
 |  | ||||||
|     _rotation: float |  | ||||||
|     """ Rotation of the individual instances in the grid (not the grid vectors). |  | ||||||
|     Radians, counterclockwise. |  | ||||||
|     """ |  | ||||||
| 
 |  | ||||||
|     _scale: float |  | ||||||
|     """ Scaling factor applied to individual instances in the grid (not the grid vectors) """ |  | ||||||
| 
 |  | ||||||
|     _mirrored: numpy.ndarray        # ndarray[bool] |  | ||||||
|     """ Whether to mirror individual instances across the x and y axes |  | ||||||
|     (Applies to individual instances in the grid, not the grid vectors) |  | ||||||
|     """ |  | ||||||
| 
 | 
 | ||||||
|     _a_vector: numpy.ndarray |     _a_vector: numpy.ndarray | ||||||
|     """ Vector `[x, y]` specifying the first lattice vector of the grid. |     """ Vector `[x, y]` specifying the first lattice vector of the grid. | ||||||
| @ -91,28 +60,14 @@ class GridRepetition: | |||||||
|     _b_count: int |     _b_count: int | ||||||
|     """ Number of instances along the direction specified by the `b_vector` """ |     """ Number of instances along the direction specified by the `b_vector` """ | ||||||
| 
 | 
 | ||||||
|     identifier: Tuple[Any, ...] |  | ||||||
|     """ Arbitrary identifier, used internally by some `masque` functions. """ |  | ||||||
| 
 |  | ||||||
|     locked: bool |  | ||||||
|     """ If `True`, disallows changes to the GridRepetition """ |  | ||||||
| 
 |  | ||||||
|     def __init__(self, |     def __init__(self, | ||||||
|                  pattern: Optional['Pattern'], |  | ||||||
|                  a_vector: numpy.ndarray, |                  a_vector: numpy.ndarray, | ||||||
|                  a_count: int, |                  a_count: int, | ||||||
|                  b_vector: Optional[numpy.ndarray] = None, |                  b_vector: Optional[numpy.ndarray] = None, | ||||||
|                  b_count: Optional[int] = 1, |                  b_count: Optional[int] = 1, | ||||||
|                  offset: vector2 = (0.0, 0.0), |                  locked: bool = False,): | ||||||
|                  rotation: float = 0.0, |  | ||||||
|                  mirrored: Optional[Sequence[bool]] = None, |  | ||||||
|                  dose: float = 1.0, |  | ||||||
|                  scale: float = 1.0, |  | ||||||
|                  locked: bool = False, |  | ||||||
|                  identifier: Tuple[Any, ...] = ()): |  | ||||||
|         """ |         """ | ||||||
|         Args: |         Args: | ||||||
|             pattern: Pattern to reference. |  | ||||||
|             a_vector: First lattice vector, of the form `[x, y]`. |             a_vector: First lattice vector, of the form `[x, y]`. | ||||||
|                 Specifies center-to-center spacing between adjacent instances. |                 Specifies center-to-center spacing between adjacent instances. | ||||||
|             a_count: Number of elements in the a_vector direction. |             a_count: Number of elements in the a_vector direction. | ||||||
| @ -121,14 +76,7 @@ class GridRepetition: | |||||||
|                 Can be omitted when specifying a 1D array. |                 Can be omitted when specifying a 1D array. | ||||||
|             b_count: Number of elements in the `b_vector` direction. |             b_count: Number of elements in the `b_vector` direction. | ||||||
|                 Should be omitted if `b_vector` was omitted. |                 Should be omitted if `b_vector` was omitted. | ||||||
|             offset: (x, y) offset applied to all instances. |             locked: Whether the `Grid` is locked after initialization. | ||||||
|             rotation: Rotation (radians, counterclockwise) applied to each instance. |  | ||||||
|                        Relative to each instance's (0, 0). |  | ||||||
|             mirrored: Whether to mirror individual instances across the x and y axes. |  | ||||||
|             dose: Scaling factor applied to the dose. |  | ||||||
|             scale: Scaling factor applied to the instances' geometry. |  | ||||||
|             locked: Whether the `GridRepetition` is locked after initialization. |  | ||||||
|             identifier: Arbitrary tuple, used internally by some `masque` functions. |  | ||||||
| 
 | 
 | ||||||
|         Raises: |         Raises: | ||||||
|             PatternError if `b_*` inputs conflict with each other |             PatternError if `b_*` inputs conflict with each other | ||||||
| @ -144,132 +92,31 @@ class GridRepetition: | |||||||
|                 b_vector = numpy.array([0.0, 0.0]) |                 b_vector = numpy.array([0.0, 0.0]) | ||||||
| 
 | 
 | ||||||
|         if a_count < 1: |         if a_count < 1: | ||||||
|             raise PatternError('Repetition has too-small a_count: ' |             raise PatternError(f'Repetition has too-small a_count: {a_count}') | ||||||
|                                '{}'.format(a_count)) |  | ||||||
|         if b_count < 1: |         if b_count < 1: | ||||||
|             raise PatternError('Repetition has too-small b_count: ' |             raise PatternError(f'Repetition has too-small b_count: {b_count}') | ||||||
|                                '{}'.format(b_count)) |  | ||||||
| 
 | 
 | ||||||
|         object.__setattr__(self, 'locked', False) |         object.__setattr__(self, 'locked', False) | ||||||
|         self.a_vector = a_vector |         self.a_vector = a_vector | ||||||
|         self.b_vector = b_vector |         self.b_vector = b_vector | ||||||
|         self.a_count = a_count |         self.a_count = a_count | ||||||
|         self.b_count = b_count |         self.b_count = b_count | ||||||
| 
 |  | ||||||
|         self.identifier = identifier |  | ||||||
|         self.pattern = pattern |  | ||||||
|         self.offset = offset |  | ||||||
|         self.rotation = rotation |  | ||||||
|         self.dose = dose |  | ||||||
|         self.scale = scale |  | ||||||
|         if mirrored is None: |  | ||||||
|             mirrored = [False, False] |  | ||||||
|         self.mirrored = mirrored |  | ||||||
|         self.locked = locked |         self.locked = locked | ||||||
| 
 | 
 | ||||||
|     def __setattr__(self, name, value): |     def  __copy__(self) -> 'Grid': | ||||||
|         if self.locked and name != 'locked': |         new = Grid(a_vector=self.a_vector.copy(), | ||||||
|             raise PatternLockedError() |                    b_vector=copy.copy(self.b_vector), | ||||||
|         object.__setattr__(self, name, value) |                    a_count=self.a_count, | ||||||
| 
 |                    b_count=self.b_count, | ||||||
|     def  __copy__(self) -> 'GridRepetition': |                    locked=self.locked) | ||||||
|         new = GridRepetition(pattern=self.pattern, |  | ||||||
|                              a_vector=self.a_vector.copy(), |  | ||||||
|                              b_vector=copy.copy(self.b_vector), |  | ||||||
|                              a_count=self.a_count, |  | ||||||
|                              b_count=self.b_count, |  | ||||||
|                              offset=self.offset.copy(), |  | ||||||
|                              rotation=self.rotation, |  | ||||||
|                              dose=self.dose, |  | ||||||
|                              scale=self.scale, |  | ||||||
|                              mirrored=self.mirrored.copy(), |  | ||||||
|                              locked=self.locked) |  | ||||||
|         return new |         return new | ||||||
| 
 | 
 | ||||||
|     def  __deepcopy__(self, memo: Dict = None) -> 'GridRepetition': |     def  __deepcopy__(self, memo: Dict = None) -> 'Grid': | ||||||
|         memo = {} if memo is None else memo |         memo = {} if memo is None else memo | ||||||
|         new = copy.copy(self).unlock() |         new = copy.copy(self).unlock() | ||||||
|         new.pattern = copy.deepcopy(self.pattern, memo) |  | ||||||
|         new.locked = self.locked |         new.locked = self.locked | ||||||
|         return new |         return new | ||||||
| 
 | 
 | ||||||
|     # pattern property |  | ||||||
|     @property |  | ||||||
|     def pattern(self) -> Optional['Pattern']: |  | ||||||
|         return self._pattern |  | ||||||
| 
 |  | ||||||
|     @pattern.setter |  | ||||||
|     def pattern(self, val: Optional['Pattern']): |  | ||||||
|         from .pattern import Pattern |  | ||||||
|         if val is not None and not isinstance(val, Pattern): |  | ||||||
|             raise PatternError('Provided pattern {} is not a Pattern object or None!'.format(val)) |  | ||||||
|         self._pattern = val |  | ||||||
| 
 |  | ||||||
|     # offset property |  | ||||||
|     @property |  | ||||||
|     def offset(self) -> numpy.ndarray: |  | ||||||
|         return self._offset |  | ||||||
| 
 |  | ||||||
|     @offset.setter |  | ||||||
|     def offset(self, val: vector2): |  | ||||||
|         if self.locked: |  | ||||||
|             raise PatternLockedError() |  | ||||||
| 
 |  | ||||||
|         if not isinstance(val, numpy.ndarray): |  | ||||||
|             val = numpy.array(val, dtype=float) |  | ||||||
| 
 |  | ||||||
|         if val.size != 2: |  | ||||||
|             raise PatternError('Offset must be convertible to size-2 ndarray') |  | ||||||
|         self._offset = val.flatten().astype(float) |  | ||||||
| 
 |  | ||||||
|     # dose property |  | ||||||
|     @property |  | ||||||
|     def dose(self) -> float: |  | ||||||
|         return self._dose |  | ||||||
| 
 |  | ||||||
|     @dose.setter |  | ||||||
|     def dose(self, val: float): |  | ||||||
|         if not is_scalar(val): |  | ||||||
|             raise PatternError('Dose must be a scalar') |  | ||||||
|         if not val >= 0: |  | ||||||
|             raise PatternError('Dose must be non-negative') |  | ||||||
|         self._dose = val |  | ||||||
| 
 |  | ||||||
|     # scale property |  | ||||||
|     @property |  | ||||||
|     def scale(self) -> float: |  | ||||||
|         return self._scale |  | ||||||
| 
 |  | ||||||
|     @scale.setter |  | ||||||
|     def scale(self, val: float): |  | ||||||
|         if not is_scalar(val): |  | ||||||
|             raise PatternError('Scale must be a scalar') |  | ||||||
|         if not val > 0: |  | ||||||
|             raise PatternError('Scale must be positive') |  | ||||||
|         self._scale = val |  | ||||||
| 
 |  | ||||||
|     # Rotation property [ccw] |  | ||||||
|     @property |  | ||||||
|     def rotation(self) -> float: |  | ||||||
|         return self._rotation |  | ||||||
| 
 |  | ||||||
|     @rotation.setter |  | ||||||
|     def rotation(self, val: float): |  | ||||||
|         if not is_scalar(val): |  | ||||||
|             raise PatternError('Rotation must be a scalar') |  | ||||||
|         self._rotation = val % (2 * pi) |  | ||||||
| 
 |  | ||||||
|     # Mirrored property |  | ||||||
|     @property |  | ||||||
|     def mirrored(self) -> numpy.ndarray:        # ndarray[bool] |  | ||||||
|         return self._mirrored |  | ||||||
| 
 |  | ||||||
|     @mirrored.setter |  | ||||||
|     def mirrored(self, val: Sequence[bool]): |  | ||||||
|         if is_scalar(val): |  | ||||||
|             raise PatternError('Mirrored must be a 2-element list of booleans') |  | ||||||
|         self._mirrored = numpy.array(val, dtype=bool, copy=True) |  | ||||||
| 
 |  | ||||||
|     # a_vector property |     # a_vector property | ||||||
|     @property |     @property | ||||||
|     def a_vector(self) -> numpy.ndarray: |     def a_vector(self) -> numpy.ndarray: | ||||||
| @ -320,69 +167,15 @@ class GridRepetition: | |||||||
|             raise PatternError('b_count must be convertable to an int!') |             raise PatternError('b_count must be convertable to an int!') | ||||||
|         self._b_count = int(val) |         self._b_count = int(val) | ||||||
| 
 | 
 | ||||||
|     def as_pattern(self) -> 'Pattern': |     @property | ||||||
|  |     def displacements(self) -> numpy.ndarray: | ||||||
|  |         aa, bb = numpy.meshgrid(numpy.arange(self.a_count), numpy.arange(self.b_count), indexing='ij') | ||||||
|  |         return (aa.flat[:, None] * self.a_vector[None, :] + | ||||||
|  |                 bb.flat[:, None] * self.b_vector[None, :]) | ||||||
|  | 
 | ||||||
|  |     def rotate(self, rotation: float) -> 'Grid': | ||||||
|         """ |         """ | ||||||
|         Returns a copy of self.pattern which has been scaled, rotated, repeated, etc. |         Rotate lattice vectors (around (0, 0)) | ||||||
|           etc. according to this `GridRepetition`'s properties. |  | ||||||
| 
 |  | ||||||
|         Returns: |  | ||||||
|             A copy of self.pattern which has been scaled, rotated, repeated, etc. |  | ||||||
|              etc. according to this `GridRepetition`'s properties. |  | ||||||
|         """ |  | ||||||
|         assert(self.pattern is not None) |  | ||||||
|         patterns = [] |  | ||||||
| 
 |  | ||||||
|         pat = self.pattern.deepcopy().deepunlock() |  | ||||||
|         pat.scale_by(self.scale) |  | ||||||
|         [pat.mirror(ax) for ax, do in enumerate(self.mirrored) if do] |  | ||||||
|         pat.rotate_around((0.0, 0.0), self.rotation) |  | ||||||
|         pat.translate_elements(self.offset) |  | ||||||
|         pat.scale_element_doses(self.dose) |  | ||||||
| 
 |  | ||||||
|         combined = type(pat)(name='__GridRepetition__') |  | ||||||
|         for a in range(self.a_count): |  | ||||||
|             for b in range(self.b_count): |  | ||||||
|                 offset = a * self.a_vector + b * self.b_vector |  | ||||||
|                 newPat = pat.deepcopy() |  | ||||||
|                 newPat.translate_elements(offset) |  | ||||||
|                 combined.append(newPat) |  | ||||||
| 
 |  | ||||||
|         return combined |  | ||||||
| 
 |  | ||||||
|     def translate(self, offset: vector2) -> 'GridRepetition': |  | ||||||
|         """ |  | ||||||
|         Translate by the given offset |  | ||||||
| 
 |  | ||||||
|         Args: |  | ||||||
|             offset: `[x, y]` to translate by |  | ||||||
| 
 |  | ||||||
|         Returns: |  | ||||||
|             self |  | ||||||
|         """ |  | ||||||
|         self.offset += offset |  | ||||||
|         return self |  | ||||||
| 
 |  | ||||||
|     def rotate_around(self, pivot: vector2, rotation: float) -> 'GridRepetition': |  | ||||||
|         """ |  | ||||||
|         Rotate the array around a point |  | ||||||
| 
 |  | ||||||
|         Args: |  | ||||||
|             pivot: Point `[x, y]` to rotate around |  | ||||||
|             rotation: Angle to rotate by (counterclockwise, radians) |  | ||||||
| 
 |  | ||||||
|         Returns: |  | ||||||
|             self |  | ||||||
|         """ |  | ||||||
|         pivot = numpy.array(pivot, dtype=float) |  | ||||||
|         self.translate(-pivot) |  | ||||||
|         self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) |  | ||||||
|         self.rotate(rotation) |  | ||||||
|         self.translate(+pivot) |  | ||||||
|         return self |  | ||||||
| 
 |  | ||||||
|     def rotate(self, rotation: float) -> 'GridRepetition': |  | ||||||
|         """ |  | ||||||
|         Rotate around (0, 0) |  | ||||||
| 
 | 
 | ||||||
|         Args: |         Args: | ||||||
|             rotation: Angle to rotate by (counterclockwise, radians) |             rotation: Angle to rotate by (counterclockwise, radians) | ||||||
| @ -390,28 +183,14 @@ class GridRepetition: | |||||||
|         Returns: |         Returns: | ||||||
|             self |             self | ||||||
|         """ |         """ | ||||||
|         self.rotate_elements(rotation) |  | ||||||
|         self.a_vector = numpy.dot(rotation_matrix_2d(rotation), self.a_vector) |         self.a_vector = numpy.dot(rotation_matrix_2d(rotation), self.a_vector) | ||||||
|         if self.b_vector is not None: |         if self.b_vector is not None: | ||||||
|             self.b_vector = numpy.dot(rotation_matrix_2d(rotation), self.b_vector) |             self.b_vector = numpy.dot(rotation_matrix_2d(rotation), self.b_vector) | ||||||
|         return self |         return self | ||||||
| 
 | 
 | ||||||
|     def rotate_elements(self, rotation: float) -> 'GridRepetition': |     def mirror(self, axis: int) -> 'Grid': | ||||||
|         """ |         """ | ||||||
|         Rotate each element around its origin |         Mirror the Grid across an axis. | ||||||
| 
 |  | ||||||
|         Args: |  | ||||||
|             rotation: Angle to rotate by (counterclockwise, radians) |  | ||||||
| 
 |  | ||||||
|         Returns: |  | ||||||
|             self |  | ||||||
|         """ |  | ||||||
|         self.rotation += rotation |  | ||||||
|         return self |  | ||||||
| 
 |  | ||||||
|     def mirror(self, axis: int) -> 'GridRepetition': |  | ||||||
|         """ |  | ||||||
|         Mirror the GridRepetition across an axis. |  | ||||||
| 
 | 
 | ||||||
|         Args: |         Args: | ||||||
|             axis: Axis to mirror across. |             axis: Axis to mirror across. | ||||||
| @ -420,43 +199,30 @@ class GridRepetition: | |||||||
|         Returns: |         Returns: | ||||||
|             self |             self | ||||||
|         """ |         """ | ||||||
|         self.mirror_elements(axis) |  | ||||||
|         self.a_vector[1-axis] *= -1 |         self.a_vector[1-axis] *= -1 | ||||||
|         if self.b_vector is not None: |         if self.b_vector is not None: | ||||||
|             self.b_vector[1-axis] *= -1 |             self.b_vector[1-axis] *= -1 | ||||||
|         return self |         return self | ||||||
| 
 | 
 | ||||||
|     def mirror_elements(self, axis: int) -> 'GridRepetition': |  | ||||||
|         """ |  | ||||||
|         Mirror each element across an axis relative to its origin. |  | ||||||
| 
 |  | ||||||
|         Args: |  | ||||||
|             axis: Axis to mirror across. |  | ||||||
|                 (0: mirror across x-axis, 1: mirror across y-axis) |  | ||||||
| 
 |  | ||||||
|         Returns: |  | ||||||
|             self |  | ||||||
|         """ |  | ||||||
|         self.mirrored[axis] = not self.mirrored[axis] |  | ||||||
|         self.rotation *= -1 |  | ||||||
|         return self |  | ||||||
| 
 |  | ||||||
|     def get_bounds(self) -> Optional[numpy.ndarray]: |     def get_bounds(self) -> Optional[numpy.ndarray]: | ||||||
|         """ |         """ | ||||||
|         Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the |         Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the | ||||||
|          extent of the `GridRepetition` in each dimension. |          extent of the `Grid` in each dimension. | ||||||
|         Returns `None` if the contained `Pattern` is empty. |  | ||||||
| 
 | 
 | ||||||
|         Returns: |         Returns: | ||||||
|             `[[x_min, y_min], [x_max, y_max]]` or `None` |             `[[x_min, y_min], [x_max, y_max]]` or `None` | ||||||
|         """ |         """ | ||||||
|         if self.pattern is None: |         a_extent = self.a_vector * self.a_count | ||||||
|             return None |         b_extent = self.b_vector * self.b_count if self.b_count != 0 else 0 | ||||||
|         return self.as_pattern().get_bounds() |  | ||||||
| 
 | 
 | ||||||
|     def scale_by(self, c: float) -> 'GridRepetition': |         corners = ((0, 0), a_extent, b_extent, a_extent + b_extent) | ||||||
|  |         xy_min = numpy.min(corners, axis=0) | ||||||
|  |         xy_max = numpy.min(corners, axis=0) | ||||||
|  |         return numpy.array((xy_min, xy_max)) | ||||||
|  | 
 | ||||||
|  |     def scale_by(self, c: float) -> 'Grid': | ||||||
|         """ |         """ | ||||||
|         Scale the GridRepetition by a factor |         Scale the Grid by a factor | ||||||
| 
 | 
 | ||||||
|         Args: |         Args: | ||||||
|             c: scaling factor |             c: scaling factor | ||||||
| @ -464,107 +230,116 @@ class GridRepetition: | |||||||
|         Returns: |         Returns: | ||||||
|             self |             self | ||||||
|         """ |         """ | ||||||
|         self.scale_elements_by(c) |  | ||||||
|         self.a_vector *= c |         self.a_vector *= c | ||||||
|         if self.b_vector is not None: |         if self.b_vector is not None: | ||||||
|             self.b_vector *= c |             self.b_vector *= c | ||||||
|         return self |         return self | ||||||
| 
 | 
 | ||||||
|     def scale_elements_by(self, c: float) -> 'GridRepetition': |     def lock(self) -> 'Grid': | ||||||
|         """ |         """ | ||||||
|         Scale each element by a factor |         Lock the `Grid`, disallowing changes. | ||||||
| 
 |  | ||||||
|         Args: |  | ||||||
|             c: scaling factor |  | ||||||
| 
 | 
 | ||||||
|         Returns: |         Returns: | ||||||
|             self |             self | ||||||
|         """ |         """ | ||||||
|         self.scale *= c |  | ||||||
|         return self |  | ||||||
| 
 |  | ||||||
|     def copy(self) -> 'GridRepetition': |  | ||||||
|         """ |  | ||||||
|         Return a shallow copy of the repetition. |  | ||||||
| 
 |  | ||||||
|         Returns: |  | ||||||
|             `copy.copy(self)` |  | ||||||
|         """ |  | ||||||
|         return copy.copy(self) |  | ||||||
| 
 |  | ||||||
|     def deepcopy(self) -> 'GridRepetition': |  | ||||||
|         """ |  | ||||||
|         Return a deep copy of the repetition. |  | ||||||
| 
 |  | ||||||
|         Returns: |  | ||||||
|             `copy.deepcopy(self)` |  | ||||||
|         """ |  | ||||||
|         return copy.deepcopy(self) |  | ||||||
| 
 |  | ||||||
|     def lock(self) -> 'GridRepetition': |  | ||||||
|         """ |  | ||||||
|         Lock the `GridRepetition`, disallowing changes. |  | ||||||
| 
 |  | ||||||
|         Returns: |  | ||||||
|             self |  | ||||||
|         """ |  | ||||||
|         self.offset.flags.writeable = False |  | ||||||
|         self.a_vector.flags.writeable = False |         self.a_vector.flags.writeable = False | ||||||
|         self.mirrored.flags.writeable = False |  | ||||||
|         if self.b_vector is not None: |         if self.b_vector is not None: | ||||||
|             self.b_vector.flags.writeable = False |             self.b_vector.flags.writeable = False | ||||||
|         object.__setattr__(self, 'locked', True) |         LockableImpl.lock(self) | ||||||
|         return self |         return self | ||||||
| 
 | 
 | ||||||
|     def unlock(self) -> 'GridRepetition': |     def unlock(self) -> 'Grid': | ||||||
|         """ |         """ | ||||||
|         Unlock the `GridRepetition` |         Unlock the `Grid` | ||||||
| 
 | 
 | ||||||
|         Returns: |         Returns: | ||||||
|             self |             self | ||||||
|         """ |         """ | ||||||
|         self.offset.flags.writeable = True |  | ||||||
|         self.a_vector.flags.writeable = True |         self.a_vector.flags.writeable = True | ||||||
|         self.mirrored.flags.writeable = True |  | ||||||
|         if self.b_vector is not None: |         if self.b_vector is not None: | ||||||
|             self.b_vector.flags.writeable = True |             self.b_vector.flags.writeable = True | ||||||
|         object.__setattr__(self, 'locked', False) |         LockableImpl.unlock(self) | ||||||
|         return self |  | ||||||
| 
 |  | ||||||
|     def deeplock(self) -> 'GridRepetition': |  | ||||||
|         """ |  | ||||||
|         Recursively lock the `GridRepetition` and its contained pattern |  | ||||||
| 
 |  | ||||||
|         Returns: |  | ||||||
|             self |  | ||||||
|         """ |  | ||||||
|         assert(self.pattern is not None) |  | ||||||
|         self.lock() |  | ||||||
|         self.pattern.deeplock() |  | ||||||
|         return self |  | ||||||
| 
 |  | ||||||
|     def deepunlock(self) -> 'GridRepetition': |  | ||||||
|         """ |  | ||||||
|         Recursively unlock the `GridRepetition` and its contained pattern |  | ||||||
| 
 |  | ||||||
|         This is dangerous unless you have just performed a deepcopy, since |  | ||||||
|             the component parts may be reused elsewhere. |  | ||||||
| 
 |  | ||||||
|         Returns: |  | ||||||
|             self |  | ||||||
|         """ |  | ||||||
|         assert(self.pattern is not None) |  | ||||||
|         self.unlock() |  | ||||||
|         self.pattern.deepunlock() |  | ||||||
|         return self |         return self | ||||||
| 
 | 
 | ||||||
|     def __repr__(self) -> str: |     def __repr__(self) -> str: | ||||||
|         name = self.pattern.name if self.pattern is not None else None |  | ||||||
|         rotation = f' r{self.rotation*180/pi:g}' if self.rotation != 0 else '' |  | ||||||
|         scale = f' d{self.scale:g}' if self.scale != 1 else '' |  | ||||||
|         mirrored = ' m{:d}{:d}'.format(*self.mirrored) if self.mirrored.any() else '' |  | ||||||
|         dose = f' d{self.dose:g}' if self.dose != 1 else '' |  | ||||||
|         locked = ' L' if self.locked else '' |         locked = ' L' if self.locked else '' | ||||||
|         bv = f', {self.b_vector}' if self.b_vector is not None else '' |         bv = f', {self.b_vector}' if self.b_vector is not None else '' | ||||||
|         return (f'<GridRepetition "{name}" at {self.offset} {rotation}{scale}{mirrored}{dose}' |         return (f'<Grid {self.a_count}x{self.b_count} ({self.a_vector}{bv}){locked}>') | ||||||
|                 f' {self.a_count}x{self.b_count} ({self.a_vector}{bv}){locked}>') | 
 | ||||||
|  |     def __eq__(self, other: Any) -> bool: | ||||||
|  |         if not isinstance(other, type(self)): | ||||||
|  |             return False | ||||||
|  |         if self.a_count != other.a_count or self.b_count != other.b_count: | ||||||
|  |             return False | ||||||
|  |         if any(self.a_vector[ii] != other.a_vector[ii] for ii in range(2)): | ||||||
|  |             return False | ||||||
|  |         if self.b_vector is None and other.b_vector is None: | ||||||
|  |             return True | ||||||
|  |         if self.b_vector is None or other.b_vector is None: | ||||||
|  |             return False | ||||||
|  |         if any(self.b_vector[ii] != other.b_vector[ii] for ii in range(2)): | ||||||
|  |             return False | ||||||
|  |         if self.locked != other.locked: | ||||||
|  |             return False | ||||||
|  |         return True | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Arbitrary(LockableImpl, Repetition, metaclass=AutoSlots): | ||||||
|  |     """ | ||||||
|  |     `Arbitrary` is a simple list of (absolute) displacements for instances. | ||||||
|  | 
 | ||||||
|  |     Attributes: | ||||||
|  |         displacements (numpy.ndarray): absolute displacements of all elements | ||||||
|  |                                        `[[x0, y0], [x1, y1], ...]` | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     _displacements: numpy.ndarray | ||||||
|  |     """ List of vectors `[[x0, y0], [x1, y1], ...]` specifying the offsets | ||||||
|  |           of the instances. | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     locked: bool | ||||||
|  |     """ If `True`, disallows changes to the object. """ | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def displacements(self) -> numpy.ndarray: | ||||||
|  |         return self._displacements | ||||||
|  | 
 | ||||||
|  |     @displacements.setter | ||||||
|  |     def displacements(self, val: Union[Sequence[Sequence[float]], numpy.ndarray]): | ||||||
|  |         val = numpy.array(val, float) | ||||||
|  |         val = numpy.sort(val.view([('', val.dtype)] * val.shape[1]), 0).view(val.dtype)    # sort rows | ||||||
|  |         self._displacements = val | ||||||
|  | 
 | ||||||
|  |     def lock(self) -> 'Arbitrary': | ||||||
|  |         """ | ||||||
|  |         Lock the object, disallowing changes. | ||||||
|  | 
 | ||||||
|  |         Returns: | ||||||
|  |             self | ||||||
|  |         """ | ||||||
|  |         self._displacements.flags.writeable = False | ||||||
|  |         LockableImpl.lock(self) | ||||||
|  |         return self | ||||||
|  | 
 | ||||||
|  |     def unlock(self) -> 'Arbitrary': | ||||||
|  |         """ | ||||||
|  |         Unlock the object | ||||||
|  | 
 | ||||||
|  |         Returns: | ||||||
|  |             self | ||||||
|  |         """ | ||||||
|  |         self._displacements.flags.writeable = True | ||||||
|  |         LockableImpl.unlock(self) | ||||||
|  |         return self | ||||||
|  | 
 | ||||||
|  |     def __repr__(self) -> str: | ||||||
|  |         locked = ' L' if self.locked else '' | ||||||
|  |         return (f'<Arbitrary {len(self.displacements)}pts {locked}>') | ||||||
|  | 
 | ||||||
|  |     def __eq__(self, other: Any) -> bool: | ||||||
|  |         if not isinstance(other, type(self)): | ||||||
|  |             return False | ||||||
|  |         if self.locked != other.locked: | ||||||
|  |             return False | ||||||
|  |         return numpy.array_equal(self.displacements, other.displacements) | ||||||
|  | |||||||
| @ -6,10 +6,10 @@ from numpy import pi | |||||||
| 
 | 
 | ||||||
| from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS | from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS | ||||||
| from .. import PatternError | from .. import PatternError | ||||||
| from ..utils import is_scalar, vector2, layer_t | from ..utils import is_scalar, vector2, layer_t, AutoSlots | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Arc(Shape): | class Arc(Shape, metaclass=AutoSlots): | ||||||
|     """ |     """ | ||||||
|     An elliptical arc, formed by cutting off an elliptical ring with two rays which exit from its |     An elliptical arc, formed by cutting off an elliptical ring with two rays which exit from its | ||||||
|      center. It has a position, two radii, a start and stop angle, a rotation, and a width. |      center. It has a position, two radii, a start and stop angle, a rotation, and a width. | ||||||
| @ -20,6 +20,7 @@ class Arc(Shape): | |||||||
|     """ |     """ | ||||||
|     __slots__ = ('_radii', '_angles', '_width', '_rotation', |     __slots__ = ('_radii', '_angles', '_width', '_rotation', | ||||||
|                  'poly_num_points', 'poly_max_arclen') |                  'poly_num_points', 'poly_max_arclen') | ||||||
|  | 
 | ||||||
|     _radii: numpy.ndarray |     _radii: numpy.ndarray | ||||||
|     """ Two radii for defining an ellipse """ |     """ Two radii for defining an ellipse """ | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -5,14 +5,15 @@ from numpy import pi | |||||||
| 
 | 
 | ||||||
| from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS | from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS | ||||||
| from .. import PatternError | from .. import PatternError | ||||||
| from ..utils import is_scalar, vector2, layer_t | from ..utils import is_scalar, vector2, layer_t, AutoSlots | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Circle(Shape): | class Circle(Shape, metaclass=AutoSlots): | ||||||
|     """ |     """ | ||||||
|     A circle, which has a position and radius. |     A circle, which has a position and radius. | ||||||
|     """ |     """ | ||||||
|     __slots__ = ('_radius', 'poly_num_points', 'poly_max_arclen') |     __slots__ = ('_radius', 'poly_num_points', 'poly_max_arclen') | ||||||
|  | 
 | ||||||
|     _radius: float |     _radius: float | ||||||
|     """ Circle radius """ |     """ Circle radius """ | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -6,16 +6,17 @@ from numpy import pi | |||||||
| 
 | 
 | ||||||
| from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS | from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS | ||||||
| from .. import PatternError | from .. import PatternError | ||||||
| from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t | from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t, AutoSlots | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Ellipse(Shape): | class Ellipse(Shape, metaclass=AutoSlots): | ||||||
|     """ |     """ | ||||||
|     An ellipse, which has a position, two radii, and a rotation. |     An ellipse, which has a position, two radii, and a rotation. | ||||||
|     The rotation gives the angle from x-axis, counterclockwise, to the first (x) radius. |     The rotation gives the angle from x-axis, counterclockwise, to the first (x) radius. | ||||||
|     """ |     """ | ||||||
|     __slots__ = ('_radii', '_rotation', |     __slots__ = ('_radii', '_rotation', | ||||||
|                  'poly_num_points', 'poly_max_arclen') |                  'poly_num_points', 'poly_max_arclen') | ||||||
|  | 
 | ||||||
|     _radii: numpy.ndarray |     _radii: numpy.ndarray | ||||||
|     """ Ellipse radii """ |     """ Ellipse radii """ | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -6,7 +6,7 @@ from numpy import pi, inf | |||||||
| 
 | 
 | ||||||
| from . import Shape, normalized_shape_tuple, Polygon, Circle | from . import Shape, normalized_shape_tuple, Polygon, Circle | ||||||
| from .. import PatternError | from .. import PatternError | ||||||
| from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t | from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t, AutoSlots | ||||||
| from ..utils import remove_colinear_vertices, remove_duplicate_vertices | from ..utils import remove_colinear_vertices, remove_duplicate_vertices | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @ -18,7 +18,7 @@ class PathCap(Enum): | |||||||
|                       #     defined by path.cap_extensions |                       #     defined by path.cap_extensions | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Path(Shape): | class Path(Shape, metaclass=AutoSlots): | ||||||
|     """ |     """ | ||||||
|     A path, consisting of a bunch of vertices (Nx2 ndarray), a width, an end-cap shape, |     A path, consisting of a bunch of vertices (Nx2 ndarray), a width, an end-cap shape, | ||||||
|         and an offset. |         and an offset. | ||||||
|  | |||||||
| @ -5,11 +5,11 @@ from numpy import pi | |||||||
| 
 | 
 | ||||||
| from . import Shape, normalized_shape_tuple | from . import Shape, normalized_shape_tuple | ||||||
| from .. import PatternError | from .. import PatternError | ||||||
| from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t | from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t, AutoSlots | ||||||
| from ..utils import remove_colinear_vertices, remove_duplicate_vertices | from ..utils import remove_colinear_vertices, remove_duplicate_vertices | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Polygon(Shape): | class Polygon(Shape, metaclass=AutoSlots): | ||||||
|     """ |     """ | ||||||
|     A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an |     A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an | ||||||
|        implicitly-closed boundary, and an offset. |        implicitly-closed boundary, and an offset. | ||||||
| @ -17,6 +17,7 @@ class Polygon(Shape): | |||||||
|     A `normalized_form(...)` is available, but can be quite slow with lots of vertices. |     A `normalized_form(...)` is available, but can be quite slow with lots of vertices. | ||||||
|     """ |     """ | ||||||
|     __slots__ = ('_vertices',) |     __slots__ = ('_vertices',) | ||||||
|  | 
 | ||||||
|     _vertices: numpy.ndarray |     _vertices: numpy.ndarray | ||||||
|     """ Nx2 ndarray of vertices `[[x0, y0], [x1, y1], ...]` """ |     """ Nx2 ndarray of vertices `[[x0, y0], [x1, y1], ...]` """ | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -5,6 +5,9 @@ import numpy | |||||||
| 
 | 
 | ||||||
| from ..error import PatternError, PatternLockedError | from ..error import PatternError, PatternLockedError | ||||||
| from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t | from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t | ||||||
|  | from ..traits import (PositionableImpl, LayerableImpl, DoseableImpl, | ||||||
|  |                       Rotatable, Mirrorable, Copyable, Scalable, | ||||||
|  |                       PivotableImpl, LockableImpl) | ||||||
| 
 | 
 | ||||||
| if TYPE_CHECKING: | if TYPE_CHECKING: | ||||||
|     from . import Polygon |     from . import Polygon | ||||||
| @ -23,38 +26,20 @@ DEFAULT_POLY_NUM_POINTS = 24 | |||||||
| T = TypeVar('T', bound='Shape') | T = TypeVar('T', bound='Shape') | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Shape(metaclass=ABCMeta): | class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable, Copyable, Scalable, PivotableImpl, LockableImpl, metaclass=ABCMeta): | ||||||
|     """ |     """ | ||||||
|     Abstract class specifying functions common to all shapes. |     Abstract class specifying functions common to all shapes. | ||||||
|     """ |     """ | ||||||
|     __slots__ = ('_offset', '_layer', '_dose', 'identifier', 'locked') |  | ||||||
| 
 |  | ||||||
|     _offset: numpy.ndarray |  | ||||||
|     """ `[x_offset, y_offset]` """ |  | ||||||
| 
 |  | ||||||
|     _layer: layer_t |  | ||||||
|     """ Layer (integer >= 0 or tuple) """ |  | ||||||
| 
 |  | ||||||
|     _dose: float |  | ||||||
|     """ Dose """ |  | ||||||
| 
 | 
 | ||||||
|     identifier: Tuple |     identifier: Tuple | ||||||
|     """ An arbitrary identifier for the shape, usually empty but used by `Pattern.flatten()` """ |     """ An arbitrary identifier for the shape, usually empty but used by `Pattern.flatten()` """ | ||||||
| 
 | 
 | ||||||
|     locked: bool | #    def __copy__(self) -> 'Shape': | ||||||
|     """ If `True`, any changes to the shape will raise a `PatternLockedError` """ | #        cls = self.__class__ | ||||||
| 
 | #        new = cls.__new__(cls) | ||||||
|     def __setattr__(self, name, value): | #        for name in Shape.__slots__ + self.__slots__: | ||||||
|         if self.locked and name != 'locked': | #            object.__setattr__(new, name, getattr(self, name)) | ||||||
|             raise PatternLockedError() | #        return new | ||||||
|         object.__setattr__(self, name, value) |  | ||||||
| 
 |  | ||||||
|     def __copy__(self) -> 'Shape': |  | ||||||
|         cls = self.__class__ |  | ||||||
|         new = cls.__new__(cls) |  | ||||||
|         for name in Shape.__slots__ + self.__slots__: |  | ||||||
|             object.__setattr__(new, name, getattr(self, name)) |  | ||||||
|         return new |  | ||||||
| 
 | 
 | ||||||
|     ''' |     ''' | ||||||
|     --- Abstract methods |     --- Abstract methods | ||||||
| @ -79,53 +64,6 @@ class Shape(metaclass=ABCMeta): | |||||||
|         """ |         """ | ||||||
|         pass |         pass | ||||||
| 
 | 
 | ||||||
|     @abstractmethod |  | ||||||
|     def get_bounds(self) -> numpy.ndarray: |  | ||||||
|         """ |  | ||||||
|         Returns `[[x_min, y_min], [x_max, y_max]]` which specify a minimal bounding box for the shape. |  | ||||||
|         """ |  | ||||||
|         pass |  | ||||||
| 
 |  | ||||||
|     @abstractmethod |  | ||||||
|     def rotate(self: T, theta: float) -> T: |  | ||||||
|         """ |  | ||||||
|         Rotate the shape around its origin (0, 0), ignoring its offset. |  | ||||||
| 
 |  | ||||||
|         Args: |  | ||||||
|             theta: Angle to rotate by (counterclockwise, radians) |  | ||||||
| 
 |  | ||||||
|         Returns: |  | ||||||
|             self |  | ||||||
|         """ |  | ||||||
|         pass |  | ||||||
| 
 |  | ||||||
|     @abstractmethod |  | ||||||
|     def mirror(self: T, axis: int) -> T: |  | ||||||
|         """ |  | ||||||
|         Mirror the shape across an axis. |  | ||||||
| 
 |  | ||||||
|         Args: |  | ||||||
|             axis: Axis to mirror across. |  | ||||||
|                 (0: mirror across x axis, 1: mirror across y axis) |  | ||||||
| 
 |  | ||||||
|         Returns: |  | ||||||
|             self |  | ||||||
|         """ |  | ||||||
|         pass |  | ||||||
| 
 |  | ||||||
|     @abstractmethod |  | ||||||
|     def scale_by(self: T, c: float) -> T: |  | ||||||
|         """ |  | ||||||
|         Scale the shape's size (eg. radius, for a circle) by a constant factor. |  | ||||||
| 
 |  | ||||||
|         Args: |  | ||||||
|             c: Factor to scale by |  | ||||||
| 
 |  | ||||||
|         Returns: |  | ||||||
|             self |  | ||||||
|         """ |  | ||||||
|         pass |  | ||||||
| 
 |  | ||||||
|     @abstractmethod |     @abstractmethod | ||||||
|     def normalized_form(self: T, norm_value: int) -> normalized_shape_tuple: |     def normalized_form(self: T, norm_value: int) -> normalized_shape_tuple: | ||||||
|         """ |         """ | ||||||
| @ -150,97 +88,9 @@ class Shape(metaclass=ABCMeta): | |||||||
|         """ |         """ | ||||||
|         pass |         pass | ||||||
| 
 | 
 | ||||||
|     ''' |  | ||||||
|     ---- Non-abstract properties |  | ||||||
|     ''' |  | ||||||
|     # offset property |  | ||||||
|     @property |  | ||||||
|     def offset(self) -> numpy.ndarray: |  | ||||||
|         """ |  | ||||||
|         [x, y] offset |  | ||||||
|         """ |  | ||||||
|         return self._offset |  | ||||||
| 
 |  | ||||||
|     @offset.setter |  | ||||||
|     def offset(self, val: vector2): |  | ||||||
|         if not isinstance(val, numpy.ndarray): |  | ||||||
|             val = numpy.array(val, dtype=float) |  | ||||||
| 
 |  | ||||||
|         if val.size != 2: |  | ||||||
|             raise PatternError('Offset must be convertible to size-2 ndarray') |  | ||||||
|         self._offset = val.flatten() |  | ||||||
| 
 |  | ||||||
|     # layer property |  | ||||||
|     @property |  | ||||||
|     def layer(self) -> layer_t: |  | ||||||
|         """ |  | ||||||
|         Layer number or name (int, tuple of ints, or string) |  | ||||||
|         """ |  | ||||||
|         return self._layer |  | ||||||
| 
 |  | ||||||
|     @layer.setter |  | ||||||
|     def layer(self, val: layer_t): |  | ||||||
|         self._layer = val |  | ||||||
| 
 |  | ||||||
|     # dose property |  | ||||||
|     @property |  | ||||||
|     def dose(self) -> float: |  | ||||||
|         """ |  | ||||||
|         Dose (float >= 0) |  | ||||||
|         """ |  | ||||||
|         return self._dose |  | ||||||
| 
 |  | ||||||
|     @dose.setter |  | ||||||
|     def dose(self, val: float): |  | ||||||
|         if not is_scalar(val): |  | ||||||
|             raise PatternError('Dose must be a scalar') |  | ||||||
|         if not val >= 0: |  | ||||||
|             raise PatternError('Dose must be non-negative') |  | ||||||
|         self._dose = val |  | ||||||
| 
 |  | ||||||
|     ''' |     ''' | ||||||
|     ---- Non-abstract methods |     ---- Non-abstract methods | ||||||
|     ''' |     ''' | ||||||
|     def copy(self: T) -> T: |  | ||||||
|         """ |  | ||||||
|         Returns a deep copy of the shape. |  | ||||||
| 
 |  | ||||||
|         Returns: |  | ||||||
|             copy.deepcopy(self) |  | ||||||
|         """ |  | ||||||
|         return copy.deepcopy(self) |  | ||||||
| 
 |  | ||||||
|     def translate(self: T, offset: vector2) -> T: |  | ||||||
|         """ |  | ||||||
|         Translate the shape by the given offset |  | ||||||
| 
 |  | ||||||
|         Args: |  | ||||||
|             offset: [x_offset, y,offset] |  | ||||||
| 
 |  | ||||||
|         Returns: |  | ||||||
|             self |  | ||||||
|         """ |  | ||||||
|         self.offset += offset |  | ||||||
|         return self |  | ||||||
| 
 |  | ||||||
|     def rotate_around(self: T, pivot: vector2, rotation: float) -> T: |  | ||||||
|         """ |  | ||||||
|         Rotate the shape around a point. |  | ||||||
| 
 |  | ||||||
|         Args: |  | ||||||
|             pivot: Point (x, y) to rotate around |  | ||||||
|             rotation: Angle to rotate by (counterclockwise, radians) |  | ||||||
| 
 |  | ||||||
|         Returns: |  | ||||||
|             self |  | ||||||
|         """ |  | ||||||
|         pivot = numpy.array(pivot, dtype=float) |  | ||||||
|         self.translate(-pivot) |  | ||||||
|         self.rotate(rotation) |  | ||||||
|         self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) |  | ||||||
|         self.translate(+pivot) |  | ||||||
|         return self |  | ||||||
| 
 |  | ||||||
|     def manhattanize_fast(self, |     def manhattanize_fast(self, | ||||||
|                           grid_x: numpy.ndarray, |                           grid_x: numpy.ndarray, | ||||||
|                           grid_y: numpy.ndarray, |                           grid_y: numpy.ndarray, | ||||||
| @ -442,37 +292,12 @@ class Shape(metaclass=ABCMeta): | |||||||
| 
 | 
 | ||||||
|         return manhattan_polygons |         return manhattan_polygons | ||||||
| 
 | 
 | ||||||
|     def set_layer(self: T, layer: layer_t) -> T: |  | ||||||
|         """ |  | ||||||
|         Chainable method for changing the layer. |  | ||||||
| 
 |  | ||||||
|         Args: |  | ||||||
|             layer: new value for self.layer |  | ||||||
| 
 |  | ||||||
|         Returns: |  | ||||||
|             self |  | ||||||
|         """ |  | ||||||
|         self.layer = layer |  | ||||||
|         return self |  | ||||||
| 
 |  | ||||||
|     def lock(self: T) -> T: |     def lock(self: T) -> T: | ||||||
|         """ |         PositionableImpl._lock(self) | ||||||
|         Lock the Shape, disallowing further changes |         LockableImpl.lock(self) | ||||||
| 
 |  | ||||||
|         Returns: |  | ||||||
|             self |  | ||||||
|         """ |  | ||||||
|         self.offset.flags.writeable = False |  | ||||||
|         object.__setattr__(self, 'locked', True) |  | ||||||
|         return self |         return self | ||||||
| 
 | 
 | ||||||
|     def unlock(self: T) -> T: |     def unlock(self: T) -> T: | ||||||
|         """ |         LockableImpl.unlock(self) | ||||||
|         Unlock the Shape |         PositionableImpl._unlock(self) | ||||||
| 
 |  | ||||||
|         Returns: |  | ||||||
|             self |  | ||||||
|         """ |  | ||||||
|         object.__setattr__(self, 'locked', False) |  | ||||||
|         self.offset.flags.writeable = True |  | ||||||
|         return self |         return self | ||||||
|  | |||||||
| @ -5,22 +5,23 @@ from numpy import pi, inf | |||||||
| 
 | 
 | ||||||
| from . import Shape, Polygon, normalized_shape_tuple | from . import Shape, Polygon, normalized_shape_tuple | ||||||
| from .. import PatternError | from .. import PatternError | ||||||
| from ..utils import is_scalar, vector2, get_bit, normalize_mirror, layer_t | from ..traits import RotatableImpl | ||||||
|  | from ..utils import is_scalar, vector2, get_bit, normalize_mirror, layer_t, AutoSlots | ||||||
| 
 | 
 | ||||||
| # Loaded on use: | # Loaded on use: | ||||||
| # from freetype import Face | # from freetype import Face | ||||||
| # from matplotlib.path import Path | # from matplotlib.path import Path | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Text(Shape): | class Text(RotatableImpl, Shape, metaclass=AutoSlots): | ||||||
|     """ |     """ | ||||||
|     Text (to be printed e.g. as a set of polygons). |     Text (to be printed e.g. as a set of polygons). | ||||||
|     This is distinct from non-printed Label objects. |     This is distinct from non-printed Label objects. | ||||||
|     """ |     """ | ||||||
|     __slots__ = ('_string', '_height', '_rotation', '_mirrored', 'font_path') |     __slots__ = ('_string', '_height', '_mirrored', 'font_path') | ||||||
|  | 
 | ||||||
|     _string: str |     _string: str | ||||||
|     _height: float |     _height: float | ||||||
|     _rotation: float |  | ||||||
|     _mirrored: numpy.ndarray        #ndarray[bool] |     _mirrored: numpy.ndarray        #ndarray[bool] | ||||||
|     font_path: str |     font_path: str | ||||||
| 
 | 
 | ||||||
| @ -33,17 +34,6 @@ class Text(Shape): | |||||||
|     def string(self, val: str): |     def string(self, val: str): | ||||||
|         self._string = val |         self._string = val | ||||||
| 
 | 
 | ||||||
|     # Rotation property |  | ||||||
|     @property |  | ||||||
|     def rotation(self) -> float: |  | ||||||
|         return self._rotation |  | ||||||
| 
 |  | ||||||
|     @rotation.setter |  | ||||||
|     def rotation(self, val: float): |  | ||||||
|         if not is_scalar(val): |  | ||||||
|             raise PatternError('Rotation must be a scalar') |  | ||||||
|         self._rotation = val % (2 * pi) |  | ||||||
| 
 |  | ||||||
|     # Height property |     # Height property | ||||||
|     @property |     @property | ||||||
|     def height(self) -> float: |     def height(self) -> float: | ||||||
| @ -120,10 +110,6 @@ class Text(Shape): | |||||||
| 
 | 
 | ||||||
|         return all_polygons |         return all_polygons | ||||||
| 
 | 
 | ||||||
|     def rotate(self, theta: float) -> 'Text': |  | ||||||
|         self.rotation += theta |  | ||||||
|         return self |  | ||||||
| 
 |  | ||||||
|     def mirror(self, axis: int) -> 'Text': |     def mirror(self, axis: int) -> 'Text': | ||||||
|         self.mirrored[axis] = not self.mirrored[axis] |         self.mirrored[axis] = not self.mirrored[axis] | ||||||
|         return self |         return self | ||||||
|  | |||||||
| @ -11,51 +11,50 @@ import numpy | |||||||
| from numpy import pi | from numpy import pi | ||||||
| 
 | 
 | ||||||
| from .error import PatternError, PatternLockedError | from .error import PatternError, PatternLockedError | ||||||
| from .utils import is_scalar, rotation_matrix_2d, vector2 | from .utils import is_scalar, rotation_matrix_2d, vector2, AutoSlots | ||||||
| from .repetition import GridRepetition | from .repetition import Repetition | ||||||
|  | from .traits import (PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, | ||||||
|  |                      Mirrorable, Pivotable, Copyable, LockableImpl, RepeatableImpl) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| if TYPE_CHECKING: | if TYPE_CHECKING: | ||||||
|     from . import Pattern |     from . import Pattern | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class SubPattern: | class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mirrorable, | ||||||
|  |                  Pivotable, Copyable, RepeatableImpl, LockableImpl, metaclass=AutoSlots): | ||||||
|     """ |     """ | ||||||
|     SubPattern provides basic support for nesting Pattern objects within each other, by adding |     SubPattern provides basic support for nesting Pattern objects within each other, by adding | ||||||
|      offset, rotation, scaling, and associated methods. |      offset, rotation, scaling, and associated methods. | ||||||
|     """ |     """ | ||||||
|     __slots__ = ('_pattern', |     __slots__ = ('_pattern', | ||||||
|                  '_offset', |  | ||||||
|                  '_rotation', |  | ||||||
|                  '_dose', |  | ||||||
|                  '_scale', |  | ||||||
|                  '_mirrored', |                  '_mirrored', | ||||||
|                  'identifier', |                  'identifier', | ||||||
|                  'locked') |                  ) | ||||||
| 
 | 
 | ||||||
|     _pattern: Optional['Pattern'] |     _pattern: Optional['Pattern'] | ||||||
|     """ The `Pattern` being instanced """ |     """ The `Pattern` being instanced """ | ||||||
| 
 | 
 | ||||||
|     _offset: numpy.ndarray | #    _offset: numpy.ndarray | ||||||
|     """ (x, y) offset for the instance """ | #    """ (x, y) offset for the instance """ | ||||||
| 
 | 
 | ||||||
|     _rotation: float | #    _rotation: float | ||||||
|     """ rotation for the instance, radians counterclockwise """ | #    """ rotation for the instance, radians counterclockwise """ | ||||||
| 
 | 
 | ||||||
|     _dose: float | #    _dose: float | ||||||
|     """ dose factor for the instance """ | #    """ dose factor for the instance """ | ||||||
| 
 | 
 | ||||||
|     _scale: float | #    _scale: float | ||||||
|     """ scale factor for the instance """ | #    """ scale factor for the instance """ | ||||||
| 
 | 
 | ||||||
|     _mirrored: numpy.ndarray        # ndarray[bool] |     _mirrored: numpy.ndarray        # ndarray[bool] | ||||||
|     """ Whether to mirror the instanc across the x and/or y axes. """ |     """ Whether to mirror the instance across the x and/or y axes. """ | ||||||
| 
 | 
 | ||||||
|     identifier: Tuple[Any, ...] |     identifier: Tuple[Any, ...] | ||||||
|     """ Arbitrary identifier, used internally by some `masque` functions. """ |     """ Arbitrary identifier, used internally by some `masque` functions. """ | ||||||
| 
 | 
 | ||||||
|     locked: bool | #    locked: bool | ||||||
|     """ If `True`, disallows changes to the GridRepetition """ | #    """ If `True`, disallows changes to the SubPattern""" | ||||||
| 
 | 
 | ||||||
|     def __init__(self, |     def __init__(self, | ||||||
|                  pattern: Optional['Pattern'], |                  pattern: Optional['Pattern'], | ||||||
| @ -64,6 +63,7 @@ class SubPattern: | |||||||
|                  mirrored: Optional[Sequence[bool]] = None, |                  mirrored: Optional[Sequence[bool]] = None, | ||||||
|                  dose: float = 1.0, |                  dose: float = 1.0, | ||||||
|                  scale: float = 1.0, |                  scale: float = 1.0, | ||||||
|  |                  repetition: Optional[Repetition] = None, | ||||||
|                  locked: bool = False, |                  locked: bool = False, | ||||||
|                  identifier: Tuple[Any, ...] = ()): |                  identifier: Tuple[Any, ...] = ()): | ||||||
|         """ |         """ | ||||||
| @ -74,10 +74,12 @@ class SubPattern: | |||||||
|             mirrored: Whether to mirror the referenced pattern across its x and y axes. |             mirrored: Whether to mirror the referenced pattern across its x and y axes. | ||||||
|             dose: Scaling factor applied to the dose. |             dose: Scaling factor applied to the dose. | ||||||
|             scale: Scaling factor applied to the pattern's geometry. |             scale: Scaling factor applied to the pattern's geometry. | ||||||
|  |             repetition: TODO | ||||||
|             locked: Whether the `SubPattern` is locked after initialization. |             locked: Whether the `SubPattern` is locked after initialization. | ||||||
|             identifier: Arbitrary tuple, used internally by some `masque` functions. |             identifier: Arbitrary tuple, used internally by some `masque` functions. | ||||||
|         """ |         """ | ||||||
|         object.__setattr__(self, 'locked', False) |         LockableImpl.unlock(self) | ||||||
|  | #        object.__setattr__(self, 'locked', False) | ||||||
|         self.identifier = identifier |         self.identifier = identifier | ||||||
|         self.pattern = pattern |         self.pattern = pattern | ||||||
|         self.offset = offset |         self.offset = offset | ||||||
| @ -87,13 +89,9 @@ class SubPattern: | |||||||
|         if mirrored is None: |         if mirrored is None: | ||||||
|             mirrored = [False, False] |             mirrored = [False, False] | ||||||
|         self.mirrored = mirrored |         self.mirrored = mirrored | ||||||
|  |         self.repetition = repetition | ||||||
|         self.locked = locked |         self.locked = locked | ||||||
| 
 | 
 | ||||||
|     def __setattr__(self, name, value): |  | ||||||
|         if self.locked and name != 'locked': |  | ||||||
|             raise PatternLockedError() |  | ||||||
|         object.__setattr__(self, name, value) |  | ||||||
| 
 |  | ||||||
|     def  __copy__(self) -> 'SubPattern': |     def  __copy__(self) -> 'SubPattern': | ||||||
|         new = SubPattern(pattern=self.pattern, |         new = SubPattern(pattern=self.pattern, | ||||||
|                          offset=self.offset.copy(), |                          offset=self.offset.copy(), | ||||||
| @ -123,57 +121,6 @@ class SubPattern: | |||||||
|             raise PatternError('Provided pattern {} is not a Pattern object or None!'.format(val)) |             raise PatternError('Provided pattern {} is not a Pattern object or None!'.format(val)) | ||||||
|         self._pattern = val |         self._pattern = val | ||||||
| 
 | 
 | ||||||
|     # offset property |  | ||||||
|     @property |  | ||||||
|     def offset(self) -> numpy.ndarray: |  | ||||||
|         return self._offset |  | ||||||
| 
 |  | ||||||
|     @offset.setter |  | ||||||
|     def offset(self, val: vector2): |  | ||||||
|         if not isinstance(val, numpy.ndarray): |  | ||||||
|             val = numpy.array(val, dtype=float) |  | ||||||
| 
 |  | ||||||
|         if val.size != 2: |  | ||||||
|             raise PatternError('Offset must be convertible to size-2 ndarray') |  | ||||||
|         self._offset = val.flatten().astype(float) |  | ||||||
| 
 |  | ||||||
|     # dose property |  | ||||||
|     @property |  | ||||||
|     def dose(self) -> float: |  | ||||||
|         return self._dose |  | ||||||
| 
 |  | ||||||
|     @dose.setter |  | ||||||
|     def dose(self, val: float): |  | ||||||
|         if not is_scalar(val): |  | ||||||
|             raise PatternError('Dose must be a scalar') |  | ||||||
|         if not val >= 0: |  | ||||||
|             raise PatternError('Dose must be non-negative') |  | ||||||
|         self._dose = val |  | ||||||
| 
 |  | ||||||
|     # scale property |  | ||||||
|     @property |  | ||||||
|     def scale(self) -> float: |  | ||||||
|         return self._scale |  | ||||||
| 
 |  | ||||||
|     @scale.setter |  | ||||||
|     def scale(self, val: float): |  | ||||||
|         if not is_scalar(val): |  | ||||||
|             raise PatternError('Scale must be a scalar') |  | ||||||
|         if not val > 0: |  | ||||||
|             raise PatternError('Scale must be positive') |  | ||||||
|         self._scale = val |  | ||||||
| 
 |  | ||||||
|     # Rotation property [ccw] |  | ||||||
|     @property |  | ||||||
|     def rotation(self) -> float: |  | ||||||
|         return self._rotation |  | ||||||
| 
 |  | ||||||
|     @rotation.setter |  | ||||||
|     def rotation(self, val: float): |  | ||||||
|         if not is_scalar(val): |  | ||||||
|             raise PatternError('Rotation must be a scalar') |  | ||||||
|         self._rotation = val % (2 * pi) |  | ||||||
| 
 |  | ||||||
|     # Mirrored property |     # Mirrored property | ||||||
|     @property |     @property | ||||||
|     def mirrored(self) -> numpy.ndarray:        # ndarray[bool] |     def mirrored(self) -> numpy.ndarray:        # ndarray[bool] | ||||||
| @ -198,52 +145,17 @@ class SubPattern: | |||||||
|         pattern.rotate_around((0.0, 0.0), self.rotation) |         pattern.rotate_around((0.0, 0.0), self.rotation) | ||||||
|         pattern.translate_elements(self.offset) |         pattern.translate_elements(self.offset) | ||||||
|         pattern.scale_element_doses(self.dose) |         pattern.scale_element_doses(self.dose) | ||||||
|  | 
 | ||||||
|  |         if pattern.repetition is not None: | ||||||
|  |             combined = type(pat)(name='__repetition__') | ||||||
|  |             for dd in pattern.repetition.displacements: | ||||||
|  |                 temp_pat = pattern.deepcopy() | ||||||
|  |                 temp_pat.translate_elements(dd) | ||||||
|  |                 combined.append(temp_pat) | ||||||
|  |             pattern = combined | ||||||
|  | 
 | ||||||
|         return pattern |         return pattern | ||||||
| 
 | 
 | ||||||
|     def translate(self, offset: vector2) -> 'SubPattern': |  | ||||||
|         """ |  | ||||||
|         Translate by the given offset |  | ||||||
| 
 |  | ||||||
|         Args: |  | ||||||
|             offset: Offset `[x, y]` to translate by |  | ||||||
| 
 |  | ||||||
|         Returns: |  | ||||||
|             self |  | ||||||
|         """ |  | ||||||
|         self.offset += offset |  | ||||||
|         return self |  | ||||||
| 
 |  | ||||||
|     def rotate_around(self, pivot: vector2, rotation: float) -> 'SubPattern': |  | ||||||
|         """ |  | ||||||
|         Rotate around a point |  | ||||||
| 
 |  | ||||||
|         Args: |  | ||||||
|             pivot: Point `[x, y]` to rotate around |  | ||||||
|             rotation: Angle to rotate by (counterclockwise, radians) |  | ||||||
| 
 |  | ||||||
|         Returns: |  | ||||||
|             self |  | ||||||
|         """ |  | ||||||
|         pivot = numpy.array(pivot, dtype=float) |  | ||||||
|         self.translate(-pivot) |  | ||||||
|         self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) |  | ||||||
|         self.rotate(rotation) |  | ||||||
|         self.translate(+pivot) |  | ||||||
|         return self |  | ||||||
| 
 |  | ||||||
|     def rotate(self, rotation: float) -> 'SubPattern': |  | ||||||
|         """ |  | ||||||
|         Rotate the instance around it's origin |  | ||||||
| 
 |  | ||||||
|         Args: |  | ||||||
|             rotation: Angle to rotate by (counterclockwise, radians) |  | ||||||
| 
 |  | ||||||
|         Returns: |  | ||||||
|             self |  | ||||||
|         """ |  | ||||||
|         self.rotation += rotation |  | ||||||
|         return self |  | ||||||
| 
 |  | ||||||
|     def mirror(self, axis: int) -> 'SubPattern': |     def mirror(self, axis: int) -> 'SubPattern': | ||||||
|         """ |         """ | ||||||
|         Mirror the subpattern across an axis. |         Mirror the subpattern across an axis. | ||||||
| @ -271,37 +183,6 @@ class SubPattern: | |||||||
|             return None |             return None | ||||||
|         return self.as_pattern().get_bounds() |         return self.as_pattern().get_bounds() | ||||||
| 
 | 
 | ||||||
|     def scale_by(self, c: float) -> 'SubPattern': |  | ||||||
|         """ |  | ||||||
|         Scale the subpattern by a factor |  | ||||||
| 
 |  | ||||||
|         Args: |  | ||||||
|             c: scaling factor |  | ||||||
| 
 |  | ||||||
|         Returns: |  | ||||||
|             self |  | ||||||
|         """ |  | ||||||
|         self.scale *= c |  | ||||||
|         return self |  | ||||||
| 
 |  | ||||||
|     def copy(self) -> 'SubPattern': |  | ||||||
|         """ |  | ||||||
|         Return a shallow copy of the subpattern. |  | ||||||
| 
 |  | ||||||
|         Returns: |  | ||||||
|             `copy.copy(self)` |  | ||||||
|         """ |  | ||||||
|         return copy.copy(self) |  | ||||||
| 
 |  | ||||||
|     def deepcopy(self) -> 'SubPattern': |  | ||||||
|         """ |  | ||||||
|         Return a deep copy of the subpattern. |  | ||||||
| 
 |  | ||||||
|         Returns: |  | ||||||
|             `copy.deepcopy(self)` |  | ||||||
|         """ |  | ||||||
|         return copy.deepcopy(self) |  | ||||||
| 
 |  | ||||||
|     def lock(self) -> 'SubPattern': |     def lock(self) -> 'SubPattern': | ||||||
|         """ |         """ | ||||||
|         Lock the SubPattern, disallowing changes |         Lock the SubPattern, disallowing changes | ||||||
| @ -309,9 +190,9 @@ class SubPattern: | |||||||
|         Returns: |         Returns: | ||||||
|             self |             self | ||||||
|         """ |         """ | ||||||
|         self.offset.flags.writeable = False |  | ||||||
|         self.mirrored.flags.writeable = False |         self.mirrored.flags.writeable = False | ||||||
|         object.__setattr__(self, 'locked', True) |         PositionableImpl._lock(self) | ||||||
|  |         LockableImpl.lock(self) | ||||||
|         return self |         return self | ||||||
| 
 | 
 | ||||||
|     def unlock(self) -> 'SubPattern': |     def unlock(self) -> 'SubPattern': | ||||||
| @ -321,9 +202,9 @@ class SubPattern: | |||||||
|         Returns: |         Returns: | ||||||
|             self |             self | ||||||
|         """ |         """ | ||||||
|         self.offset.flags.writeable = True |         LockableImpl.unlock(self) | ||||||
|  |         PositionableImpl._unlock(self) | ||||||
|         self.mirrored.flags.writeable = True |         self.mirrored.flags.writeable = True | ||||||
|         object.__setattr__(self, 'locked', False) |  | ||||||
|         return self |         return self | ||||||
| 
 | 
 | ||||||
|     def deeplock(self) -> 'SubPattern': |     def deeplock(self) -> 'SubPattern': | ||||||
| @ -361,6 +242,3 @@ class SubPattern: | |||||||
|         dose = f' d{self.dose:g}' if self.dose != 1 else '' |         dose = f' d{self.dose:g}' if self.dose != 1 else '' | ||||||
|         locked = ' L' if self.locked else '' |         locked = ' L' if self.locked else '' | ||||||
|         return f'<SubPattern "{name}" at {self.offset}{rotation}{scale}{mirrored}{dose}{locked}>' |         return f'<SubPattern "{name}" at {self.offset}{rotation}{scale}{mirrored}{dose}{locked}>' | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| subpattern_t = Union[SubPattern, GridRepetition] |  | ||||||
|  | |||||||
							
								
								
									
										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 typing import Any, Union, Tuple, Sequence | ||||||
|  | from abc import ABCMeta | ||||||
| 
 | 
 | ||||||
| import numpy | import numpy | ||||||
| 
 | 
 | ||||||
| @ -133,3 +134,25 @@ def remove_colinear_vertices(vertices: numpy.ndarray, closed_path: bool = True) | |||||||
|         slopes_equal[[0, -1]] = False |         slopes_equal[[0, -1]] = False | ||||||
| 
 | 
 | ||||||
|     return vertices[~slopes_equal] |     return vertices[~slopes_equal] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class AutoSlots(ABCMeta): | ||||||
|  |     """ | ||||||
|  |     Metaclass for automatically generating __slots__ based on superclass type annotations. | ||||||
|  | 
 | ||||||
|  |     Superclasses must set `__slots__ = ()` to make this work properly. | ||||||
|  | 
 | ||||||
|  |     This is a workaround for the fact that non-empty `__slots__` can't be used | ||||||
|  |     with multiple inheritance. Since we only use multiple inheritance with abstract | ||||||
|  |     classes, they can have empty `__slots__` and their attribute type annotations | ||||||
|  |     can be used to generate a full `__slots__` for the concrete class. | ||||||
|  |     """ | ||||||
|  |     def __new__(cls, name, bases, dctn): | ||||||
|  |         slots = tuple(dctn.get('__slots__', tuple())) | ||||||
|  |         for base in bases: | ||||||
|  |             if not hasattr(base, '__annotations__'): | ||||||
|  |                 continue | ||||||
|  |             slots += tuple(getattr(base, '__annotations__').keys()) | ||||||
|  |         dctn['__slots__'] = slots | ||||||
|  |         return super().__new__(cls,name,bases,dctn) | ||||||
|  | 
 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user