diff --git a/examples/test_rep.py b/examples/test_rep.py index 80496ad..22e4b65 100644 --- a/examples/test_rep.py +++ b/examples/test_rep.py @@ -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), diff --git a/masque/__init__.py b/masque/__init__.py index 896d2ac..9b8efb1 100644 --- a/masque/__init__.py +++ b/masque/__init__.py @@ -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' diff --git a/masque/file/dxf.py b/masque/file/dxf.py index 07eb1db..7ae4b6d 100644 --- a/masque/file/dxf.py +++ b/masque/file/dxf.py @@ -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 diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index dcb9649..7192ecc 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -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 diff --git a/masque/file/oasis.py b/masque/file/oasis.py index 9a27a9f..34208b0 100644 --- a/masque/file/oasis.py +++ b/masque/file/oasis.py @@ -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 diff --git a/masque/file/svg.py b/masque/file/svg.py index fb49b5b..0ce5750 100644 --- a/masque/file/svg.py +++ b/masque/file/svg.py @@ -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 diff --git a/masque/label.py b/masque/label.py index 72b7266..f157f46 100644 --- a/masque/label.py +++ b/masque/label.py @@ -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': diff --git a/masque/pattern.py b/masque/pattern.py index 9c45749..de7ecd2 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -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) diff --git a/masque/repetition.py b/masque/repetition.py index 5c9f105..4ed29f4 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -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 diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 4918ea7..ea9b4c7 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -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, diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index 2834b2a..447145f 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -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, diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index e3836a1..0b73dd1 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -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, diff --git a/masque/shapes/path.py b/masque/shapes/path.py index 7570c7c..4d11d9e 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -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 diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 08da89e..24d609c 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -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 diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index 7a5e3f3..b9a0cdd 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -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 diff --git a/masque/shapes/text.py b/masque/shapes/text.py index 599d377..c8af2cc 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -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, diff --git a/masque/subpattern.py b/masque/subpattern.py index ebd6876..6462cff 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -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 diff --git a/masque/traits/__init__.py b/masque/traits/__init__.py index 8af6434..d885264 100644 --- a/masque/traits/__init__.py +++ b/masque/traits/__init__.py @@ -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 diff --git a/masque/traits/annotatable.py b/masque/traits/annotatable.py new file mode 100644 index 0000000..0285cfd --- /dev/null +++ b/masque/traits/annotatable.py @@ -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 diff --git a/masque/traits/doseable.py b/masque/traits/doseable.py index 419d0bd..96c535c 100644 --- a/masque/traits/doseable.py +++ b/masque/traits/doseable.py @@ -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 diff --git a/masque/traits/layerable.py b/masque/traits/layerable.py index 5382450..e3d5f7b 100644 --- a/masque/traits/layerable.py +++ b/masque/traits/layerable.py @@ -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 diff --git a/masque/traits/lockable.py b/masque/traits/lockable.py index cc12760..fadaaa3 100644 --- a/masque/traits/lockable.py +++ b/masque/traits/lockable.py @@ -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 diff --git a/masque/traits/mirrorable.py b/masque/traits/mirrorable.py index 1e10800..a66c074 100644 --- a/masque/traits/mirrorable.py +++ b/masque/traits/mirrorable.py @@ -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') diff --git a/masque/traits/positionable.py b/masque/traits/positionable.py index d433a91..150daf0 100644 --- a/masque/traits/positionable.py +++ b/masque/traits/positionable.py @@ -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 diff --git a/masque/traits/repeatable.py b/masque/traits/repeatable.py index 3971a94..67183ad 100644 --- a/masque/traits/repeatable.py +++ b/masque/traits/repeatable.py @@ -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 diff --git a/masque/traits/rotatable.py b/masque/traits/rotatable.py index ef7e748..c79e89e 100644 --- a/masque/traits/rotatable.py +++ b/masque/traits/rotatable.py @@ -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 diff --git a/masque/traits/scalable.py b/masque/traits/scalable.py index ac349a2..bebda69 100644 --- a/masque/traits/scalable.py +++ b/masque/traits/scalable.py @@ -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 diff --git a/masque/utils.py b/masque/utils.py index 2f2e499..c33b8c4 100644 --- a/masque/utils.py +++ b/masque/utils.py @@ -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: