add support for annotations
and other fixes
This commit is contained in:
		
							parent
							
								
									c611699fc8
								
							
						
					
					
						commit
						73c9050138
					
				| @ -17,7 +17,8 @@ def main(): | ||||
|         pat.shapes.append(shapes.Arc( | ||||
|             radii=(rmin, rmin), | ||||
|             width=0.1, | ||||
|             angles=(0*-numpy.pi/4, numpy.pi/4) | ||||
|             angles=(0*-numpy.pi/4, numpy.pi/4), | ||||
|             annotations={'1': ['blah']}, | ||||
|         )) | ||||
| 
 | ||||
|     pat.scale_by(1000) | ||||
| @ -27,7 +28,7 @@ def main(): | ||||
| 
 | ||||
|     pat3 = Pattern('sref_test') | ||||
|     pat3.subpatterns = [ | ||||
|         SubPattern(pat, offset=(1e5, 3e5)), | ||||
|         SubPattern(pat, offset=(1e5, 3e5), annotations={'4': ['Hello I am the base subpattern']}), | ||||
|         SubPattern(pat, offset=(2e5, 3e5), rotation=pi/3), | ||||
|         SubPattern(pat, offset=(3e5, 3e5), rotation=pi/2), | ||||
|         SubPattern(pat, offset=(4e5, 3e5), rotation=pi), | ||||
|  | ||||
| @ -31,7 +31,7 @@ from .shapes import Shape | ||||
| from .label import Label | ||||
| from .subpattern import SubPattern | ||||
| from .pattern import Pattern | ||||
| from .utils import layer_t | ||||
| from .utils import layer_t, annotations_t | ||||
| 
 | ||||
| 
 | ||||
| __author__ = 'Jan Petykiewicz' | ||||
|  | ||||
| @ -10,10 +10,10 @@ import struct | ||||
| import logging | ||||
| import pathlib | ||||
| import gzip | ||||
| import numpy | ||||
| from numpy import pi | ||||
| 
 | ||||
| import ezdxf | ||||
| import numpy        # type: ignore | ||||
| from numpy import pi | ||||
| import ezdxf        # type: ignore | ||||
| 
 | ||||
| from .utils import mangle_name, make_dose_table | ||||
| from .. import Pattern, SubPattern, PatternError, Label, Shape | ||||
| @ -264,13 +264,12 @@ def _read_block(block, clean_vertices): | ||||
|                 } | ||||
| 
 | ||||
