allow locking of all objects

This commit is contained in:
Jan Petykiewicz 2019-12-12 00:38:11 -08:00
parent 09711116a7
commit e0db621595
14 changed files with 290 additions and 36 deletions

View File

@ -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

View File

@ -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')

View File

@ -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,

View File

@ -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

View File

@ -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':
"""

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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