diff --git a/masque/__init__.py b/masque/__init__.py index 919a183..bd6908a 100644 --- a/masque/__init__.py +++ b/masque/__init__.py @@ -25,7 +25,7 @@ import pathlib -from .error import PatternError +from .error import PatternError, PatternLockedError from .shapes import Shape from .label import Label from .subpattern import SubPattern diff --git a/masque/error.py b/masque/error.py index 8a67b6e..4a5c21a 100644 --- a/masque/error.py +++ b/masque/error.py @@ -7,3 +7,11 @@ class PatternError(Exception): def __str__(self): return repr(self.value) + + +class PatternLockedError(PatternError): + """ + Exception raised when trying to modify a locked pattern + """ + def __init__(self): + PatternError.__init__(self, 'Tried to modify a locked Pattern, subpattern, or shape') diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index ad63711..f5ecc83 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -76,7 +76,7 @@ def write(patterns: Pattern or List[Pattern], :param library_name: Library name written into the GDSII file. Default 'masque-gdsii-write'. :param modify_originals: If True, the original pattern is modified as part of the writing - process. Otherwise, a copy is made. + process. Otherwise, a copy is made and deepunlock()-ed. Default False. :param disambiguate_func: Function which takes a list of patterns and alters them to make their names valid and unique. Default is `disambiguate_pattern_names`, which @@ -90,7 +90,7 @@ def write(patterns: Pattern or List[Pattern], disambiguate_func = disambiguate_pattern_names if not modify_originals: - patterns = copy.deepcopy(patterns) + patterns = [p.deepcopy().deepunlock() for p in patterns] # Create library lib = gdsii.library.Library(version=600, diff --git a/masque/label.py b/masque/label.py index 3e2e8c7..d9fcd3e 100644 --- a/masque/label.py +++ b/masque/label.py @@ -3,7 +3,7 @@ import copy import numpy from numpy import pi -from . import PatternError +from .error import PatternError, PatternLockedError from .utils import is_scalar, vector2, rotation_matrix_2d @@ -12,9 +12,9 @@ __author__ = 'Jan Petykiewicz' class Label: """ - A circle, which has a position and radius. + A text annotation with a position and layer (but no size; it is not drawn) """ - __slots__ = ('_offset', '_layer', '_string', 'identifier') + __slots__ = ('_offset', '_layer', '_string', 'identifier', 'locked') # [x_offset, y_offset] _offset: numpy.ndarray @@ -27,6 +27,13 @@ class Label: # Arbitrary identifier tuple identifier: Tuple + locked: bool # If True, any changes to the label will raise a PatternLockedError + + def __setattr__(self, name, value): + if self.locked and name != 'locked': + raise PatternLockedError() + object.__setattr__(self, name, value) + # ---- Properties # offset property @property @@ -78,11 +85,20 @@ class Label: def __init__(self, string: str, offset: vector2=(0.0, 0.0), - layer: int=0): + layer: int=0, + locked: bool = False): + self.unlock() self.identifier = () self.string = string self.offset = numpy.array(offset, dtype=float) self.layer = layer + self.locked = locked + + def __copy__(self) -> 'Label': + return Label(string=self.string, + offset=self.offset.copy(), + layer=self.layer, + locked=self.locked) def __deepcopy__(self, memo: Dict = None) -> 'Label': memo = {} if memo is None else memo @@ -134,4 +150,20 @@ class Label: """ return numpy.array([self.offset, self.offset]) + def lock(self) -> 'Label': + """ + Lock the Label + :return: self + """ + object.__setattr__(self, 'locked', True) + return self + + def unlock(self) -> 'Label': + """ + Unlock the Label + + :return: self + """ + object.__setattr__(self, 'locked', False) + return self diff --git a/masque/pattern.py b/masque/pattern.py index 1de2f28..50d4e34 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -17,7 +17,7 @@ from .repetition import GridRepetition from .shapes import Shape, Polygon from .label import Label from .utils import rotation_matrix_2d, vector2, normalize_mirror -from .error import PatternError +from .error import PatternError, PatternLockedError __author__ = 'Jan Petykiewicz' @@ -37,17 +37,19 @@ class Pattern: may reference the same Pattern object. :var name: An identifier for this object. Not necessarily unique. """ - __slots__ = ('shapes', 'labels', 'subpatterns', 'name') + __slots__ = ('shapes', 'labels', 'subpatterns', 'name', 'locked') shapes: List[Shape] labels: List[Label] subpatterns: List[SubPattern or GridRepetition] name: str + locked: bool def __init__(self, name: str = '', shapes: List[Shape] = (), labels: List[Label] = (), subpatterns: List[SubPattern] = (), + locked: bool = False, ): """ Basic init; arguments get assigned to member variables. @@ -57,7 +59,9 @@ class Pattern: :param labels: Initial labels in the Pattern :param subpatterns: Initial subpatterns in the Pattern :param name: An identifier for the Pattern + :param locked: Whether to lock the pattern after construction """ + self.unlock() if isinstance(shapes, list): self.shapes = shapes else: @@ -74,14 +78,27 @@ class Pattern: self.subpatterns = list(subpatterns) self.name = name + self.locked = locked + + def __setattr__(self, name, value): + if self.locked and name != 'locked': + raise PatternLockedError() + object.__setattr__(self, name, value) + + def __copy__(self, memo: Dict = None) -> 'Pattern': + return Pattern(name=self.name, + shapes=copy.deepcopy(self.shapes), + labels=copy.deepcopy(self.labels), + subpatterns=[copy.copy(sp) for sp in self.subpatterns], + locked=self.locked) def __deepcopy__(self, memo: Dict = None) -> 'Pattern': memo = {} if memo is None else memo - new = copy.copy(self) - new.name = self.name - new.shapes = copy.deepcopy(self.shapes, memo) - new.labels = copy.deepcopy(self.labels, memo) - new.subpatterns = copy.deepcopy(self.subpatterns, memo) + new = Pattern(name=self.name, + shapes=copy.deepcopy(self.shapes, memo), + labels=copy.deepcopy(self.labels, memo), + subpatterns=copy.deepcopy(self.subpatterns, memo), + locked=self.locked) return new def append(self, other_pattern: 'Pattern') -> 'Pattern': @@ -363,7 +380,7 @@ class Pattern: :return: A list of (Ni, 2) numpy.ndarrays specifying vertices of the polygons. Each ndarray is of the form [[x0, y0], [x1, y1],...]. """ - pat = copy.deepcopy(self).polygonize().flatten() + pat = self.deepcopy().deepunlock().polygonize().flatten() return [shape.vertices + shape.offset for shape in pat.shapes] def referenced_patterns_by_id(self) -> Dict[int, 'Pattern']: @@ -564,11 +581,7 @@ class Pattern: :return: A copy of the current Pattern. """ - cp = copy.copy(self) - cp.shapes = copy.deepcopy(cp.shapes) - cp.labels = copy.deepcopy(cp.labels) - cp.subpatterns = [copy.copy(subpat) for subpat in cp.subpatterns] - return cp + return copy.copy(self) def deepcopy(self) -> 'Pattern': """ @@ -588,6 +601,52 @@ class Pattern: len(self.shapes) == 0 and len(self.labels) == 0) + def lock(self) -> 'Pattern': + """ + Lock the pattern + + :return: self + """ + object.__setattr__(self, 'locked', True) + return self + + def unlock(self) -> 'Pattern': + """ + Unlock the pattern + + :return: self + """ + object.__setattr__(self, 'locked', False) + return self + + def deeplock(self) -> 'Pattern': + """ + Recursively lock the pattern, all referenced shapes, subpatterns, and labels + + :return: self + """ + self.lock() + for ss in self.shapes + self.labels: + ss.lock() + for sp in self.subpatterns: + sp.deeplock() + return self + + def deepunlock(self) -> 'Pattern': + """ + Recursively unlock the pattern, all referenced shapes, subpatterns, and labels + + This is dangerous unless you have just performed a deepcopy! + + :return: self + """ + self.unlock() + for ss in self.shapes + self.labels: + ss.unlock() + for sp in self.subpatterns: + sp.deepunlock() + return self + @staticmethod def load(filename: str) -> 'Pattern': """ diff --git a/masque/repetition.py b/masque/repetition.py index 2a9fa27..0c3234a 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -9,7 +9,7 @@ import copy import numpy from numpy import pi -from .error import PatternError +from .error import PatternError, PatternLockedError from .utils import is_scalar, rotation_matrix_2d, vector2 @@ -33,7 +33,8 @@ class GridRepetition: '_b_vector', '_a_count', '_b_count', - 'identifier') + 'identifier', + 'locked') pattern: 'Pattern' @@ -49,6 +50,7 @@ class GridRepetition: _b_count: int identifier: Tuple + locked: bool def __init__(self, pattern: 'Pattern', @@ -60,7 +62,8 @@ class GridRepetition: rotation: float = 0.0, mirrored: List[bool] = None, dose: float = 1.0, - scale: float = 1.0): + scale: float = 1.0, + locked: bool = False): """ :param a_vector: First lattice vector, of the form [x, y]. Specifies center-to-center spacing between adjacent elements. @@ -70,6 +73,7 @@ class GridRepetition: Can be omitted when specifying a 1D array. :param b_count: Number of elements in the b_vector direction. Should be omitted if b_vector was omitted. + :param locked: Whether the subpattern is locked after initialization. :raises: InvalidDataError if b_* inputs conflict with each other or a_count < 1. """ @@ -85,6 +89,7 @@ class GridRepetition: if b_count < 1: raise InvalidDataError('Repetition has too-small b_count: ' '{}'.format(b_count)) + self.unlock() self.a_vector = a_vector self.b_vector = b_vector self.a_count = a_count @@ -99,6 +104,12 @@ class GridRepetition: if mirrored is None: mirrored = [False, False] self.mirrored = mirrored + self.locked = locked + + def __setattr__(self, name, value): + if self.locked and name != 'locked': + raise PatternLockedError() + object.__setattr__(self, name, value) def __copy__(self) -> 'GridRepetition': new = GridRepetition(pattern=self.pattern, @@ -110,7 +121,8 @@ class GridRepetition: rotation=self.rotation, dose=self.dose, scale=self.scale, - mirrored=self.mirrored.copy()) + mirrored=self.mirrored.copy(), + locked=self.locked) return new def __deepcopy__(self, memo: Dict = None) -> 'GridReptition': @@ -126,6 +138,9 @@ class GridRepetition: @offset.setter def offset(self, val: vector2): + if self.locked: + raise PatternLockedError() + if not isinstance(val, numpy.ndarray): val = numpy.array(val, dtype=float) @@ -243,7 +258,7 @@ class GridRepetition: for a in range(self.a_count): for b in range(self.b_count): offset = a * self.a_vector + b * self.b_vector - newPat = self.pattern.deepcopy() + newPat = self.pattern.deepcopy().deepunlock() newPat.translate_elements(offset) patterns.append(newPat) @@ -343,3 +358,42 @@ class GridRepetition: """ return copy.deepcopy(self) + def lock(self) -> 'GridRepetition': + """ + Lock the GridRepetition + + :return: self + """ + object.__setattr__(self, 'locked', True) + return self + + def unlock(self) -> 'GridRepetition': + """ + Unlock the GridRepetition + + :return: self + """ + object.__setattr__(self, 'locked', False) + return self + + def deeplock(self) -> 'GridRepetition': + """ + Recursively lock the GridRepetition and its contained pattern + + :return: self + """ + self.lock() + self.pattern.deeplock() + return self + + def deepunlock(self) -> 'GridRepetition': + """ + Recursively unlock the GridRepetition and its contained pattern + + This is dangerous unless you have just performed a deepcopy! + + :return: self + """ + self.unlock() + self.pattern.deepunlock() + return self diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 92204a4..49c8fbe 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -147,7 +147,9 @@ class Arc(Shape): rotation: float = 0, mirrored: Tuple[bool] = (False, False), layer: int = 0, - dose: float = 1.0): + dose: float = 1.0, + locked: bool = False): + self.unlock() self.identifier = () self.radii = radii self.angles = angles @@ -159,6 +161,7 @@ class Arc(Shape): self.dose = dose self.poly_num_points = poly_num_points self.poly_max_arclen = poly_max_arclen + self.locked = locked def __deepcopy__(self, memo: Dict = None) -> 'Arc': memo = {} if memo is None else memo diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index 5ee901f..f9b2192 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -44,7 +44,9 @@ class Circle(Shape): poly_max_arclen: float = None, offset: vector2 = (0.0, 0.0), layer: int = 0, - dose: float = 1.0): + dose: float = 1.0, + locked: bool = False): + self.unlock() self.identifier = () self.offset = numpy.array(offset, dtype=float) self.layer = layer @@ -52,6 +54,7 @@ class Circle(Shape): self.radius = radius self.poly_num_points = poly_num_points self.poly_max_arclen = poly_max_arclen + self.locked = locked def __deepcopy__(self, memo: Dict = None) -> 'Circle': memo = {} if memo is None else memo diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index fba194a..7fe2c66 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -88,7 +88,9 @@ class Ellipse(Shape): rotation: float = 0, mirrored: Tuple[bool] = (False, False), layer: int = 0, - dose: float = 1.0): + dose: float = 1.0, + locked: bool = False): + self.unlock() self.identifier = () self.radii = radii self.offset = offset @@ -98,6 +100,7 @@ class Ellipse(Shape): self.dose = dose self.poly_num_points = poly_num_points self.poly_max_arclen = poly_max_arclen + self.locked = locked def __deepcopy__(self, memo: Dict = None) -> 'Ellipse': memo = {} if memo is None else memo diff --git a/masque/shapes/path.py b/masque/shapes/path.py index d7ede07..7062fd5 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -151,7 +151,9 @@ class Path(Shape): mirrored: Tuple[bool] = (False, False), layer: int = 0, dose: float = 1.0, + locked: bool = False, ) -> 'Path': + self.unlock() self._cap_extensions = None # Since .cap setter might access it self.identifier = () @@ -165,6 +167,7 @@ class Path(Shape): self.cap_extensions = cap_extensions self.rotate(rotation) [self.mirror(a) for a, do in enumerate(mirrored) if do] + self.locked = locked def __deepcopy__(self, memo: Dict = None) -> 'Path': memo = {} if memo is None else memo diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index a899229..02ac891 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -77,7 +77,9 @@ class Polygon(Shape): mirrored: Tuple[bool] = (False, False), layer: int = 0, dose: float = 1.0, + locked: bool = False, ): + self.unlock() self.identifier = () self.layer = layer self.dose = dose @@ -85,6 +87,7 @@ class Polygon(Shape): self.offset = offset self.rotate(rotation) [self.mirror(a) for a, do in enumerate(mirrored) if do] + self.locked = locked def __deepcopy__(self, memo: Dict = None) -> 'Polygon': memo = {} if memo is None else memo diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index 497e7e9..429e9dd 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -3,7 +3,7 @@ from abc import ABCMeta, abstractmethod import copy import numpy -from .. import PatternError +from ..error import PatternError, PatternLockedError from ..utils import is_scalar, rotation_matrix_2d, vector2 @@ -24,13 +24,26 @@ class Shape(metaclass=ABCMeta): """ Abstract class specifying functions common to all shapes. """ - __slots__ = ('_offset', '_layer', '_dose', 'identifier') + __slots__ = ('_offset', '_layer', '_dose', 'identifier', 'locked') _offset: numpy.ndarray # [x_offset, y_offset] _layer: int or Tuple # Layer (integer >= 0 or tuple) _dose: float # Dose identifier: Tuple # An arbitrary identifier for the shape, # usually empty but used by Pattern.flatten() + locked: bool # If True, any changes to the shape will raise a PatternLockedError + + def __setattr__(self, name, value): + if self.locked and name != 'locked': + raise PatternLockedError() + object.__setattr__(self, name, value) + + def __copy__(self) -> 'Shape': + cls = self.__class__ + new = cls.__new__(cls) + for name in Shape.__slots__ + self.__slots__: + object.__setattr__(new, name, getattr(self, name)) + return new # --- Abstract methods @abstractmethod @@ -388,3 +401,20 @@ class Shape(metaclass=ABCMeta): return manhattan_polygons + def lock(self) -> 'Shape': + """ + Lock the Shape + + :return: self + """ + object.__setattr__(self, 'locked', True) + return self + + def unlock(self) -> 'Shape': + """ + Unlock the Shape + + :return: self + """ + object.__setattr__(self, 'locked', False) + return self diff --git a/masque/shapes/text.py b/masque/shapes/text.py index f05e7bd..2d082d5 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -77,7 +77,10 @@ class Text(Shape): rotation: float = 0.0, mirrored: Tuple[bool] = (False, False), layer: int = 0, - dose: float = 1.0): + dose: float = 1.0, + locked: bool = False, + ): + self.unlock() self.identifier = () self.offset = offset self.layer = layer @@ -87,6 +90,7 @@ class Text(Shape): self.rotation = rotation self.font_path = font_path self.mirrored = mirrored + self.locked = locked def __deepcopy__(self, memo: Dict = None) -> 'Text': memo = {} if memo is None else memo diff --git a/masque/subpattern.py b/masque/subpattern.py index b8441d5..b2da81f 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -9,7 +9,7 @@ import copy import numpy from numpy import pi -from .error import PatternError +from .error import PatternError, PatternLockedError from .utils import is_scalar, rotation_matrix_2d, vector2 @@ -21,7 +21,8 @@ class SubPattern: SubPattern provides basic support for nesting Pattern objects within each other, by adding offset, rotation, scaling, and associated methods. """ - __slots__ = ('pattern', '_offset', '_rotation', '_dose', '_scale', '_mirrored', 'identifier') + __slots__ = ('pattern', '_offset', '_rotation', '_dose', '_scale', '_mirrored', + 'identifier', 'locked') pattern: 'Pattern' _offset: numpy.ndarray _rotation: float @@ -29,14 +30,18 @@ class SubPattern: _scale: float _mirrored: List[bool] identifier: Tuple + locked: bool + #TODO more documentation? def __init__(self, pattern: 'Pattern', offset: vector2 = (0.0, 0.0), rotation: float = 0.0, mirrored: List[bool] = None, dose: float = 1.0, - scale: float = 1.0): + scale: float = 1.0, + locked: bool = False): + self.unlock() self.identifier = () self.pattern = pattern self.offset = offset @@ -46,6 +51,12 @@ class SubPattern: if mirrored is None: mirrored = [False, False] self.mirrored = mirrored + self.locked = locked + + def __setattr__(self, name, value): + if self.locked and name != 'locked': + raise PatternLockedError() + object.__setattr__(self, name, value) def __copy__(self) -> 'SubPattern': new = SubPattern(pattern=self.pattern, @@ -53,7 +64,8 @@ class SubPattern: rotation=self.rotation, dose=self.dose, scale=self.scale, - mirrored=self.mirrored.copy()) + mirrored=self.mirrored.copy(), + locked=self.locked) return new def __deepcopy__(self, memo: Dict = None) -> 'SubPattern': @@ -130,7 +142,7 @@ class SubPattern: SubPattern's properties. :return: Copy of self.pattern that has been altered to reflect the SubPattern's properties. """ - pattern = self.pattern.deepcopy() + pattern = self.pattern.deepcopy().deepunlock() pattern.scale_by(self.scale) [pattern.mirror(ax) for ax, do in enumerate(self.mirrored) if do] pattern.rotate_around((0.0, 0.0), self.rotation) @@ -218,3 +230,43 @@ class SubPattern: :return: copy.copy(self) """ return copy.deepcopy(self) + + def lock(self) -> 'SubPattern': + """ + Lock the SubPattern + + :return: self + """ + object.__setattr__(self, 'locked', True) + return self + + def unlock(self) -> 'SubPattern': + """ + Unlock the SubPattern + + :return: self + """ + object.__setattr__(self, 'locked', False) + return self + + def deeplock(self) -> 'SubPattern': + """ + Recursively lock the SubPattern and its contained pattern + + :return: self + """ + self.lock() + self.pattern.deeplock() + return self + + def deepunlock(self) -> 'SubPattern': + """ + Recursively unlock the SubPattern and its contained pattern + + This is dangerous unless you have just performed a deepcopy! + + :return: self + """ + self.unlock() + self.pattern.deepunlock() + return self