""" 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()