You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
masque/masque/pattern.py

413 lines
15 KiB
Python

"""
Base object for containing a lithography mask.
"""
from typing import List, Callable, Tuple, Dict, Union
import copy
import itertools
import pickle
from collections import defaultdict
import numpy
# .visualize imports matplotlib and matplotlib.collections
from .subpattern import SubPattern
from .shapes import Shape, Polygon
from .utils import rotation_matrix_2d, vector2
__author__ = 'Jan Petykiewicz'
class Pattern(object):
"""
2D layout consisting of some set of shapes and references to other Pattern objects
(via SubPattern). Shapes are assumed to inherit from .shapes.Shape or provide equivalent
functions.
:var shapes: List of all shapes in this Pattern. Elements in this list are assumed to inherit
from Shape or provide equivalent functions.
:var subpatterns: List of all SubPattern objects in this Pattern. Multiple SubPattern objects
may reference the same Pattern object.
:var name: An identifier for this object. Not necessarily unique.
"""
shapes = List[Shape]
subpatterns = List[SubPattern]
name = str
def __init__(self,
shapes: List[Shape]=(),
subpatterns: List[SubPattern]=(),
name: str='',
):
"""
Basic init; arguments get assigned to member variables.
Non-list inputs for shapes and subpatterns get converted to lists.
:param shapes: Initial shapes in the Pattern
:param subpatterns: Initial subpatterns in the Pattern
:param name: An identifier for the Pattern
"""
if isinstance(shapes, list):
self.shapes = shapes
else:
self.shapes = list(shapes)
if isinstance(subpatterns, list):
self.subpatterns = subpatterns
else:
self.subpatterns = list(subpatterns)
self.name = name
def append(self, other_pattern: 'Pattern') -> 'Pattern':
"""
Appends all shapes and subpatterns from other_pattern to self's shapes and subpatterns.
:param other_pattern: The Pattern to append
:return: self
"""
self.subpatterns += other_pattern.subpatterns
self.shapes += other_pattern.shapes
return self
def subset(self,
shapes_func: Callable[[Shape], bool]=None,
subpatterns_func: Callable[[SubPattern], bool]=None,
) -> 'Pattern':
"""
Returns a Pattern containing only the shapes and subpatterns for which shapes_func or
subpatterns_func returns True.
Self is _not_ altered, but shapes and subpatterns are _not_ copied.
:param shapes_func: Given a shape, returns a boolean denoting whether the shape is a member
of the subset
:param subpatterns_func: Given a subpattern, returns a boolean denoting if it is a member
of the subset
:return: A Pattern containing all the shapes and subpatterns for which the parameter
functions return True
"""
pat = Pattern()
if shapes_func is not None:
pat.shapes = [s for s in self.shapes if shapes_func(s)]
if subpatterns_func is not None:
pat.subpatterns = [s for s in self.subpatterns if subpatterns_func(s)]
return pat
def polygonize(self,
poly_num_points: int=None,
poly_max_arclen: 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(...).
:param 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.
:param poly_max_arclen: Maximum arclength which can be approximated by a single line
segment. Optional, defaults to shapes' internal defaults.
:return: self
"""
old_shapes = self.shapes
self.shapes = list(itertools.chain.from_iterable(
(shape.to_polygons(poly_num_points, poly_max_arclen)
for shape in old_shapes)))
for subpat in self.subpatterns:
subpat.pattern.polygonize(poly_num_points, poly_max_arclen)
return self
def subpatternize(self,
recursive: bool=True,
norm_value: int=1e6,
exclude_types: Tuple[Shape]=(Polygon,)
) -> 'Pattern':
"""
Iterates through this Pattern and all referenced Patterns. 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 that the default norm_value was chosen to give a reasonable precision when converting
to GDSII, which uses integer values for pixel coordinates.
:param recursive: Whether to call recursively on self's subpatterns. Default True.
:param norm_value: Passed to shape.normalized_form(norm_value). Default 1e6 (see function
note about GDSII)
:param exclude_types: Shape types passed in this argument are always left untouched, for
speed or convenience. Default: (Shapes.Polygon,)
:return: self
"""
if exclude_types is None:
exclude_types = ()
if recursive:
for subpat in self.subpatterns:
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 = 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, dose) = values
subpat = SubPattern(pattern=pat, offset=offset, scale=scale,
rotation=rotation, dose=dose)
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.
: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()
return [shape.vertices + shape.offset for shape in pat.shapes]
def referenced_patterns_by_id(self) -> Dict[int, 'Pattern']:
"""
Create a dictionary of {id(pat): pat} for all Pattern objects referenced by this
Pattern (operates recursively on all referenced Patterns as well)
:return: Dictionary of {id(pat): pat} for all referenced Pattern objects
"""
ids = {}
for subpat in self.subpatterns:
if id(subpat.pattern) not in ids:
ids[id(subpat.pattern)] = subpat.pattern
ids.update(subpat.pattern.referenced_patterns_by_id())
return 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.
:return: [[x_min, y_min], [x_max, y_max]] or None
"""
entries = self.shapes + self.subpatterns
if not entries:
return None
init_bounds = entries[0].get_bounds()
min_bounds = init_bounds[0, :]
max_bounds = init_bounds[1, :]
for entry in entries[1:]:
bounds = entry.get_bounds()
min_bounds = numpy.minimum(min_bounds, bounds[0, :])
max_bounds = numpy.maximum(max_bounds, bounds[1, :])
return numpy.vstack((min_bounds, max_bounds))
def flatten(self) -> 'Pattern':
"""
Removes all subpatterns and adds equivalent shapes.
:return: self
"""
subpatterns = copy.deepcopy(self.subpatterns)
self.subpatterns = []
for subpat in subpatterns:
subpat.pattern.flatten()
self.shapes += subpat.as_pattern().shapes
return self
def translate_elements(self, offset: vector2) -> 'Pattern':
"""
Translates all shapes and subpatterns by the given offset.
:param offset: Offset to translate by
:return: self
"""
for entry in self.shapes + self.subpatterns:
entry.translate(offset)
return self
def scale_elements(self, scale: float) -> 'Pattern':
""""
Scales all shapes and subpatterns by the given value.
:param scale: value to scale by
:return: self
"""
for entry in self.shapes + self.subpatterns:
entry.scale(scale)
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)
:param c: value to scale by
:return: self
"""
for entry in self.shapes + self.subpatterns:
entry.offset *= c
entry.scale_by(c)
return self
def rotate_around(self, pivot: vector2, rotation: float) -> 'Pattern':
"""
Rotate the Pattern around the a location.
:param pivot: Location to rotate around
:param rotation: Angle to rotate by (counter-clockwise, radians)
:return: 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 and subpatterns around (0, 0)
:param rotation: Angle to rotate by (counter-clockwise, radians)
:return: self
"""
for entry in self.shapes + self.subpatterns:
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)
:param rotation: Angle to rotate by (counter-clockwise, radians)
:return: self
"""
for entry in self.shapes + self.subpatterns:
entry.rotate(rotation)
return self
def scale_element_doses(self, factor: float) -> 'Pattern':
"""
Multiply all shape and subpattern doses by a factor
:param factor: Factor to multiply doses by
:return: self
"""
for entry in self.shapes + self.subpatterns:
entry.dose *= factor
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.
:return: A copy of the current Pattern.
"""
cp = copy.copy(self)
cp.shapes = copy.deepcopy(cp.shapes)
cp.subpatterns = [copy.copy(subpat) for subpat in cp.subpatterns]
return cp
@staticmethod
def load(filename: str) -> 'Pattern':
"""
Load a Pattern from a file
:param filename: Filename to load from
:return: Loaded Pattern
"""
with open(filename, 'rb') as f:
tmp_dict = pickle.load(f)
pattern = Pattern()
pattern.__dict__.update(tmp_dict)
return pattern
def save(self, filename: str) -> 'Pattern':
"""
Save the Pattern to a file
:param filename: Filename to save to
:return: self
"""
with open(filename, 'wb') as f:
pickle.dump(self.__dict__, f, protocol=2)
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.
:param offset: Coordinates to offset by before drawing
:param line_color: Outlines are drawn with this color (passed to matplotlib PolyCollection)
:param fill_color: Interiors are drawn with this color (passed to matplotlib PolyCollection)
:param overdraw: Whether to create a new figure or draw on a pre-existing one
"""
from matplotlib import pyplot
import matplotlib.collections
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()