|             if 'column_count' in attr: | ||||
|                 args['a_vector'] = (attr['column_spacing'], 0) | ||||
|                 args['b_vector'] = (0, attr['row_spacing']) | ||||
|                 args['a_count'] = attr['column_count'] | ||||
|                 args['b_count'] = attr['row_count'] | ||||
|                 pat.subpatterns.append(GridRepetition(**args)) | ||||
|             else: | ||||
|                 pat.subpatterns.append(SubPattern(**args)) | ||||
|                 args['repetition'] = Grid( | ||||
|                            a_vector=(attr['column_spacing'], 0), | ||||
|                            b_vector=(0, attr['row_spacing']), | ||||
|                            a_count=attr['column_count'], | ||||
|                            b_count=attr['row_count']) | ||||
|             pat.subpatterns.append(SubPattern(**args)) | ||||
|         else: | ||||
|             logger.warning(f'Ignoring DXF element {element.dxftype()} (not implemented).') | ||||
|     return pat | ||||
|  | ||||
| @ -11,17 +11,18 @@ Note that GDSII references follow the same convention as `masque`, | ||||
|   Scaling, rotation, and mirroring apply to individual instances, not grid | ||||
|    vectors or offsets. | ||||
| """ | ||||
| from typing import List, Any, Dict, Tuple, Callable, Union, Sequence, Iterable, Optional, Sequence | ||||
| from typing import List, Any, Dict, Tuple, Callable, Union, Sequence, Iterable, Optional | ||||
| from typing import Sequence, Mapping | ||||
| import re | ||||
| import io | ||||
| import copy | ||||
| import numpy | ||||
| import base64 | ||||
| import struct | ||||
| import logging | ||||
| import pathlib | ||||
| import gzip | ||||
| 
 | ||||
| import numpy        # type: ignore | ||||
| # python-gdsii | ||||
| import gdsii.library | ||||
| import gdsii.structure | ||||
| @ -32,7 +33,7 @@ from .. import Pattern, SubPattern, PatternError, Label, Shape | ||||
| from ..shapes import Polygon, Path | ||||
| from ..repetition import Grid | ||||
| from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar, layer_t | ||||
| from ..utils import remove_colinear_vertices, normalize_mirror | ||||
| from ..utils import remove_colinear_vertices, normalize_mirror, annotations_t | ||||
| 
 | ||||
| #TODO absolute positioning | ||||
| 
 | ||||
| @ -99,6 +100,7 @@ def build(patterns: Union[Pattern, List[Pattern]], | ||||
| 
 | ||||
|     if disambiguate_func is None: | ||||
|         disambiguate_func = disambiguate_pattern_names | ||||
|     assert(disambiguate_func is not None)       # placate mypy | ||||
| 
 | ||||
|     if not modify_originals: | ||||
|         patterns = [p.deepunlock() for p in copy.deepcopy(patterns)] | ||||
| @ -124,6 +126,8 @@ def build(patterns: Union[Pattern, List[Pattern]], | ||||
|         structure = gdsii.structure.Structure(name=pat.name.encode('ASCII')) | ||||
|         lib.append(structure) | ||||
| 
 | ||||
|         structure.properties = _annotations_to_properties(pat.annotations, 512) | ||||
| 
 | ||||
|         structure += _shapes_to_elements(pat.shapes) | ||||
|         structure += _labels_to_texts(pat.labels) | ||||
|         structure += _subpatterns_to_refs(pat.subpatterns) | ||||
| @ -238,6 +242,9 @@ def read(stream: io.BufferedIOBase, | ||||
|     patterns = [] | ||||
|     for structure in lib: | ||||
|         pat = Pattern(name=structure.name.decode('ASCII')) | ||||
|         if pat.annotations: | ||||
|             logger.warning('Dropping Pattern-level annotations; they are not supported by python-gdsii') | ||||
| #        pat.annotations = {str(k): v for k, v in structure.properties} | ||||
|         for element in structure: | ||||
|             # Switch based on element type: | ||||
|             if isinstance(element, gdsii.elements.Boundary): | ||||
| @ -343,6 +350,7 @@ def _ref_to_subpat(element: Union[gdsii.elements.SRef, | ||||
|                         rotation=rotation, | ||||
|                         scale=scale, | ||||
|                         mirrored=(mirror_across_x, False), | ||||
|                         annotations=_properties_to_annotations(element.properties), | ||||
|                         repetition=repetition) | ||||
|     subpat.identifier = (element.struct_name,) | ||||
|     return subpat | ||||
| @ -359,6 +367,7 @@ def _gpath_to_mpath(element: gdsii.elements.Path, raw_mode: bool) -> Path: | ||||
|             'width': element.width if element.width is not None else 0.0, | ||||
|             'cap': cap, | ||||
|             'offset': numpy.zeros(2), | ||||
|             'annotations':_properties_to_annotations(element.properties), | ||||
|             'raw': raw_mode, | ||||
|            } | ||||
| 
 | ||||
| @ -376,6 +385,7 @@ def _boundary_to_polygon(element: gdsii.elements.Boundary, raw_mode: bool) -> Po | ||||
|     args = {'vertices': element.xy[:-1].astype(float), | ||||
|             'layer': (element.layer, element.data_type), | ||||
|             'offset': numpy.zeros(2), | ||||
|             'annotations':_properties_to_annotations(element.properties), | ||||
|             'raw': raw_mode, | ||||
|            } | ||||
|     return Polygon(**args) | ||||
| @ -420,11 +430,38 @@ def _subpatterns_to_refs(subpatterns: List[SubPattern] | ||||
|             #  strans must be non-None for angle and mag to take effect | ||||
|             ref.strans = set_bit(0, 15 - 0, mirror_across_x) | ||||
|             ref.mag = subpat.scale | ||||
|             ref.properties = _annotations_to_properties(subpat.annotations, 512) | ||||
| 
 | ||||
|         refs += new_refs | ||||
|     return refs | ||||
| 
 | ||||
| 
 | ||||
| def _properties_to_annotations(properties: List[Tuple[int, bytes]]) -> annotations_t: | ||||
|     return {str(k): [v.decode()] for k, v in properties} | ||||
| 
 | ||||
| 
 | ||||
| def _annotations_to_properties(annotations: annotations_t, max_len: int = 126) -> List[Tuple[int, bytes]]: | ||||
|     cum_len = 0 | ||||
|     props = [] | ||||
|     for key, vals in annotations.items(): | ||||
|         try: | ||||
|             i = int(key) | ||||
|         except: | ||||
|             raise PatternError(f'Annotation key {key} is not convertable to an integer') | ||||
|         if not (0 < i < 126): | ||||
|             raise PatternError(f'Annotation key {key} converts to {i} (must be in the range [1,125])') | ||||
| 
 | ||||
|         val_strings = ' '.join(str(val) for val in vals) | ||||
|         b = val_strings.encode() | ||||
|         if len(b) > 126: | ||||
|             raise PatternError(f'Annotation value {b!r} is longer than 126 characters!') | ||||
|         cum_len += numpy.ceil(len(b) / 2) * 2 + 2 | ||||
|         if cum_len > max_len: | ||||
|             raise PatternError(f'Sum of annotation data will be longer than {max_len} bytes! Generated bytes were {b!r}') | ||||
|         props.append((i, b)) | ||||
|     return props | ||||
| 
 | ||||
| 
 | ||||
| def _shapes_to_elements(shapes: List[Shape], | ||||
|                         polygonize_paths: bool = False | ||||
|                        ) -> List[Union[gdsii.elements.Boundary, gdsii.elements.Path]]: | ||||
| @ -432,6 +469,7 @@ def _shapes_to_elements(shapes: List[Shape], | ||||
|     # Add a Boundary element for each shape, and Path elements if necessary | ||||
|     for shape in shapes: | ||||
|         layer, data_type = _mlayer2gds(shape.layer) | ||||
|         properties = _annotations_to_properties(shape.annotations, 128) | ||||
|         if isinstance(shape, Path) and not polygonize_paths: | ||||
|             xy = numpy.round(shape.vertices + shape.offset).astype(int) | ||||
|             width = numpy.round(shape.width).astype(int) | ||||
| @ -441,26 +479,32 @@ def _shapes_to_elements(shapes: List[Shape], | ||||
|                                        xy=xy) | ||||
|             path.path_type = path_type | ||||
|             path.width = width | ||||
|             path.properties = properties | ||||
|             elements.append(path) | ||||
|         else: | ||||
|             for polygon in shape.to_polygons(): | ||||
|                 xy_open = numpy.round(polygon.vertices + polygon.offset).astype(int) | ||||
|                 xy_closed = numpy.vstack((xy_open, xy_open[0, :])) | ||||
|                 elements.append(gdsii.elements.Boundary(layer=layer, | ||||
|                                                         data_type=data_type, | ||||
|                                                         xy=xy_closed)) | ||||
|                 boundary = gdsii.elements.Boundary(layer=layer, | ||||
|                                                    data_type=data_type, | ||||
|                                                    xy=xy_closed) | ||||
|                 boundary.properties = properties | ||||
|                 elements.append(boundary) | ||||
|     return elements | ||||
| 
 | ||||
| 
 | ||||
| def _labels_to_texts(labels: List[Label]) -> List[gdsii.elements.Text]: | ||||
|     texts = [] | ||||
|     for label in labels: | ||||
|         properties = _annotations_to_properties(label.annotations, 128) | ||||
|         layer, text_type = _mlayer2gds(label.layer) | ||||
|         xy = numpy.round([label.offset]).astype(int) | ||||
|         texts.append(gdsii.elements.Text(layer=layer, | ||||
|                                          text_type=text_type, | ||||
|                                          xy=xy, | ||||
|                                          string=label.string.encode('ASCII'))) | ||||
|         text = gdsii.elements.Text(layer=layer, | ||||
|                                    text_type=text_type, | ||||
|                                    xy=xy, | ||||
|                                    string=label.string.encode('ASCII')) | ||||
|         text.properties = properties | ||||
|         texts.append(text) | ||||
|     return texts | ||||
| 
 | ||||
| 
 | ||||
|  | ||||
| @ -20,19 +20,19 @@ import struct | ||||
| import logging | ||||
| import pathlib | ||||
| import gzip | ||||
| import numpy | ||||
| from numpy import pi | ||||
| 
 | ||||
| import numpy        # type: ignore | ||||
| from numpy import pi | ||||
| import fatamorgana | ||||
| import fatamorgana.records as fatrec | ||||
| from fatamorgana.basic import PathExtensionScheme | ||||
| from fatamorgana.basic import PathExtensionScheme, AString, NString, PropStringReference | ||||
| 
 | ||||
| from .utils import mangle_name, make_dose_table | ||||
| from .. import Pattern, SubPattern, PatternError, Label, Shape | ||||
| from ..shapes import Polygon, Path, Circle | ||||
| from ..repetition import Grid, Arbitrary, Repetition | ||||
| from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar, layer_t | ||||
| from ..utils import remove_colinear_vertices, normalize_mirror | ||||
| from ..utils import remove_colinear_vertices, normalize_mirror, annotations_t | ||||
| 
 | ||||
| 
 | ||||
| logger = logging.getLogger(__name__) | ||||
| @ -52,9 +52,11 @@ path_cap_map = { | ||||
| 
 | ||||
| def build(patterns: Union[Pattern, List[Pattern]], | ||||
|           units_per_micron: int, | ||||
|           layer_map: Dict[str, Union[int, Tuple[int, int]]] = None, | ||||
|           layer_map: Optional[Dict[str, Union[int, Tuple[int, int]]]] = None, | ||||
|           *, | ||||
|           modify_originals: bool = False, | ||||
|           disambiguate_func: Callable[[Iterable[Pattern]], None] = None, | ||||
|           disambiguate_func: Optional[Callable[[Iterable[Pattern]], None]] = None, | ||||
|           annotations: Optional[annotations_t] = None | ||||
|           ) -> fatamorgana.OasisLayout: | ||||
|     """ | ||||
|     Convert a `Pattern` or list of patterns to an OASIS stream, writing patterns | ||||
| @ -91,6 +93,7 @@ def build(patterns: Union[Pattern, List[Pattern]], | ||||
|             Default `False`. | ||||
|         disambiguate_func: Function which takes a list of patterns and alters them | ||||
|             to make their names valid and unique. Default is `disambiguate_pattern_names`. | ||||
|         annotations: dictionary of key-value pairs which are saved as library-level properties | ||||
| 
 | ||||
|     Returns: | ||||
|         `fatamorgana.OasisLayout` | ||||
| @ -104,11 +107,15 @@ def build(patterns: Union[Pattern, List[Pattern]], | ||||
|     if disambiguate_func is None: | ||||
|         disambiguate_func = disambiguate_pattern_names | ||||
| 
 | ||||
|     if annotations is None: | ||||
|         annotations = {} | ||||
| 
 | ||||
|     if not modify_originals: | ||||
|         patterns = [p.deepunlock() for p in copy.deepcopy(patterns)] | ||||
| 
 | ||||
|     # Create library | ||||
|     lib = fatamorgana.OasisLayout(unit=units_per_micron, validation=None) | ||||
|     lib.properties = annotations_to_properties(annotations) | ||||
| 
 | ||||
|     if layer_map: | ||||
|         for name, layer_num in layer_map.items(): | ||||
| @ -139,9 +146,11 @@ def build(patterns: Union[Pattern, List[Pattern]], | ||||
|         structure = fatamorgana.Cell(name=pat.name) | ||||
|         lib.cells.append(structure) | ||||
| 
 | ||||
|         structure.properties += annotations_to_properties(pat.annotations) | ||||
| 
 | ||||
|         structure.geometry += _shapes_to_elements(pat.shapes, layer2oas) | ||||
|         structure.geometry += _labels_to_texts(pat.labels, layer2oas) | ||||
|         structure.placements += _subpatterns_to_refs(pat.subpatterns) | ||||
|         structure.placements += _subpatterns_to_placements(pat.subpatterns) | ||||
| 
 | ||||
|     return lib | ||||
| 
 | ||||
| @ -226,6 +235,8 @@ def read(stream: io.BufferedIOBase, | ||||
| 
 | ||||
|     Additional library info is returned in a dict, containing: | ||||
|       'units_per_micrometer': number of database units per micrometer (all values are in database units) | ||||
|       'layer_map': Mapping from layer names to fatamorgana.LayerName objects | ||||
|       'annotations': Mapping of {key: value} pairs from library's properties | ||||
| 
 | ||||
|     Args: | ||||
|         stream: Stream to read from. | ||||
| @ -242,6 +253,7 @@ def read(stream: io.BufferedIOBase, | ||||
| 
 | ||||
|     library_info: Dict[str, Any] = { | ||||
|             'units_per_micrometer': lib.unit, | ||||
|             'annotations': properties_to_annotations(lib.properties, lib.propnames, lib.propstrings), | ||||
|             } | ||||
| 
 | ||||
|     layer_map = {} | ||||
| @ -252,7 +264,7 @@ def read(stream: io.BufferedIOBase, | ||||
|     patterns = [] | ||||
|     for cell in lib.cells: | ||||
|         if isinstance(cell.name, int): | ||||
|             cell_name = lib.cellnames[cell.name].string | ||||
|             cell_name = lib.cellnames[cell.name].nstring.string | ||||
|         else: | ||||
|             cell_name = cell.name.string | ||||
| 
 | ||||
| @ -263,15 +275,16 @@ def read(stream: io.BufferedIOBase, | ||||
|                 # note XELEMENT has no repetition | ||||
|                 continue | ||||
| 
 | ||||
| 
 | ||||
|             repetition = repetition_fata2masq(element.repetition) | ||||
| 
 | ||||
|             # Switch based on element type: | ||||
|             if isinstance(element, fatrec.Polygon): | ||||
|                 vertices = numpy.cumsum(numpy.vstack(((0, 0), element.get_point_list())), axis=0) | ||||
|                 annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings) | ||||
|                 poly = Polygon(vertices=vertices, | ||||
|                                layer=element.get_layer_tuple(), | ||||
|                                offset=element.get_xy(), | ||||
|                                annotations=annotations, | ||||
|                                repetition=repetition) | ||||
| 
 | ||||
|                 if clean_vertices: | ||||
| @ -295,10 +308,13 @@ def read(stream: io.BufferedIOBase, | ||||
|                 if cap == Path.Cap.SquareCustom: | ||||
|                     path_args['cap_extensions'] = numpy.array((element.get_extension_start()[1], | ||||
|                                                                element.get_extension_end()[1])) | ||||
| 
 | ||||
|                 annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings) | ||||
|                 path = Path(vertices=vertices, | ||||
|                             layer=element.get_layer_tuple(), | ||||
|                             offset=element.get_xy(), | ||||
|                             repetition=repetition, | ||||
|                             annotations=annotations, | ||||
|                             width=element.get_half_width() * 2, | ||||
|                             cap=cap, | ||||
|                             **path_args) | ||||
| @ -314,10 +330,12 @@ def read(stream: io.BufferedIOBase, | ||||
|             elif isinstance(element, fatrec.Rectangle): | ||||
|                 width = element.get_width() | ||||
|                 height = element.get_height() | ||||
|                 annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings) | ||||
|                 rect = Polygon(layer=element.get_layer_tuple(), | ||||
|                                offset=element.get_xy(), | ||||
|                                repetition=repetition, | ||||
|                                vertices=numpy.array(((0, 0), (1, 0), (1, 1), (0, 1))) * (width, height), | ||||
|                                annotations=annotations, | ||||
|                                ) | ||||
|                 pat.shapes.append(rect) | ||||
| 
 | ||||
| @ -346,10 +364,12 @@ def read(stream: io.BufferedIOBase, | ||||
|                     else: | ||||
|                         vertices[2, 0] -= b | ||||
| 
 | ||||
|                 annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings) | ||||
|                 trapz = Polygon(layer=element.get_layer_tuple(), | ||||
|                                 offset=element.get_xy(), | ||||
|                                 repetition=repetition, | ||||
|                                 vertices=vertices, | ||||
|                                 annotations=annotations, | ||||
|                                 ) | ||||
|                 pat.shapes.append(trapz) | ||||
| 
 | ||||
| @ -399,24 +419,30 @@ def read(stream: io.BufferedIOBase, | ||||
|                     vertices = vertices[[0, 2, 3], :] | ||||
|                     vertices[0, 1] += width | ||||
| 
 | ||||
|                 annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings) | ||||
|                 ctrapz = Polygon(layer=element.get_layer_tuple(), | ||||
|                                  offset=element.get_xy(), | ||||
|                                  repetition=repetition, | ||||
|                                  vertices=vertices, | ||||
|                                  annotations=annotations, | ||||
|                                  ) | ||||
|                 pat.shapes.append(ctrapz) | ||||
| 
 | ||||
|             elif isinstance(element, fatrec.Circle): | ||||
|                 annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings) | ||||
|                 circle = Circle(layer=element.get_layer_tuple(), | ||||
|                                 offset=element.get_xy(), | ||||
|                                 repetition=repetition, | ||||
|                                 annotations=annotations, | ||||
|                                 radius=float(element.get_radius())) | ||||
|                 pat.shapes.append(circle) | ||||
| 
 | ||||
|             elif isinstance(element, fatrec.Text): | ||||
|                 annotations = properties_to_annotations(element.properties, lib.propnames, lib.propstrings) | ||||
|                 label = Label(layer=element.get_layer_tuple(), | ||||
|                               offset=element.get_xy(), | ||||
|                               repetition=repetition, | ||||
|                               annotations=annotations, | ||||
|                               string=str(element.get_string())) | ||||
|                 pat.labels.append(label) | ||||
| 
 | ||||
| @ -425,7 +451,7 @@ def read(stream: io.BufferedIOBase, | ||||
|                 continue | ||||
| 
 | ||||
|         for placement in cell.placements: | ||||
|             pat.subpatterns.append(_placement_to_subpat(placement)) | ||||
|             pat.subpatterns.append(_placement_to_subpat(placement, lib)) | ||||
| 
 | ||||
|         patterns.append(pat) | ||||
| 
 | ||||
| @ -435,7 +461,7 @@ def read(stream: io.BufferedIOBase, | ||||
|     for p in patterns_dict.values(): | ||||
|         for sp in p.subpatterns: | ||||
|             ident = sp.identifier[0] | ||||
|             name = ident if isinstance(ident, str) else lib.cellnames[ident].string | ||||
|             name = ident if isinstance(ident, str) else lib.cellnames[ident].nstring.string | ||||
|             sp.pattern = patterns_dict[name] | ||||
|             del sp.identifier | ||||
| 
 | ||||
| @ -459,7 +485,7 @@ def _mlayer2oas(mlayer: layer_t) -> Tuple[int, int]: | ||||
|     return layer, data_type | ||||
| 
 | ||||
| 
 | ||||
| def _placement_to_subpat(placement: fatrec.Placement) -> SubPattern: | ||||
| def _placement_to_subpat(placement: fatrec.Placement, lib: fatamorgana.OasisLayout) -> SubPattern: | ||||
|     """ | ||||
|     Helper function to create a SubPattern from a placment. Sets subpat.pattern to None | ||||
|      and sets the instance .identifier to (struct_name,). | ||||
| @ -468,21 +494,20 @@ def _placement_to_subpat(placement: fatrec.Placement) -> SubPattern: | ||||
|     mag = placement.magnification if placement.magnification is not None else 1 | ||||
|     pname = placement.get_name() | ||||
|     name = pname if isinstance(pname, int) else pname.string | ||||
|     args: Dict[str, Any] = { | ||||
|        'pattern': None, | ||||
|        'mirrored': (placement.flip, False), | ||||
|        'rotation': float(placement.angle * pi/180), | ||||
|        'scale': mag, | ||||
|        'identifier': (name,), | ||||
|        'repetition': repetition_fata2masq(placement.repetition), | ||||
|        } | ||||
| 
 | ||||
|     subpat = SubPattern(offset=xy, **args) | ||||
|     annotations = properties_to_annotations(placement.properties, lib.propnames, lib.propstrings) | ||||
|     subpat = SubPattern(offset=xy, | ||||
|                         pattern=None, | ||||
|                         mirrored=(placement.flip, False), | ||||
|                         rotation=float(placement.angle * pi/180), | ||||
|                         scale=float(mag), | ||||
|                         identifier=(name,), | ||||
|                         repetition=repetition_fata2masq(placement.repetition), | ||||
|                         annotations=annotations) | ||||
|     return subpat | ||||
| 
 | ||||
| 
 | ||||
| def _subpatterns_to_refs(subpatterns: List[SubPattern] | ||||
|                         ) -> List[fatrec.Placement]: | ||||
| def _subpatterns_to_placements(subpatterns: List[SubPattern] | ||||
|                                ) -> List[fatrec.Placement]: | ||||
|     refs = [] | ||||
|     for subpat in subpatterns: | ||||
|         if subpat.pattern is None: | ||||
| @ -493,19 +518,16 @@ def _subpatterns_to_refs(subpatterns: List[SubPattern] | ||||
|         frep, rep_offset = repetition_masq2fata(subpat.repetition) | ||||
| 
 | ||||
|         offset = numpy.round(subpat.offset + rep_offset).astype(int) | ||||
|         args: Dict[str, Any] = { | ||||
|             'x': offset[0], | ||||
|             'y': offset[1], | ||||
|             'repetition': frep, | ||||
|             } | ||||
| 
 | ||||
|         angle = numpy.rad2deg(subpat.rotation + extra_angle) % 360 | ||||
|         ref = fatrec.Placement( | ||||
|                 name=subpat.pattern.name, | ||||
|                 flip=mirror_across_x, | ||||
|                 angle=angle, | ||||
|                 magnification=subpat.scale, | ||||
|                 **args) | ||||
|                 properties=annotations_to_properties(subpat.annotations), | ||||
|                 x=offset[0], | ||||
|                 y=offset[1], | ||||
|                 repetition=frep) | ||||
| 
 | ||||
|         refs.append(ref) | ||||
|     return refs | ||||
| @ -519,6 +541,7 @@ def _shapes_to_elements(shapes: List[Shape], | ||||
|     for shape in shapes: | ||||
|         layer, datatype = layer2oas(shape.layer) | ||||
|         repetition, rep_offset = repetition_masq2fata(shape.repetition) | ||||
|         properties = annotations_to_properties(shape.annotations) | ||||
|         if isinstance(shape, Circle): | ||||
|             offset = numpy.round(shape.offset + rep_offset).astype(int) | ||||
|             radius = numpy.round(shape.radius).astype(int) | ||||
| @ -527,6 +550,7 @@ def _shapes_to_elements(shapes: List[Shape], | ||||
|                                    radius=radius, | ||||
|                                    x=offset[0], | ||||
|                                    y=offset[1], | ||||
|                                    properties=properties, | ||||
|                                    repetition=repetition) | ||||
|             elements.append(circle) | ||||
|         elif isinstance(shape, Path): | ||||
| @ -544,6 +568,7 @@ def _shapes_to_elements(shapes: List[Shape], | ||||
|                                y=xy[1], | ||||
|                                extension_start=extension_start,       #TODO implement multiple cap types? | ||||
|                                extension_end=extension_end, | ||||
|                                properties=properties, | ||||
|                                repetition=repetition, | ||||
|                                ) | ||||
|             elements.append(path) | ||||
| @ -556,6 +581,7 @@ def _shapes_to_elements(shapes: List[Shape], | ||||
|                                                x=xy[0], | ||||
|                                                y=xy[1], | ||||
|                                                point_list=points, | ||||
|                                                properties=properties, | ||||
|                                                repetition=repetition)) | ||||
|     return elements | ||||
| 
 | ||||
| @ -568,11 +594,13 @@ def _labels_to_texts(labels: List[Label], | ||||
|         layer, datatype = layer2oas(label.layer) | ||||
|         repetition, rep_offset = repetition_masq2fata(label.repetition) | ||||
|         xy = numpy.round(label.offset + rep_offset).astype(int) | ||||
|         properties = annotations_to_properties(label.annotations) | ||||
|         texts.append(fatrec.Text(layer=layer, | ||||
|                                  datatype=datatype, | ||||
|                                  x=xy[0], | ||||
|                                  y=xy[1], | ||||
|                                  string=label.string, | ||||
|                                  properties=properties, | ||||
|                                  repetition=repetition)) | ||||
|     return texts | ||||
| 
 | ||||
| @ -609,6 +637,7 @@ def disambiguate_pattern_names(patterns, | ||||
| 
 | ||||
| def repetition_fata2masq(rep: Union[fatamorgana.GridRepetition, fatamorgana.ArbitraryRepetition, None] | ||||
|                          ) -> Optional[Repetition]: | ||||
|     mrep: Optional[Repetition] | ||||
|     if isinstance(rep, fatamorgana.GridRepetition): | ||||
|         mrep = Grid(a_vector=rep.a_vector, | ||||
|                     b_vector=rep.b_vector, | ||||
| @ -624,7 +653,12 @@ def repetition_fata2masq(rep: Union[fatamorgana.GridRepetition, fatamorgana.Arbi | ||||
|     return mrep | ||||
| 
 | ||||
| 
 | ||||
| def repetition_masq2fata(rep: Optional[Repetition]): | ||||
| def repetition_masq2fata(rep: Optional[Repetition] | ||||
|                         ) -> Tuple[Union[fatamorgana.GridRepetition, | ||||
|                                          fatamorgana.ArbitraryRepetition, | ||||
|                                          None], | ||||
|                                    Tuple[int, int]]: | ||||
|     frep: Union[fatamorgana.GridRepetition, fatamorgana.ArbitraryRepetition, None] | ||||
|     if isinstance(rep, Grid): | ||||
|         frep = fatamorgana.GridRepetition( | ||||
|                           a_vector=numpy.round(rep.a_vector).astype(int), | ||||
| @ -642,3 +676,46 @@ def repetition_masq2fata(rep: Optional[Repetition]): | ||||
|         frep = None | ||||
|         offset = (0, 0) | ||||
|     return frep, offset | ||||
| 
 | ||||
| 
 | ||||
| def annotations_to_properties(annotations: annotations_t) -> List[fatrec.Property]: | ||||
|     #TODO determine is_standard based on key? | ||||
|     properties = [] | ||||
|     for key, values in annotations.items(): | ||||
|         vals = [AString(v) if isinstance(v, str) else v | ||||
|                 for v in values] | ||||
|         properties.append(fatrec.Property(key, vals, is_standard=False)) | ||||
|     return properties | ||||
| 
 | ||||
| 
 | ||||
| def properties_to_annotations(properties: List[fatrec.Property], | ||||
|                               propnames: Dict[int, NString], | ||||
|                               propstrings: Dict[int, AString], | ||||
|                               ) -> annotations_t: | ||||
|     annotations = {} | ||||
|     for proprec in properties: | ||||
|         assert(proprec.name is not None) | ||||
|         if isinstance(proprec.name, int): | ||||
|             key = propnames[proprec.name].string | ||||
|         else: | ||||
|             key = proprec.name.string | ||||
|         values: List[Union[str, float, int]] = [] | ||||
| 
 | ||||
|         assert(proprec.values is not None) | ||||
|         for value in proprec.values: | ||||
|             if isinstance(value, (float, int)): | ||||
|                 values.append(value) | ||||
|             elif isinstance(value, (NString, AString)): | ||||
|                 values.append(value.string) | ||||
|             elif isinstance(value, PropStringReference): | ||||
|                 values.append(propstrings[value.ref].string)  # dereference | ||||
|             else: | ||||
|                 string = repr(value) | ||||
|                 logger.warning(f'Converting property value for key ({key}) to string ({string})') | ||||
|                 values.append(string) | ||||
|         annotations[key] = values | ||||
|     return annotations | ||||
| 
 | ||||
|     properties = [fatrec.Property(key, vals, is_standard=False) | ||||
|                   for key, vals in annotations.items()] | ||||
|     return properties | ||||
|  | ||||
| @ -2,10 +2,11 @@ | ||||
| SVG file format readers and writers | ||||
| """ | ||||
| from typing import Dict, Optional | ||||
| import svgwrite | ||||
| import numpy | ||||
| import warnings | ||||
| 
 | ||||
| import numpy        # type: ignore | ||||
| import svgwrite     # type: ignore | ||||
| 
 | ||||
| from .utils import mangle_name | ||||
| from .. import Pattern | ||||
| 
 | ||||
|  | ||||
| @ -1,15 +1,16 @@ | ||||
| from typing import List, Tuple, Dict, Optional | ||||
| import copy | ||||
| import numpy | ||||
| import numpy        # type: ignore | ||||
| from numpy import pi | ||||
| 
 | ||||
| from .repetition import Repetition | ||||
| from .error import PatternError, PatternLockedError | ||||
| from .utils import is_scalar, vector2, rotation_matrix_2d, layer_t, AutoSlots | ||||
| from .utils import is_scalar, vector2, rotation_matrix_2d, layer_t, AutoSlots, annotations_t | ||||
| from .traits import PositionableImpl, LayerableImpl, Copyable, Pivotable, LockableImpl, RepeatableImpl | ||||
| from .traits import AnnotatableImpl | ||||
| 
 | ||||
| 
 | ||||
| class Label(PositionableImpl, LayerableImpl, LockableImpl, RepeatableImpl, | ||||
| class Label(PositionableImpl, LayerableImpl, LockableImpl, RepeatableImpl, AnnotatableImpl, | ||||
|             Pivotable, Copyable, metaclass=AutoSlots): | ||||
|     """ | ||||
|     A text annotation with a position and layer (but no size; it is not drawn) | ||||
| @ -42,14 +43,17 @@ class Label(PositionableImpl, LayerableImpl, LockableImpl, RepeatableImpl, | ||||
|                  offset: vector2 = (0.0, 0.0), | ||||
|                  layer: layer_t = 0, | ||||
|                  repetition: Optional[Repetition] = None, | ||||
|                  locked: bool = False): | ||||
|         object.__setattr__(self, 'locked', False) | ||||
|                  annotations: Optional[annotations_t] = None, | ||||
|                  locked: bool = False, | ||||
|                  ): | ||||
|         LockableImpl.unlock(self) | ||||
|         self.identifier = () | ||||
|         self.string = string | ||||
|         self.offset = numpy.array(offset, dtype=float, copy=True) | ||||
|         self.layer = layer | ||||
|         self.repetition = repetition | ||||
|         self.locked = locked | ||||
|         self.annotations = annotations if annotations is not None else {} | ||||
|         self.set_locked(locked) | ||||
| 
 | ||||
|     def __copy__(self) -> 'Label': | ||||
|         return Label(string=self.string, | ||||
| @ -62,7 +66,7 @@ class Label(PositionableImpl, LayerableImpl, LockableImpl, RepeatableImpl, | ||||
|         memo = {} if memo is None else memo | ||||
|         new = copy.copy(self).unlock() | ||||
|         new._offset = self._offset.copy() | ||||
|         new.locked = self.locked | ||||
|         new.set_locked(self.locked) | ||||
|         return new | ||||
| 
 | ||||
|     def rotate_around(self, pivot: vector2, rotation: float) -> 'Label': | ||||
|  | ||||
| @ -9,26 +9,27 @@ import itertools | ||||
| import pickle | ||||
| from collections import defaultdict | ||||
| 
 | ||||
| import numpy | ||||
| import numpy        # type: ignore | ||||
| from numpy import inf | ||||
| # .visualize imports matplotlib and matplotlib.collections | ||||
| 
 | ||||
| from .subpattern import SubPattern | ||||
| from .shapes import Shape, Polygon | ||||
| from .label import Label | ||||
| from .utils import rotation_matrix_2d, vector2, normalize_mirror | ||||
| from .utils import rotation_matrix_2d, vector2, normalize_mirror, AutoSlots, annotations_t | ||||
| from .error import PatternError, PatternLockedError | ||||
| from .traits import LockableImpl, AnnotatableImpl | ||||
| 
 | ||||
| 
 | ||||
| visitor_function_t = Callable[['Pattern', Tuple['Pattern'], Dict, numpy.ndarray], 'Pattern'] | ||||
| 
 | ||||
| 
 | ||||
| class Pattern: | ||||
| class Pattern(LockableImpl, AnnotatableImpl, metaclass=AutoSlots): | ||||
|     """ | ||||
|     2D layout consisting of some set of shapes, labels, and references to other Pattern objects | ||||
|      (via SubPattern). Shapes are assumed to inherit from masque.shapes.Shape or provide equivalent functions. | ||||
|     """ | ||||
|     __slots__ = ('shapes', 'labels', 'subpatterns', 'name', 'locked') | ||||
|     __slots__ = ('shapes', 'labels', 'subpatterns', 'name') | ||||
| 
 | ||||
|     shapes: List[Shape] | ||||
|     """ List of all shapes in this Pattern. | ||||
| @ -47,14 +48,12 @@ class Pattern: | ||||
|     name: str | ||||
|     """ A name for this pattern """ | ||||
| 
 | ||||
|     locked: bool | ||||
|     """ When the pattern is locked, no changes may be made. """ | ||||
| 
 | ||||
|     def __init__(self, | ||||
|                  name: str = '', | ||||
|                  shapes: Sequence[Shape] = (), | ||||
|                  labels: Sequence[Label] = (), | ||||
|                  subpatterns: Sequence[SubPattern] = (), | ||||
|                  annotations: Optional[annotations_t] = None, | ||||
|                  locked: bool = False, | ||||
|                  ): | ||||
|         """ | ||||
| @ -68,7 +67,7 @@ class Pattern: | ||||
|             name: An identifier for the Pattern | ||||
|             locked: Whether to lock the pattern after construction | ||||
|         """ | ||||
|         object.__setattr__(self, 'locked', False) | ||||
|         LockableImpl.unlock(self) | ||||
|         if isinstance(shapes, list): | ||||
|             self.shapes = shapes | ||||
|         else: | ||||
| @ -84,8 +83,9 @@ class Pattern: | ||||
|         else: | ||||
|             self.subpatterns = list(subpatterns) | ||||
| 
 | ||||
|         self.annotations = annotations if annotations is not None else {} | ||||
|         self.name = name | ||||
|         self.locked = locked | ||||
|         self.set_locked(locked) | ||||
| 
 | ||||
|     def __setattr__(self, name, value): | ||||
|         if self.locked and name != 'locked': | ||||
| @ -97,6 +97,7 @@ class Pattern: | ||||
|                        shapes=copy.deepcopy(self.shapes), | ||||
|                        labels=copy.deepcopy(self.labels), | ||||
|                        subpatterns=[copy.copy(sp) for sp in self.subpatterns], | ||||
|                        annotations=copy.deepcopy(self.annotations), | ||||
|                        locked=self.locked) | ||||
| 
 | ||||
|     def  __deepcopy__(self, memo: Dict = None) -> 'Pattern': | ||||
| @ -105,6 +106,7 @@ class Pattern: | ||||
|                 shapes=copy.deepcopy(self.shapes, memo), | ||||
|                 labels=copy.deepcopy(self.labels, memo), | ||||
|                 subpatterns=copy.deepcopy(self.subpatterns, memo), | ||||
|                 annotations=copy.deepcopy(self.annotations, memo), | ||||
|                 locked=self.locked) | ||||
|         return new | ||||
| 
 | ||||
| @ -815,7 +817,7 @@ class Pattern: | ||||
|             self.shapes = tuple(self.shapes) | ||||
|             self.labels = tuple(self.labels) | ||||
|             self.subpatterns = tuple(self.subpatterns) | ||||
|             object.__setattr__(self, 'locked', True) | ||||
|             LockableImpl.lock(self) | ||||
|         return self | ||||
| 
 | ||||
|     def unlock(self) -> 'Pattern': | ||||
| @ -826,7 +828,7 @@ class Pattern: | ||||
|             self | ||||
|         """ | ||||
|         if self.locked: | ||||
|             object.__setattr__(self, 'locked', False) | ||||
|             LockableImpl.unlock(self) | ||||
|             self.shapes = list(self.shapes) | ||||
|             self.labels = list(self.labels) | ||||
|             self.subpatterns = list(self.subpatterns) | ||||
|  | ||||
| @ -7,7 +7,7 @@ from typing import Union, List, Dict, Tuple, Optional, Sequence, TYPE_CHECKING, | ||||
| import copy | ||||
| from abc import ABCMeta, abstractmethod | ||||
| 
 | ||||
| import numpy | ||||
| import numpy        # type: ignore | ||||
| 
 | ||||
| from .error import PatternError, PatternLockedError | ||||
| from .utils import rotation_matrix_2d, vector2, AutoSlots | ||||
|  | ||||
| @ -1,13 +1,15 @@ | ||||
| from typing import List, Tuple, Dict, Optional, Sequence | ||||
| import copy | ||||
| import math | ||||
| import numpy | ||||
| 
 | ||||
| import numpy        # type: ignore | ||||
| from numpy import pi | ||||
| 
 | ||||
| from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS | ||||
| from .. import PatternError | ||||
| from ..repetition import Repetition | ||||
| from ..utils import is_scalar, vector2, layer_t, AutoSlots | ||||
| from ..utils import is_scalar, vector2, layer_t, AutoSlots, annotations_t | ||||
| from ..traits import LockableImpl | ||||
| 
 | ||||
| 
 | ||||
| class Arc(Shape, metaclass=AutoSlots): | ||||
| @ -160,10 +162,11 @@ class Arc(Shape, metaclass=AutoSlots): | ||||
|                  layer: layer_t = 0, | ||||
|                  dose: float = 1.0, | ||||
|                  repetition: Optional[Repetition] = None, | ||||
|                  annotations: Optional[annotations_t] = None, | ||||
|                  locked: bool = False, | ||||
|                  raw: bool = False, | ||||
|                  ): | ||||
|         object.__setattr__(self, 'locked', False) | ||||
|         LockableImpl.unlock(self) | ||||
|         self.identifier = () | ||||
|         if raw: | ||||
|             self._radii = radii | ||||
| @ -172,6 +175,7 @@ class Arc(Shape, metaclass=AutoSlots): | ||||
|             self._offset = offset | ||||
|             self._rotation = rotation | ||||
|             self._repetition = repetition | ||||
|             self._annotations = annotations if annotations is not None else {} | ||||
|             self._layer = layer | ||||
|             self._dose = dose | ||||
|         else: | ||||
| @ -181,12 +185,13 @@ class Arc(Shape, metaclass=AutoSlots): | ||||
|             self.offset = offset | ||||
|             self.rotation = rotation | ||||
|             self.repetition = repetition | ||||
|             self.annotations = annotations if annotations is not None else {} | ||||
|             self.layer = layer | ||||
|             self.dose = dose | ||||
|         self.poly_num_points = poly_num_points | ||||
|         self.poly_max_arclen = poly_max_arclen | ||||
|         [self.mirror(a) for a, do in enumerate(mirrored) if do] | ||||
|         self.locked = locked | ||||
|         self.set_locked(locked) | ||||
| 
 | ||||
|     def  __deepcopy__(self, memo: Dict = None) -> 'Arc': | ||||
|         memo = {} if memo is None else memo | ||||
| @ -194,7 +199,8 @@ class Arc(Shape, metaclass=AutoSlots): | ||||
|         new._offset = self._offset.copy() | ||||
|         new._radii = self._radii.copy() | ||||
|         new._angles = self._angles.copy() | ||||
|         new.locked = self.locked | ||||
|         new._annotations = copy.deepcopy(self._annotations) | ||||
|         new.set_locked(self.locked) | ||||
|         return new | ||||
| 
 | ||||
|     def to_polygons(self, | ||||
|  | ||||
| @ -1,12 +1,14 @@ | ||||
| from typing import List, Dict, Optional | ||||
| import copy | ||||
| import numpy | ||||
| 
 | ||||
| import numpy        # type: ignore | ||||
| from numpy import pi | ||||
| 
 | ||||
| from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS | ||||
| from .. import PatternError | ||||
| from ..repetition import Repetition | ||||
| from ..utils import is_scalar, vector2, layer_t, AutoSlots | ||||
| from ..utils import is_scalar, vector2, layer_t, AutoSlots, annotations_t | ||||
| from ..traits import LockableImpl | ||||
| 
 | ||||
| 
 | ||||
| class Circle(Shape, metaclass=AutoSlots): | ||||
| @ -48,23 +50,36 @@ class Circle(Shape, metaclass=AutoSlots): | ||||
|                  layer: layer_t = 0, | ||||
|                  dose: float = 1.0, | ||||
|                  repetition: Optional[Repetition] = None, | ||||
|                  locked: bool = False): | ||||
|         object.__setattr__(self, 'locked', False) | ||||
|                  annotations: Optional[annotations_t] = None, | ||||
|                  locked: bool = False, | ||||
|                  raw: bool = False, | ||||
|                  ): | ||||
|         LockableImpl.unlock(self) | ||||
|         self.identifier = () | ||||
|         self.offset = numpy.array(offset, dtype=float) | ||||
|         self.layer = layer | ||||
|         self.dose = dose | ||||
|         self.radius = radius | ||||
|         if raw: | ||||
|             self._radius = radius | ||||
|             self._offset = offset | ||||
|             self._repetition = repetition | ||||
|             self._annotations = annotations if annotations is not None else {} | ||||
|             self._layer = layer | ||||
|             self._dose = dose | ||||
|         else: | ||||
|             self.radius = radius | ||||
|             self.offset = offset | ||||
|             self.repetition = repetition | ||||
|             self.annotations = annotations if annotations is not None else {} | ||||
|             self.layer = layer | ||||
|             self.dose = dose | ||||
|         self.poly_num_points = poly_num_points | ||||
|         self.poly_max_arclen = poly_max_arclen | ||||
|         self.repetition = repetition | ||||
|         self.locked = locked | ||||
|         self.set_locked(locked) | ||||
| 
 | ||||
|     def  __deepcopy__(self, memo: Dict = None) -> 'Circle': | ||||
|         memo = {} if memo is None else memo | ||||
|         new = copy.copy(self).unlock() | ||||
|         new._offset = self._offset.copy() | ||||
|         new.locked = self.locked | ||||
|         new._annotations = copy.deepcopy(self._annotations) | ||||
|         new.set_locked(self.locked) | ||||
|         return new | ||||
| 
 | ||||
|     def to_polygons(self, | ||||
|  | ||||
| @ -1,13 +1,15 @@ | ||||
| from typing import List, Tuple, Dict, Sequence, Optional | ||||
| import copy | ||||
| import math | ||||
| import numpy | ||||
| 
 | ||||
| import numpy        # type: ignore | ||||
| from numpy import pi | ||||
| 
 | ||||
| from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS | ||||
| from .. import PatternError | ||||
| from ..repetition import Repetition | ||||
| from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t, AutoSlots | ||||
| from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t, AutoSlots, annotations_t | ||||
| from ..traits import LockableImpl | ||||
| 
 | ||||
| 
 | ||||
| class Ellipse(Shape, metaclass=AutoSlots): | ||||
| @ -95,16 +97,18 @@ class Ellipse(Shape, metaclass=AutoSlots): | ||||
|                  layer: layer_t = 0, | ||||
|                  dose: float = 1.0, | ||||
|                  repetition: Optional[Repetition] = None, | ||||
|                  annotations: Optional[annotations_t] = None, | ||||
|                  locked: bool = False, | ||||
|                  raw: bool = False, | ||||
|                  ): | ||||
|         object.__setattr__(self, 'locked', False) | ||||
|         LockableImpl.unlock(self) | ||||
|         self.identifier = () | ||||
|         if raw: | ||||
|             self._radii = radii | ||||
|             self._offset = offset | ||||
|             self._rotation = rotation | ||||
|             self._repetition = repetition | ||||
|             self._annotations = annotations if annotations is not None else {} | ||||
|             self._layer = layer | ||||
|             self._dose = dose | ||||
|         else: | ||||
| @ -112,19 +116,21 @@ class Ellipse(Shape, metaclass=AutoSlots): | ||||
|             self.offset = offset | ||||
|             self.rotation = rotation | ||||
|             self.repetition = repetition | ||||
|             self.annotations = annotations if annotations is not None else {} | ||||
|             self.layer = layer | ||||
|             self.dose = dose | ||||
|         [self.mirror(a) for a, do in enumerate(mirrored) if do] | ||||
|         self.poly_num_points = poly_num_points | ||||
|         self.poly_max_arclen = poly_max_arclen | ||||
|         self.locked = locked | ||||
|         self.set_locked(locked) | ||||
| 
 | ||||
|     def  __deepcopy__(self, memo: Dict = None) -> 'Ellipse': | ||||
|         memo = {} if memo is None else memo | ||||
|         new = copy.copy(self).unlock() | ||||
|         new._offset = self._offset.copy() | ||||
|         new._radii = self._radii.copy() | ||||
|         new.locked = self.locked | ||||
|         new._annotations = copy.deepcopy(self._annotations) | ||||
|         new.set_locked(self.locked) | ||||
|         return new | ||||
| 
 | ||||
|     def to_polygons(self, | ||||
|  | ||||
| @ -1,14 +1,16 @@ | ||||
| from typing import List, Tuple, Dict, Optional, Sequence | ||||
| import copy | ||||
| from enum import Enum | ||||
| import numpy | ||||
| 
 | ||||
| import numpy        # type: ignore | ||||
| from numpy import pi, inf | ||||
| 
 | ||||
| from . import Shape, normalized_shape_tuple, Polygon, Circle | ||||
| from .. import PatternError | ||||
| from ..repetition import Repetition | ||||
| 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, annotations_t | ||||
| from ..traits import LockableImpl | ||||
| 
 | ||||
| 
 | ||||
| class PathCap(Enum): | ||||
| @ -149,10 +151,11 @@ class Path(Shape, metaclass=AutoSlots): | ||||
|                  layer: layer_t = 0, | ||||
|                  dose: float = 1.0, | ||||
|                  repetition: Optional[Repetition] = None, | ||||
|                  annotations: Optional[annotations_t] = None, | ||||
|                  locked: bool = False, | ||||
|                  raw: bool = False, | ||||
|                  ): | ||||
|         object.__setattr__(self, 'locked', False) | ||||
|         LockableImpl.unlock(self) | ||||
|         self._cap_extensions = None     # Since .cap setter might access it | ||||
| 
 | ||||
|         self.identifier = () | ||||
| @ -160,6 +163,7 @@ class Path(Shape, metaclass=AutoSlots): | ||||
|             self._vertices = vertices | ||||
|             self._offset = offset | ||||
|             self._repetition = repetition | ||||
|             self._annotations = annotations if annotations is not None else {} | ||||
|             self._layer = layer | ||||
|             self._dose = dose | ||||
|             self._width = width | ||||
| @ -169,6 +173,7 @@ class Path(Shape, metaclass=AutoSlots): | ||||
|             self.vertices = vertices | ||||
|             self.offset = offset | ||||
|             self.repetition = repetition | ||||
|             self.annotations = annotations if annotations is not None else {} | ||||
|             self.layer = layer | ||||
|             self.dose = dose | ||||
|             self.width = width | ||||
| @ -176,7 +181,7 @@ class Path(Shape, metaclass=AutoSlots): | ||||
|             self.cap_extensions = cap_extensions | ||||
|         self.rotate(rotation) | ||||
|         [self.mirror(a) for a, do in enumerate(mirrored) if do] | ||||
|         self.locked = locked | ||||
|         self.set_locked(locked) | ||||
| 
 | ||||
|     def  __deepcopy__(self, memo: Dict = None) -> 'Path': | ||||
|         memo = {} if memo is None else memo | ||||
| @ -185,7 +190,8 @@ class Path(Shape, metaclass=AutoSlots): | ||||
|         new._vertices = self._vertices.copy() | ||||
|         new._cap = copy.deepcopy(self._cap, memo) | ||||
|         new._cap_extensions = copy.deepcopy(self._cap_extensions, memo) | ||||
|         new.locked = self.locked | ||||
|         new._annotations = copy.deepcopy(self._annotations) | ||||
|         new.set_locked(self.locked) | ||||
|         return new | ||||
| 
 | ||||
|     @staticmethod | ||||
|  | ||||
| @ -1,13 +1,15 @@ | ||||
| from typing import List, Tuple, Dict, Optional, Sequence | ||||
| import copy | ||||
| import numpy | ||||
| 
 | ||||
| import numpy        # type: ignore | ||||
| from numpy import pi | ||||
| 
 | ||||
| from . import Shape, normalized_shape_tuple | ||||
| from .. import PatternError | ||||
| from ..repetition import Repetition | ||||
| 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, annotations_t | ||||
| from ..traits import LockableImpl | ||||
| 
 | ||||
| 
 | ||||
| class Polygon(Shape, metaclass=AutoSlots): | ||||
| @ -77,33 +79,37 @@ class Polygon(Shape, metaclass=AutoSlots): | ||||
|                  layer: layer_t = 0, | ||||
|                  dose: float = 1.0, | ||||
|                  repetition: Optional[Repetition] = None, | ||||
|                  annotations: Optional[annotations_t] = None, | ||||
|                  locked: bool = False, | ||||
|                  raw: bool = False, | ||||
|                  ): | ||||
|         object.__setattr__(self, 'locked', False) | ||||
|         LockableImpl.unlock(self) | ||||
|         self.identifier = () | ||||
|         if raw: | ||||
|             self._vertices = vertices | ||||
|             self._offset = offset | ||||
|             self._repetition = repetition | ||||
|             self._annotations = annotations if annotations is not None else {} | ||||
|             self._layer = layer | ||||
|             self._dose = dose | ||||
|         else: | ||||
|             self.vertices = vertices | ||||
|             self.offset = offset | ||||
|             self.repetition = repetition | ||||
|             self.annotations = annotations if annotations is not None else {} | ||||
|             self.layer = layer | ||||
|             self.dose = dose | ||||
|         self.rotate(rotation) | ||||
|         [self.mirror(a) for a, do in enumerate(mirrored) if do] | ||||
|         self.locked = locked | ||||
|         self.set_locked(locked) | ||||
| 
 | ||||
|     def  __deepcopy__(self, memo: Optional[Dict] = None) -> 'Polygon': | ||||
|         memo = {} if memo is None else memo | ||||
|         new = copy.copy(self).unlock() | ||||
|         new._offset = self._offset.copy() | ||||
|         new._vertices = self._vertices.copy() | ||||
|         new.locked = self.locked | ||||
|         new._annotations = copy.deepcopy(self._annotations) | ||||
|         new.set_locked(self.locked) | ||||
|         return new | ||||
| 
 | ||||
|     @staticmethod | ||||
|  | ||||
| @ -1,13 +1,15 @@ | ||||
| from typing import List, Tuple, Callable, TypeVar, Optional, TYPE_CHECKING | ||||
| from abc import ABCMeta, abstractmethod | ||||
| import copy | ||||
| import numpy | ||||
| 
 | ||||
| import numpy        # type: ignore | ||||
| 
 | ||||
| from ..error import PatternError, PatternLockedError | ||||
| from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t | ||||
| from ..traits import (PositionableImpl, LayerableImpl, DoseableImpl, | ||||
|                       Rotatable, Mirrorable, Copyable, Scalable, | ||||
|                       PivotableImpl, LockableImpl, RepeatableImpl) | ||||
|                       PivotableImpl, LockableImpl, RepeatableImpl, | ||||
|                       AnnotatableImpl) | ||||
| 
 | ||||
| if TYPE_CHECKING: | ||||
|     from . import Polygon | ||||
| @ -27,7 +29,7 @@ T = TypeVar('T', bound='Shape') | ||||
| 
 | ||||
| 
 | ||||
| class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable, Copyable, Scalable, | ||||
|             PivotableImpl, RepeatableImpl, LockableImpl, metaclass=ABCMeta): | ||||
|             PivotableImpl, RepeatableImpl, LockableImpl, AnnotatableImpl, metaclass=ABCMeta): | ||||
|     """ | ||||
|     Abstract class specifying functions common to all shapes. | ||||
|     """ | ||||
| @ -39,7 +41,7 @@ class Shape(PositionableImpl, LayerableImpl, DoseableImpl, Rotatable, Mirrorable | ||||
|     def __copy__(self) -> 'Shape': | ||||
|         cls = self.__class__ | ||||
|         new = cls.__new__(cls) | ||||
|         for name in self.__slots__: | ||||
|         for name in self.__slots__:     # type: str | ||||
|             object.__setattr__(new, name, getattr(self, name)) | ||||
|         return new | ||||
| 
 | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| from typing import List, Tuple, Dict, Sequence, Optional, MutableSequence | ||||
| import copy | ||||
| import numpy | ||||
| 
 | ||||
| import numpy        # type: ignore | ||||
| from numpy import pi, inf | ||||
| 
 | ||||
| from . import Shape, Polygon, normalized_shape_tuple | ||||
| @ -8,6 +9,8 @@ from .. import PatternError | ||||
| from ..repetition import Repetition | ||||
| from ..traits import RotatableImpl | ||||
| from ..utils import is_scalar, vector2, get_bit, normalize_mirror, layer_t, AutoSlots | ||||
| from ..utils import annotations_t | ||||
| from ..traits import LockableImpl | ||||
| 
 | ||||
| # Loaded on use: | ||||
| # from freetype import Face | ||||
| @ -67,10 +70,11 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots): | ||||
|                  layer: layer_t = 0, | ||||
|                  dose: float = 1.0, | ||||
|                  repetition: Optional[Repetition] = None, | ||||
|                  annotations: Optional[annotations_t] = None, | ||||
|                  locked: bool = False, | ||||
|                  raw: bool = False, | ||||
|                  ): | ||||
|         object.__setattr__(self, 'locked', False) | ||||
|         LockableImpl.unlock(self) | ||||
|         self.identifier = () | ||||
|         if raw: | ||||
|             self._offset = offset | ||||
| @ -81,6 +85,7 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots): | ||||
|             self._rotation = rotation | ||||
|             self._mirrored = mirrored | ||||
|             self._repetition = repetition | ||||
|             self._annotations = annotations if annotations is not None else {} | ||||
|         else: | ||||
|             self.offset = offset | ||||
|             self.layer = layer | ||||
| @ -90,15 +95,17 @@ class Text(RotatableImpl, Shape, metaclass=AutoSlots): | ||||
|             self.rotation = rotation | ||||
|             self.mirrored = mirrored | ||||
|             self.repetition = repetition | ||||
|             self.annotations = annotations if annotations is not None else {} | ||||
|         self.font_path = font_path | ||||
|         self.locked = locked | ||||
|         self.set_locked(locked) | ||||
| 
 | ||||
|     def  __deepcopy__(self, memo: Dict = None) -> 'Text': | ||||
|         memo = {} if memo is None else memo | ||||
|         new = copy.copy(self).unlock() | ||||
|         new._offset = self._offset.copy() | ||||
|         new._mirrored = copy.deepcopy(self._mirrored, memo) | ||||
|         new.locked = self.locked | ||||
|         new._annotations = copy.deepcopy(self._annotations) | ||||
|         new.set_locked(self.locked) | ||||
|         return new | ||||
| 
 | ||||
|     def to_polygons(self, | ||||
|  | ||||
| @ -7,14 +7,15 @@ | ||||
| from typing import Union, List, Dict, Tuple, Optional, Sequence, TYPE_CHECKING, Any | ||||
| import copy | ||||
| 
 | ||||
| import numpy | ||||
| import numpy        # type: ignore | ||||
| from numpy import pi | ||||
| 
 | ||||
| from .error import PatternError, PatternLockedError | ||||
| from .utils import is_scalar, rotation_matrix_2d, vector2, AutoSlots | ||||
| from .utils import is_scalar, rotation_matrix_2d, vector2, AutoSlots, annotations_t | ||||
| from .repetition import Repetition | ||||
| from .traits import (PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, | ||||
|                      Mirrorable, PivotableImpl, Copyable, LockableImpl, RepeatableImpl) | ||||
|                      Mirrorable, PivotableImpl, Copyable, LockableImpl, RepeatableImpl, | ||||
|                      AnnotatableImpl) | ||||
| 
 | ||||
| 
 | ||||
| if TYPE_CHECKING: | ||||
| @ -22,7 +23,8 @@ if TYPE_CHECKING: | ||||
| 
 | ||||
| 
 | ||||
| class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mirrorable, | ||||
|                  PivotableImpl, Copyable, RepeatableImpl, LockableImpl, metaclass=AutoSlots): | ||||
|                  PivotableImpl, Copyable, RepeatableImpl, LockableImpl, AnnotatableImpl, | ||||
|                  metaclass=AutoSlots): | ||||
|     """ | ||||
|     SubPattern provides basic support for nesting Pattern objects within each other, by adding | ||||
|      offset, rotation, scaling, and associated methods. | ||||
| @ -49,8 +51,10 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi | ||||
|                  dose: float = 1.0, | ||||
|                  scale: float = 1.0, | ||||
|                  repetition: Optional[Repetition] = None, | ||||
|                  annotations: Optional[annotations_t] = None, | ||||
|                  locked: bool = False, | ||||
|                  identifier: Tuple[Any, ...] = ()): | ||||
|                  identifier: Tuple[Any, ...] = (), | ||||
|                  ): | ||||
|         """ | ||||
|         Args: | ||||
|             pattern: Pattern to reference. | ||||
| @ -74,7 +78,8 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi | ||||
|             mirrored = [False, False] | ||||
|         self.mirrored = mirrored | ||||
|         self.repetition = repetition | ||||
|         self.locked = locked | ||||
|         self.annotations = annotations if annotations is not None else {} | ||||
|         self.set_locked(locked) | ||||
| 
 | ||||
|     def  __copy__(self) -> 'SubPattern': | ||||
|         new = SubPattern(pattern=self.pattern, | ||||
| @ -84,6 +89,7 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi | ||||
|                          scale=self.scale, | ||||
|                          mirrored=self.mirrored.copy(), | ||||
|                          repetition=copy.deepcopy(self.repetition), | ||||
|                          annotations=copy.deepcopy(self.annotations), | ||||
|                          locked=self.locked) | ||||
|         return new | ||||
| 
 | ||||
| @ -92,7 +98,8 @@ class SubPattern(PositionableImpl, DoseableImpl, RotatableImpl, ScalableImpl, Mi | ||||
|         new = copy.copy(self).unlock() | ||||
|         new.pattern = copy.deepcopy(self.pattern, memo) | ||||
|         new.repetition = copy.deepcopy(self.repetition, memo) | ||||
|         new.locked = self.locked | ||||
|         new.annotations = copy.deepcopy(self.annotations, memo) | ||||
|         new.set_locked(self.locked) | ||||
|         return new | ||||
| 
 | ||||
|     # pattern property | ||||
|  | ||||
| @ -7,3 +7,4 @@ from .scalable import Scalable, ScalableImpl | ||||
| from .mirrorable import Mirrorable | ||||
| from .copyable import Copyable | ||||
| from .lockable import Lockable, LockableImpl | ||||
| from .annotatable import Annotatable, AnnotatableImpl | ||||
|  | ||||
							
								
								
									
										56
									
								
								masque/traits/annotatable.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								masque/traits/annotatable.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,56 @@ | ||||
| from typing import TypeVar | ||||
| from types import MappingProxyType | ||||
| from abc import ABCMeta, abstractmethod | ||||
| import copy | ||||
| 
 | ||||
| from ..utils import annotations_t | ||||
| from ..error import PatternError | ||||
| 
 | ||||
| 
 | ||||
| T = TypeVar('T', bound='Annotatable') | ||||
| I = TypeVar('I', bound='AnnotatableImpl') | ||||
| 
 | ||||
| 
 | ||||
| class Annotatable(metaclass=ABCMeta): | ||||
|     """ | ||||
|     Abstract class for all annotatable entities | ||||
|     Annotations correspond to GDS/OASIS "properties" | ||||
|     """ | ||||
|     __slots__ = () | ||||
| 
 | ||||
|     ''' | ||||
|     ---- Properties | ||||
|     ''' | ||||
|     @property | ||||
|     @abstractmethod | ||||
|     def annotations(self) -> annotations_t: | ||||
|         """ | ||||
|         Dictionary mapping annotation names to values | ||||
|         """ | ||||
|         pass | ||||
| 
 | ||||
| 
 | ||||
| class AnnotatableImpl(Annotatable, metaclass=ABCMeta): | ||||
|     """ | ||||
|     Simple implementation of `Annotatable`. | ||||
|     """ | ||||
|     __slots__ = () | ||||
| 
 | ||||
|     _annotations: annotations_t | ||||
|     """ Dictionary storing annotation name/value pairs """ | ||||
| 
 | ||||
|     ''' | ||||
|     ---- Non-abstract properties | ||||
|     ''' | ||||
|     @property | ||||
|     def annotations(self) -> annotations_t: | ||||
|         # TODO: Find a way to make sure the subclass implements Lockable without dealing with diamond inheritance or this extra hasattr | ||||
|         if hasattr(self, 'is_locked') and self.is_locked(): | ||||
|             return MappingProxyType(self._annotations) | ||||
|         return self._annotations | ||||
| 
 | ||||
|     @annotations.setter | ||||
|     def annotations(self, annotations: annotations_t): | ||||
|         if not isinstance(annotations, dict): | ||||
|             raise PatternError(f'annotations expected dict, got {type(annotations)}') | ||||
|         self._annotations = annotations | ||||
| @ -1,7 +1,6 @@ | ||||
| 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 | ||||
|  | ||||
| @ -1,7 +1,6 @@ | ||||
| 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 | ||||
|  | ||||
| @ -1,7 +1,6 @@ | ||||
| from typing import List, Tuple, Callable, TypeVar, Optional | ||||
| from abc import ABCMeta, abstractmethod | ||||
| import copy | ||||
| import numpy | ||||
| 
 | ||||
| from ..error import PatternError, PatternLockedError | ||||
| 
 | ||||
| @ -19,6 +18,7 @@ class Lockable(metaclass=ABCMeta): | ||||
|     ''' | ||||
|     ---- Methods | ||||
|     ''' | ||||
|     @abstractmethod | ||||
|     def lock(self: T) -> T: | ||||
|         """ | ||||
|         Lock the object, disallowing further changes | ||||
| @ -28,6 +28,7 @@ class Lockable(metaclass=ABCMeta): | ||||
|         """ | ||||
|         pass | ||||
| 
 | ||||
|     @abstractmethod | ||||
|     def unlock(self: T) -> T: | ||||
|         """ | ||||
|         Unlock the object, reallowing changes | ||||
| @ -37,6 +38,32 @@ class Lockable(metaclass=ABCMeta): | ||||
|         """ | ||||
|         pass | ||||
| 
 | ||||
|     @abstractmethod | ||||
|     def is_locked(self) -> bool: | ||||
|         """ | ||||
|         Returns: | ||||
|             True if the object is locked | ||||
|         """ | ||||
|         pass | ||||
| 
 | ||||
|     def set_locked(self: T, locked: bool) -> T: | ||||
|         """ | ||||
|         Locks or unlocks based on the argument. | ||||
|         No action if already in the requested state. | ||||
| 
 | ||||
|         Args: | ||||
|             locked: State to set. | ||||
| 
 | ||||
|         Returns: | ||||
|             self | ||||
|         """ | ||||
|         if locked != self.is_locked(): | ||||
|             if locked: | ||||
|                 self.lock() | ||||
|             else: | ||||
|                 self.unlock() | ||||
|         return self | ||||
| 
 | ||||
| 
 | ||||
| class LockableImpl(Lockable, metaclass=ABCMeta): | ||||
|     """ | ||||
| @ -62,3 +89,6 @@ class LockableImpl(Lockable, metaclass=ABCMeta): | ||||
|     def unlock(self: I) -> I: | ||||
|         object.__setattr__(self, 'locked', False) | ||||
|         return self | ||||
| 
 | ||||
|     def is_locked(self) -> bool: | ||||
|         return self.locked | ||||
|  | ||||
| @ -1,10 +1,10 @@ | ||||
| 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') | ||||
| #I = TypeVar('I', bound='MirrorableImpl') | ||||
| 
 | ||||
|  | ||||
| @ -3,7 +3,7 @@ | ||||
| from typing import List, Tuple, Callable, TypeVar, Optional | ||||
| from abc import ABCMeta, abstractmethod | ||||
| import copy | ||||
| import numpy | ||||
| import numpy        # type: ignore | ||||
| 
 | ||||
| from ..error import PatternError, PatternLockedError | ||||
| from ..utils import is_scalar, rotation_matrix_2d, vector2 | ||||
|  | ||||
| @ -1,7 +1,6 @@ | ||||
| from typing import List, Tuple, Callable, TypeVar, Optional, TYPE_CHECKING | ||||
| from abc import ABCMeta, abstractmethod | ||||
| import copy | ||||
| import numpy | ||||
| 
 | ||||
| from ..error import PatternError, PatternLockedError | ||||
| 
 | ||||
|  | ||||
| @ -2,7 +2,7 @@ from typing import List, Tuple, Callable, TypeVar, Optional | ||||
| from abc import ABCMeta, abstractmethod | ||||
| import copy | ||||
| 
 | ||||
| import numpy | ||||
| import numpy        # type: ignore | ||||
| from numpy import pi | ||||
| 
 | ||||
| from .positionable import Positionable | ||||
|  | ||||
| @ -1,7 +1,6 @@ | ||||
| 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 | ||||
|  | ||||
| @ -1,15 +1,16 @@ | ||||
| """ | ||||
| Various helper functions | ||||
| """ | ||||
| 
 | ||||
| from typing import Any, Union, Tuple, Sequence | ||||
| from typing import Any, Union, Tuple, Sequence, Dict, List | ||||
| from abc import ABCMeta | ||||
| 
 | ||||
| import numpy | ||||
| import numpy        # type: ignore | ||||
| 
 | ||||
| 
 | ||||
| # Type definitions | ||||
| vector2 = Union[numpy.ndarray, Tuple[float, float], Sequence[float]] | ||||
| layer_t = Union[int, Tuple[int, int], str] | ||||
| annotations_t = Dict[str, List[Union[int, float, str]]] | ||||
| 
 | ||||
| 
 | ||||
| def is_scalar(var: Any) -> bool: | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user