Add GridRepetition: a SubPattern-like object which implements regular spatial arrays.

Also rework masque.file.gdsii to consolidate write() and
write_dose2dtype()
This commit is contained in:
jan 2019-03-31 20:57:10 -07:00
parent 539198435c
commit c50bd8e148
4 changed files with 492 additions and 132 deletions

View File

@ -27,6 +27,7 @@ from .error import PatternError
from .shapes import Shape from .shapes import Shape
from .label import Label from .label import Label
from .subpattern import SubPattern from .subpattern import SubPattern
from .repetition import GridRepetition
from .pattern import Pattern from .pattern import Pattern

View File

@ -6,12 +6,12 @@ import gdsii.library
import gdsii.structure import gdsii.structure
import gdsii.elements import gdsii.elements
from typing import List, Any, Dict from typing import List, Any, Dict, Tuple
import re import re
import numpy import numpy
from .utils import mangle_name, make_dose_table from .utils import mangle_name, make_dose_table
from .. import Pattern, SubPattern, PatternError, Label from .. import Pattern, SubPattern, GridRepetition, PatternError, Label, Shape
from ..shapes import Polygon from ..shapes import Polygon
from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar
@ -74,45 +74,13 @@ def write(patterns: Pattern or List[Pattern],
structure = gdsii.structure.Structure(name=encoded_name) structure = gdsii.structure.Structure(name=encoded_name)
lib.append(structure) lib.append(structure)
# Add a Boundary element for each shape # Add a Boundary element for each shape
for shape in pat.shapes: structure += _shapes_to_boundaries(pat.shapes)
layer, data_type = _mlayer2gds(shape.layer)
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, :]))
structure.append(gdsii.elements.Boundary(layer=layer,
data_type=data_type,
xy=xy_closed))
for label in pat.labels:
layer, text_type = _mlayer2gds(label.layer)
xy = numpy.round([label.offset]).astype(int)
structure.append(gdsii.elements.Text(layer=layer,
text_type=text_type,
xy=xy,
string=label.string.encode('ASCII')))
# Add an SREF for each subpattern entry structure += _labels_to_texts(pat.labels)
# strans must be set for angle and mag to take effect
for subpat in pat.subpatterns: # Add an SREF / AREF for each subpattern entry
sanitized_name = re.compile('[^A-Za-z0-9_\?\$]').sub('_', subpat.pattern.name) structure += _subpatterns_to_refs(pat.subpatterns)
encoded_name = sanitized_name.encode('ASCII')
if len(encoded_name) == 0:
raise PatternError('Zero-length name after sanitize+encode, originally "{}"'.format(subpat.pattern.name))
sref = gdsii.elements.SRef(struct_name=encoded_name,
xy=numpy.round([subpat.offset]).astype(int))
sref.strans = 0
sref.angle = subpat.rotation * 180 / numpy.pi
mirror_x, mirror_y = subpat.mirrored
if mirror_y and mirror_y:
sref.angle += 180
elif mirror_x:
sref.strans = set_bit(sref.strans, 15 - 0, True)
elif mirror_y:
sref.angle += 180
sref.strans = set_bit(sref.strans, 15 - 0, True)
sref.mag = subpat.scale
structure.append(sref)
with open(filename, mode='wb') as stream: with open(filename, mode='wb') as stream:
lib.save(stream) lib.save(stream)
@ -155,12 +123,31 @@ def write_dose2dtype(patterns: Pattern or List[Pattern],
:returns: A list of doses, providing a mapping between datatype (int, list index) :returns: A list of doses, providing a mapping between datatype (int, list index)
and dose (float, list entry). and dose (float, list entry).
""" """
# Create library patterns, dose_vals = dose2dtype(patterns)
lib = gdsii.library.Library(version=600, write(patterns, filename, meters_per_unit, logical_units_per_unit)
name='masque-write_dose2dtype'.encode('ASCII'), return dose_vals
logical_unit=logical_units_per_unit,
physical_unit=meters_per_unit)
def dose2dtype(patterns: Pattern or List[Pattern],
) -> Tuple[List[Pattern], List[float]]:
"""
For each shape in each pattern, set shape.layer to the tuple
(base_layer, datatype), where:
layer is chosen to be equal to the original shape.layer if it is an int,
or shape.layer[0] if it is a tuple
datatype is chosen arbitrarily, based on calcualted dose for each shape.
Shapes with equal calcualted dose will have the same datatype.
A list of doses is retured, providing a mapping between datatype
(list index) and dose (list entry).
Note that this function modifies the input Pattern(s).
:param patterns: A Pattern or list of patterns to write to file. Modified by this function.
:returns: (patterns, dose_list)
patterns: modified input patterns
dose_list: A list of doses, providing a mapping between datatype (int, list index)
and dose (float, list entry).
"""
if isinstance(patterns, Pattern): if isinstance(patterns, Pattern):
patterns = [patterns] patterns = [patterns]
@ -183,66 +170,36 @@ def write_dose2dtype(patterns: Pattern or List[Pattern],
if len(dose_vals) > 256: if len(dose_vals) > 256:
raise PatternError('Too many dose values: {}, maximum 256 when using dtypes.'.format(len(dose_vals))) raise PatternError('Too many dose values: {}, maximum 256 when using dtypes.'.format(len(dose_vals)))
dose_vals_list = list(dose_vals) # Create a new pattern for each non-1-dose entry in the dose table
# and update the shapes to reflect their new dose
# Now create a structure for each row in sd_table (ie, each pattern + dose combination) new_pats = {} # (id, dose) -> new_pattern mapping
# and add in any Boundary and SREF elements
for pat_id, pat_dose in sd_table: for pat_id, pat_dose in sd_table:
pat = patterns_by_id[pat_id] if pat_dose == 1:
new_pats[(pat_id, pat_dose)] = patterns_by_id[pat_id]
continue
pat = patterns_by_id[pat_id].deepcopy()
encoded_name = mangle_name(pat, pat_dose).encode('ASCII') encoded_name = mangle_name(pat, pat_dose).encode('ASCII')
if len(encoded_name) == 0: if len(encoded_name) == 0:
raise PatternError('Zero-length name after mangle+encode, originally "{}"'.format(pat.name)) raise PatternError('Zero-length name after mangle+encode, originally "{}"'.format(pat.name))
structure = gdsii.structure.Structure(name=encoded_name)
lib.append(structure)
# Add a Boundary element for each shape
for shape in pat.shapes: for shape in pat.shapes:
for polygon in shape.to_polygons(): data_type = dose_vals_list.index(shape.dose * pat_dose)
data_type = dose_vals_list.index(polygon.dose * pat_dose) if is_scalar(shape.layer):
xy_open = numpy.round(polygon.vertices + polygon.offset).astype(int) layer = (shape.layer, data_type)
xy_closed = numpy.vstack((xy_open, xy_open[0, :])) else:
if is_scalar(polygon.layer): layer = (shape.layer[0], data_type)
layer = polygon.layer
else:
layer = polygon.layer[0]
structure.append(gdsii.elements.Boundary(layer=layer,
data_type=data_type,
xy=xy_closed))
for label in pat.labels:
layer, text_type = _mlayer2gds(label.layer)
xy = numpy.round([label.offset]).astype(int)
structure.append(gdsii.elements.Text(layer=layer,
text_type=text_type,
xy=xy,
string=label.string.encode('ASCII')))
# Add an SREF for each subpattern entry new_pats[(pat_id, pat_dose)] = pat
# strans must be set for angle and mag to take effect
# Go back through all the dose-specific patterns and fix up their subpattern entries
for (pat_id, pat_dose), pat in new_pats.items():
for subpat in pat.subpatterns: for subpat in pat.subpatterns:
dose_mult = subpat.dose * pat_dose dose_mult = subpat.dose * pat_dose
encoded_name = mangle_name(subpat.pattern, dose_mult).encode('ASCII') subpat.pattern = new_pats[(id(subpat.pattern), dose_mult)]
if len(encoded_name) == 0:
raise PatternError('Zero-length name after mangle+encode, originally "{}"'.format(subpat.pattern.name))
sref = gdsii.elements.SRef(struct_name=encoded_name,
xy=numpy.round([subpat.offset]).astype(int))
sref.strans = 0
sref.angle = subpat.rotation * 180 / numpy.pi
sref.mag = subpat.scale
mirror_x, mirror_y = subpat.mirrored
if mirror_y and mirror_y:
sref.angle += 180
elif mirror_x:
sref.strans = set_bit(sref.strans, 15 - 0, True)
elif mirror_y:
sref.angle += 180
sref.strans = set_bit(sref.strans, 15 - 0, True)
structure.append(sref)
with open(filename, mode='wb') as stream: return patterns, list(dose_vals)
lib.save(stream)
return dose_vals_list
def read_dtype2dose(filename: str) -> (List[Pattern], Dict[str, Any]): def read_dtype2dose(filename: str) -> (List[Pattern], Dict[str, Any]):
@ -285,34 +242,6 @@ def read(filename: str,
'logical_units_per_unit': lib.logical_unit, 'logical_units_per_unit': lib.logical_unit,
} }
def ref_element_to_subpat(element, offset: vector2) -> SubPattern:
# Helper function to create a SubPattern from an SREF or AREF. Sets subpat.pattern to None
# and sets the instance attribute .ref_name to the struct_name.
#
# BUG: "Absolute" means not affected by parent elements.
# That's not currently supported by masque at all, so need to either tag it and
# undo the parent transformations, or implement it in masque.
subpat = SubPattern(pattern=None, offset=offset)
subpat.ref_name = element.struct_name
if element.strans is not None:
if element.mag is not None:
subpat.scale = element.mag
# Bit 13 means absolute scale
if get_bit(element.strans, 15 - 13):
#subpat.offset *= subpat.scale
raise PatternError('Absolute scale is not implemented yet!')
if element.angle is not None:
subpat.rotation = element.angle * numpy.pi / 180
# Bit 14 means absolute rotation
if get_bit(element.strans, 15 - 14):
#subpat.offset = numpy.dot(rotation_matrix_2d(subpat.rotation), subpat.offset)
raise PatternError('Absolute rotation is not implemented yet!')
# Bit 0 means mirror x-axis
if get_bit(element.strans, 15 - 0):
subpat.mirror(axis=0)
return subpat
patterns = [] patterns = []
for structure in lib: for structure in lib:
pat = Pattern(name=structure.name.decode('ASCII')) pat = Pattern(name=structure.name.decode('ASCII'))
@ -341,18 +270,10 @@ def read(filename: str,
pat.labels.append(label) pat.labels.append(label)
elif isinstance(element, gdsii.elements.SRef): elif isinstance(element, gdsii.elements.SRef):
pat.subpatterns.append(ref_element_to_subpat(element, element.xy)) pat.subpatterns.append(_sref_to_subpat(element))
elif isinstance(element, gdsii.elements.ARef): elif isinstance(element, gdsii.elements.ARef):
xy = numpy.array(element.xy) pat.subpatterns.append(_aref_to_gridrep(element))
origin = xy[0]
col_spacing = (xy[1] - origin) / element.cols
row_spacing = (xy[2] - origin) / element.rows
for c in range(element.cols):
for r in range(element.rows):
offset = origin + c * col_spacing + r * row_spacing
pat.subpatterns.append(ref_element_to_subpat(element, offset))
patterns.append(pat) patterns.append(pat)
@ -378,3 +299,149 @@ def _mlayer2gds(mlayer):
else: else:
data_type = 0 data_type = 0
return layer, data_type return layer, data_type
def _sref_to_subpat(element: gdsii.elements.SRef) -> SubPattern:
# Helper function to create a SubPattern from an SREF. Sets subpat.pattern to None
# and sets the instance attribute .ref_name to the struct_name.
#
# BUG: "Absolute" means not affected by parent elements.
# That's not currently supported by masque at all, so need to either tag it and
# undo the parent transformations, or implement it in masque.
subpat = SubPattern(pattern=None, offset=element.xy)
subpat.ref_name = element.struct_name
if element.strans is not None:
if element.mag is not None:
subpat.scale = element.mag
# Bit 13 means absolute scale
if get_bit(element.strans, 15 - 13):
#subpat.offset *= subpat.scale
raise PatternError('Absolute scale is not implemented yet!')
if element.angle is not None:
subpat.rotation = element.angle * numpy.pi / 180
# Bit 14 means absolute rotation
if get_bit(element.strans, 15 - 14):
#subpat.offset = numpy.dot(rotation_matrix_2d(subpat.rotation), subpat.offset)
raise PatternError('Absolute rotation is not implemented yet!')
# Bit 0 means mirror x-axis
if get_bit(element.strans, 15 - 0):
subpat.mirror(axis=0)
return subpat
def _aref_to_gridrep(element: gdsii.elements.ARef) -> GridRepetition:
# Helper function to create a GridRepetition from an AREF. Sets gridrep.pattern to None
# and sets the instance attribute .ref_name to the struct_name.
#
# BUG: "Absolute" means not affected by parent elements.
# That's not currently supported by masque at all, so need to either tag it and
# undo the parent transformations, or implement it in masque.i
rotation = 0
offset = numpy.array(element.xy[0])
scale = 1
mirror_signs = numpy.ones(2)
if element.strans is not None:
if element.mag is not None:
scale = element.mag
# Bit 13 means absolute scale
if get_bit(element.strans, 15 - 13):
raise PatternError('Absolute scale is not implemented yet!')
if element.angle is not None:
rotation = element.angle * numpy.pi / 180
# Bit 14 means absolute rotation
if get_bit(element.strans, 15 - 14):
raise PatternError('Absolute rotation is not implemented yet!')
# Bit 0 means mirror x-axis
if get_bit(element.strans, 15 - 0):
mirror_signs[0] = -1
counts = [element.cols, element.rows]
vec_a0 = element.xy[1] - offset
vec_b0 = element.xy[2] - offset
a_vector = numpy.dot(rotation_matrix_2d(-rotation), vec_a0 / scale / counts[0]) * mirror_signs[0]
b_vector = numpy.dot(rotation_matrix_2d(-rotation), vec_b0 / scale / counts[1]) * mirror_signs[1]
gridrep = GridRepetition(pattern=None,
a_vector=a_vector,
b_vector=b_vector,
a_count=counts[0],
b_count=counts[1],
offset=offset,
rotation=rotation,
scale=scale,
mirrored=(mirror_signs == -1))
gridrep.ref_name = element.struct_name
return gridrep
def _subpatterns_to_refs(subpatterns: List[SubPattern or GridRepetition]
) -> List[gdsii.elements.ARef or gdsii.elements.SRef]:
# strans must be set for angle and mag to take effect
refs = []
for subpat in subpatterns:
sanitized_name = re.compile('[^A-Za-z0-9_\?\$]').sub('_', subpat.pattern.name)
encoded_name = sanitized_name.encode('ASCII')
if len(encoded_name) == 0:
raise PatternError('Zero-length name after sanitize+encode, originally "{}"'.format(subpat.pattern.name))
if isinstance(subpat, GridRepetition):
mirror_signs = (-1) ** numpy.array(subpat.mirrored)
xy = numpy.array(subpat.offset) + [
[0, 0],
numpy.dot(rotation_matrix_2d(subpat.rotation), subpat.a_vector * mirror_signs) * subpat.scale * subpat.a_count,
numpy.dot(rotation_matrix_2d(subpat.rotation), subpat.b_vector * mirror_signs) * subpat.scale * subpat.b_count,
]
ref = gdsii.elements.ARef(struct_name=encoded_name,
xy=numpy.round(xy).astype(int),
cols=subpat.a_count,
rows=subpat.b_count)
else:
ref = gdsii.elements.SRef(struct_name=encoded_name,
xy=numpy.round([subpat.offset]).astype(int))
ref.strans = 0
ref.angle = subpat.rotation * 180 / numpy.pi
mirror_x, mirror_y = subpat.mirrored
if mirror_y and mirror_y:
ref.angle += 180
elif mirror_x:
ref.strans = set_bit(ref.strans, 15 - 0, True)
elif mirror_y:
ref.angle += 180
ref.strans = set_bit(ref.strans, 15 - 0, True)
ref.mag = subpat.scale
refs.append(ref)
return refs
def _shapes_to_boundaries(shapes: List[Shape]
) -> List[gdsii.elements.Boundary]:
# Add a Boundary element for each shape
boundaries = []
for shape in shapes:
layer, data_type = _mlayer2gds(shape.layer)
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, :]))
boundaries.append(gdsii.elements.Boundary(layer=layer,
data_type=data_type,
xy=xy_closed))
return boundaries
def _labels_to_texts(labels: List[Label]) -> List[gdsii.elements.Text]:
texts = []
for label in labels:
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')))
return texts

