431 lines
16 KiB
Python
431 lines
16 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:
|
|
"""
|
|
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 = None # type: List[Shape]
|
|
subpatterns = None # type: List[SubPattern]
|
|
name = None # type: 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 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.
|
|
|
|
:param grid_x: List of allowed x-coordinates for the Manhattanized polygon edges.
|
|
:param grid_y: List of allowed y-coordinates for the Manhattanized polygon edges.
|
|
:return: self
|
|
"""
|
|
|
|
self.polygonize().flatten()
|
|
old_shapes = self.shapes
|
|
self.shapes = list(itertools.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=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()
|