masque/masque/pattern.py

981 lines
36 KiB
Python

"""
Base object representing a lithography mask.
"""
from typing import List, Callable, Tuple, Dict, Union, Set, Sequence, Optional, Type, overload
from typing import MutableMapping, Iterable
import copy
import pickle
from itertools import chain
from collections import defaultdict
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, 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(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')
shapes: List[Shape]
""" List of all shapes in this Pattern.
Elements in this list are assumed to inherit from Shape or provide equivalent functions.
"""
labels: List[Label]
""" List of all labels in this Pattern. """
subpatterns: List[SubPattern]
""" List of all references to other patterns (`SubPattern`s) in this `Pattern`.
Multiple objects in this list may reference the same Pattern object
(i.e. multiple instances of the same object).
"""
name: str
""" A name for this pattern """
def __init__(self,
name: str = '',
*,
shapes: Sequence[Shape] = (),
labels: Sequence[Label] = (),
subpatterns: Sequence[SubPattern] = (),
annotations: Optional[annotations_t] = None,
locked: bool = False,
):
"""
Basic init; arguments get assigned to member variables.
Non-list inputs for shapes and subpatterns get converted to lists.
Args:
shapes: Initial shapes in the Pattern
labels: Initial labels in the Pattern
subpatterns: Initial subpatterns in the Pattern
name: An identifier for the Pattern
locked: Whether to lock the pattern after construction
"""
LockableImpl.unlock(self)
if isinstance(shapes, list):
self.shapes = shapes
else:
self.shapes = list(shapes)
if isinstance(labels, list):
self.labels = labels
else:
self.labels = list(labels)
if isinstance(subpatterns, list):
self.subpatterns = subpatterns
else:
self.subpatterns = list(subpatterns)
self.annotations = annotations if annotations is not None else {}
self.name = name
self.set_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],
annotations=copy.deepcopy(self.annotations),
locked=self.locked)
def __deepcopy__(self, memo: Dict = None) -> 'Pattern':
memo = {} if memo is None else memo
new = Pattern(name=self.name,
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
def rename(self, name: str) -> 'Pattern':
self.name = name
return self
def append(self, other_pattern: 'Pattern') -> 'Pattern':
"""
Appends all shapes, labels and subpatterns from other_pattern to self's shapes,
labels, and supbatterns.
Args:
other_pattern: The Pattern to append
Returns:
self
"""
self.subpatterns += other_pattern.subpatterns
self.shapes += other_pattern.shapes
self.labels += other_pattern.labels
return self
def subset(self,
shapes_func: Callable[[Shape], bool] = None,
labels_func: Callable[[Label], bool] = None,
subpatterns_func: Callable[[SubPattern], bool] = None,
recursive: bool = False,
) -> 'Pattern':
"""
Returns a Pattern containing only the entities (e.g. shapes) for which the
given entity_func returns True.
Self is _not_ altered, but shapes, labels, and subpatterns are _not_ copied.
Args:
shapes_func: Given a shape, returns a boolean denoting whether the shape is a member
of the subset. Default always returns False.
labels_func: Given a label, returns a boolean denoting whether the label is a member
of the subset. Default always returns False.
subpatterns_func: Given a subpattern, returns a boolean denoting if it is a member
of the subset. Default always returns False.
recursive: If True, also calls .subset() recursively on patterns referenced by this
pattern.
Returns:
A Pattern containing all the shapes and subpatterns for which the parameter
functions return True
"""
def do_subset(src: Optional['Pattern']) -> Optional['Pattern']:
if src is None:
return None
pat = Pattern(name=src.name)
if shapes_func is not None:
pat.shapes = [s for s in src.shapes if shapes_func(s)]
if labels_func is not None:
pat.labels = [s for s in src.labels if labels_func(s)]
if subpatterns_func is not None:
pat.subpatterns = [s for s in src.subpatterns if subpatterns_func(s)]
return pat
if recursive:
pat = self.apply(do_subset)
else:
pat = do_subset(self)
assert(pat is not None)
return pat
def apply(self,
func: Callable[[Optional['Pattern']], Optional['Pattern']],
memo: Optional[Dict[int, Optional['Pattern']]] = None,
) -> Optional['Pattern']:
"""
Recursively apply func() to this pattern and any pattern it references.
func() is expected to take and return a Pattern.
func() is first applied to the pattern as a whole, then any referenced patterns.
It is only applied to any given pattern once, regardless of how many times it is
referenced.
Args:
func: Function which accepts a Pattern, and returns a pattern.
memo: Dictionary used to avoid re-running on multiply-referenced patterns.
Stores `{id(pattern): func(pattern)}` for patterns which have already been processed.
Default `None` (no already-processed patterns).
Returns:
The result of applying func() to this pattern and all subpatterns.
Raises:
PatternError if called on a pattern containing a circular reference.
"""
if memo is None:
memo = {}
pat_id = id(self)
if pat_id not in memo:
memo[pat_id] = None
pat = func(self)
if pat is not None:
for subpat in pat.subpatterns:
if subpat.pattern is None:
subpat.pattern = func(None)
else:
subpat.pattern = subpat.pattern.apply(func, memo)
memo[pat_id] = pat
elif memo[pat_id] is None:
raise PatternError('.apply() called on pattern with circular reference')
else:
pat = memo[pat_id]
return pat
def dfs(self,
visit_before: visitor_function_t = None,
visit_after: visitor_function_t = None,
transform: Union[numpy.ndarray, bool, None] = False,
memo: Optional[Dict] = None,
hierarchy: Tuple['Pattern', ...] = (),
) -> 'Pattern':
"""
Experimental convenience function.
Performs a depth-first traversal of this pattern and its subpatterns.
At each pattern in the tree, the following sequence is called:
```
current_pattern = visit_before(current_pattern, **vist_args)
for sp in current_pattern.subpatterns]
sp.pattern = sp.pattern.df(visit_before, visit_after, updated_transform,
memo, (current_pattern,) + hierarchy)
current_pattern = visit_after(current_pattern, **visit_args)
```
where `visit_args` are
`hierarchy`: (top_pattern, L1_pattern, L2_pattern, ..., parent_pattern)
tuple of all parent-and-higher patterns
`transform`: numpy.ndarray containing cumulative
[x_offset, y_offset, rotation (rad), mirror_x (0 or 1)]
for the instance being visited
`memo`: Arbitrary dict (not altered except by visit_*())
Args:
visit_before: Function to call before traversing subpatterns.
Should accept a `Pattern` and `**visit_args`, and return the (possibly modified)
pattern. Default `None` (not called).
visit_after: Function to call after traversing subpatterns.
Should accept a Pattern and **visit_args, and return the (possibly modified)
pattern. Default `None` (not called).
transform: Initial value for `visit_args['transform']`.
Can be `False`, in which case the transform is not calculated.
`True` or `None` is interpreted as `[0, 0, 0, 0]`.
memo: Arbitrary dict for use by `visit_*()` functions. Default `None` (empty dict).
hierarchy: Tuple of patterns specifying the hierarchy above the current pattern.
Appended to the start of the generated `visit_args['hierarchy']`.
Default is an empty tuple.
Returns:
The result, including `visit_before(self, ...)` and `visit_after(self, ...)`.
Note that `self` may also be altered!
"""
if memo is None:
memo = {}
if transform is None or transform is True:
transform = numpy.zeros(4)
if self in hierarchy:
raise PatternError('.dfs() called on pattern with circular reference')
pat = self
if visit_before is not None:
pat = visit_before(pat, hierarchy=hierarchy, memo=memo, transform=transform) # type: ignore
for subpattern in self.subpatterns:
if transform is not False:
sign = numpy.ones(2)
if transform[3]:
sign[1] = -1
xy = numpy.dot(rotation_matrix_2d(transform[2]), subpattern.offset * sign)
mirror_x, angle = normalize_mirror(subpattern.mirrored)
angle += subpattern.rotation
sp_transform = transform + (xy[0], xy[1], angle, mirror_x)
sp_transform[3] %= 2
else:
sp_transform = False
if subpattern.pattern is not None:
subpattern.pattern = subpattern.pattern.dfs(visit_before=visit_before,
visit_after=visit_after,
transform=sp_transform,
memo=memo,
hierarchy=hierarchy + (self,))
if visit_after is not None:
pat = visit_after(pat, hierarchy=hierarchy, memo=memo, transform=transform) # type: ignore
return pat
def polygonize(self,
poly_num_points: Optional[int] = None,
poly_max_arclen: Optional[float] = None,
) -> 'Pattern':
"""
Calls `.to_polygons(...)` on all the shapes in this Pattern and any referenced patterns,
replacing them with the returned polygons.
Arguments are passed directly to `shape.to_polygons(...)`.
Args:
poly_num_points: Number of points to use for each polygon. Can be overridden by
`poly_max_arclen` if that results in more points. Optional, defaults to shapes'
internal defaults.
poly_max_arclen: Maximum arclength which can be approximated by a single line
segment. Optional, defaults to shapes' internal defaults.
Returns:
self
"""
old_shapes = self.shapes
self.shapes = list(chain.from_iterable(
(shape.to_polygons(poly_num_points, poly_max_arclen)
for shape in old_shapes)))
for subpat in self.subpatterns:
if subpat.pattern is not None:
subpat.pattern.polygonize(poly_num_points, poly_max_arclen)
return self
def manhattanize(self,
grid_x: numpy.ndarray,
grid_y: numpy.ndarray,
) -> 'Pattern':
"""
Calls `.polygonize()` and `.flatten()` on the pattern, then calls `.manhattanize()` on all the
resulting shapes, replacing them with the returned Manhattan polygons.
Args:
grid_x: List of allowed x-coordinates for the Manhattanized polygon edges.
grid_y: List of allowed y-coordinates for the Manhattanized polygon edges.
Returns:
self
"""
self.polygonize().flatten()
old_shapes = self.shapes
self.shapes = list(chain.from_iterable(
(shape.manhattanize(grid_x, grid_y) for shape in old_shapes)))
return self
def subpatternize(self,
recursive: bool = True,
norm_value: int = int(1e6),
exclude_types: Tuple[Type] = (Polygon,)
) -> 'Pattern':
"""
Iterates through this `Pattern` and all referenced `Pattern`s. Within each `Pattern`, it iterates
over all shapes, calling `.normalized_form(norm_value)` on them to retrieve a scale-,
offset-, dose-, and rotation-independent form. Each shape whose normalized form appears
more than once is removed and re-added using subpattern objects referencing a newly-created
`Pattern` containing only the normalized form of the shape.
Note:
The default norm_value was chosen to give a reasonable precision when converting
to GDSII, which uses integer values for pixel coordinates.
Args:
recursive: Whether to call recursively on self's subpatterns. Default `True`.
norm_value: Passed to `shape.normalized_form(norm_value)`. Default `1e6` (see function
note about GDSII)
exclude_types: Shape types passed in this argument are always left untouched, for
speed or convenience. Default: `(shapes.Polygon,)`
Returns:
self
"""
if exclude_types is None:
exclude_types = ()
if recursive:
for subpat in self.subpatterns:
if subpat.pattern is None:
continue
subpat.pattern.subpatternize(recursive=True,
norm_value=norm_value,
exclude_types=exclude_types)
# Create a dict which uses the label tuple from `.normalized_form()` as a key, and which
# stores `(function_to_create_normalized_shape, [(index_in_shapes, values), ...])`, where
# values are the `(offset, scale, rotation, dose)` values as calculated by `.normalized_form()`
shape_table: MutableMapping[Tuple, List] = defaultdict(lambda: [None, list()])
for i, shape in enumerate(self.shapes):
if not any((isinstance(shape, t) for t in exclude_types)):
label, values, func = shape.normalized_form(norm_value)
shape_table[label][0] = func
shape_table[label][1].append((i, values))
# Iterate over the normalized shapes in the table. If any normalized shape occurs more than
# once, create a `Pattern` holding a normalized shape object, and add `self.subpatterns`
# entries for each occurrence in self. Also, note down that we should delete the
# `self.shapes` entries for which we made SubPatterns.
shapes_to_remove = []
for label in shape_table:
if len(shape_table[label][1]) > 1:
shape = shape_table[label][0]()
pat = Pattern(shapes=[shape])
for i, values in shape_table[label][1]:
(offset, scale, rotation, mirror_x, dose) = values
subpat = SubPattern(pattern=pat, offset=offset, scale=scale,
rotation=rotation, dose=dose, mirrored=(mirror_x, False))
self.subpatterns.append(subpat)
shapes_to_remove.append(i)
# Remove any shapes for which we have created subpatterns.
for i in sorted(shapes_to_remove, reverse=True):
del self.shapes[i]
return self
def as_polygons(self) -> List[numpy.ndarray]:
"""
Represents the pattern as a list of polygons.
Deep-copies the pattern, then calls `.polygonize()` and `.flatten()` on the copy in order to
generate the list of polygons.
Returns:
A list of `(Ni, 2)` `numpy.ndarray`s specifying vertices of the polygons. Each ndarray
is of the form `[[x0, y0], [x1, y1],...]`.
"""
pat = self.deepcopy().deepunlock().polygonize().flatten()
return [shape.vertices + shape.offset for shape in pat.shapes] # type: ignore # mypy can't figure out that shapes are all Polygons now
@overload
def referenced_patterns_by_id(self) -> Dict[int, 'Pattern']:
pass
@overload
def referenced_patterns_by_id(self, include_none: bool) -> Dict[int, Optional['Pattern']]:
pass
def referenced_patterns_by_id(self,
include_none: bool = False,
recursive: bool = True,
) -> Union[Dict[int, Optional['Pattern']],
Dict[int, 'Pattern']]:
"""
Create a dictionary with `{id(pat): pat}` for all Pattern objects referenced by this
Pattern (by default, operates recursively on all referenced Patterns as well).
Args:
include_none: If `True`, references to `None` will be included. Default `False`.
recursive: If `True`, operates recursively on all referenced patterns. Default `True`.
Returns:
Dictionary with `{id(pat): pat}` for all referenced Pattern objects
"""
ids: Dict[int, Optional['Pattern']] = {}
for subpat in self.subpatterns:
pat = subpat.pattern
if id(pat) in ids:
continue
if include_none or pat is not None:
ids[id(pat)] = pat
if recursive and pat is not None:
ids.update(pat.referenced_patterns_by_id())
return ids
def referenced_patterns_by_name(self, **kwargs) -> List[Tuple[Optional[str], Optional['Pattern']]]:
"""
Create a list of `(pat.name, pat)` tuples for all Pattern objects referenced by this
Pattern (operates recursively on all referenced Patterns as well).
Note that names are not necessarily unique, so a list of tuples is returned
rather than a dict.
Args:
**kwargs: passed to `referenced_patterns_by_id()`.
Returns:
List of `(pat.name, pat)` tuples for all referenced Pattern objects
"""
pats_by_id = self.referenced_patterns_by_id(**kwargs)
pat_list = [(p.name if p is not None else None, p) for p in pats_by_id.values()]
return pat_list
def subpatterns_by_id(self,
include_none: bool = False,
recursive: bool = True,
) -> Dict[int, List[SubPattern]]:
"""
Create a dictionary which maps `{id(referenced_pattern): [subpattern0, ...]}`
for all SubPattern objects referenced by this Pattern (by default, operates
recursively on all referenced Patterns as well).
Args:
include_none: If `True`, references to `None` will be included. Default `False`.
recursive: If `True`, operates recursively on all referenced patterns. Default `True`.
Returns:
Dictionary mapping each pattern id to a list of subpattern objects referencing the pattern.
"""
ids: Dict[int, List[SubPattern]] = defaultdict(list)
for subpat in self.subpatterns:
pat = subpat.pattern
if include_none or pat is not None:
ids[id(pat)].append(subpat)
if recursive and pat is not None:
ids.update(pat.subpatterns_by_id(include_none=include_none))
return dict(ids)
def get_bounds(self) -> Union[numpy.ndarray, None]:
"""
Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the
extent of the Pattern's contents in each dimension.
Returns `None` if the Pattern is empty.
Returns:
`[[x_min, y_min], [x_max, y_max]]` or `None`
"""
if self.is_empty():
return None
min_bounds = numpy.array((+inf, +inf))
max_bounds = numpy.array((-inf, -inf))
for entry in chain(self.shapes, self.subpatterns, self.labels):
bounds = entry.get_bounds()
if bounds is None:
continue
min_bounds = numpy.minimum(min_bounds, bounds[0, :])
max_bounds = numpy.maximum(max_bounds, bounds[1, :])
if (max_bounds < min_bounds).any():
return None
else:
return numpy.vstack((min_bounds, max_bounds))
def flatten(self) -> 'Pattern':
"""
Removes all subpatterns and adds equivalent shapes.
Shape identifiers are changed to represent their original position in the
pattern hierarchy:
`(L1_name (str), L1_index (int), L2_name, L2_index, ..., *original_shape_identifier)`
where
`L1_name` is the first-level subpattern's name (e.g. `self.subpatterns[0].pattern.name`),
`L2_name` is the next-level subpattern's name (e.g.
`self.subpatterns[0].pattern.subpatterns[0].pattern.name`) and
`L1_index` is an integer used to differentiate between multiple instance ofi the same
(or same-named) subpatterns.
Returns:
self
"""
subpatterns = copy.deepcopy(self.subpatterns)
self.subpatterns = []
shape_counts: Dict[Tuple, int] = {}
for subpat in subpatterns:
if subpat.pattern is None:
continue
subpat.pattern.flatten()
p = subpat.as_pattern()
# Update identifiers so each shape has a unique one
for shape in p.shapes:
combined_identifier = (subpat.pattern.name,) + shape.identifier
shape_count = shape_counts.get(combined_identifier, 0)
shape.identifier = (subpat.pattern.name, shape_count) + shape.identifier
shape_counts[combined_identifier] = shape_count + 1
self.append(p)
return self
def wrap_repeated_shapes(self,
name_func: Callable[['Pattern', Union[Shape, Label]], str] = lambda p, s: '_repetition',
recursive: bool = True,
) -> 'Pattern':
"""
Wraps all shapes and labels with a non-`None` `repetition` attribute
into a `SubPattern`/`Pattern` combination, and applies the `repetition`
to each `SubPattern` instead of its contained shape.
Args:
name_func: Function f(this_pattern, shape) which generates a name for the
wrapping pattern. Default always returns '_repetition'.
recursive: If `True`, this function is also applied to all referenced patterns
recursively. Default `True`.
Returns:
self
"""
def do_wrap(pat: Optional[Pattern]) -> Optional[Pattern]:
if pat is None:
return pat
new_subpatterns = []
for shape in pat.shapes:
if shape.repetition is None:
continue
new_subpatterns.append(SubPattern(Pattern(name_func(pat, shape), shapes=[shape])))
shape.repetition = None
for label in self.labels:
if label.repetition is None:
continue
new_subpatterns.append(SubPattern(Pattern(name_func(pat, shape), labels=[label])))
label.repetition = None
pat.subpatterns += new_subpatterns
return pat
if recursive:
self.apply(do_wrap)
else:
do_wrap(self)
return self
def translate_elements(self, offset: vector2) -> 'Pattern':
"""
Translates all shapes, label, and subpatterns by the given offset.
Args:
offset: (x, y) to translate by
Returns:
self
"""
for entry in chain(self.shapes, self.subpatterns, self.labels):
entry.translate(offset)
return self
def scale_elements(self, c: float) -> 'Pattern':
""""
Scales all shapes and subpatterns by the given value.
Args:
c: factor to scale by
Returns:
self
"""
for entry in chain(self.shapes, self.subpatterns):
entry.scale_by(c)
return self
def scale_by(self, c: float) -> 'Pattern':
"""
Scale this Pattern by the given value
(all shapes and subpatterns and their offsets are scaled)
Args:
c: factor to scale by
Returns:
self
"""
entry: Scalable
for entry in chain(self.shapes, self.subpatterns):
entry.offset *= c
entry.scale_by(c)
for label in self.labels:
label.offset *= c
return self
def rotate_around(self, pivot: vector2, rotation: float) -> 'Pattern':
"""
Rotate the Pattern around the a location.
Args:
pivot: (x, y) location to rotate around
rotation: Angle to rotate by (counter-clockwise, radians)
Returns:
self
"""
pivot = numpy.array(pivot)
self.translate_elements(-pivot)
self.rotate_elements(rotation)
self.rotate_element_centers(rotation)
self.translate_elements(+pivot)
return self
def rotate_element_centers(self, rotation: float) -> 'Pattern':
"""
Rotate the offsets of all shapes, labels, and subpatterns around (0, 0)
Args:
rotation: Angle to rotate by (counter-clockwise, radians)
Returns:
self
"""
for entry in chain(self.shapes, self.subpatterns, self.labels):
entry.offset = numpy.dot(rotation_matrix_2d(rotation), entry.offset)
return self
def rotate_elements(self, rotation: float) -> 'Pattern':
"""
Rotate each shape and subpattern around its center (offset)
Args:
rotation: Angle to rotate by (counter-clockwise, radians)
Returns:
self
"""
for entry in chain(self.shapes, self.subpatterns):
entry.rotate(rotation)
return self
def mirror_element_centers(self, axis: int) -> 'Pattern':
"""
Mirror the offsets of all shapes, labels, and subpatterns across an axis
Args:
axis: Axis to mirror across
(0: mirror across x axis, 1: mirror across y axis)
Returns:
self
"""
for entry in chain(self.shapes, self.subpatterns, self.labels):
entry.offset[axis - 1] *= -1
return self
def mirror_elements(self, axis: int) -> 'Pattern':
"""
Mirror each shape and subpattern across an axis, relative to its
offset
Args:
axis: Axis to mirror across
(0: mirror across x axis, 1: mirror across y axis)
Returns:
self
"""
for entry in chain(self.shapes, self.subpatterns):
entry.mirror(axis)
return self
def mirror(self, axis: int) -> 'Pattern':
"""
Mirror the Pattern across an axis
Args:
axis: Axis to mirror across
(0: mirror across x axis, 1: mirror across y axis)
Returns:
self
"""
self.mirror_elements(axis)
self.mirror_element_centers(axis)
return self
def scale_element_doses(self, c: float) -> 'Pattern':
"""
Multiply all shape and subpattern doses by a factor
Args:
c: Factor to multiply doses by
Return:
self
"""
for entry in chain(self.shapes, self.subpatterns):
entry.dose *= c
return self
def copy(self) -> 'Pattern':
"""
Return a copy of the Pattern, deep-copying shapes and copying subpattern
entries, but not deep-copying any referenced patterns.
See also: `Pattern.deepcopy()`
Returns:
A copy of the current Pattern.
"""
return copy.copy(self)
def deepcopy(self) -> 'Pattern':
"""
Convenience method for `copy.deepcopy(pattern)`
Returns:
A deep copy of the current Pattern.
"""
return copy.deepcopy(self)
def is_empty(self) -> bool:
"""
Returns:
True if the pattern is contains no shapes, labels, or subpatterns.
"""
return (len(self.subpatterns) == 0 and
len(self.shapes) == 0 and
len(self.labels) == 0)
def lock(self) -> 'Pattern':
"""
Lock the pattern, raising an exception if it is modified.
Also see `deeplock()`.
Returns:
self
"""
if not self.locked:
self.shapes = tuple(self.shapes)
self.labels = tuple(self.labels)
self.subpatterns = tuple(self.subpatterns)
LockableImpl.lock(self)
return self
def unlock(self) -> 'Pattern':
"""
Unlock the pattern
Returns:
self
"""
if self.locked:
LockableImpl.unlock(self)
self.shapes = list(self.shapes)
self.labels = list(self.labels)
self.subpatterns = list(self.subpatterns)
return self
def deeplock(self) -> 'Pattern':
"""
Recursively lock the pattern, all referenced shapes, subpatterns, and labels.
Returns:
self
"""
self.lock()
for ss in chain(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, since anything
you change will be changed everywhere it is referenced!
Return:
self
"""
self.unlock()
for ss in chain(self.shapes, self.labels):
ss.unlock()
for sp in self.subpatterns:
sp.deepunlock()
return self
@staticmethod
def load(filename: str) -> 'Pattern':
"""
Load a Pattern from a file using pickle
Args:
filename: Filename to load from
Returns:
Loaded Pattern
"""
with open(filename, 'rb') as f:
pattern = pickle.load(f)
return pattern
def save(self, filename: str) -> 'Pattern':
"""
Save the Pattern to a file using pickle
Args:
filename: Filename to save to
Returns:
self
"""
with open(filename, 'wb') as f:
pickle.dump(self, f, protocol=pickle.HIGHEST_PROTOCOL)
return self
def visualize(self,
offset: vector2 = (0., 0.),
line_color: str = 'k',
fill_color: str = 'none',
overdraw: bool = False):
"""
Draw a picture of the Pattern and wait for the user to inspect it
Imports `matplotlib`.
Note that this can be slow; it is often faster to export to GDSII and use
klayout or a different GDS viewer!
Args:
offset: Coordinates to offset by before drawing
line_color: Outlines are drawn with this color (passed to `matplotlib.collections.PolyCollection`)
fill_color: Interiors are drawn with this color (passed to `matplotlib.collections.PolyCollection`)
overdraw: Whether to create a new figure or draw on a pre-existing one
"""
# TODO: add text labels to visualize()
from matplotlib import pyplot # type: ignore
import matplotlib.collections # type: ignore
offset = numpy.array(offset, dtype=float)
if not overdraw:
figure = pyplot.figure()
pyplot.axis('equal')
else:
figure = pyplot.gcf()
axes = figure.gca()
polygons = []
for shape in self.shapes:
polygons += [offset + s.offset + s.vertices for s in shape.to_polygons()]
mpl_poly_collection = matplotlib.collections.PolyCollection(polygons,
facecolors=fill_color,
edgecolors=line_color)
axes.add_collection(mpl_poly_collection)
pyplot.axis('equal')
for subpat in self.subpatterns:
subpat.as_pattern().visualize(offset=offset, overdraw=True,
line_color=line_color, fill_color=fill_color)
if not overdraw:
pyplot.show()
@staticmethod
def find_toplevel(patterns: Iterable['Pattern']) -> List['Pattern']:
"""
Given a list of Pattern objects, return those that are not referenced by
any other pattern.
Args:
patterns: A list of patterns to filter.
Returns:
A filtered list in which no pattern is referenced by any other pattern.
"""
def get_children(pat: Pattern, memo: Set) -> Set:
children = set(sp.pattern for sp in pat.subpatterns if sp.pattern is not None)
new_children = children - memo
memo |= new_children
for child_pat in new_children:
memo |= get_children(child_pat, memo)
return memo
patterns = set(patterns)
not_toplevel: Set['Pattern'] = set()
for pattern in patterns:
not_toplevel |= get_children(pattern, not_toplevel)
toplevel = list(patterns - not_toplevel)
return toplevel
def __repr__(self) -> str:
locked = ' L' if self.locked else ''
return (f'<Pattern "{self.name}": sh{len(self.shapes)} sp{len(self.subpatterns)} la{len(self.labels)}{locked}>')