View File

@ -12,6 +12,7 @@ import numpy
# .visualize imports matplotlib and matplotlib.collections # .visualize imports matplotlib and matplotlib.collections
from .subpattern import SubPattern from .subpattern import SubPattern
from .repetition import GridRepetition
from .shapes import Shape, Polygon from .shapes import Shape, Polygon
from .label import Label from .label import Label
from .utils import rotation_matrix_2d, vector2 from .utils import rotation_matrix_2d, vector2
@ -34,7 +35,7 @@ class Pattern:
""" """
shapes = None # type: List[Shape] shapes = None # type: List[Shape]
labels = None # type: List[Labels] labels = None # type: List[Labels]
subpatterns = None # type: List[SubPattern] subpatterns = None # type: List[SubPattern or GridRepetition]
name = None # type: str name = None # type: str
def __init__(self, def __init__(self,

291
masque/repetition.py Normal file
View File

@ -0,0 +1,291 @@
"""
Repetitions provides support for efficiently nesting multiple identical
instances of a Pattern in the same parent Pattern.
"""
from typing import Union, List
import copy
import numpy
from numpy import pi
from .error import PatternError
from .utils import is_scalar, rotation_matrix_2d, vector2
__author__ = 'Jan Petykiewicz'
# TODO need top-level comment about what order rotation/scale/offset/mirror/array are applied
class GridRepetition:
"""
GridRepetition provides support for efficiently embedding multiple copies of a Pattern
into another Pattern at regularly-spaced offsets.
"""
pattern = None # type: Pattern
_offset = (0.0, 0.0) # type: numpy.ndarray
_rotation = 0.0 # type: float
_dose = 1.0 # type: float
_scale = 1.0 # type: float
_mirrored = None # type: List[bool]
_a_vector = None # type: numpy.ndarray
_b_vector = None # type: numpy.ndarray
a_count = None # type: int
b_count = 1 # type: int
def __init__(self,
pattern: 'Pattern',
a_vector: numpy.ndarray,
a_count: int,
b_vector: numpy.ndarray = None,
b_count: int = 1,
offset: vector2 = (0.0, 0.0),
rotation: float = 0.0,
mirrored: List[bool] = None,
dose: float = 1.0,
scale: float = 1.0):
"""
:param a_vector: First lattice vector, of the form [x, y].
Specifies center-to-center spacing between adjacent elements.
:param a_count: Number of elements in the a_vector direction.
:param b_vector: Second lattice vector, of the form [x, y].
Specifies center-to-center spacing between adjacent elements.
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.
:raises: InvalidDataError if b_* inputs conflict with each other
or a_count < 1.
"""
if b_vector is None:
if b_count > 1:
raise PatternError('Repetition has b_count > 1 but no b_vector')
else:
b_vector = numpy.array([0.0, 0.0])
if a_count < 1:
raise InvalidDataError('Repetition has too-small a_count: '
'{}'.format(a_count))
if b_count < 1:
raise InvalidDataError('Repetition has too-small b_count: '
'{}'.format(b_count))
self.a_vector = a_vector
self.b_vector = b_vector
self.a_count = a_count
self.b_count = b_count
self.pattern = pattern
self.offset = offset
self.rotation = rotation
self.dose = dose
self.scale = scale
if mirrored is None:
mirrored = [False, False]
self.mirrored = mirrored
# offset property
@property
def offset(self) -> numpy.ndarray:
return self._offset
@offset.setter
def offset(self, val: vector2):
if not isinstance(val, numpy.ndarray):
val = numpy.array(val, dtype=float)
if val.size != 2:
raise PatternError('Offset must be convertible to size-2 ndarray')
self._offset = val.flatten()
# dose property
@property
def dose(self) -> float:
return self._dose
@dose.setter
def dose(self, val: float):
if not is_scalar(val):
raise PatternError('Dose must be a scalar')
if not val >= 0:
raise PatternError('Dose must be non-negative')
self._dose = val
# scale property
@property
def scale(self) -> float:
return self._scale
@scale.setter
def scale(self, val: float):
if not is_scalar(val):
raise PatternError('Scale must be a scalar')
if not val > 0:
raise PatternError('Scale must be positive')
self._scale = val
# Rotation property [ccw]
@property
def rotation(self) -> float:
return self._rotation
@rotation.setter
def rotation(self, val: float):
if not is_scalar(val):
raise PatternError('Rotation must be a scalar')
self._rotation = val % (2 * pi)
# Mirrored property
@property
def mirrored(self) -> List[bool]:
return self._mirrored
@mirrored.setter
def mirrored(self, val: List[bool]):
if is_scalar(val):
raise PatternError('Mirrored must be a 2-element list of booleans')
self._mirrored = val
# a_vector property
@property
def a_vector(self) -> numpy.ndarray:
return self._a_vector
@a_vector.setter
def a_vector(self, val: vector2):
if not isinstance(val, numpy.ndarray):
val = numpy.array(val, dtype=float)
if val.size != 2:
raise PatternError('a_vector must be convertible to size-2 ndarray')
self._a_vector = val.flatten()
# b_vector property
@property
def b_vector(self) -> numpy.ndarray:
return self._b_vector
@b_vector.setter
def b_vector(self, val: vector2):
if not isinstance(val, numpy.ndarray):
val = numpy.array(val, dtype=float)
if val.size != 2:
raise PatternError('b_vector must be convertible to size-2 ndarray')
self._b_vector = val.flatten()
def as_pattern(self) -> 'Pattern':
"""
Returns a copy of self.pattern which has been scaled, rotated, etc. according to this
SubPattern's properties.
:return: Copy of self.pattern that has been altered to reflect the SubPattern's properties.
"""
#xy = numpy.array(element.xy)
#origin = xy[0]
#col_spacing = (xy[1] - origin) / element.cols
#row_spacing = (xy[2] - origin) / element.rows
patterns = []
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.translate_elements(offset)
patterns.append(newPat)
combined = patterns[0]
for p in patterns[1:]:
combined.append(p)
combined.scale_by(self.scale)
[combined.mirror(ax) for ax, do in enumerate(self.mirrored) if do]
combined.rotate_around((0.0, 0.0), self.rotation)
combined.translate_elements(self.offset)
combined.scale_element_doses(self.dose)
return combined
def translate(self, offset: vector2) -> 'GridRepetition':
"""
Translate by the given offset
:param offset: Translate by this offset
:return: self
"""
self.offset += offset
return self
def rotate_around(self, pivot: vector2, rotation: float) -> 'GridRepetition':
"""
Rotate around a point
:param pivot: Point to rotate around
:param rotation: Angle to rotate by (counterclockwise, radians)
:return: self
"""
pivot = numpy.array(pivot, dtype=float)
self.translate(-pivot)
self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset)
self.rotate(rotation)
self.translate(+pivot)
return self
def rotate(self, rotation: float) -> 'GridRepetition':
"""
Rotate around (0, 0)
:param rotation: Angle to rotate by (counterclockwise, radians)
:return: self
"""
self.rotation += rotation
return self
def mirror(self, axis: int) -> 'GridRepetition':
"""
Mirror the subpattern across an axis.
:param axis: Axis to mirror across.
:return: self
"""
self.mirrored[axis] = not self.mirrored[axis]
return self
def get_bounds(self) -> numpy.ndarray or None:
"""
Return a numpy.ndarray containing [[x_min, y_min], [x_max, y_max]], corresponding to the
extent of the SubPattern in each dimension.
Returns None if the contained Pattern is empty.
:return: [[x_min, y_min], [x_max, y_max]] or None
"""
return self.as_pattern().get_bounds()
def scale_by(self, c: float) -> 'GridRepetition':
"""
Scale the subpattern by a factor
:param c: scaling factor
"""
self.scale *= c
return self
def copy(self) -> 'GridRepetition':
"""
Return a shallow copy of the repetition.
:return: copy.copy(self)
"""
return copy.copy(self)
def deepcopy(self) -> 'SubPattern':
"""
Return a deep copy of the repetition.
:return: copy.copy(self)
"""
return copy.deepcopy(self)