From 5bf486ac815ca204dbc314744c00ae462bb2821a Mon Sep 17 00:00:00 2001 From: jan Date: Tue, 15 Mar 2016 19:12:39 -0700 Subject: [PATCH 001/124] Add all files to repository --- .gitignore | 3 + README | 19 ++ masque/__init__.py | 32 +++ masque/error.py | 9 + masque/file/__init__.py | 3 + masque/file/gdsii.py | 171 ++++++++++++++++ masque/file/svg.py | 139 +++++++++++++ masque/file/utils.py | 41 ++++ masque/pattern.py | 412 ++++++++++++++++++++++++++++++++++++++ masque/shapes/__init__.py | 14 ++ masque/shapes/arc.py | 273 +++++++++++++++++++++++++ masque/shapes/circle.py | 95 +++++++++ masque/shapes/ellipse.py | 161 +++++++++++++++ masque/shapes/polygon.py | 174 ++++++++++++++++ masque/shapes/shape.py | 182 +++++++++++++++++ masque/shapes/text.py | 57 ++++++ masque/subpattern.py | 159 +++++++++++++++ masque/utils.py | 41 ++++ setup.py | 13 ++ 19 files changed, 1998 insertions(+) create mode 100644 .gitignore create mode 100644 README create mode 100644 masque/__init__.py create mode 100644 masque/error.py create mode 100644 masque/file/__init__.py create mode 100644 masque/file/gdsii.py create mode 100644 masque/file/svg.py create mode 100644 masque/file/utils.py create mode 100644 masque/pattern.py create mode 100644 masque/shapes/__init__.py create mode 100644 masque/shapes/arc.py create mode 100644 masque/shapes/circle.py create mode 100644 masque/shapes/ellipse.py create mode 100644 masque/shapes/polygon.py create mode 100644 masque/shapes/shape.py create mode 100644 masque/shapes/text.py create mode 100644 masque/subpattern.py create mode 100644 masque/utils.py create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..715503a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.pyc +__pycache__ +*.idea diff --git a/README b/README new file mode 100644 index 0000000..ded6941 --- /dev/null +++ b/README @@ -0,0 +1,19 @@ +# Masque README + +Masque is a Python module for designing lithography masks. + +The general idea is to implement something resembling the GDSII file-format, but +with some vectorized element types (eg. circles, not just polygons), better support for +E-beam doses, and the ability to output to multiple formats. + +## Installation + +Requirements: +* python 3 (written and tested with 3.5) +* numpy +* matplotlib (optional, used for visualization functions) +* python-gdsii (optional, used for gdsii i/o) +* svgwrite (optional, used for svg output) + + + diff --git a/masque/__init__.py b/masque/__init__.py new file mode 100644 index 0000000..25652dc --- /dev/null +++ b/masque/__init__.py @@ -0,0 +1,32 @@ +""" + masque 2D CAD library + + masque is an attempt to make a relatively small library for designing lithography + masks. The general idea is to implement something resembling the GDSII file-format, but + with some vectorized element types (eg. circles, not just polygons), better support for + E-beam doses, and the ability to output to multiple formats. + + Pattern is a basic object containing a 2D lithography mask, composed of a list of Shape + objects and a list of SubPattern objects. + + SubPattern provides basic support for nesting Pattern objects within each other, by adding + offset, rotation, scaling, and other such properties to a Pattern reference. + + Note that the methods for these classes try to avoid copying wherever possible, so unless + otherwise noted, assume that arguments are stored by-reference. + + + Dependencies: + - numpy + - matplotlib [Pattern.visualize(...)] + - python-gdsii [masque.file.gdsii] + - svgwrite [masque.file.svgwrite] +""" + +from .error import PatternError +from .shapes import Shape +from .subpattern import SubPattern +from .pattern import Pattern + + +__author__ = 'Jan Petykiewicz' diff --git a/masque/error.py b/masque/error.py new file mode 100644 index 0000000..8a67b6e --- /dev/null +++ b/masque/error.py @@ -0,0 +1,9 @@ +class PatternError(Exception): + """ + Simple Exception for Pattern objects and their contents + """ + def __init__(self, value): + self.value = value + + def __str__(self): + return repr(self.value) diff --git a/masque/file/__init__.py b/masque/file/__init__.py new file mode 100644 index 0000000..8de11a7 --- /dev/null +++ b/masque/file/__init__.py @@ -0,0 +1,3 @@ +""" +Functions for reading from and writing to various file formats. +""" \ No newline at end of file diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py new file mode 100644 index 0000000..3cf1e74 --- /dev/null +++ b/masque/file/gdsii.py @@ -0,0 +1,171 @@ +""" +GDSII file format readers and writers +""" + +import gdsii.library +import gdsii.structure +import gdsii.elements + +from typing import List, Any, Dict +import numpy + +from .utils import mangle_name, make_dose_table +from .. import Pattern, SubPattern, PatternError +from ..shapes import Polygon +from ..utils import rotation_matrix_2d, get_bit, vector2 + + +__author__ = 'Jan Petykiewicz' + + +def write_dose2dtype(pattern: Pattern, + filename: str, + meters_per_unit: float): + """ + Write a Pattern to a GDSII file, by first calling .polygonize() on it + to change the shapes into polygons, and then writing patterns as GDSII + structures, polygons as boundary elements, and subpatterns as structure + references (sref). + + Note that this function modifies the Pattern. + + It is often a good idea to run pattern.subpatternize() prior to calling this function, + especially if calling .polygonize() will result in very many vertices. + + If you want pattern polygonized with non-default arguments, just call pattern.polygonize() + prior to calling this function. + + :param pattern: A Pattern to write to file. Modified by this function. + :param filename: Filename to write to. + :param meters_per_unit: Written into the GDSII file, meters per length unit. + """ + # Create library + lib = gdsii.library.Library(version=600, + name='masque-write_dose2dtype'.encode('ASCII'), + logical_unit=1, + physical_unit=meters_per_unit) + + # Polygonize pattern + pattern.polygonize() + + # Get a dict of id(pattern) -> pattern + patterns_by_id = {**(pattern.referenced_patterns_by_id()), id(pattern): pattern} + + # Get a table of (id(subpat.pattern), written_dose) for each subpattern + sd_table = make_dose_table(pattern) + + # Figure out all the unique doses necessary to write this pattern + # This means going through each row in sd_table and adding the dose values needed to write + # that subpattern at that dose level + dose_vals = set() + for pat_id, pat_dose in sd_table: + pat = patterns_by_id[pat_id] + [dose_vals.add(shape.dose * pat_dose) for shape in pat.shapes] + + if len(dose_vals) > 256: + raise PatternError('Too many dose values: {}, maximum 256 when using dtypes.'.format(len(dose_vals))) + + dose_vals_list = list(dose_vals) + + # Now create a structure for each row in sd_table (ie, each pattern + dose combination) + # and add in any Boundary and SREF elements + for pat_id, pat_dose in sd_table: + pat = patterns_by_id[pat_id] + + structure = gdsii.structure.Structure(name=mangle_name(pat, pat_dose).encode('ASCII')) + lib.append(structure) + + # Add a Boundary element for each shape + for shape in pat.shapes: + for polygon in shape.to_polygons(): + data_type = dose_vals_list.index(polygon.dose * pat_dose) + 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=polygon.layer, + data_type=data_type, + xy=xy_closed)) + # Add an SREF for each subpattern entry + # strans must be set for angle and mag to take effect + for subpat in pat.subpatterns: + dose_mult = subpat.dose * pat_dose + sref = gdsii.elements.SRef(struct_name=mangle_name(subpat.pattern, dose_mult).encode('ASCII'), + xy=numpy.round([subpat.offset]).astype(int)) + sref.strans = 0 + sref.angle = subpat.rotation + sref.mag = subpat.scale + structure.append(sref) + + with open(filename, mode='wb') as stream: + lib.save(stream) + + return dose_vals_list + + +def read_dtype2dose(filename: str) -> (List[Pattern], Dict[str, Any]): + """ + Read a gdsii file and translate it into a list of Pattern objects. GDSII structures are + translated into Pattern objects; boundaries are translated into polygons, and srefs and arefs + are translated into SubPattern objects. + + :param filename: Filename specifying a GDSII file to read from. + :return: Tuple: (List of Patterns generated GDSII structures, Dict of GDSII library info) + """ + + with open(filename, mode='rb') as stream: + lib = gdsii.library.Library.load(stream) + + library_info = {'name': lib.name.decode('ASCII'), + 'physical_unit': lib.physical_unit, + 'logical_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: Figure out what "absolute" means in the context of elements and if the current + # behavior is correct + # BUG: Need to check STRANS bit 0 to handle x-reflection + 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, 13): + subpat.offset *= subpat.scale + if element.angle is not None: + subpat.rotation = element.angle + # Bit 14 means absolute rotation + if get_bit(element.strans, 14): + subpat.offset = numpy.dot(rotation_matrix_2d(subpat.rotation), subpat.offset) + return subpat + + patterns = [] + for structure in lib: + pat = Pattern(name=structure.name.decode('ASCII')) + for element in structure: + # Switch based on element type: + if isinstance(element, gdsii.elements.Boundary): + pat.shapes.append( + Polygon(vertices=element.xy[:-1], + dose=element.data_type, + layer=element.layer)) + + elif isinstance(element, gdsii.elements.SRef): + pat.subpatterns.append(ref_element_to_subpat(element, element.xy)) + + elif isinstance(element, gdsii.elements.ARef): + for offset in element.xy: + pat.subpatterns.append(ref_element_to_subpat(element, offset)) + patterns.append(pat) + + # Create a dict of {pattern.name: pattern, ...}, then fix up all subpattern.pattern entries + # according to the subpattern.ref_name (which is deleted after use). + patterns_dict = dict(((p.name, p) for p in patterns)) + for p in patterns_dict.values(): + for sp in p.subpatterns: + sp.pattern = patterns_dict[sp.ref_name.decode('ASCII')] + del sp.ref_name + + return patterns_dict, library_info diff --git a/masque/file/svg.py b/masque/file/svg.py new file mode 100644 index 0000000..6a28c25 --- /dev/null +++ b/masque/file/svg.py @@ -0,0 +1,139 @@ +""" +SVG file format readers and writers +""" + +import svgwrite +import numpy + +from .utils import mangle_name +from .. import Pattern + + +__author__ = 'Jan Petykiewicz' + + +def write(pattern: Pattern, + filename: str, + custom_attributes: bool=False): + """ + Write a Pattern to an SVG file, by first calling .polygonize() on it + to change the shapes into polygons, and then writing patterns as SVG + groups (, inside ), polygons as paths (), and subpatterns + as elements. + + Note that this function modifies the Pattern. + + If custom_attributes is True, non-standard pattern_layer and pattern_dose attributes + are written to the relevant elements. + + It is often a good idea to run pattern.subpatternize() on pattern prior to + calling this function, especially if calling .polygonize() will result in very + many vertices. + + If you want pattern polygonized with non-default arguments, just call pattern.polygonize() + prior to calling this function. + + :param pattern: Pattern to write to file. Modified by this function. + :param filename: Filename to write to. + :param custom_attributes: Whether to write non-standard pattern_layer and + pattern_dose attributes to the SVG elements. + """ + + # Polygonize pattern + pattern.polygonize() + + [bounds_min, bounds_max] = pattern.get_bounds() + + viewbox = numpy.hstack((bounds_min - 1, (bounds_max - bounds_min) + 2)) + viewbox_string = '{:g} {:g} {:g} {:g}'.format(*viewbox) + + # Create file + svg = svgwrite.Drawing(filename, profile='full', viewBox=viewbox_string, + debug=(not custom_attributes)) + + # Get a dict of id(pattern) -> pattern + patterns_by_id = {**(pattern.referenced_patterns_by_id()), id(pattern): pattern} + + # Now create a group for each row in sd_table (ie, each pattern + dose combination) + # and add in any Boundary and Use elements + for pat in patterns_by_id.values(): + svg_group = svg.g(id=mangle_name(pat), fill='blue', stroke='red') + + for shape in pat.shapes: + for polygon in shape.to_polygons(): + path_spec = poly2path(polygon.vertices + polygon.offset) + + path = svg.path(d=path_spec) + if custom_attributes: + path['pattern_layer'] = polygon.layer + path['pattern_dose'] = polygon.dose + + svg_group.add(path) + + for subpat in pat.subpatterns: + transform = 'scale({:g}) rotate({:g}) translate({:g},{:g})'.format( + subpat.scale, subpat.rotation, subpat.offset[0], subpat.offset[1]) + use = svg.use(href='#' + mangle_name(subpat.pattern), transform=transform) + if custom_attributes: + use['pattern_dose'] = subpat.dose + svg_group.add(use) + + svg.defs.add(svg_group) + svg.add(svg.use(href='#' + mangle_name(pattern))) + svg.save() + + +def write_inverted(pattern: Pattern, filename: str): + """ + Write an inverted Pattern to an SVG file, by first calling .polygonize() and + .flatten() on it to change the shapes into polygons, then drawing a bounding + box and drawing the polygons with reverse vertex order inside it, all within + one element. + + Note that this function modifies the Pattern. + + If you want pattern polygonized with non-default arguments, just call pattern.polygonize() + prior to calling this function. + + :param pattern: Pattern to write to file. Modified by this function. + :param filename: Filename to write to. + """ + # Polygonize and flatten pattern + pattern.polygonize().flatten() + + [bounds_min, bounds_max] = pattern.get_bounds() + + viewbox = numpy.hstack((bounds_min - 1, (bounds_max - bounds_min) + 2)) + viewbox_string = '{:g} {:g} {:g} {:g}'.format(*viewbox) + + # Create file + svg = svgwrite.Drawing(filename, profile='full', viewBox=viewbox_string) + + # Draw bounding box + slab_edge = [[bounds_min[0] - 1, bounds_max[1] + 1], + [bounds_max[0] + 1, bounds_max[1] + 1], + [bounds_max[0] + 1, bounds_min[1] - 1], + [bounds_min[0] - 1, bounds_min[1] - 1]] + path_spec = poly2path(slab_edge) + + # Draw polygons with reversed vertex order + for shape in pattern.shapes: + for polygon in shape.to_polygons(): + path_spec += poly2path(polygon.vertices[::-1] + polygon.offset) + + svg.add(svg.path(d=path_spec, fill='blue', stroke='red')) + svg.save() + + +def poly2path(vertices: numpy.ndarray) -> str: + """ + Create an SVG path string from an Nx2 list of vertices. + + :param vertices: Nx2 array of vertices. + :return: SVG path-string. + """ + commands = 'M{:g},{:g} '.format(vertices[0][0], vertices[0][1]) + for vertex in vertices[1:]: + commands += 'L{:g},{:g}'.format(vertex[0], vertex[1]) + commands += ' Z ' + return commands diff --git a/masque/file/utils.py b/masque/file/utils.py new file mode 100644 index 0000000..615d455 --- /dev/null +++ b/masque/file/utils.py @@ -0,0 +1,41 @@ +""" +Helper functions for file reading and writing +""" +import re +from typing import Set, Tuple + +from masque.pattern import Pattern + + +__author__ = 'Jan Petykiewicz' + + +def mangle_name(pattern: Pattern, dose_multiplier: float=1.0) -> str: + """ + Create a name using pattern.name, id(pattern), and the dose multiplier. + + :param pattern: Pattern whose name we want to mangle. + :param dose_multiplier: Dose multiplier to mangle with. + :return: Mangled name. + """ + expression = re.compile('[^A-Za-z0-9_\?\$]') + sanitized_name = expression.sub('_', pattern.name) + full_name = '{}_{}_{}'.format(sanitized_name, dose_multiplier, id(pattern)) + return full_name + + +def make_dose_table(pattern: Pattern, dose_multiplier: float=1.0) -> Set[Tuple[int, float]]: + """ + Create a set containing (id(subpat.pattern), written_dose) for each subpattern + + :param pattern: Source Pattern. + :param dose_multiplier: Multiplier for all written_dose entries. + :return: {(id(subpat.pattern), written_dose), ...} + """ + dose_table = {(id(pattern), dose_multiplier)} + for subpat in pattern.subpatterns: + subpat_dose_entry = (id(subpat.pattern), subpat.dose * dose_multiplier) + if subpat_dose_entry not in dose_table: + subpat_dose_table = make_dose_table(subpat.pattern, subpat.dose * dose_multiplier) + dose_table = dose_table.union(subpat_dose_table) + return dose_table diff --git a/masque/pattern.py b/masque/pattern.py new file mode 100644 index 0000000..b187b9e --- /dev/null +++ b/masque/pattern.py @@ -0,0 +1,412 @@ +""" + 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() diff --git a/masque/shapes/__init__.py b/masque/shapes/__init__.py new file mode 100644 index 0000000..cb7e708 --- /dev/null +++ b/masque/shapes/__init__.py @@ -0,0 +1,14 @@ +""" +Shapes for use with the Pattern class, as well as the Shape abstract class from + which they are derived. +""" + +from .shape import Shape, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS + +from .polygon import Polygon +from .circle import Circle +from .ellipse import Ellipse +from .arc import Arc + + + diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py new file mode 100644 index 0000000..1cba2ef --- /dev/null +++ b/masque/shapes/arc.py @@ -0,0 +1,273 @@ +from typing import List +import math +import numpy +from numpy import pi + +from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS +from .. import PatternError +from ..utils import is_scalar, vector2 + + +__author__ = 'Jan Petykiewicz' + + +class Arc(Shape): + """ + An elliptical arc, formed by cutting off an elliptical ring with two rays which exit from its + center. It has a position, two radii, a start and stop angle, a rotation, and a width. + + The radii define an ellipse; the ring is formed with radii +/- width/2. + The rotation gives the angle from x-axis, counterclockwise, to the first (x) radius. + The start and stop angle are measure counterclockwise from the first (x) radius. + """ + + _radii = None # type: numpy.ndarray + _angles = None # type: numpy.ndarray + _width = 1.0 # type: float + _rotation = 0.0 # type: float + + # Defaults for to_polygons + poly_num_points = DEFAULT_POLY_NUM_POINTS # type: int + poly_max_arclen = None # type: float + + # radius properties + @property + def radii(self) -> numpy.ndarray: + """ + Return the radii [rx, ry] + + :return: [rx, ry] + """ + return self.radii + + @radii.setter + def radii(self, val: vector2): + val = numpy.array(val, dtype=float).flatten() + if not val.size == 2: + raise PatternError('Radii must have length 2') + if not val.min() >= 0: + raise PatternError('Radii must be non-negative') + self.radii = val + + @property + def radius_x(self) -> float: + return self.radii[0] + + @radius_x.setter + def radius_x(self, val: float): + if not val >= 0: + raise PatternError('Radius must be non-negative') + self.radii[0] = val + + @property + def radius_y(self) -> float: + return self.radii[1] + + @radius_y.setter + def radius_y(self, val: float): + if not val >= 0: + raise PatternError('Radius must be non-negative') + self.radii[1] = val + + # arc start/stop angle properties + @property + def angles(self) -> vector2: + """ + Return the start and stop angles [a_start, a_stop]. + Angles are measured from x-axis after rotation, and are stored mod 2*pi + + :return: [a_start, a_stop] + """ + return self._angles + + @angles.setter + def angles(self, val: vector2): + val = numpy.array(val, dtype=float).flatten() + if not val.size == 2: + raise PatternError('Angles must have length 2') + angles = val % (2 * pi) + if angles[0] > pi: + self.rotation += pi + angles -= pi + self._angles = angles + + @property + def start_angle(self) -> float: + return self.angles[0] + + @start_angle.setter + def start_angle(self, val: float): + self.angles[0] = val % (2 * pi) + + @property + def stop_angle(self) -> float: + return self.angles[1] + + @stop_angle.setter + def stop_angle(self, val: float): + self.angles[1] = val % (2 * pi) + + # Rotation property + @property + def rotation(self) -> float: + """ + Rotation of radius_x from x_axis, counterclockwise, in radians. Stored mod 2*pi + + :return: rotation counterclockwise in radians + """ + 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) + + # Width + @property + def width(self) -> float: + """ + Width of the arc (difference between inner and outer radii) + + :return: width + """ + return self._width + + @width.setter + def width(self, val: float): + if not is_scalar(val): + raise PatternError('Width must be a scalar') + if not val > 0: + raise PatternError('Width must be positive') + self._width = val + + def __init__(self, + radii: vector2, + angles: vector2, + rotation: float=0, + poly_num_points: int=DEFAULT_POLY_NUM_POINTS, + poly_max_arclen: float=None, + offset: vector2=(0.0, 0.0), + layer: int=0, + dose: float=1.0): + self.offset = offset + self.layer = layer + self.dose = dose + self.radii = radii + self.angles = angles + self.rotation = rotation + self.poly_num_points = poly_num_points + self.poly_max_arclen = poly_max_arclen + + def to_polygons(self, poly_num_points: int=None, poly_max_arclen: float=None) -> List[Polygon]: + if poly_num_points is None: + poly_num_points = self.poly_num_points + if poly_max_arclen is None: + poly_max_arclen = self.poly_max_arclen + + if (poly_num_points is None) and (poly_max_arclen is None): + raise PatternError('Max number of points and arclength left unspecified' + + ' (default was also overridden)') + + rxy = self.radii + ang = self.angles + + # Approximate perimeter + # Ramanujan, S., "Modular Equations and Approximations to ," + # Quart. J. Pure. Appl. Math., vol. 45 (1913-1914), pp. 350-372 + h = ((rxy[1] - rxy[0]) / rxy.sum()) ** 2 + ellipse_perimeter = pi * rxy.sum() * (1 + 3 * h / (10 + math.sqrt(4 - 3 * h))) + perimeter = abs(ang[0] - ang[1]) / (2 * pi) * ellipse_perimeter + + n = [] + if poly_num_points is not None: + n += [poly_num_points] + if poly_max_arclen is not None: + n += [perimeter / poly_max_arclen] + thetas = numpy.linspace(2 * pi, 0, max(n), endpoint=False) + + sin_th, cos_th = (numpy.sin(thetas), numpy.cos(thetas)) + wh = self.width / 2.0 + + xs1 = (rxy[0] + wh) * cos_th - (rxy[1] + wh) * sin_th + ys1 = (rxy[0] + wh) * cos_th - (rxy[1] + wh) * sin_th + xs2 = (rxy[0] - wh) * cos_th - (rxy[1] - wh) * sin_th + ys2 = (rxy[0] - wh) * cos_th - (rxy[1] - wh) * sin_th + + xs = numpy.hstack((xs1, xs2[::-1])) + ys = numpy.hstack((ys1, ys2[::-1])) + xys = numpy.vstack((xs, ys)).T + + poly = Polygon(xys, dose=self.dose, layer=self.layer, offset=self.offset) + poly.rotate(self.rotation) + return [poly] + + def get_bounds(self) -> numpy.ndarray: + a = self.angles - 0.5 * pi + + mins = [] + maxs = [] + for sgn in (+1, -1): + wh = sgn * self.width/2 + rx = self.radius_x + wh + ry = self.radius_y + wh + + sin_r = numpy.sin(self.rotation) + cos_r = numpy.cos(self.rotation) + tan_r = numpy.tan(self.rotation) + sin_a = numpy.sin(a) + cos_a = numpy.cos(a) + + xpt = numpy.arctan(-ry / rx * tan_r) + ypt = numpy.arctan(+ry / rx / tan_r) + xnt = numpy.arcsin(numpy.sin(xpt - pi)) + ynt = numpy.arcsin(numpy.sin(ypt - pi)) + + xr = numpy.sqrt((rx * cos_r) ** 2 + (ry * sin_r) ** 2) + yr = numpy.sqrt((rx * sin_r) ** 2 + (ry * cos_r) ** 2) + + xn, xp = sorted(rx * cos_r * cos_a - ry * sin_r * sin_a) + yn, yp = sorted(rx * sin_r * cos_a - ry * cos_r * sin_a) + + if min(a) < xpt < max(a): + xp = xr + + if min(a) < xnt < max(a): + xn = -xr + + if min(a) < ypt < max(a): + yp = yr + + if min(a) < ynt < max(a): + yn = -yr + + mins.append([xn, yn]) + maxs.append([xp, yp]) + return numpy.vstack((numpy.min(mins, axis=0) + self.offset, + numpy.max(maxs, axis=0) + self.offset)) + + def rotate(self, theta: float) -> 'Arc': + self.rotation += theta + return self + + def scale_by(self, c: float) -> 'Arc': + self.radii *= c + self.width *= c + return self + + def normalized_form(self, norm_value: float) -> normalized_shape_tuple: + if self.radius_x < self.radius_y: + radii = self.radii / self.radius_x + scale = self.radius_x + rotation = self.rotation + angles = self.angles + else: # rotate by 90 degrees and swap radii + radii = self.radii[::-1] / self.radius_y + scale = self.radius_y + rotation = self.rotation + pi / 2 + angles = self.angles - pi / 2 + return (type(self), radii, angles, self.layer), \ + (self.offset, scale/norm_value, rotation, self.dose), \ + lambda: Arc(radii=radii*norm_value, angles=angles, layer=self.layer) + + diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py new file mode 100644 index 0000000..f1ddb38 --- /dev/null +++ b/masque/shapes/circle.py @@ -0,0 +1,95 @@ +from typing import List +import numpy +from numpy import pi + +from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS +from .. import PatternError +from ..utils import is_scalar, vector2 + + +__author__ = 'Jan Petykiewicz' + + +class Circle(Shape): + """ + A circle, which has a position and radius. + """ + + _radius = None # type: float + + # Defaults for to_polygons + poly_num_points = DEFAULT_POLY_NUM_POINTS # type: int + poly_max_arclen = None # type: float + + # radius property + @property + def radius(self) -> float: + """ + Circle's radius (float, >= 0) + + :return: radius + """ + return self._radius + + @radius.setter + def radius(self, val: float): + if not is_scalar(val): + raise PatternError('Radius must be a scalar') + if not val >= 0: + raise PatternError('Radius must be non-negative') + self._radius = val + + def __init__(self, + radius: float, + poly_num_points: int=DEFAULT_POLY_NUM_POINTS, + poly_max_arclen: float=None, + offset: vector2=(0.0, 0.0), + layer: int=0, + dose: float=1.0): + self.offset = numpy.array(offset, dtype=float) + self.layer = layer + self.dose = dose + self.radius = radius + self.poly_num_points = poly_num_points + self.poly_max_arclen = poly_max_arclen + + def to_polygons(self, poly_num_points: int=None, poly_max_arclen: float=None) -> List[Polygon]: + if poly_num_points is None: + poly_num_points = self.poly_num_points + if poly_max_arclen is None: + poly_max_arclen = self.poly_max_arclen + + if (poly_num_points is None) and (poly_max_arclen is None): + raise PatternError('Number of points and arclength left ' + 'unspecified (default was also overridden)') + + n = [] + if poly_num_points is not None: + n += [poly_num_points] + if poly_max_arclen is not None: + n += [2 * pi * self.radius / poly_max_arclen] + thetas = numpy.linspace(2 * pi, 0, max(n), endpoint=False) + xs = numpy.cos(thetas) * self.radius + ys = numpy.sin(thetas) * self.radius + xys = numpy.vstack((xs, ys)).T + + return [Polygon(xys, offset=self.offset, dose=self.dose, layer=self.layer)] + + def get_bounds(self) -> numpy.ndarray: + return numpy.vstack((self.offset - self.radius, + self.offset + self.radius)) + + def rotate(self, theta: float) -> 'Circle': + return self + + def scale_by(self, c: float) -> 'Circle': + self.radius *= c + return self + + def normalized_form(self, norm_value) -> normalized_shape_tuple: + rotation = 0.0 + magnitude = self.radius / norm_value + return (type(self), self.layer), \ + (self.offset, magnitude, rotation, self.dose), \ + lambda: Circle(radius=norm_value, layer=self.layer) + diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py new file mode 100644 index 0000000..a9d22e7 --- /dev/null +++ b/masque/shapes/ellipse.py @@ -0,0 +1,161 @@ +from typing import List +import math +import numpy +from numpy import pi + +from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS +from .. import PatternError +from ..utils import is_scalar, rotation_matrix_2d, vector2 + + +__author__ = 'Jan Petykiewicz' + + +class Ellipse(Shape): + """ + An ellipse, which has a position, two radii, and a rotation. + The rotation gives the angle from x-axis, counterclockwise, to the first (x) radius. + """ + + _radii = None # type: numpy.ndarray + _rotation = 0.0 # type: float + + # Defaults for to_polygons + poly_num_points = DEFAULT_POLY_NUM_POINTS # type: int + poly_max_arclen = None # type: float + + # radius properties + @property + def radii(self) -> numpy.ndarray: + """ + Return the radii [rx, ry] + + :return: [rx, ry] + """ + return self.radii + + @radii.setter + def radii(self, val: vector2): + val = numpy.array(val).flatten() + if not val.size == 2: + raise PatternError('Radii must have length 2') + if not val.min() >= 0: + raise PatternError('Radii must be non-negative') + self.radii = val + + @property + def radius_x(self) -> float: + return self.radii[0] + + @radius_x.setter + def radius_x(self, val: float): + if not val >= 0: + raise PatternError('Radius must be non-negative') + self.radii[0] = val + + @property + def radius_y(self) -> float: + return self.radii[1] + + @radius_y.setter + def radius_y(self, val: float): + if not val >= 0: + raise PatternError('Radius must be non-negative') + self.radii[1] = val + + # Rotation property + @property + def rotation(self) -> float: + """ + Rotation of rx from the x axis. Uses the interval [0, pi) in radians (counterclockwise + is positive) + + :return: counterclockwise rotation in radians + """ + 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 % pi + + def __init__(self, + radii: vector2, + rotation: float=0, + poly_num_points: int=DEFAULT_POLY_NUM_POINTS, + poly_max_arclen: float=None, + offset: vector2=(0.0, 0.0), + layer: int=0, + dose: float=1.0): + self.offset = offset + self.layer = layer + self.dose = dose + self.radii = radii + self.rotation = rotation + self.poly_num_points = poly_num_points + self.poly_max_arclen = poly_max_arclen + + def to_polygons(self, + poly_num_points: int=None, + poly_max_arclen: float=None + ) -> List[Polygon]: + if poly_num_points is None: + poly_num_points = self.poly_num_points + if poly_max_arclen is None: + poly_max_arclen = self.poly_max_arclen + + if (poly_num_points is None) and (poly_max_arclen is None): + raise PatternError('Number of points and arclength left unspecified' + ' (default was also overridden)') + + rxy = self.radii + + # Approximate perimeter + # Ramanujan, S., "Modular Equations and Approximations to ," + # Quart. J. Pure. Appl. Math., vol. 45 (1913-1914), pp. 350-372 + h = ((rxy[1] - rxy[0]) / rxy.sum()) ** 2 + perimeter = pi * rxy.sum() * (1 + 3 * h / (10 + math.sqrt(4 - 3 * h))) + + n = [] + if poly_num_points is not None: + n += [poly_num_points] + if poly_max_arclen is not None: + n += [perimeter / poly_max_arclen] + thetas = numpy.linspace(2 * pi, 0, max(n), endpoint=False) + + sin_th, cos_th = (numpy.sin(thetas), numpy.cos(thetas)) + xs = rxy[0] * cos_th - rxy[1] * sin_th + ys = rxy[0] * sin_th - rxy[1] * cos_th + xys = numpy.vstack((xs, ys)).T + + poly = Polygon(xys, dose=self.dose, layer=self.layer, offset=self.offset) + poly.rotate(self.rotation) + return [poly] + + def get_bounds(self) -> numpy.ndarray: + rot_radii = numpy.dot(rotation_matrix_2d(self.rotation), self.radii) + return numpy.vstack((self.offset - rot_radii[0], + self.offset + rot_radii[1])) + + def rotate(self, theta: float) -> 'Ellipse': + self.rotation += theta + return self + + def scale_by(self, c: float) -> 'Ellipse': + self.radii *= c + return self + + def normalized_form(self, norm_value: float) -> normalized_shape_tuple: + if self.radius_x < self.radius_y: + radii = self.radii / self.radius_x + scale = self.radius_x + angle = self.rotation + else: + radii = self.radii[::-1] / self.radius_y + scale = self.radius_y + angle = (self.rotation + pi / 2) % pi + return (type(self), radii, self.layer), \ + (self.offset, scale/norm_value, angle, self.dose), \ + lambda: Ellipse(radii=radii*norm_value, layer=self.layer) + diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py new file mode 100644 index 0000000..761c1fa --- /dev/null +++ b/masque/shapes/polygon.py @@ -0,0 +1,174 @@ +from typing import List +import copy +import numpy +from numpy import pi + +from . import Shape, normalized_shape_tuple +from .. import PatternError +from ..utils import is_scalar, rotation_matrix_2d, vector2 + +__author__ = 'Jan Petykiewicz' + + +class Polygon(Shape): + """ + A polygon, consisting of a bunch of vertices (Nx2 ndarray) along with an offset. + + A normalized_form(...) is available, but can be quite slow with lots of vertices. + """ + _vertices = None # type: numpy.ndarray + + # vertices property + @property + def vertices(self) -> numpy.ndarray: + """ + Vertices of the polygon (Nx2 ndarray: [[x0, y0], [x1, y1], ...] + + :return: vertices + """ + return self._vertices + + @vertices.setter + def vertices(self, val: numpy.ndarray): + val = numpy.array(val, dtype=float) + if val.shape[1] != 2: + raise PatternError('Vertices must be an Nx2 array') + if val.shape[0] < 3: + raise PatternError('Must have at least 3 vertices (Nx2, N>3)') + self._vertices = val + + # xs property + @property + def xs(self) -> numpy.ndarray: + """ + All x vertices in a 1D ndarray + """ + return self.vertices[:, 0] + + @xs.setter + def xs(self, val: numpy.ndarray): + val = numpy.array(val, dtype=float).flatten() + if val.size != self.vertices.shape[0]: + raise PatternError('Wrong number of vertices') + self.vertices[:, 0] = val + + # ys property + @property + def ys(self) -> numpy.ndarray: + """ + All y vertices in a 1D ndarray + """ + return self.vertices[:, 1] + + @ys.setter + def ys(self, val: numpy.ndarray): + val = numpy.array(val, dtype=float).flatten() + if val.size != self.vertices.shape[0]: + raise PatternError('Wrong number of vertices') + self.vertices[:, 1] = val + + def __init__(self, + vertices: numpy.ndarray, + offset: vector2=(0.0, 0.0), + layer: int=0, + dose: float=1.0): + self.offset = offset + self.layer = layer + self.dose = dose + self.vertices = vertices + + @staticmethod + def square(side_length: float, + rotation: float=0.0, + offset: vector2=(0.0, 0.0), + layer: int=0, + dose: float=1.0 + ) -> 'Polygon': + """ + Draw a square given side_length + + :param side_length: Length of one side + :param rotation: Rotation counterclockwise, in radians + :param offset: Offset, default (0, 0) + :param layer: Layer, default 0 + :param dose: Dose, default 1.0 + :return: A Polygon object containing the requested square + """ + norm_square = numpy.array([[-1, -1], + [-1, +1], + [+1, +1], + [+1, -1]], dtype=float) + vertices = 0.5 * side_length * norm_square + poly = Polygon(vertices, offset, layer, dose) + poly.rotate(rotation) + return poly + + @staticmethod + def rectangle(lx: float, + ly: float, + rotation: float=0, + offset: vector2=(0.0, 0.0), + layer: int=0, + dose: float=1.0 + ) -> 'Polygon': + """ + Draw a rectangle with side lengths lx and ly + + :param lx: Length along x (before rotation) + :param ly: Length along y (before rotation) + :param rotation: Rotation counterclockwise, in radians + :param offset: Offset, default (0, 0) + :param layer: Layer, default 0 + :param dose: Dose, default 1.0 + :return: A Polygon object containing the requested rectangle + """ + vertices = 0.5 * numpy.array([[-lx, -ly], + [-lx, +ly], + [+lx, +ly], + [+lx, -ly]], dtype=float) + poly = Polygon(vertices, offset, layer, dose) + poly.rotate(rotation) + return poly + + def to_polygons(self, + _poly_num_points: int=None, + _poly_max_arclen: float=None, + ) -> List['Polygon']: + return [copy.deepcopy(self)] + + def get_bounds(self) -> numpy.ndarray: + return numpy.vstack((self.offset + numpy.min(self.vertices, axis=0), + self.offset + numpy.max(self.vertices, axis=0))) + + def rotate(self, theta: float) -> 'Polygon': + self.vertices = numpy.dot(rotation_matrix_2d(theta), self.vertices.T).T + return self + + def scale_by(self, c: float) -> 'Polygon': + self.vertices *= c + return self + + def normalized_form(self, norm_value: float) -> normalized_shape_tuple: + # Note: this function is going to be pretty slow for many-vertexed polygons, relative to + # other shapes + offset = self.vertices.mean(axis=0) + self.offset + zeroed_vertices = self.vertices - offset + + scale = zeroed_vertices.std() + normed_vertices = zeroed_vertices / scale + + _, _, vertex_axis = numpy.linalg.svd(zeroed_vertices) + rotation = numpy.arctan2(vertex_axis[0][1], vertex_axis[0][0]) % (2 * pi) + rotated_vertices = numpy.vstack([numpy.dot(rotation_matrix_2d(-rotation), v) + for v in normed_vertices]) + + # Reorder the vertices so that the one with lowest x, then y, comes first. + x_min = rotated_vertices[:, 0].argmin() + if not is_scalar(x_min): + y_min = rotated_vertices[x_min, 1].argmin() + x_min = x_min[y_min] + reordered_vertices = numpy.roll(rotated_vertices, -x_min, axis=0) + + return (type(self), reordered_vertices.data.tobytes(), self.layer), \ + (offset, scale/norm_value, rotation, self.dose), \ + lambda: Polygon(reordered_vertices*norm_value, layer=self.layer) diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py new file mode 100644 index 0000000..cacac44 --- /dev/null +++ b/masque/shapes/shape.py @@ -0,0 +1,182 @@ +from typing import List, Tuple, Callable +from abc import ABCMeta, abstractmethod +import numpy + +from .. import PatternError +from ..utils import is_scalar, rotation_matrix_2d, vector2 + + +__author__ = 'Jan Petykiewicz' + + +# Type definitions +normalized_shape_tuple = Tuple[Tuple, + Tuple[numpy.ndarray, float, float, float], + Callable[[], 'Shape']] + +# ## Module-wide defaults +# Default number of points per polygon for shapes +DEFAULT_POLY_NUM_POINTS = 24 + + +class Shape(object, metaclass=ABCMeta): + """ + Abstract class specifying functions common to all shapes. + """ + + # [x_offset, y_offset] + _offset = numpy.array([0.0, 0.0]) # type: numpy.ndarray + + # Layer (integer >= 0) + _layer = 0 # type: int + + # Dose + _dose = 1.0 # type: float + + # --- Abstract methods + @abstractmethod + def to_polygons(self, num_vertices: int, max_arclen: float) -> List['Polygon']: + """ + Returns a list of polygons which approximate the shape. + + :param num_vertices: Number of points to use for each polygon. Can be overridden by + max_arclen if that results in more points. Optional, defaults to shapes' + internal defaults. + :param max_arclen: Maximum arclength which can be approximated by a single line + segment. Optional, defaults to shapes' internal defaults. + :return: List of polygons equivalent to the shape + """ + pass + + @abstractmethod + def get_bounds(self) -> numpy.ndarray: + """ + Returns [[x_min, y_min], [x_max, y_max]] which specify a minimal bounding box for the shape. + + :return: [[x_min, y_min], [x_max, y_max]] + """ + pass + + @abstractmethod + def rotate(self, theta: float) -> 'Shape': + """ + Rotate the shape around its center (0, 0), ignoring its offset. + + :param theta: Angle to rotate by (counterclockwise, radians) + :return: self + """ + pass + + @abstractmethod + def scale_by(self, c: float) -> 'Shape': + """ + Scale the shape's size (eg. radius, for a circle) by a constant factor. + + :param c: Factor to scale by + :return: self + """ + pass + + @abstractmethod + def normalized_form(self, norm_value: int) -> normalized_shape_tuple: + """ + Writes the shape in a standardized notation, with offset, scale, rotation, and dose + information separated out from the remaining values. + + :param norm_value: This value is used to normalize lengths intrinsic to teh shape; + eg. for a circle, the returned magnitude value will be (radius / norm_value), and + the returned callable will create a Circle(radius=norm_value, ...). This is useful + when you find it important for quantities to remain in a certain range, eg. for + GDSII where vertex locations are stored as integers. + :return: The returned information takes the form of a 3-element tuple, + (intrinsic, extrinsic, constructor). These are further broken down as: + extrinsic: ([x_offset, y_offset], scale, rotation, dose) + intrinsic: A tuple of basic types containing all information about the instance that + is not contained in 'extrinsic'. Usually, intrinsic[0] == type(self). + constructor: A callable (no arguments) which returns an instance of type(self) with + internal state equivalent to 'intrinsic'. + """ + pass + + # ---- Non-abstract properties + # offset property + @property + def offset(self) -> numpy.ndarray: + """ + [x, y] offset + + :return: [x_offset, y_offset] + """ + 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() + + # layer property + @property + def layer(self) -> int: + """ + Layer number (int, >=0) + + :return: Layer + """ + return self._layer + + @layer.setter + def layer(self, val: int): + if not isinstance(val, int): + raise PatternError('Layer must be an integer') + if not val >= 0: + raise PatternError('Layer must be non-negative') + self._layer = val + + # dose property + @property + def dose(self) -> float: + """ + Dose (float >= 0) + + :return: Dose value + """ + 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 + + # ---- Non-abstract methods + def translate(self, offset: vector2) -> 'Shape': + """ + Translate the shape by the given offset + + :param offset: [x_offset, y,offset] + :return: self + """ + self.offset += offset + return self + + def rotate_around(self, pivot: vector2, rotation: float) -> 'Shape': + """ + Rotate the shape around a point. + + :param pivot: Point (x, y) to rotate around + :param rotation: Angle to rotate by (counterclockwise, radians) + :return: self + """ + pivot = numpy.array(pivot, dtype=float) + self.translate(-pivot) + self.rotate(rotation) + self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) + self.translate(+pivot) + return self + diff --git a/masque/shapes/text.py b/masque/shapes/text.py new file mode 100644 index 0000000..de2eae1 --- /dev/null +++ b/masque/shapes/text.py @@ -0,0 +1,57 @@ + +# +# class Text(Shape): +# _string = '' +# _height = 1.0 +# _rotation = 0.0 +# font_path = '' +# +# # vertices property +# @property +# def string(self): +# return self._string +# +# @string.setter +# def string(self, val): +# self._string = val +# +# # Rotation property +# @property +# def rotation(self): +# return self._rotation +# +# @rotation.setter +# def rotation(self, val): +# if not is_scalar(val): +# raise PatternError('Rotation must be a scalar') +# self._rotation = val % (2 * pi) +# +# # Height property +# @property +# def height(self): +# return self._height +# +# @height.setter +# def height(self, val): +# if not is_scalar(val): +# raise PatternError('Height must be a scalar') +# self._height = val +# +# def __init__(self, text, height, font_path, rotation=0.0, offset=(0.0, 0.0), layer=0, dose=1.0): +# self.offset = offset +# self.layer = layer +# self.dose = dose +# self.text = text +# self.height = height +# self.rotation = rotation +# self.font_path = font_path +# +# def to_polygon(self, _poly_num_points=None, _poly_max_arclen=None): +# +# return copy.deepcopy(self) +# +# def rotate(self, theta): +# self.rotation += theta +# +# def scale_by(self, c): +# self.height *= c diff --git a/masque/subpattern.py b/masque/subpattern.py new file mode 100644 index 0000000..8b389b7 --- /dev/null +++ b/masque/subpattern.py @@ -0,0 +1,159 @@ +""" + SubPattern provides basic support for nesting Pattern objects within each other, by adding + offset, rotation, scaling, and other such properties to the reference. +""" + +from typing import Union + +import numpy +from numpy import pi + +from .error import PatternError +from .utils import is_scalar, rotation_matrix_2d, vector2 + + +__author__ = 'Jan Petykiewicz' + + +class SubPattern(object): + """ + SubPattern provides basic support for nesting Pattern objects within each other, by adding + offset, rotation, scaling, and associated methods. + """ + + 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 + + def __init__(self, + pattern: 'Pattern', + offset: vector2=(0.0, 0.0), + rotation: float=0.0, + dose: float=1.0, + scale: float=1.0): + self.pattern = pattern + self.offset = offset + self.rotation = rotation + self.dose = dose + self.scale = scale + + # 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) + + 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. + """ + pattern = self.pattern.copy() + pattern.scale_by(self.scale) + pattern.rotate_around((0.0, 0.0), self.rotation) + pattern.translate_elements(self.offset) + pattern.scale_element_doses(self.dose) + return pattern + + def translate(self, offset: vector2) -> 'SubPattern': + """ + 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) -> 'SubPattern': + """ + 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) -> 'SubPattern': + """ + Rotate around (0, 0) + + :param rotation: Angle to rotate by (counterclockwise, radians) + :return: self + """ + self.rotation += rotation + 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) -> 'SubPattern': + """ + Scale the subpattern by a factor + + :param c: scaling factor + """ + self.scale *= c + return self + diff --git a/masque/utils.py b/masque/utils.py new file mode 100644 index 0000000..59e8169 --- /dev/null +++ b/masque/utils.py @@ -0,0 +1,41 @@ +""" +Various helper functions +""" + +from typing import Any, Union, Tuple + +import numpy + +# Type definitions +vector2 = Union[numpy.ndarray, Tuple[float, float]] + + +def is_scalar(var: Any) -> bool: + """ + Alias for 'not hasattr(var, "__len__")' + + :param var: Checks if var has a length. + """ + return not hasattr(var, "__len__") + + +def get_bit(bit_string: Any, bit_id: int) -> bool: + """ + Returns true iff bit number 'bit_id' from the right of 'bitstring' is 1 + + :param bit_string: st + :param bit_id: + :return: value of the requested bit (bool) + """ + return bit_string & (1 << bit_id) != 0 + + +def rotation_matrix_2d(theta: float) -> numpy.ndarray: + """ + 2D rotation matrix for rotating counterclockwise around the origin. + + :param theta: Angle to rotate, in radians + :return: rotation matrix + """ + return numpy.array([[numpy.cos(theta), -numpy.sin(theta)], + [numpy.sin(theta), +numpy.cos(theta)]]) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..6189400 --- /dev/null +++ b/setup.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python + +from distutils.core import setup + +setup(name='masque', + version='0.1', + description='Lithography mask library', + author='Jan Petykiewicz', + author_email='anewusername@gmail.com', + url='https://mpxd.net/gogs/jan/masque', + packages=['masque'], + ) + From 44b157dcc534173ae53ade340af4d701d8941499 Mon Sep 17 00:00:00 2001 From: jan Date: Tue, 15 Mar 2016 19:14:01 -0700 Subject: [PATCH 002/124] add extension to readme --- README => README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename README => README.md (100%) diff --git a/README b/README.md similarity index 100% rename from README rename to README.md From 4945567544601f4f5a3d90ded3922fe5179c2fde Mon Sep 17 00:00:00 2001 From: jan Date: Tue, 15 Mar 2016 19:33:36 -0700 Subject: [PATCH 003/124] Add install instructions --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index ded6941..17701cb 100644 --- a/README.md +++ b/README.md @@ -16,4 +16,6 @@ Requirements: * svgwrite (optional, used for svg output) +Install with pip, via git: +>pip install git+https://mpxd.net/gogs/jan/masque.git --upgrade From 5f8e238ad822032f0de068f09da69027e5a3ee04 Mon Sep 17 00:00:00 2001 From: jan Date: Tue, 15 Mar 2016 20:49:38 -0700 Subject: [PATCH 004/124] set readme to point at release branch --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 17701cb..8c13876 100644 --- a/README.md +++ b/README.md @@ -18,4 +18,4 @@ Requirements: Install with pip, via git: ->pip install git+https://mpxd.net/gogs/jan/masque.git --upgrade +>pip install --upgrade git+https://mpxd.net/gogs/jan/masque.git@release From 2a20a540b95ee0ae299bfcab48939de517b4428c Mon Sep 17 00:00:00 2001 From: jan Date: Wed, 16 Mar 2016 15:16:01 -0700 Subject: [PATCH 005/124] remove extra spaces --- masque/shapes/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/masque/shapes/__init__.py b/masque/shapes/__init__.py index cb7e708..d00ee96 100644 --- a/masque/shapes/__init__.py +++ b/masque/shapes/__init__.py @@ -10,5 +10,3 @@ from .circle import Circle from .ellipse import Ellipse from .arc import Arc - - From 3bfe71b9c75943b29e3a64bf361e566daef02615 Mon Sep 17 00:00:00 2001 From: jan Date: Tue, 22 Mar 2016 14:41:18 -0700 Subject: [PATCH 006/124] Add dependencies to setup.py --- setup.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6189400..b48d06c 100644 --- a/setup.py +++ b/setup.py @@ -9,5 +9,16 @@ setup(name='masque', author_email='anewusername@gmail.com', url='https://mpxd.net/gogs/jan/masque', packages=['masque'], - ) + install_requires=[ + 'numpy' + ], + dependency_links=[ + + ], + extras_require={ + 'visualization': ['matplotlib'], + 'gdsii': ['python-gdsii'], + 'svg': ['svgwrite'], + }, + ) From 1e7b9751be6b77616f2d019a0b3550b8a3f15cf0 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 27 Mar 2016 15:20:19 -0700 Subject: [PATCH 007/124] remove empty dependency block --- setup.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.py b/setup.py index b48d06c..5d0b1eb 100644 --- a/setup.py +++ b/setup.py @@ -11,9 +11,6 @@ setup(name='masque', packages=['masque'], install_requires=[ 'numpy' - ], - dependency_links=[ - ], extras_require={ 'visualization': ['matplotlib'], From 77f36206f948d9820b1181561ff038c613369253 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 27 Mar 2016 17:23:18 -0700 Subject: [PATCH 008/124] use find_packages() --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5d0b1eb..2d65b51 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup(name='masque', author='Jan Petykiewicz', author_email='anewusername@gmail.com', url='https://mpxd.net/gogs/jan/masque', - packages=['masque'], + packages=find_packages(), install_requires=[ 'numpy' ], From ff6c4f71c1d6226a7b414fc71d84c833c0e5022f Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 27 Mar 2016 17:29:46 -0700 Subject: [PATCH 009/124] fix import and switch to setuptools --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2d65b51..9a064ed 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -from distutils.core import setup +from setuptools import setup, find_packages setup(name='masque', version='0.1', From 496d07008d89fb663d78ad0ba96c37e3577b0d84 Mon Sep 17 00:00:00 2001 From: jan Date: Tue, 29 Mar 2016 13:56:36 -0700 Subject: [PATCH 010/124] Switch to code style and remove --upgrade --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8c13876..2bec3e3 100644 --- a/README.md +++ b/README.md @@ -17,5 +17,6 @@ Requirements: Install with pip, via git: - ->pip install --upgrade git+https://mpxd.net/gogs/jan/masque.git@release +```bash +pip install git+https://mpxd.net/gogs/jan/masque.git@release +``` From 3e1ff192706556ac4af3f6929b4b7e10bd4c2025 Mon Sep 17 00:00:00 2001 From: jan Date: Wed, 13 Apr 2016 03:51:25 -0700 Subject: [PATCH 011/124] Add license --- LICENSE.md | 651 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 651 insertions(+) create mode 100644 LICENSE.md diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..4ef32f0 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,651 @@ +GNU Affero General Public License +================================= + +_Version 3, 19 November 2007_ +_Copyright © 2007 Free Software Foundation, Inc. <>_ + +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + +## Preamble + +The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +Developers that use our General Public Licenses protect your rights +with two steps: **(1)** assert copyright on the software, and **(2)** offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + +A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + +The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + +An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + +The precise terms and conditions for copying, distribution and +modification follow. + +## TERMS AND CONDITIONS + +### 0. Definitions + +“This License” refers to version 3 of the GNU Affero General Public License. + +“Copyright” also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + +“The Program” refers to any copyrightable work licensed under this +License. Each licensee is addressed as “you”. “Licensees” and +“recipients” may be individuals or organizations. + +To “modify” a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a “modified version” of the +earlier work or a work “based on” the earlier work. + +A “covered work” means either the unmodified Program or a work based +on the Program. + +To “propagate” a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To “convey” a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays “Appropriate Legal Notices” +to the extent that it includes a convenient and prominently visible +feature that **(1)** displays an appropriate copyright notice, and **(2)** +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +### 1. Source Code + +The “source code” for a work means the preferred form of the work +for making modifications to it. “Object code” means any non-source +form of a work. + +A “Standard Interface” means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The “System Libraries” of an executable work include anything, other +than the work as a whole, that **(a)** is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and **(b)** serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +“Major Component”, in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The “Corresponding Source” for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + +The Corresponding Source for a work in source code form is that +same work. + +### 2. Basic Permissions + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + +### 3. Protecting Users' Legal Rights From Anti-Circumvention Law + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + +### 4. Conveying Verbatim Copies + +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +### 5. Conveying Modified Source Versions + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + +* **a)** The work must carry prominent notices stating that you modified +it, and giving a relevant date. +* **b)** The work must carry prominent notices stating that it is +released under this License and any conditions added under section 7. +This requirement modifies the requirement in section 4 to +“keep intact all notices”. +* **c)** You must license the entire work, as a whole, under this +License to anyone who comes into possession of a copy. This +License will therefore apply, along with any applicable section 7 +additional terms, to the whole of the work, and all its parts, +regardless of how they are packaged. This License gives no +permission to license the work in any other way, but it does not +invalidate such permission if you have separately received it. +* **d)** If the work has interactive user interfaces, each must display +Appropriate Legal Notices; however, if the Program has interactive +interfaces that do not display Appropriate Legal Notices, your +work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +“aggregate” if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +### 6. Conveying Non-Source Forms + +You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + +* **a)** Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by the +Corresponding Source fixed on a durable physical medium +customarily used for software interchange. +* **b)** Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by a +written offer, valid for at least three years and valid for as +long as you offer spare parts or customer support for that product +model, to give anyone who possesses the object code either **(1)** a +copy of the Corresponding Source for all the software in the +product that is covered by this License, on a durable physical +medium customarily used for software interchange, for a price no +more than your reasonable cost of physically performing this +conveying of source, or **(2)** access to copy the +Corresponding Source from a network server at no charge. +* **c)** Convey individual copies of the object code with a copy of the +written offer to provide the Corresponding Source. This +alternative is allowed only occasionally and noncommercially, and +only if you received the object code with such an offer, in accord +with subsection 6b. +* **d)** Convey the object code by offering access from a designated +place (gratis or for a charge), and offer equivalent access to the +Corresponding Source in the same way through the same place at no +further charge. You need not require recipients to copy the +Corresponding Source along with the object code. If the place to +copy the object code is a network server, the Corresponding Source +may be on a different server (operated by you or a third party) +that supports equivalent copying facilities, provided you maintain +clear directions next to the object code saying where to find the +Corresponding Source. Regardless of what server hosts the +Corresponding Source, you remain obligated to ensure that it is +available for as long as needed to satisfy these requirements. +* **e)** Convey the object code using peer-to-peer transmission, provided +you inform other peers where the object code and Corresponding +Source of the work are being offered to the general public at no +charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A “User Product” is either **(1)** a “consumer product”, which means any +tangible personal property which is normally used for personal, family, +or household purposes, or **(2)** anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, “normally used” refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + +“Installation Information” for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +### 7. Additional Terms + +“Additional permissions” are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + +* **a)** Disclaiming warranty or limiting liability differently from the +terms of sections 15 and 16 of this License; or +* **b)** Requiring preservation of specified reasonable legal notices or +author attributions in that material or in the Appropriate Legal +Notices displayed by works containing it; or +* **c)** Prohibiting misrepresentation of the origin of that material, or +requiring that modified versions of such material be marked in +reasonable ways as different from the original version; or +* **d)** Limiting the use for publicity purposes of names of licensors or +authors of the material; or +* **e)** Declining to grant rights under trademark law for use of some +trade names, trademarks, or service marks; or +* **f)** Requiring indemnification of licensors and authors of that +material by anyone who conveys the material (or modified versions of +it) with contractual assumptions of liability to the recipient, for +any liability that these contractual assumptions directly impose on +those licensors and authors. + +All other non-permissive additional terms are considered “further +restrictions” within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + +### 8. Termination + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated **(a)** +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and **(b)** permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +### 9. Acceptance Not Required for Having Copies + +You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +### 10. Automatic Licensing of Downstream Recipients + +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An “entity transaction” is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +### 11. Patents + +A “contributor” is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's “contributor version”. + +A contributor's “essential patent claims” are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, “control” includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a “patent license” is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To “grant” such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either **(1)** cause the Corresponding Source to be so +available, or **(2)** arrange to deprive yourself of the benefit of the +patent license for this particular work, or **(3)** arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. “Knowingly relying” means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is “discriminatory” if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license **(a)** in connection with copies of the covered work +conveyed by you (or copies made from those copies), or **(b)** primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +### 12. No Surrender of Others' Freedom + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + +### 13. Remote Network Interaction; Use with the GNU General Public License + +Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + +### 14. Revised Versions of this License + +The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License “or any later version” applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +### 15. Disclaimer of Warranty + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +### 16. Limitation of Liability + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + +### 17. Interpretation of Sections 15 and 16 + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + +_END OF TERMS AND CONDITIONS_ + +## How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the “copyright” line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a “Source” link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + +You should also get your employer (if you work as a programmer) or school, +if any, to sign a “copyright disclaimer” for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +<>. From 3a460a92966740ead41eb086d73d20d325f28941 Mon Sep 17 00:00:00 2001 From: jan Date: Wed, 19 Oct 2016 16:52:28 -0700 Subject: [PATCH 012/124] Add Text shape Rendered using freetype-py and matplotlib Can eliminate the matplotlib dependency if I write my own bezier code, but that's work (and I already use matplotlib...). --- README.md | 3 +- masque/shapes/__init__.py | 2 +- masque/shapes/text.py | 256 +++++++++++++++++++++++++++++--------- setup.py | 1 + 4 files changed, 204 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 2bec3e3..d8b54cb 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,10 @@ E-beam doses, and the ability to output to multiple formats. Requirements: * python 3 (written and tested with 3.5) * numpy -* matplotlib (optional, used for visualization functions) +* matplotlib (optional, used for visualization functions and text) * python-gdsii (optional, used for gdsii i/o) * svgwrite (optional, used for svg output) +* freetype (optional, used for text) Install with pip, via git: diff --git a/masque/shapes/__init__.py b/masque/shapes/__init__.py index d00ee96..4c64204 100644 --- a/masque/shapes/__init__.py +++ b/masque/shapes/__init__.py @@ -9,4 +9,4 @@ from .polygon import Polygon from .circle import Circle from .ellipse import Ellipse from .arc import Arc - +from .text import Text diff --git a/masque/shapes/text.py b/masque/shapes/text.py index de2eae1..e645920 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -1,57 +1,201 @@ +from typing import List, Tuple +import numpy +from numpy import pi, inf -# -# class Text(Shape): -# _string = '' -# _height = 1.0 -# _rotation = 0.0 -# font_path = '' -# -# # vertices property -# @property -# def string(self): -# return self._string -# -# @string.setter -# def string(self, val): -# self._string = val -# -# # Rotation property -# @property -# def rotation(self): -# return self._rotation -# -# @rotation.setter -# def rotation(self, val): -# if not is_scalar(val): -# raise PatternError('Rotation must be a scalar') -# self._rotation = val % (2 * pi) -# -# # Height property -# @property -# def height(self): -# return self._height -# -# @height.setter -# def height(self, val): -# if not is_scalar(val): -# raise PatternError('Height must be a scalar') -# self._height = val -# -# def __init__(self, text, height, font_path, rotation=0.0, offset=(0.0, 0.0), layer=0, dose=1.0): -# self.offset = offset -# self.layer = layer -# self.dose = dose -# self.text = text -# self.height = height -# self.rotation = rotation -# self.font_path = font_path -# -# def to_polygon(self, _poly_num_points=None, _poly_max_arclen=None): -# -# return copy.deepcopy(self) -# -# def rotate(self, theta): -# self.rotation += theta -# -# def scale_by(self, c): -# self.height *= c +from . import Shape, Polygon, normalized_shape_tuple +from .. import PatternError +from ..utils import is_scalar, vector2, get_bit + +# Loaded on use: +# from freetype import Face +# from matplotlib.path import Path + + +__author__ = 'Jan Petykiewicz' + + +class Text(Shape): + _string = '' + _height = 1.0 + _rotation = 0.0 + font_path = '' + + # vertices property + @property + def string(self) -> str: + return self._string + + @string.setter + def string(self, val: str): + self._string = val + + # Rotation property + @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) + + # Height property + @property + def height(self) -> float: + return self._height + + @height.setter + def height(self, val: float): + if not is_scalar(val): + raise PatternError('Height must be a scalar') + self._height = val + + def __init__(self, + string: str, + height: float, + font_path: str, + rotation: float=0.0, + offset: vector2=(0.0, 0.0), + layer: int=0, + dose: float=1.0): + self.offset = offset + self.layer = layer + self.dose = dose + self.string = string + self.height = height + self.rotation = rotation + self.font_path = font_path + + def to_polygons(self, + _poly_num_points: int=None, + _poly_max_arclen: float=None + ) -> List[Polygon]: + all_polygons = [] + total_advance = 0 + for char in self.string: + raw_polys, advance = get_char_as_polygons(self.font_path, char) + + # Move these polygons to the right of the previous letter + for xys in raw_polys: + poly = Polygon(xys, dose=self.dose, layer=self.layer) + poly.scale_by(self.height) + poly.offset = self.offset + [total_advance, 0] + # poly.scale_by(self.height) + poly.rotate_around(self.offset, self.rotation) + all_polygons += [poly] + + # Update the list of all polygons and how far to advance + total_advance += advance * self.height + + return all_polygons + + def rotate(self, theta: float): + self.rotation += theta + + def scale_by(self, c: float): + self.height *= c + + def normalized_form(self, norm_value: float) -> normalized_shape_tuple: + return (type(self), self.string, self.font_path, self.layer), \ + (self.offset, self.height / norm_value, self.rotation, self.dose), \ + lambda: Text(string=self.string, + height=self.height * norm_value, + font_path=self.font_path, + layer=self.layer) + + def get_bounds(self) -> numpy.ndarray: + # rotation makes this a huge pain when using slot.advance and glyph.bbox(), so + # just convert to polygons instead + bounds = [[+inf, +inf], [-inf, -inf]] + polys = self.to_polygons() + for poly in polys: + poly_bounds = poly.get_bounds() + bounds[0, :] = numpy.minimum(bounds[0, :], poly_bounds[0, :]) + bounds[1, :] = numpy.maximum(bounds[1, :], poly_bounds[1, :]) + + return bounds + + +def get_char_as_polygons(font_path: str, + char: str, + resolution: float=48*64, + ) -> Tuple[List[List[List[float]]], float]: + from freetype import Face + from matplotlib.path import Path + + """ + Get a list of polygons representing a single character. + + The output is normalized so that the font size is 1 unit. + + :param font_path: File path specifying a font loadable by freetype + :param char: Character to convert to polygons + :param resolution: Internal resolution setting (used for freetype + Face.set_font_size(resolution)). Modify at your own peril! + :return: List of polygons [[[x0, y0], [x1, y1], ...], ...] and 'advance' distance (distance + from the start of this glyph to the start of the next one) + """ + if len(char) != 1: + raise Exception('get_char_as_polygons called with non-char') + + face = Face(font_path) + face.set_char_size(resolution) + face.load_char(char) + slot = face.glyph + outline = slot.outline + + start = 0 + all_verts, all_codes = [], [] + for end in outline.contours: + points = outline.points[start:end + 1] + points.append(points[0]) + + tags = outline.tags[start:end + 1] + tags.append(tags[0]) + + segments = [] + for j, point in enumerate(points): + # If we already have a segment, add this point to it + if j > 0: + segments[-1].append(point) + + # If not bezier control point, start next segment + if get_bit(tags[j], 0) and j < (len(points) - 1): + segments.append([point]) + + verts = [points[0]] + codes = [Path.MOVETO] + for segment in segments: + if len(segment) == 2: + verts.extend(segment[1:]) + codes.extend([Path.LINETO]) + elif len(segment) == 3: + verts.extend(segment[1:]) + codes.extend([Path.CURVE3, Path.CURVE3]) + else: + verts.append(segment[1]) + codes.append(Path.CURVE3) + for i in range(1, len(segment) - 2): + a, b = segment[i], segment[i + 1] + c = ((a[0] + b[0]) / 2.0, (a[1] + b[1]) / 2.0) + verts.extend([c, b]) + codes.extend([Path.CURVE3, Path.CURVE3]) + verts.append(segment[-1]) + codes.append(Path.CURVE3) + all_verts.extend(verts) + all_codes.extend(codes) + start = end + 1 + + all_verts = numpy.array(all_verts) / resolution + + advance = slot.advance.x / resolution + + if len(all_verts) == 0: + polygons = [] + else: + path = Path(all_verts, all_codes) + path.should_simplify = False + polygons = path.to_polygons() + + return polygons, advance diff --git a/setup.py b/setup.py index 9a064ed..251edf7 100644 --- a/setup.py +++ b/setup.py @@ -16,6 +16,7 @@ setup(name='masque', 'visualization': ['matplotlib'], 'gdsii': ['python-gdsii'], 'svg': ['svgwrite'], + 'text': ['freetype-py', 'matplotlib'] }, ) From 103e72628c5ad99df46b0f009bea322e4e2a5b1a Mon Sep 17 00:00:00 2001 From: jan Date: Wed, 19 Oct 2016 16:52:40 -0700 Subject: [PATCH 013/124] Remove extra spaces --- masque/shapes/arc.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 1cba2ef..268c0c7 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -269,5 +269,3 @@ class Arc(Shape): return (type(self), radii, angles, self.layer), \ (self.offset, scale/norm_value, rotation, self.dose), \ lambda: Arc(radii=radii*norm_value, angles=angles, layer=self.layer) - - From d355d84f6d63608b7b6c27da892bc834da1644a0 Mon Sep 17 00:00:00 2001 From: jan Date: Wed, 19 Oct 2016 16:59:20 -0700 Subject: [PATCH 014/124] fix gds name mangling in cases with fractional dose --- masque/file/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/masque/file/utils.py b/masque/file/utils.py index 615d455..e71e500 100644 --- a/masque/file/utils.py +++ b/masque/file/utils.py @@ -19,9 +19,9 @@ def mangle_name(pattern: Pattern, dose_multiplier: float=1.0) -> str: :return: Mangled name. """ expression = re.compile('[^A-Za-z0-9_\?\$]') - sanitized_name = expression.sub('_', pattern.name) - full_name = '{}_{}_{}'.format(sanitized_name, dose_multiplier, id(pattern)) - return full_name + full_name = '{}_{}_{}'.format(pattern.name, dose_multiplier, id(pattern)) + sanitized_name = expression.sub('_', full_name) + return sanitized_name def make_dose_table(pattern: Pattern, dose_multiplier: float=1.0) -> Set[Tuple[int, float]]: From 113671e591ca70050c75e266db3d44d65153899e Mon Sep 17 00:00:00 2001 From: jan Date: Wed, 19 Oct 2016 17:00:00 -0700 Subject: [PATCH 015/124] bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 251edf7..c24baf3 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import setup, find_packages setup(name='masque', - version='0.1', + version='0.2', description='Lithography mask library', author='Jan Petykiewicz', author_email='anewusername@gmail.com', From ff76626e217fa65ce57d54d3db31003273e6aa62 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 19 Apr 2017 18:54:58 -0700 Subject: [PATCH 016/124] fix multiple bugs in arc and ellipse --- masque/shapes/arc.py | 57 ++++++++++++++++++++++------------------ masque/shapes/ellipse.py | 14 +++++----- 2 files changed, 39 insertions(+), 32 deletions(-) diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 268c0c7..41e09b9 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -38,7 +38,7 @@ class Arc(Shape): :return: [rx, ry] """ - return self.radii + return self._radii @radii.setter def radii(self, val: vector2): @@ -47,27 +47,27 @@ class Arc(Shape): raise PatternError('Radii must have length 2') if not val.min() >= 0: raise PatternError('Radii must be non-negative') - self.radii = val + self._radii = val @property def radius_x(self) -> float: - return self.radii[0] + return self._radii[0] @radius_x.setter def radius_x(self, val: float): if not val >= 0: raise PatternError('Radius must be non-negative') - self.radii[0] = val + self._radii[0] = val @property def radius_y(self) -> float: - return self.radii[1] + return self._radii[1] @radius_y.setter def radius_y(self, val: float): if not val >= 0: raise PatternError('Radius must be non-negative') - self.radii[1] = val + self._radii[1] = val # arc start/stop angle properties @property @@ -85,11 +85,7 @@ class Arc(Shape): val = numpy.array(val, dtype=float).flatten() if not val.size == 2: raise PatternError('Angles must have length 2') - angles = val % (2 * pi) - if angles[0] > pi: - self.rotation += pi - angles -= pi - self._angles = angles + self._angles = val @property def start_angle(self) -> float: @@ -97,7 +93,7 @@ class Arc(Shape): @start_angle.setter def start_angle(self, val: float): - self.angles[0] = val % (2 * pi) + self.angles = (val, self.angles[1]) @property def stop_angle(self) -> float: @@ -105,7 +101,7 @@ class Arc(Shape): @stop_angle.setter def stop_angle(self, val: float): - self.angles[1] = val % (2 * pi) + self.angles = (self.angles[0], val) # Rotation property @property @@ -144,6 +140,7 @@ class Arc(Shape): def __init__(self, radii: vector2, angles: vector2, + width: float, rotation: float=0, poly_num_points: int=DEFAULT_POLY_NUM_POINTS, poly_max_arclen: float=None, @@ -155,6 +152,7 @@ class Arc(Shape): self.dose = dose self.radii = radii self.angles = angles + self.width = width self.rotation = rotation self.poly_num_points = poly_num_points self.poly_max_arclen = poly_max_arclen @@ -169,30 +167,30 @@ class Arc(Shape): raise PatternError('Max number of points and arclength left unspecified' + ' (default was also overridden)') - rxy = self.radii - ang = self.angles + r0, r1 = self.radii + a0, a1 = self.angles # Approximate perimeter # Ramanujan, S., "Modular Equations and Approximations to ," # Quart. J. Pure. Appl. Math., vol. 45 (1913-1914), pp. 350-372 - h = ((rxy[1] - rxy[0]) / rxy.sum()) ** 2 - ellipse_perimeter = pi * rxy.sum() * (1 + 3 * h / (10 + math.sqrt(4 - 3 * h))) - perimeter = abs(ang[0] - ang[1]) / (2 * pi) * ellipse_perimeter + h = ((r1 - r0) / (r1 + r0)) ** 2 + ellipse_perimeter = pi * (r1 + r0) * (1 + 3 * h / (10 + math.sqrt(4 - 3 * h))) + perimeter = abs(a0 - a1) / (2 * pi) * ellipse_perimeter # TODO: make this more accurate n = [] if poly_num_points is not None: n += [poly_num_points] if poly_max_arclen is not None: n += [perimeter / poly_max_arclen] - thetas = numpy.linspace(2 * pi, 0, max(n), endpoint=False) + thetas = numpy.linspace(a1, a0, max(n), endpoint=True) sin_th, cos_th = (numpy.sin(thetas), numpy.cos(thetas)) wh = self.width / 2.0 - xs1 = (rxy[0] + wh) * cos_th - (rxy[1] + wh) * sin_th - ys1 = (rxy[0] + wh) * cos_th - (rxy[1] + wh) * sin_th - xs2 = (rxy[0] - wh) * cos_th - (rxy[1] - wh) * sin_th - ys2 = (rxy[0] - wh) * cos_th - (rxy[1] - wh) * sin_th + xs1 = (r0 + wh) * cos_th + ys1 = (r1 + wh) * sin_th + xs2 = (r0 - wh) * cos_th + ys2 = (r1 - wh) * sin_th xs = numpy.hstack((xs1, xs2[::-1])) ys = numpy.hstack((ys1, ys2[::-1])) @@ -266,6 +264,15 @@ class Arc(Shape): scale = self.radius_y rotation = self.rotation + pi / 2 angles = self.angles - pi / 2 - return (type(self), radii, angles, self.layer), \ + + if angles[0] >= pi: + angles -= pi + rotation += pi + + angles %= 2 * pi + rotation %= 2 * pi + width = self.width + + return (type(self), radii, angles, width, self.layer), \ (self.offset, scale/norm_value, rotation, self.dose), \ - lambda: Arc(radii=radii*norm_value, angles=angles, layer=self.layer) + lambda: Arc(radii=radii*norm_value, angles=angles, width=width, layer=self.layer) diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index a9d22e7..ea6e3bf 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -32,7 +32,7 @@ class Ellipse(Shape): :return: [rx, ry] """ - return self.radii + return self._radii @radii.setter def radii(self, val: vector2): @@ -41,7 +41,7 @@ class Ellipse(Shape): raise PatternError('Radii must have length 2') if not val.min() >= 0: raise PatternError('Radii must be non-negative') - self.radii = val + self._radii = val @property def radius_x(self) -> float: @@ -109,13 +109,13 @@ class Ellipse(Shape): raise PatternError('Number of points and arclength left unspecified' ' (default was also overridden)') - rxy = self.radii + r0, r1 = self.radii # Approximate perimeter # Ramanujan, S., "Modular Equations and Approximations to ," # Quart. J. Pure. Appl. Math., vol. 45 (1913-1914), pp. 350-372 - h = ((rxy[1] - rxy[0]) / rxy.sum()) ** 2 - perimeter = pi * rxy.sum() * (1 + 3 * h / (10 + math.sqrt(4 - 3 * h))) + h = ((r1 - r0) / (r1 + r0)) ** 2 + perimeter = pi * (r1 + r0) * (1 + 3 * h / (10 + math.sqrt(4 - 3 * h))) n = [] if poly_num_points is not None: @@ -125,8 +125,8 @@ class Ellipse(Shape): thetas = numpy.linspace(2 * pi, 0, max(n), endpoint=False) sin_th, cos_th = (numpy.sin(thetas), numpy.cos(thetas)) - xs = rxy[0] * cos_th - rxy[1] * sin_th - ys = rxy[0] * sin_th - rxy[1] * cos_th + xs = r0 * cos_th + ys = r1 * sin_th xys = numpy.vstack((xs, ys)).T poly = Polygon(xys, dose=self.dose, layer=self.layer, offset=self.offset) From f25f6966e0b5362b5eb5468abd15a71604c172d5 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 19 Apr 2017 18:55:44 -0700 Subject: [PATCH 017/124] add example showing how to make an elliptical grating --- examples/ellip_grating.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 examples/ellip_grating.py diff --git a/examples/ellip_grating.py b/examples/ellip_grating.py new file mode 100644 index 0000000..e4e2672 --- /dev/null +++ b/examples/ellip_grating.py @@ -0,0 +1,22 @@ +# Quick script for testing arcs + +import numpy + +import masque +from masque import shapes + + +def main(): + pat = masque.Pattern() + for rmin in numpy.arange(10, 15, 0.5): + pat.shapes.append(shapes.Arc( + radii=(rmin, rmin), + width=0.1, + angles=(-numpy.pi/4, numpy.pi/4) + )) + + pat.visualize() + + +if __name__ == '__main__': + main() From 542da868c9ff2907a95d792e9386be22e6a96306 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 19 Apr 2017 18:56:14 -0700 Subject: [PATCH 018/124] add TODO section --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index d8b54cb..89c7b44 100644 --- a/README.md +++ b/README.md @@ -21,3 +21,11 @@ Install with pip, via git: ```bash pip install git+https://mpxd.net/gogs/jan/masque.git@release ``` + +## TODO +* Mirroring +* Polygon de-embedding +### Maybe +* Construct from bitmap +* Boolean operations on polygons (eg. using pyclipper) +* Output to OASIS From 2c159f279886d905046336d690c9691023d6b316 Mon Sep 17 00:00:00 2001 From: jan Date: Thu, 20 Apr 2017 13:00:40 -0700 Subject: [PATCH 019/124] remove extra polygonize --- masque/file/gdsii.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 3cf1e74..b43e1ac 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -45,9 +45,6 @@ def write_dose2dtype(pattern: Pattern, logical_unit=1, physical_unit=meters_per_unit) - # Polygonize pattern - pattern.polygonize() - # Get a dict of id(pattern) -> pattern patterns_by_id = {**(pattern.referenced_patterns_by_id()), id(pattern): pattern} From 434178c85321b52796c8df903f29832e6c1a7e01 Mon Sep 17 00:00:00 2001 From: jan Date: Thu, 20 Apr 2017 13:01:31 -0700 Subject: [PATCH 020/124] correctly preserve total arc angle during normalize --- masque/shapes/arc.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 41e09b9..2ae12c9 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -265,11 +265,14 @@ class Arc(Shape): rotation = self.rotation + pi / 2 angles = self.angles - pi / 2 - if angles[0] >= pi: - angles -= pi + delta_angle = angles[1] - angles[0] + start_angle = angles[0] % (2 * pi) + if start_angle >= pi: + start_angle -= pi rotation += pi - angles %= 2 * pi + + angles %= (start_angle, start_angle + delta_angle) rotation %= 2 * pi width = self.width From 6cc6556e8a05e4a04480bdf673de3321bd6946da Mon Sep 17 00:00:00 2001 From: jan Date: Thu, 20 Apr 2017 13:05:58 -0700 Subject: [PATCH 021/124] typo fix --- masque/shapes/arc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 2ae12c9..8763291 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -272,7 +272,7 @@ class Arc(Shape): rotation += pi - angles %= (start_angle, start_angle + delta_angle) + angles = (start_angle, start_angle + delta_angle) rotation %= 2 * pi width = self.width From 0a7c26bb08fc5c08601a8809cb9d9fb68eb621c3 Mon Sep 17 00:00:00 2001 From: jan Date: Sat, 12 Aug 2017 19:30:31 -0700 Subject: [PATCH 022/124] No need to subclass 'object' in python3 --- masque/pattern.py | 2 +- masque/shapes/shape.py | 2 +- masque/subpattern.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/masque/pattern.py b/masque/pattern.py index b187b9e..1883f5d 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -19,7 +19,7 @@ from .utils import rotation_matrix_2d, vector2 __author__ = 'Jan Petykiewicz' -class Pattern(object): +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 diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index cacac44..c12a58b 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -19,7 +19,7 @@ normalized_shape_tuple = Tuple[Tuple, DEFAULT_POLY_NUM_POINTS = 24 -class Shape(object, metaclass=ABCMeta): +class Shape(metaclass=ABCMeta): """ Abstract class specifying functions common to all shapes. """ diff --git a/masque/subpattern.py b/masque/subpattern.py index 8b389b7..c45a4d9 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -15,7 +15,7 @@ from .utils import is_scalar, rotation_matrix_2d, vector2 __author__ = 'Jan Petykiewicz' -class SubPattern(object): +class SubPattern: """ SubPattern provides basic support for nesting Pattern objects within each other, by adding offset, rotation, scaling, and associated methods. From b99509289a246cb2e62c535f97c2e06c0eecc640 Mon Sep 17 00:00:00 2001 From: jan Date: Sat, 12 Aug 2017 19:31:02 -0700 Subject: [PATCH 023/124] Let layer be a tuple of ints (or just a single int like before) --- masque/shapes/shape.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index c12a58b..bb4687e 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -120,20 +120,16 @@ class Shape(metaclass=ABCMeta): # layer property @property - def layer(self) -> int: + def layer(self) -> int or Tuple[int]: """ - Layer number (int, >=0) + Layer number (int or tuple of ints) :return: Layer """ return self._layer @layer.setter - def layer(self, val: int): - if not isinstance(val, int): - raise PatternError('Layer must be an integer') - if not val >= 0: - raise PatternError('Layer must be non-negative') + def layer(self, val: int or List[int]): self._layer = val # dose property From bf1cabe0b0d13d988176cc4bd46d5d263f5e6896 Mon Sep 17 00:00:00 2001 From: jan Date: Sat, 12 Aug 2017 19:31:49 -0700 Subject: [PATCH 024/124] Let gdsii output handle list-specified errors --- masque/file/gdsii.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index b43e1ac..cb170d4 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -27,6 +27,9 @@ def write_dose2dtype(pattern: Pattern, structures, polygons as boundary elements, and subpatterns as structure references (sref). + For each shape, + layer is chosen to be equal to shape.layer if it is an int, + or shape.layer[0] if it is a tuple Note that this function modifies the Pattern. It is often a good idea to run pattern.subpatternize() prior to calling this function, @@ -78,7 +81,11 @@ def write_dose2dtype(pattern: Pattern, data_type = dose_vals_list.index(polygon.dose * pat_dose) 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=polygon.layer, + if hasattr('__len__', polygon.layer): + layer = polygon.layer[0] + else: + layer = polygon.layer + structure.append(gdsii.elements.Boundary(layer=layer, data_type=data_type, xy=xy_closed)) # Add an SREF for each subpattern entry From dcf34536ada2174dc862ca1ad87e49c6f486bae9 Mon Sep 17 00:00:00 2001 From: jan Date: Sat, 12 Aug 2017 19:32:15 -0700 Subject: [PATCH 025/124] Improve docs for gdsii output --- masque/file/gdsii.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index cb170d4..06ff2d4 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -20,7 +20,7 @@ __author__ = 'Jan Petykiewicz' def write_dose2dtype(pattern: Pattern, filename: str, - meters_per_unit: float): + meters_per_unit: float) -> List[float]: """ Write a Pattern to a GDSII file, by first calling .polygonize() on it to change the shapes into polygons, and then writing patterns as GDSII @@ -30,6 +30,11 @@ def write_dose2dtype(pattern: Pattern, For each shape, layer is chosen to be equal to 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 Pattern. It is often a good idea to run pattern.subpatternize() prior to calling this function, @@ -41,6 +46,8 @@ def write_dose2dtype(pattern: Pattern, :param pattern: A Pattern to write to file. Modified by this function. :param filename: Filename to write to. :param meters_per_unit: Written into the GDSII file, meters per length unit. + :returns: A list of doses, providing a mapping between datatype (int, list index) + and dose (float, list entry). """ # Create library lib = gdsii.library.Library(version=600, From 1127242aa06cf1359bf00f7ad2a33762d2442643 Mon Sep 17 00:00:00 2001 From: jan Date: Thu, 24 Aug 2017 15:35:10 -0700 Subject: [PATCH 026/124] fix typo --- masque/file/gdsii.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 06ff2d4..f3d3f8c 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -88,7 +88,7 @@ def write_dose2dtype(pattern: Pattern, data_type = dose_vals_list.index(polygon.dose * pat_dose) xy_open = numpy.round(polygon.vertices + polygon.offset).astype(int) xy_closed = numpy.vstack((xy_open, xy_open[0, :])) - if hasattr('__len__', polygon.layer): + if hasattr(polygon.layer, '__len__'): layer = polygon.layer[0] else: layer = polygon.layer From fdd18ca7d87a6c1725c6899107da005b26da50db Mon Sep 17 00:00:00 2001 From: jan Date: Tue, 29 Aug 2017 15:45:00 -0700 Subject: [PATCH 027/124] add functions for reading/writing tuple-valued layers to gds --- masque/file/gdsii.py | 98 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 94 insertions(+), 4 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index f3d3f8c..394cec7 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -18,6 +18,83 @@ from ..utils import rotation_matrix_2d, get_bit, vector2 __author__ = 'Jan Petykiewicz' +def write(pattern: Pattern, + filename: str, + meters_per_unit: float): + """ + Write a Pattern to a GDSII file, by first calling .polygonize() on it + to change the shapes into polygons, and then writing patterns as GDSII + structures, polygons as boundary elements, and subpatterns as structure + references (sref). + + For each shape, + layer is chosen to be equal to shape.layer if it is an int, + or shape.layer[0] if it is a tuple + datatype is chosen to be shape.layer[1] if available, + otherwise 0 + + Note that this function modifies the Pattern. + + It is often a good idea to run pattern.subpatternize() prior to calling this function, + especially if calling .polygonize() will result in very many vertices. + + If you want pattern polygonized with non-default arguments, just call pattern.polygonize() + prior to calling this function. + + :param pattern: A Pattern to write to file. Modified by this function. + :param filename: Filename to write to. + :param meters_per_unit: Written into the GDSII file, meters per length unit. + """ + # Create library + lib = gdsii.library.Library(version=600, + name='masque-write_dose2dtype'.encode('ASCII'), + logical_unit=1, + physical_unit=meters_per_unit) + + # Get a dict of id(pattern) -> pattern + patterns_by_id = {**(pattern.referenced_patterns_by_id()), id(pattern): pattern} + + # Now create a structure for each row in sd_table (ie, each pattern + dose combination) + # and add in any Boundary and SREF elements + for pat_id, pat_dose in sd_table: + pat = patterns_by_id[pat_id] + + sanitized_name = re.compile('[^A-Za-z0-9_\?\$]').sub('_', pattern.name) + structure = gdsii.structure.Structure(name=sanitized_name.encode('ASCII')) + lib.append(structure) + + # Add a Boundary element for each shape + for shape in pat.shapes: + 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, :])) + if hasattr(polygon.layer, '__len__'): + layer = polygon.layer[0] + if len(polygon.layer) > 1: + data_type = polygon.layer[1] + else: + data_type = 0 + else: + layer = polygon.layer + data_type = 0 + structure.append(gdsii.elements.Boundary(layer=layer, + data_type=data_type, + xy=xy_closed)) + # Add an SREF for each subpattern entry + # strans must be set for angle and mag to take effect + for subpat in pat.subpatterns: + sanitized_name = re.compile('[^A-Za-z0-9_\?\$]').sub('_', subpat.name) + sref = gdsii.elements.SRef(struct_name=sanitized_name.encode('ASCII'), + xy=numpy.round([subpat.offset]).astype(int)) + sref.strans = 0 + sref.angle = subpat.rotation + sref.mag = subpat.scale + structure.append(sref) + + with open(filename, mode='wb') as stream: + lib.save(stream) + + def write_dose2dtype(pattern: Pattern, filename: str, meters_per_unit: float) -> List[float]: @@ -113,12 +190,21 @@ def write_dose2dtype(pattern: Pattern, def read_dtype2dose(filename: str) -> (List[Pattern], Dict[str, Any]): + """ + Alias for read(filename, use_dtype_as_dose=True) + """ + return read(filename, use_dtype_as_dose=True) + + +def read(filename: str, use_dtype_as_dose=False) -> (List[Pattern], Dict[str, Any]): """ Read a gdsii file and translate it into a list of Pattern objects. GDSII structures are translated into Pattern objects; boundaries are translated into polygons, and srefs and arefs are translated into SubPattern objects. :param filename: Filename specifying a GDSII file to read from. + :param use_dtype_as_dose: If false, set each polygon's layer to (gds_layer, gds_datatype). + If true, set the layer to gds_layer and the dose to gds_datatype. :return: Tuple: (List of Patterns generated GDSII structures, Dict of GDSII library info) """ @@ -158,10 +244,14 @@ def read_dtype2dose(filename: str) -> (List[Pattern], Dict[str, Any]): for element in structure: # Switch based on element type: if isinstance(element, gdsii.elements.Boundary): - pat.shapes.append( - Polygon(vertices=element.xy[:-1], - dose=element.data_type, - layer=element.layer)) + if use_dtype_as_dose: + shape = Polygon(vertices=element.xy[:-1], + dose=element.data_type, + layer=element.layer) + else: + shape = Polygon(vertices=element.xy[:-1], + layer=(element.layer, element.data_type)) + pat.shapes.append(shape) elif isinstance(element, gdsii.elements.SRef): pat.subpatterns.append(ref_element_to_subpat(element, element.xy)) From 8256a540dca857679a9d9d0bd36a60a331eb4f9a Mon Sep 17 00:00:00 2001 From: jan Date: Tue, 29 Aug 2017 15:51:00 -0700 Subject: [PATCH 028/124] Use polar angle for ellipse bounds --- masque/shapes/arc.py | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 8763291..679de9a 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -74,7 +74,7 @@ class Arc(Shape): def angles(self) -> vector2: """ Return the start and stop angles [a_start, a_stop]. - Angles are measured from x-axis after rotation, and are stored mod 2*pi + Angles are measured from x-axis after rotation :return: [a_start, a_stop] """ @@ -168,7 +168,12 @@ class Arc(Shape): ' (default was also overridden)') r0, r1 = self.radii - a0, a1 = self.angles + + # Convert from polar angle to ellipse parameter (for [rx*cos(t), ry*sin(t)] representation) + a0, a1 = (numpy.arctan2(r0*numpy.sin(a), r1*numpy.cos(a)) for a in self.angles) + sign = numpy.sign(self.angles[1] - self.angles[0]) + if sign != numpy.sign(a1 - a0): + a1 += sign * 2 * pi # Approximate perimeter # Ramanujan, S., "Modular Equations and Approximations to ," @@ -201,8 +206,6 @@ class Arc(Shape): return [poly] def get_bounds(self) -> numpy.ndarray: - a = self.angles - 0.5 * pi - mins = [] maxs = [] for sgn in (+1, -1): @@ -210,33 +213,45 @@ class Arc(Shape): rx = self.radius_x + wh ry = self.radius_y + wh + # Create paremeter 'a' for parametrized ellipse + a0, a1 = (numpy.arctan2(rx*numpy.sin(a), ry*numpy.cos(a)) for a in self.angles) + sign = numpy.sign(self.angles[1] - self.angles[0]) + if sign != numpy.sign(a1 - a0): + a1 += sign * 2 * pi + + a = numpy.array((a0, a1)) + a0_offset = a0 - (a0 % (2 * pi)) + sin_r = numpy.sin(self.rotation) cos_r = numpy.cos(self.rotation) tan_r = numpy.tan(self.rotation) sin_a = numpy.sin(a) cos_a = numpy.cos(a) - xpt = numpy.arctan(-ry / rx * tan_r) - ypt = numpy.arctan(+ry / rx / tan_r) - xnt = numpy.arcsin(numpy.sin(xpt - pi)) - ynt = numpy.arcsin(numpy.sin(ypt - pi)) + # Cutoff angles + xpt = (-self.rotation) % (2 * pi) + a0_offset + ypt = self.rotation % (2 * pi) + a0_offset + xnt = (xpt - pi) % (2 * pi) + a0_offset + ynt = (ypt - pi) % (2 * pi) + a0_offset + # Points along coordinate axes xr = numpy.sqrt((rx * cos_r) ** 2 + (ry * sin_r) ** 2) yr = numpy.sqrt((rx * sin_r) ** 2 + (ry * cos_r) ** 2) + # Arc endpoints xn, xp = sorted(rx * cos_r * cos_a - ry * sin_r * sin_a) yn, yp = sorted(rx * sin_r * cos_a - ry * cos_r * sin_a) - if min(a) < xpt < max(a): + if a0 < xpt < a1: xp = xr - if min(a) < xnt < max(a): + if a0 < xnt < a1: xn = -xr - if min(a) < ypt < max(a): + if a0 < ypt < a1: yp = yr - if min(a) < ynt < max(a): + if a0 < ynt < a1: yn = -yr mins.append([xn, yn]) From 85e2c662cc78206c962079d366fbc5ba679ec145 Mon Sep 17 00:00:00 2001 From: jan Date: Tue, 29 Aug 2017 15:57:37 -0700 Subject: [PATCH 029/124] fix incomplete commit --- masque/file/gdsii.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 394cec7..3d4ab48 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -54,11 +54,8 @@ def write(pattern: Pattern, # Get a dict of id(pattern) -> pattern patterns_by_id = {**(pattern.referenced_patterns_by_id()), id(pattern): pattern} - # Now create a structure for each row in sd_table (ie, each pattern + dose combination) - # and add in any Boundary and SREF elements - for pat_id, pat_dose in sd_table: - pat = patterns_by_id[pat_id] - + # Now create a structure for each pattern, and add in any Boundary and SREF elements + for pat in patterns_by_id.values(): sanitized_name = re.compile('[^A-Za-z0-9_\?\$]').sub('_', pattern.name) structure = gdsii.structure.Structure(name=sanitized_name.encode('ASCII')) lib.append(structure) From 3d89cd42a04169be02e0f0b97bc3a553aacc243b Mon Sep 17 00:00:00 2001 From: jan Date: Tue, 29 Aug 2017 16:55:06 -0700 Subject: [PATCH 030/124] further fixes to ellipse bounding box --- masque/shapes/arc.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 679de9a..904a8e9 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -206,6 +206,18 @@ class Arc(Shape): return [poly] def get_bounds(self) -> numpy.ndarray: + ''' + Equation for rotated ellipse is + x = x0 + a * cos(t) * cos(rot) - b * sin(t) * sin(phi) + y = y0 + a * cos(t) * sin(rot) + b * sin(t) * cos(rot) + where t is our parameter. + + Differentiating and solving for 0 slope wrt. t, we find + tan(t) = -+ b/a cot(phi) + where -+ is for x, y cases, so that's where the extrema are. + + If the extrema are innaccessible due to arc constraints, check the arc endpoints instead. + ''' mins = [] maxs = [] for sgn in (+1, -1): @@ -235,13 +247,18 @@ class Arc(Shape): ynt = (ypt - pi) % (2 * pi) + a0_offset # Points along coordinate axes - xr = numpy.sqrt((rx * cos_r) ** 2 + (ry * sin_r) ** 2) - yr = numpy.sqrt((rx * sin_r) ** 2 + (ry * cos_r) ** 2) + xs = rx * sin_r + yc = ry * cos_r + sin_ax = yc / (yc * yc + xs * xs) + cos_ax = xs / (yc * yc + xs * xs) + xr = rx * cos_r * cos_ax - ry * sin_r * sin_ax + yr = ry * sin_r * cos_ax + ry * cos_r * sin_ax # Arc endpoints xn, xp = sorted(rx * cos_r * cos_a - ry * sin_r * sin_a) - yn, yp = sorted(rx * sin_r * cos_a - ry * cos_r * sin_a) + yn, yp = sorted(rx * sin_r * cos_a + ry * cos_r * sin_a) + # If if a0 < xpt < a1: xp = xr From 3b3ee216c2977bb38b05df09b46675d06f396b07 Mon Sep 17 00:00:00 2001 From: jan Date: Tue, 29 Aug 2017 16:55:58 -0700 Subject: [PATCH 031/124] add missing import --- masque/file/gdsii.py | 1 + 1 file changed, 1 insertion(+) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 3d4ab48..8425629 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -7,6 +7,7 @@ import gdsii.structure import gdsii.elements from typing import List, Any, Dict +import re import numpy from .utils import mangle_name, make_dose_table From 583dd7b01851d7fbf4d625e6d26fcacd3ee3cc5c Mon Sep 17 00:00:00 2001 From: jan Date: Tue, 5 Sep 2017 11:00:36 -0700 Subject: [PATCH 032/124] allow caller to specify gdsii logical unit --- masque/file/gdsii.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 8425629..e859e1f 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -21,7 +21,8 @@ __author__ = 'Jan Petykiewicz' def write(pattern: Pattern, filename: str, - meters_per_unit: float): + meters_per_unit: float, + logical_units_per_unit: float = 1): """ Write a Pattern to a GDSII file, by first calling .polygonize() on it to change the shapes into polygons, and then writing patterns as GDSII @@ -44,12 +45,16 @@ def write(pattern: Pattern, :param pattern: A Pattern to write to file. Modified by this function. :param filename: Filename to write to. - :param meters_per_unit: Written into the GDSII file, meters per length unit. + :param meters_per_unit: Written into the GDSII file, meters per (database) length unit. + All distances are assumed to be an integer multiple of this unit, and are stored as such. + :param logical_units_per_unit: Written into the GDSII file. Allows the GDSII to specify a + "logical" unit which is different from the "database" unit, for display purposes. + Default 1. """ # Create library lib = gdsii.library.Library(version=600, name='masque-write_dose2dtype'.encode('ASCII'), - logical_unit=1, + logical_unit=logical_units_per_unit, physical_unit=meters_per_unit) # Get a dict of id(pattern) -> pattern @@ -95,7 +100,9 @@ def write(pattern: Pattern, def write_dose2dtype(pattern: Pattern, filename: str, - meters_per_unit: float) -> List[float]: + meters_per_unit: float, + logical_units_per_unit: float = 1 + ) -> List[float]: """ Write a Pattern to a GDSII file, by first calling .polygonize() on it to change the shapes into polygons, and then writing patterns as GDSII @@ -120,14 +127,18 @@ def write_dose2dtype(pattern: Pattern, :param pattern: A Pattern to write to file. Modified by this function. :param filename: Filename to write to. - :param meters_per_unit: Written into the GDSII file, meters per length unit. + :param meters_per_unit: Written into the GDSII file, meters per (database) length unit. + All distances are assumed to be an integer multiple of this unit, and are stored as such. + :param logical_units_per_unit: Written into the GDSII file. Allows the GDSII to specify a + "logical" unit which is different from the "database" unit, for display purposes. + Default 1. :returns: A list of doses, providing a mapping between datatype (int, list index) and dose (float, list entry). """ # Create library lib = gdsii.library.Library(version=600, name='masque-write_dose2dtype'.encode('ASCII'), - logical_unit=1, + logical_unit=logical_units_per_unit, physical_unit=meters_per_unit) # Get a dict of id(pattern) -> pattern From ccfd0f7f4f89cd693eaa6cc2541e7da58668033e Mon Sep 17 00:00:00 2001 From: jan Date: Tue, 5 Sep 2017 11:00:50 -0700 Subject: [PATCH 033/124] remove whitespace --- masque/shapes/arc.py | 1 - 1 file changed, 1 deletion(-) diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 904a8e9..b4c233d 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -303,7 +303,6 @@ class Arc(Shape): start_angle -= pi rotation += pi - angles = (start_angle, start_angle + delta_angle) rotation %= 2 * pi width = self.width From 934bfcd74e9276df5c32bbd3f2f56194b336e80b Mon Sep 17 00:00:00 2001 From: jan Date: Wed, 6 Sep 2017 01:14:27 -0700 Subject: [PATCH 034/124] Clean up type info --- masque/pattern.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/masque/pattern.py b/masque/pattern.py index 1883f5d..9b9c888 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -31,10 +31,9 @@ class Pattern: may reference the same Pattern object. :var name: An identifier for this object. Not necessarily unique. """ - - shapes = List[Shape] - subpatterns = List[SubPattern] - name = str + shapes = None # type: List[Shape] + subpatterns = None # type: List[SubPattern] + name = None # type: str def __init__(self, shapes: List[Shape]=(), From 3d525660163b98279bb770c46e78a24b2793b436 Mon Sep 17 00:00:00 2001 From: jan Date: Wed, 6 Sep 2017 01:14:46 -0700 Subject: [PATCH 035/124] Improve error handling --- masque/shapes/polygon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 761c1fa..558c788 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -31,7 +31,7 @@ class Polygon(Shape): @vertices.setter def vertices(self, val: numpy.ndarray): val = numpy.array(val, dtype=float) - if val.shape[1] != 2: + if len(val.shape) < 2 or val.shape[1] != 2: raise PatternError('Vertices must be an Nx2 array') if val.shape[0] < 3: raise PatternError('Must have at least 3 vertices (Nx2, N>3)') From ffbaf8f4c48e50a61a8b758c2173868f330697aa Mon Sep 17 00:00:00 2001 From: jan Date: Wed, 6 Sep 2017 01:16:24 -0700 Subject: [PATCH 036/124] Add manhattanization functionality --- masque/pattern.py | 19 +++++++++ masque/shapes/shape.py | 91 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/masque/pattern.py b/masque/pattern.py index 9b9c888..7af9654 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -118,6 +118,25 @@ class Pattern: 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, diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index bb4687e..b12fdcb 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -176,3 +176,94 @@ class Shape(metaclass=ABCMeta): self.translate(+pivot) return self + def manhattanize(self, grid_x: numpy.ndarray, grid_y: numpy.ndarray) -> List['Polygon']: + """ + Returns a list of polygons with grid-aligned ("Manhattan") edges approximating the shape. + + This function works by + 1) Converting the shape to polygons using .to_polygons() + 2) Accurately rasterizing each polygon on a grid, + where the edges of each grid cell correspond to the allowed coordinates + 3) Thresholding the (anti-aliased) rasterized image + 4) Finding the contours which outline the filled areas in the thresholded image + This process results in a fairly accurate Manhattan representation of the shape. Possible + caveats include: + a) If high accuracy is important, perform any polygonization and clipping operations + prior to calling this function. This allows you to specify any arguments you may + need for .to_polygons(), and also avoids calling .manhattanize() multiple times for + the same grid location (which causes inaccuracies in the final representation). + b) If the shape is very large or the grid very fine, memory requirements can be reduced + by breaking the shape apart into multiple, smaller shapes. + c) Inaccuracies in edge shape can result from Manhattanization of edges which are + equidistant from allowed edge location. + + Implementation notes: + i) Rasterization is performed using float_raster, giving a high-precision anti-aliased + rasterized image. + ii) To find the exact polygon edges, the thresholded rasterized image is supersampled + prior to calling skimage.measure.find_contours(), which uses marching squares + to find the contours. This is done because find_contours() performs interpolation, + which has to be undone in order to regain the axis-aligned contours. A targetted + rewrite of find_contours() for this specific application, or use of a different + boundary tracing method could remove this requirement, but for now this seems to + be the most performant approach. + + :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: List of Polygon objects with grid-aligned edges. + """ + from . import Polygon + import skimage.measure + import float_raster + + grid_x = numpy.unique(grid_x) + grid_y = numpy.unique(grid_y) + + polygon_contours = [] + for polygon in self.to_polygons(): + mins, maxs = polygon.get_bounds() + keep_x = numpy.logical_and(grid_x > mins[0], grid_x < maxs[0]) + keep_y = numpy.logical_and(grid_y > mins[1], grid_y < maxs[1]) + for k in (keep_x, keep_y): + for s in (1, 2): + k[s:] += k[:-s] + k[:-s] += k[s:] + k = k > 0 + + gx = grid_x[keep_x] + gy = grid_y[keep_y] + + if len(gx) == 0 or len(gy) == 0: + continue + + offset = (numpy.where(keep_x)[0][0], + numpy.where(keep_y)[0][0]) + + rastered = float_raster.raster((polygon.vertices + polygon.offset).T, gx, gy) + binary_rastered = (rastered >= 0.5) + supersampled = binary_rastered.repeat(2, axis=0).repeat(2, axis=1) + + from matplotlib import pyplot + pyplot.pcolormesh(binary_rastered) + pyplot.colorbar() + pyplot.show() + + contours = skimage.measure.find_contours(supersampled, 0.5) + polygon_contours.append((offset, contours)) + + manhattan_polygons = [] + for offset_i, contours in polygon_contours: + for contour in contours: + # /2 deals with supersampling + # +.5 deals with the fact that our 0-edge becomes -.5 in the super-sampled contour output + snapped_contour = numpy.round((contour + .5) / 2).astype(int) + vertices = numpy.hstack((grid_x[snapped_contour[:, None, 0] + offset_i[0]], + grid_y[snapped_contour[:, None, 1] + offset_i[1]])) + + manhattan_polygons.append(Polygon( + vertices=vertices, + layer=self.layer, + dose=self.dose)) + + return manhattan_polygons + From 44661989242c1d2087185e90a7d8913c25a66bd4 Mon Sep 17 00:00:00 2001 From: jan Date: Wed, 6 Sep 2017 01:16:44 -0700 Subject: [PATCH 037/124] Add cut() function for polygon --- masque/shapes/polygon.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 558c788..03aa5f2 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -172,3 +172,38 @@ class Polygon(Shape): return (type(self), reordered_vertices.data.tobytes(), self.layer), \ (offset, scale/norm_value, rotation, self.dose), \ lambda: Polygon(reordered_vertices*norm_value, layer=self.layer) + + def cut(self, + cut_xs: numpy.ndarray = None, + cut_ys: numpy.ndarray = None + ) -> List['Polygon']: + import float_raster + xy = (self.offset + self.vertices).T + + if cut_xs is None: + cut_xs = tuple() + if cut_ys is None: + cut_ys = tuple() + + mins, maxs = self.get_bounds() + dx, dy = maxs - mins + + cx = numpy.hstack((min(tuple(cut_xs) + (mins[0],)) - dx, cut_xs, max((maxs[0],) + tuple(cut_xs)) + dx)) + cy = numpy.hstack((min(tuple(cut_ys) + (mins[1],)) - dy, cut_ys, max((maxs[1],) + tuple(cut_ys)) + dy)) + + shape_with_extra_verts = float_raster.create_vertices(xy, cx, cy) + + polygons = [] + for cx_min, cx_max in zip(cx, cx[1:]): + for cy_min, cy_max in zip(cy, cy[1:]): + clipped_verts = float_raster.clip_vertices_to_window( + copy.deepcopy(shape_with_extra_verts), + cx_min, cx_max, cy_min, cy_max) + final_verts = numpy.hstack(( + numpy.real(clipped_verts)[:, None], + numpy.imag(clipped_verts)[:, None])) + polygons.append(Polygon( + vertices=final_verts, + layer=self.layer, + dose=self.dose)) + return polygons From cea172e7f275f1aa2a547e2319cf82c8998010ee Mon Sep 17 00:00:00 2001 From: jan Date: Wed, 6 Sep 2017 21:03:23 -0700 Subject: [PATCH 038/124] Make cut generate clean polygons --- masque/shapes/polygon.py | 41 +++++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 03aa5f2..419809d 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -191,19 +191,46 @@ class Polygon(Shape): cx = numpy.hstack((min(tuple(cut_xs) + (mins[0],)) - dx, cut_xs, max((maxs[0],) + tuple(cut_xs)) + dx)) cy = numpy.hstack((min(tuple(cut_ys) + (mins[1],)) - dy, cut_ys, max((maxs[1],) + tuple(cut_ys)) + dy)) - shape_with_extra_verts = float_raster.create_vertices(xy, cx, cy) + all_verts = float_raster.create_vertices(xy, cx, cy) polygons = [] for cx_min, cx_max in zip(cx, cx[1:]): for cy_min, cy_max in zip(cy, cy[1:]): - clipped_verts = float_raster.clip_vertices_to_window( - copy.deepcopy(shape_with_extra_verts), - cx_min, cx_max, cy_min, cy_max) - final_verts = numpy.hstack(( - numpy.real(clipped_verts)[:, None], - numpy.imag(clipped_verts)[:, None])) + clipped_verts = (numpy.real(all_verts).clip(cx_min, cx_max) + 1j * + numpy.imag(all_verts).clip(cy_min, cy_max)) + + cleaned_verts = _clean_complex_vertices(clipped_verts) + if len(cleaned_verts) == 0: + continue + + final_verts = numpy.hstack((numpy.real(clipped_verts)[:, None], + numpy.imag(clipped_verts)[:, None])) polygons.append(Polygon( vertices=final_verts, layer=self.layer, dose=self.dose)) return polygons + + +def _clean_complex_vertices(vertices: numpy.ndarray) -> numpy.ndarray: + eps = numpy.finfo(vertices.dtype).eps + + def cleanup(vertices): + # Remove duplicate points + dv = v - numpy.roll(v, 1) + v = v[numpy.abs(dv) > eps] + + # Remove colinear points + dv = v - numpy.roll(v, 1) + m = numpy.angle(dv) % pi + diff_m = numpy.abs(m - numpy.roll(m, -1)) + return v[diff_m > eps] + + n = len(vertices) + cleaned = cleanup(vertices) + while n != len(cleaned): + n = len(cleaned) + cleaned = cleanup(cleaned) + + return cleaned + From 723944018ef1247ae642ba11e835e38f94ea0b17 Mon Sep 17 00:00:00 2001 From: jan Date: Wed, 6 Sep 2017 21:03:39 -0700 Subject: [PATCH 039/124] add documentation to Polygon.cut() --- masque/shapes/polygon.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 419809d..b341535 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -177,6 +177,14 @@ class Polygon(Shape): cut_xs: numpy.ndarray = None, cut_ys: numpy.ndarray = None ) -> List['Polygon']: + """ + Decomposes the polygon into a list of constituents by cutting along the + specified x and/or y coordinates. + + :param cut_xs: list of x-coordinates to cut along (e.g., [1, 1.4, 6]) + :param cut_ys: list of y-coordinates to cut along (e.g., [1, 3, 5.4]) + :return: List of Polygon objects + """ import float_raster xy = (self.offset + self.vertices).T From 10cd0778b8d19730c7e5ff0bd22834efb09e60a3 Mon Sep 17 00:00:00 2001 From: jan Date: Wed, 6 Sep 2017 21:04:44 -0700 Subject: [PATCH 040/124] Add copy() method to Shape --- masque/shapes/shape.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index b12fdcb..2112cce 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -1,5 +1,6 @@ from typing import List, Tuple, Callable from abc import ABCMeta, abstractmethod +import copy import numpy from .. import PatternError @@ -151,6 +152,14 @@ class Shape(metaclass=ABCMeta): self._dose = val # ---- Non-abstract methods + def copy(self) -> 'Shape': + """ + Returns a deep copy of the shape. + + :return: Deep copy of self + """ + return copy.deepcopy(self) + def translate(self, offset: vector2) -> 'Shape': """ Translate the shape by the given offset From a817bf664203a63f6e1e800f0a5251bf82bd1f2f Mon Sep 17 00:00:00 2001 From: jan Date: Wed, 6 Sep 2017 21:04:57 -0700 Subject: [PATCH 041/124] Remove debug code --- masque/shapes/shape.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index 2112cce..02124d6 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -252,11 +252,6 @@ class Shape(metaclass=ABCMeta): binary_rastered = (rastered >= 0.5) supersampled = binary_rastered.repeat(2, axis=0).repeat(2, axis=1) - from matplotlib import pyplot - pyplot.pcolormesh(binary_rastered) - pyplot.colorbar() - pyplot.show() - contours = skimage.measure.find_contours(supersampled, 0.5) polygon_contours.append((offset, contours)) From 01395134ee426b508dd39f2bbf104861c4ef6ed8 Mon Sep 17 00:00:00 2001 From: jan Date: Thu, 7 Sep 2017 21:59:44 -0700 Subject: [PATCH 042/124] Also clean vertices before cutting --- masque/shapes/polygon.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index b341535..7de2183 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -186,7 +186,10 @@ class Polygon(Shape): :return: List of Polygon objects """ import float_raster - xy = (self.offset + self.vertices).T + xy_complex = self.vertices[:, 1] + 1j * self.vertices[:, 2] + xy_cleaned = _clean_complex_vertices(xy_complex) + xy = numpy.vstack((numpy.real(xy_cleaned)[None, :], + numpy.imag(xy_cleaned)[None, :])) if cut_xs is None: cut_xs = tuple() From f4b8f513d450b2bc44c07e64d1bed63b32c4b4d9 Mon Sep 17 00:00:00 2001 From: jan Date: Fri, 8 Sep 2017 10:30:56 -0700 Subject: [PATCH 043/124] fix typos --- masque/shapes/polygon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 7de2183..74a0530 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -186,7 +186,7 @@ class Polygon(Shape): :return: List of Polygon objects """ import float_raster - xy_complex = self.vertices[:, 1] + 1j * self.vertices[:, 2] + xy_complex = self.vertices[:, 0] + 1j * self.vertices[:, 1] xy_cleaned = _clean_complex_vertices(xy_complex) xy = numpy.vstack((numpy.real(xy_cleaned)[None, :], numpy.imag(xy_cleaned)[None, :])) @@ -226,7 +226,7 @@ class Polygon(Shape): def _clean_complex_vertices(vertices: numpy.ndarray) -> numpy.ndarray: eps = numpy.finfo(vertices.dtype).eps - def cleanup(vertices): + def cleanup(v): # Remove duplicate points dv = v - numpy.roll(v, 1) v = v[numpy.abs(dv) > eps] From 83d163a1020796756bd83099177ed4adf21b88f8 Mon Sep 17 00:00:00 2001 From: jan Date: Fri, 8 Sep 2017 14:26:27 -0700 Subject: [PATCH 044/124] fix typo --- masque/shapes/polygon.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 74a0530..12760a7 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -214,8 +214,8 @@ class Polygon(Shape): if len(cleaned_verts) == 0: continue - final_verts = numpy.hstack((numpy.real(clipped_verts)[:, None], - numpy.imag(clipped_verts)[:, None])) + final_verts = numpy.hstack((numpy.real(cleaned_verts)[:, None], + numpy.imag(cleaned_verts)[:, None])) polygons.append(Polygon( vertices=final_verts, layer=self.layer, @@ -234,8 +234,8 @@ def _clean_complex_vertices(vertices: numpy.ndarray) -> numpy.ndarray: # Remove colinear points dv = v - numpy.roll(v, 1) m = numpy.angle(dv) % pi - diff_m = numpy.abs(m - numpy.roll(m, -1)) - return v[diff_m > eps] + diff_m = m - numpy.roll(m, -1) + return v[numpy.abs(diff_m) > eps] n = len(vertices) cleaned = cleanup(vertices) From b3f99ee1231424f1bd6d0bd41337f6accb15e0a5 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 15 Oct 2017 11:42:12 -0700 Subject: [PATCH 045/124] abs when thresholding raster for manhattanization --- masque/shapes/shape.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index 02124d6..b795ba2 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -249,7 +249,7 @@ class Shape(metaclass=ABCMeta): numpy.where(keep_y)[0][0]) rastered = float_raster.raster((polygon.vertices + polygon.offset).T, gx, gy) - binary_rastered = (rastered >= 0.5) + binary_rastered = (numpy.abs(rastered) >= 0.5) supersampled = binary_rastered.repeat(2, axis=0).repeat(2, axis=1) contours = skimage.measure.find_contours(supersampled, 0.5) From 87c2ef594899d9c1502140e3a0b690593b0a9cf1 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 15 Oct 2017 11:39:37 -0700 Subject: [PATCH 046/124] use pyclipper for polygon cutting --- masque/shapes/polygon.py | 84 ++++++++++++++-------------------------- masque/subpattern.py | 1 - setup.py | 3 +- 3 files changed, 31 insertions(+), 57 deletions(-) diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 12760a7..60eba42 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -185,63 +185,37 @@ class Polygon(Shape): :param cut_ys: list of y-coordinates to cut along (e.g., [1, 3, 5.4]) :return: List of Polygon objects """ - import float_raster - xy_complex = self.vertices[:, 0] + 1j * self.vertices[:, 1] - xy_cleaned = _clean_complex_vertices(xy_complex) - xy = numpy.vstack((numpy.real(xy_cleaned)[None, :], - numpy.imag(xy_cleaned)[None, :])) + import pyclipper + from pyclipper import scale_to_clipper, scale_from_clipper - if cut_xs is None: - cut_xs = tuple() - if cut_ys is None: - cut_ys = tuple() + min_x, min_y = numpy.min(self.vertices, axis=0) + max_x, max_y = numpy.max(self.vertices, axis=0) + range_x = max_x - min_x + range_y = max_y - min_y - mins, maxs = self.get_bounds() - dx, dy = maxs - mins + edge_xs = (min_x - range_x - 1,) + tuple(cut_xs) + (max_x + range_x + 1,) + edge_ys = (min_y - range_y - 1,) + tuple(cut_ys) + (max_y + range_y + 1,) - cx = numpy.hstack((min(tuple(cut_xs) + (mins[0],)) - dx, cut_xs, max((maxs[0],) + tuple(cut_xs)) + dx)) - cy = numpy.hstack((min(tuple(cut_ys) + (mins[1],)) - dy, cut_ys, max((maxs[1],) + tuple(cut_ys)) + dy)) + clipped_shapes = [] + for i in range(2): + for j in range(2): + clipper = pyclipper.Pyclipper() + clipper.AddPath(scale_to_clipper(self.vertices), pyclipper.PT_SUBJECT, True) - all_verts = float_raster.create_vertices(xy, cx, cy) - - polygons = [] - for cx_min, cx_max in zip(cx, cx[1:]): - for cy_min, cy_max in zip(cy, cy[1:]): - clipped_verts = (numpy.real(all_verts).clip(cx_min, cx_max) + 1j * - numpy.imag(all_verts).clip(cy_min, cy_max)) - - cleaned_verts = _clean_complex_vertices(clipped_verts) - if len(cleaned_verts) == 0: - continue - - final_verts = numpy.hstack((numpy.real(cleaned_verts)[:, None], - numpy.imag(cleaned_verts)[:, None])) - polygons.append(Polygon( - vertices=final_verts, - layer=self.layer, - dose=self.dose)) - return polygons - - -def _clean_complex_vertices(vertices: numpy.ndarray) -> numpy.ndarray: - eps = numpy.finfo(vertices.dtype).eps - - def cleanup(v): - # Remove duplicate points - dv = v - numpy.roll(v, 1) - v = v[numpy.abs(dv) > eps] - - # Remove colinear points - dv = v - numpy.roll(v, 1) - m = numpy.angle(dv) % pi - diff_m = m - numpy.roll(m, -1) - return v[numpy.abs(diff_m) > eps] - - n = len(vertices) - cleaned = cleanup(vertices) - while n != len(cleaned): - n = len(cleaned) - cleaned = cleanup(cleaned) - - return cleaned + for start_x, stop_x in zip(edge_xs[i::2], edge_xs[(i+1)::2]): + for start_y, stop_y in zip(edge_ys[j::2], edge_ys[(j+1)::2]): + clipper.AddPath(scale_to_clipper(( + (start_x, start_y), + (start_x, stop_y), + (stop_x, stop_y), + (stop_x, start_y), + )), pyclipper.PT_CLIP, True) + clipped_parts = scale_from_clipper(clipper.Execute(pyclipper.CT_INTERSECTION, + pyclipper.PFT_EVENODD, + pyclipper.PFT_EVENODD)) + for part in clipped_parts: + poly = self.copy() + poly.vertices = part + clipped_shapes.append(poly) + return clipped_shapes diff --git a/masque/subpattern.py b/masque/subpattern.py index c45a4d9..6242468 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -156,4 +156,3 @@ class SubPattern: """ self.scale *= c return self - diff --git a/setup.py b/setup.py index c24baf3..3d68302 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,8 @@ setup(name='masque', 'visualization': ['matplotlib'], 'gdsii': ['python-gdsii'], 'svg': ['svgwrite'], - 'text': ['freetype-py', 'matplotlib'] + 'text': ['freetype-py', 'matplotlib'], + 'clipping': ['pyclipper'], }, ) From 9308454ad4812715b7fe526849950aa8dea1f79b Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 15 Oct 2017 13:48:55 -0700 Subject: [PATCH 047/124] allow cutting any shape, always require pyclipper --- README.md | 7 +++--- masque/__init__.py | 1 + masque/shapes/polygon.py | 52 +++++++++------------------------------- masque/shapes/shape.py | 49 +++++++++++++++++++++++++++++++++++++ setup.py | 4 ++-- 5 files changed, 67 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 89c7b44..2281ef7 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,9 @@ E-beam doses, and the ability to output to multiple formats. ## Installation Requirements: -* python 3 (written and tested with 3.5) +* python >= 3.5 (written and tested with 3.6) * numpy +* pyclipper * matplotlib (optional, used for visualization functions and text) * python-gdsii (optional, used for gdsii i/o) * svgwrite (optional, used for svg output) @@ -27,5 +28,5 @@ pip install git+https://mpxd.net/gogs/jan/masque.git@release * Polygon de-embedding ### Maybe * Construct from bitmap -* Boolean operations on polygons (eg. using pyclipper) -* Output to OASIS +* Boolean operations on polygons (using pyclipper) +* Output to OASIS (using fatamorgana) diff --git a/masque/__init__.py b/masque/__init__.py index 25652dc..53eab35 100644 --- a/masque/__init__.py +++ b/masque/__init__.py @@ -18,6 +18,7 @@ Dependencies: - numpy + - pyclipper - matplotlib [Pattern.visualize(...)] - python-gdsii [masque.file.gdsii] - svgwrite [masque.file.svgwrite] diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 60eba42..34f6285 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -2,6 +2,8 @@ from typing import List import copy import numpy from numpy import pi +import pyclipper +from pyclipper import scale_to_clipper, scale_from_clipper from . import Shape, normalized_shape_tuple from .. import PatternError @@ -173,49 +175,17 @@ class Polygon(Shape): (offset, scale/norm_value, rotation, self.dose), \ lambda: Polygon(reordered_vertices*norm_value, layer=self.layer) - def cut(self, - cut_xs: numpy.ndarray = None, - cut_ys: numpy.ndarray = None - ) -> List['Polygon']: + def clean_vertices(self) -> 'Polygon': """ - Decomposes the polygon into a list of constituents by cutting along the - specified x and/or y coordinates. + Removes duplicate, co-linear and otherwise redundant vertices. - :param cut_xs: list of x-coordinates to cut along (e.g., [1, 1.4, 6]) - :param cut_ys: list of y-coordinates to cut along (e.g., [1, 3, 5.4]) - :return: List of Polygon objects + :returns: self """ - import pyclipper - from pyclipper import scale_to_clipper, scale_from_clipper + self.vertices = scale_from_clipper( + pyclipper.CleanPolygon( + scale_to_clipper( + self.vertices + ))) + return self - min_x, min_y = numpy.min(self.vertices, axis=0) - max_x, max_y = numpy.max(self.vertices, axis=0) - range_x = max_x - min_x - range_y = max_y - min_y - edge_xs = (min_x - range_x - 1,) + tuple(cut_xs) + (max_x + range_x + 1,) - edge_ys = (min_y - range_y - 1,) + tuple(cut_ys) + (max_y + range_y + 1,) - - clipped_shapes = [] - for i in range(2): - for j in range(2): - clipper = pyclipper.Pyclipper() - clipper.AddPath(scale_to_clipper(self.vertices), pyclipper.PT_SUBJECT, True) - - for start_x, stop_x in zip(edge_xs[i::2], edge_xs[(i+1)::2]): - for start_y, stop_y in zip(edge_ys[j::2], edge_ys[(j+1)::2]): - clipper.AddPath(scale_to_clipper(( - (start_x, start_y), - (start_x, stop_y), - (stop_x, stop_y), - (stop_x, start_y), - )), pyclipper.PT_CLIP, True) - - clipped_parts = scale_from_clipper(clipper.Execute(pyclipper.CT_INTERSECTION, - pyclipper.PFT_EVENODD, - pyclipper.PFT_EVENODD)) - for part in clipped_parts: - poly = self.copy() - poly.vertices = part - clipped_shapes.append(poly) - return clipped_shapes diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index b795ba2..1036068 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -3,6 +3,9 @@ from abc import ABCMeta, abstractmethod import copy import numpy +import pyclipper +from pyclipper import scale_to_clipper, scale_from_clipper + from .. import PatternError from ..utils import is_scalar, rotation_matrix_2d, vector2 @@ -271,3 +274,49 @@ class Shape(metaclass=ABCMeta): return manhattan_polygons + def cut(self, + cut_xs: numpy.ndarray = None, + cut_ys: numpy.ndarray = None + ) -> List['Polygon']: + """ + Decomposes the shape into a list of constituent polygons by polygonizing and + then cutting along the specified x and/or y coordinates. + + :param cut_xs: list of x-coordinates to cut along (e.g., [1, 1.4, 6]) + :param cut_ys: list of y-coordinates to cut along (e.g., [1, 3, 5.4]) + :return: List of Polygon objects + """ + from . import Polygon + + clipped_shapes = [] + for polygon in self.to_polygons(): + min_x, min_y = numpy.min(polygon.vertices, axis=0) + max_x, max_y = numpy.max(polygon.vertices, axis=0) + range_x = max_x - min_x + range_y = max_y - min_y + + edge_xs = (min_x - range_x - 1,) + tuple(cut_xs) + (max_x + range_x + 1,) + edge_ys = (min_y - range_y - 1,) + tuple(cut_ys) + (max_y + range_y + 1,) + + for i in range(2): + for j in range(2): + clipper = pyclipper.Pyclipper() + clipper.AddPath(scale_to_clipper(polygon.vertices), pyclipper.PT_SUBJECT, True) + + for start_x, stop_x in zip(edge_xs[i::2], edge_xs[(i+1)::2]): + for start_y, stop_y in zip(edge_ys[j::2], edge_ys[(j+1)::2]): + clipper.AddPath(scale_to_clipper(( + (start_x, start_y), + (start_x, stop_y), + (stop_x, stop_y), + (stop_x, start_y), + )), pyclipper.PT_CLIP, True) + + clipped_parts = scale_from_clipper(clipper.Execute(pyclipper.CT_INTERSECTION, + pyclipper.PFT_EVENODD, + pyclipper.PFT_EVENODD)) + for part in clipped_parts: + poly = polygon.copy() + poly.vertices = part + clipped_shapes.append(poly) + return clipped_shapes diff --git a/setup.py b/setup.py index 3d68302..af04fee 100644 --- a/setup.py +++ b/setup.py @@ -10,14 +10,14 @@ setup(name='masque', url='https://mpxd.net/gogs/jan/masque', packages=find_packages(), install_requires=[ - 'numpy' + 'numpy', + 'pyclipper', ], extras_require={ 'visualization': ['matplotlib'], 'gdsii': ['python-gdsii'], 'svg': ['svgwrite'], 'text': ['freetype-py', 'matplotlib'], - 'clipping': ['pyclipper'], }, ) From 7396e83f35816b1f50a620e98f8f1f57522c825f Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 17 Oct 2017 12:56:06 -0700 Subject: [PATCH 048/124] bump version number --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index af04fee..4767f5d 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import setup, find_packages setup(name='masque', - version='0.2', + version='0.3', description='Lithography mask library', author='Jan Petykiewicz', author_email='anewusername@gmail.com', From 6aa1787ba03aa242494841b3d8fe4409ce1b1b24 Mon Sep 17 00:00:00 2001 From: jan Date: Sat, 4 Nov 2017 12:12:05 -0700 Subject: [PATCH 049/124] add comment specifying what gdsii lib is used --- masque/file/gdsii.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index e859e1f..223c7d7 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -1,7 +1,7 @@ """ GDSII file format readers and writers """ - +# python-gdsii import gdsii.library import gdsii.structure import gdsii.elements From c451e93df09d4d4e9dba9ae4907fb4d034253638 Mon Sep 17 00:00:00 2001 From: jan Date: Sat, 4 Nov 2017 12:15:35 -0700 Subject: [PATCH 050/124] Add option to check for invalid polygons when reading gds --- masque/file/gdsii.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 223c7d7..649a15d 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -205,7 +205,10 @@ def read_dtype2dose(filename: str) -> (List[Pattern], Dict[str, Any]): return read(filename, use_dtype_as_dose=True) -def read(filename: str, use_dtype_as_dose=False) -> (List[Pattern], Dict[str, Any]): +def read(filename: str, + use_dtype_as_dose: bool = False, + clean_vertices: bool = True, + ) -> (List[Pattern], Dict[str, Any]): """ Read a gdsii file and translate it into a list of Pattern objects. GDSII structures are translated into Pattern objects; boundaries are translated into polygons, and srefs and arefs @@ -214,6 +217,10 @@ def read(filename: str, use_dtype_as_dose=False) -> (List[Pattern], Dict[str, An :param filename: Filename specifying a GDSII file to read from. :param use_dtype_as_dose: If false, set each polygon's layer to (gds_layer, gds_datatype). If true, set the layer to gds_layer and the dose to gds_datatype. + Default False. + :param clean_vertices: If true, remove any redundant vertices when loading polygons. + The cleaning process removes any polygons with zero area or <3 vertices. + Default True. :return: Tuple: (List of Patterns generated GDSII structures, Dict of GDSII library info) """ @@ -260,6 +267,12 @@ def read(filename: str, use_dtype_as_dose=False) -> (List[Pattern], Dict[str, An else: shape = Polygon(vertices=element.xy[:-1], layer=(element.layer, element.data_type)) + if do_clean: + try: + shape.clean_vertices() + except PatternError: + continue + pat.shapes.append(shape) elif isinstance(element, gdsii.elements.SRef): From b7b0da7432c6cef089a0e04ec1970d91440f8ae4 Mon Sep 17 00:00:00 2001 From: jan Date: Sat, 4 Nov 2017 12:18:42 -0700 Subject: [PATCH 051/124] Allow writing a list of patterns to gds (multiple topcells) --- masque/file/gdsii.py | 48 ++++++++++++++++++++++++++------------------ masque/file/utils.py | 21 ++++++++++--------- 2 files changed, 40 insertions(+), 29 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 649a15d..8d120c7 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -19,14 +19,14 @@ from ..utils import rotation_matrix_2d, get_bit, vector2 __author__ = 'Jan Petykiewicz' -def write(pattern: Pattern, +def write(patterns: Pattern or List[Pattern], filename: str, meters_per_unit: float, logical_units_per_unit: float = 1): """ - Write a Pattern to a GDSII file, by first calling .polygonize() on it - to change the shapes into polygons, and then writing patterns as GDSII - structures, polygons as boundary elements, and subpatterns as structure + Write a Pattern or list of patterns to a GDSII file, by first calling + .polygonize() to change the shapes into polygons, and then writing patterns + as GDSII structures, polygons as boundary elements, and subpatterns as structure references (sref). For each shape, @@ -43,7 +43,7 @@ def write(pattern: Pattern, If you want pattern polygonized with non-default arguments, just call pattern.polygonize() prior to calling this function. - :param pattern: A Pattern to write to file. Modified by this function. + :param patterns: A Pattern or list of patterns to write to file. Modified by this function. :param filename: Filename to write to. :param meters_per_unit: Written into the GDSII file, meters per (database) length unit. All distances are assumed to be an integer multiple of this unit, and are stored as such. @@ -57,12 +57,17 @@ def write(pattern: Pattern, logical_unit=logical_units_per_unit, physical_unit=meters_per_unit) + if isinstance(patterns, Pattern): + patterns = [patterns] + # Get a dict of id(pattern) -> pattern - patterns_by_id = {**(pattern.referenced_patterns_by_id()), id(pattern): pattern} + patterns_by_id = {id(pattern): pattern for pattern in patterns} + for pattern in patterns: + patterns_by_id.update(pattern.referenced_patterns_by_id()) # Now create a structure for each pattern, and add in any Boundary and SREF elements for pat in patterns_by_id.values(): - sanitized_name = re.compile('[^A-Za-z0-9_\?\$]').sub('_', pattern.name) + sanitized_name = re.compile('[^A-Za-z0-9_\?\$]').sub('_', pat.name) structure = gdsii.structure.Structure(name=sanitized_name.encode('ASCII')) lib.append(structure) @@ -86,7 +91,7 @@ def write(pattern: Pattern, # Add an SREF for each subpattern entry # strans must be set for angle and mag to take effect for subpat in pat.subpatterns: - sanitized_name = re.compile('[^A-Za-z0-9_\?\$]').sub('_', subpat.name) + sanitized_name = re.compile('[^A-Za-z0-9_\?\$]').sub('_', subpat.pattern.name) sref = gdsii.elements.SRef(struct_name=sanitized_name.encode('ASCII'), xy=numpy.round([subpat.offset]).astype(int)) sref.strans = 0 @@ -98,15 +103,15 @@ def write(pattern: Pattern, lib.save(stream) -def write_dose2dtype(pattern: Pattern, +def write_dose2dtype(patterns: Pattern or List[Pattern], filename: str, meters_per_unit: float, logical_units_per_unit: float = 1 ) -> List[float]: """ - Write a Pattern to a GDSII file, by first calling .polygonize() on it - to change the shapes into polygons, and then writing patterns as GDSII - structures, polygons as boundary elements, and subpatterns as structure + Write a Pattern or list of patterns to a GDSII file, by first calling + .polygonize() to change the shapes into polygons, and then writing patterns + as GDSII structures, polygons as boundary elements, and subpatterns as structure references (sref). For each shape, @@ -117,7 +122,7 @@ def write_dose2dtype(pattern: Pattern, A list of doses is retured, providing a mapping between datatype (list index) and dose (list entry). - Note that this function modifies the Pattern. + Note that this function modifies the Pattern(s). It is often a good idea to run pattern.subpatternize() prior to calling this function, especially if calling .polygonize() will result in very many vertices. @@ -125,7 +130,7 @@ def write_dose2dtype(pattern: Pattern, If you want pattern polygonized with non-default arguments, just call pattern.polygonize() prior to calling this function. - :param pattern: A Pattern to write to file. Modified by this function. + :param patterns: A Pattern or list of patterns to write to file. Modified by this function. :param filename: Filename to write to. :param meters_per_unit: Written into the GDSII file, meters per (database) length unit. All distances are assumed to be an integer multiple of this unit, and are stored as such. @@ -141,11 +146,16 @@ def write_dose2dtype(pattern: Pattern, logical_unit=logical_units_per_unit, physical_unit=meters_per_unit) - # Get a dict of id(pattern) -> pattern - patterns_by_id = {**(pattern.referenced_patterns_by_id()), id(pattern): pattern} + if isinstance(patterns, Pattern): + patterns = [patterns] - # Get a table of (id(subpat.pattern), written_dose) for each subpattern - sd_table = make_dose_table(pattern) + # Get a dict of id(pattern) -> pattern + patterns_by_id = {id(pattern): pattern for pattern in patterns} + for pattern in patterns: + patterns_by_id.update(pattern.referenced_patterns_by_id()) + + # Get a table of (id(pat), written_dose) for each pattern and subpattern + sd_table = make_dose_table(patterns) # Figure out all the unique doses necessary to write this pattern # This means going through each row in sd_table and adding the dose values needed to write @@ -185,7 +195,7 @@ def write_dose2dtype(pattern: Pattern, # strans must be set for angle and mag to take effect for subpat in pat.subpatterns: dose_mult = subpat.dose * pat_dose - sref = gdsii.elements.SRef(struct_name=mangle_name(subpat.pattern, dose_mult).encode('ASCII'), + sref = gdsii.elements.SRef(struct_name=mangle_name(subpat.pattern, dose_mult).encode('ASCII'), xy=numpy.round([subpat.offset]).astype(int)) sref.strans = 0 sref.angle = subpat.rotation diff --git a/masque/file/utils.py b/masque/file/utils.py index e71e500..97e3d36 100644 --- a/masque/file/utils.py +++ b/masque/file/utils.py @@ -2,7 +2,7 @@ Helper functions for file reading and writing """ import re -from typing import Set, Tuple +from typing import Set, Tuple, List from masque.pattern import Pattern @@ -24,18 +24,19 @@ def mangle_name(pattern: Pattern, dose_multiplier: float=1.0) -> str: return sanitized_name -def make_dose_table(pattern: Pattern, dose_multiplier: float=1.0) -> Set[Tuple[int, float]]: +def make_dose_table(patterns: List[Pattern], dose_multiplier: float=1.0) -> Set[Tuple[int, float]]: """ - Create a set containing (id(subpat.pattern), written_dose) for each subpattern + Create a set containing (id(pat), written_dose) for each pattern (including subpatterns) - :param pattern: Source Pattern. + :param pattern: Source Patterns. :param dose_multiplier: Multiplier for all written_dose entries. :return: {(id(subpat.pattern), written_dose), ...} """ - dose_table = {(id(pattern), dose_multiplier)} - for subpat in pattern.subpatterns: - subpat_dose_entry = (id(subpat.pattern), subpat.dose * dose_multiplier) - if subpat_dose_entry not in dose_table: - subpat_dose_table = make_dose_table(subpat.pattern, subpat.dose * dose_multiplier) - dose_table = dose_table.union(subpat_dose_table) + dose_table = {(id(pattern), dose_multiplier) for pattern in patterns} + for pattern in patterns: + for subpat in pattern.subpatterns: + subpat_dose_entry = (id(subpat.pattern), subpat.dose * dose_multiplier) + if subpat_dose_entry not in dose_table: + subpat_dose_table = make_dose_table([subpat.pattern], subpat.dose * dose_multiplier) + dose_table = dose_table.union(subpat_dose_table) return dose_table From bc5d4f62d8ba71ccba28362b8af94e1c6cbe3a3f Mon Sep 17 00:00:00 2001 From: jan Date: Sat, 4 Nov 2017 12:18:58 -0700 Subject: [PATCH 052/124] Test writing to gds in ellip_grating --- examples/ellip_grating.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/examples/ellip_grating.py b/examples/ellip_grating.py index e4e2672..0a4ce43 100644 --- a/examples/ellip_grating.py +++ b/examples/ellip_grating.py @@ -3,11 +3,12 @@ import numpy import masque +import masque.file.gdsii from masque import shapes def main(): - pat = masque.Pattern() + pat = masque.Pattern(name='ellip_grating') for rmin in numpy.arange(10, 15, 0.5): pat.shapes.append(shapes.Arc( radii=(rmin, rmin), @@ -15,7 +16,12 @@ def main(): angles=(-numpy.pi/4, numpy.pi/4) )) + pat.scale_by(1000) pat.visualize() + pat2 = pat.copy() + pat2.name = 'grating2' + + masque.file.gdsii.write_dose2dtype((pat, pat2), 'out.gds', 1e-9, 1e-3) if __name__ == '__main__': From 2ca27b07926136c0194f75638fef96294b528f93 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 12 Nov 2017 19:57:24 -0800 Subject: [PATCH 053/124] fix typo --- masque/file/gdsii.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 8d120c7..79a8f67 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -277,7 +277,7 @@ def read(filename: str, else: shape = Polygon(vertices=element.xy[:-1], layer=(element.layer, element.data_type)) - if do_clean: + if clean_vertices: try: shape.clean_vertices() except PatternError: From fca3d8fda1fde0a90593f317964f713ee1f19cf0 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 15 Jan 2018 22:35:12 -0800 Subject: [PATCH 054/124] move code to new location --- README.md | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2281ef7..d2b7e35 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Requirements: Install with pip, via git: ```bash -pip install git+https://mpxd.net/gogs/jan/masque.git@release +pip install git+https://mpxd.net/code/jan/masque.git@release ``` ## TODO diff --git a/setup.py b/setup.py index 4767f5d..9e2b5ba 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup(name='masque', description='Lithography mask library', author='Jan Petykiewicz', author_email='anewusername@gmail.com', - url='https://mpxd.net/gogs/jan/masque', + url='https://mpxd.net/code/jan/masque', packages=find_packages(), install_requires=[ 'numpy', From e2b05d7c863a7fc7422bd054eb37d306e635d35d Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 15 Jan 2018 23:55:28 -0800 Subject: [PATCH 055/124] add fast approximate manhattanization function --- masque/shapes/shape.py | 85 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index 1036068..4c6ab05 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -188,6 +188,90 @@ class Shape(metaclass=ABCMeta): self.translate(+pivot) return self + def manhattanize_fast(self, grid_x: numpy.ndarray, grid_y: numpy.ndarray) -> List['Polygon']: + from . import Polygon + + grid_x = numpy.unique(grid_x) + grid_y = numpy.unique(grid_y) + + polygon_contours = [] + for polygon in self.to_polygons(): + mins, maxs = polygon.get_bounds() + + vertex_lists = [] + p_verts = polygon.vertices + polygon.offset + for v, v_next in zip(p_verts, numpy.roll(p_verts, -1, axis=0)): + dv = v_next - v + + if abs(dv[0]) < 1e-20: + xs = numpy.array([v[0], v[0]]) # TODO maybe pick between v[0] and v_next[0]? + ys = numpy.array([v[1], v_next[1]]) + xi = numpy.digitize(xs, grid_x).clip(1, len(grid_x) - 1) + yi = numpy.digitize(ys, grid_y).clip(1, len(grid_y) - 1) + err_x = (xs - grid_x[xi]) / (grid_x[xi] - grid_x[xi - 1]) + err_y = (ys - grid_y[yi]) / (grid_y[yi] - grid_y[yi - 1]) + xi[err_y < 0.5] -= 1 + yi[err_y < 0.5] -= 1 + + segment = numpy.column_stack((grid_x[xi], grid_y[yi])) + vertex_lists.append(segment) + continue + + m = dv[1]/dv[0] + def get_grid_inds(xes): + ys = m * (xes - v[0]) + v[1] + + # (inds - 1) is the index of the y-grid line below the edge's intersection with the x-grid + inds = numpy.digitize(ys, grid_y).clip(1, len(grid_y) - 1) + + # err is what fraction of the cell upwards we have to go to reach our y + # (can be negative at bottom edge due to clip above) + err = (ys - grid_y[inds - 1]) / (grid_y[inds] - grid_y[inds - 1]) + + # now set inds to the index of the nearest y-grid line + inds[err < 0.5] -= 1 + #if dv[0] >= 0: + # inds[err <= 0.5] -= 1 + #else: + # inds[err < 0.5] -= 1 + return inds + + gxi_range = numpy.digitize([v[0], v_next[0]], grid_x) + gxi_min = numpy.min(gxi_range - 1).clip(0, len(grid_x)) + gxi_max = numpy.max(gxi_range).clip(0, len(grid_x)) + + xs = grid_x[gxi_min:gxi_max] + inds = get_grid_inds(xs) + + # Find intersections for midpoints + xs2 = (xs[:-1] + xs[1:]) / 2 + inds2 = get_grid_inds(xs2) + + xinds = numpy.round(numpy.arange(gxi_min, gxi_max - 0.99, 1/3)).astype(int) + + # interleave the results + yinds = xinds.copy() + yinds[0::3] = inds + yinds[1::3] = inds2 + yinds[2::3] = inds2 + + vlist = numpy.column_stack((grid_x[xinds], grid_y[yinds])) + if dv[0] < 0: + vlist = vlist[::-1] + + vertex_lists.append(vlist) + polygon_contours.append(numpy.vstack(vertex_lists)) + + manhattan_polygons = [] + for contour in polygon_contours: + manhattan_polygons.append(Polygon( + vertices=contour, + layer=self.layer, + dose=self.dose)) + + return manhattan_polygons + + def manhattanize(self, grid_x: numpy.ndarray, grid_y: numpy.ndarray) -> List['Polygon']: """ Returns a list of polygons with grid-aligned ("Manhattan") edges approximating the shape. @@ -233,6 +317,7 @@ class Shape(metaclass=ABCMeta): polygon_contours = [] for polygon in self.to_polygons(): + # Get rid of unused gridlines (anything not within 2 lines of the polygon bounds) mins, maxs = polygon.get_bounds() keep_x = numpy.logical_and(grid_x > mins[0], grid_x < maxs[0]) keep_y = numpy.logical_and(grid_y > mins[1], grid_y < maxs[1]) From 3d7df45c2b25a84acf824fd6b70e9a6d0cf69d03 Mon Sep 17 00:00:00 2001 From: jan Date: Sat, 14 Apr 2018 14:34:26 -0700 Subject: [PATCH 056/124] fix get_bit docs --- masque/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/masque/utils.py b/masque/utils.py index 59e8169..0f5db05 100644 --- a/masque/utils.py +++ b/masque/utils.py @@ -21,10 +21,10 @@ def is_scalar(var: Any) -> bool: def get_bit(bit_string: Any, bit_id: int) -> bool: """ - Returns true iff bit number 'bit_id' from the right of 'bitstring' is 1 + Returns true iff bit number 'bit_id' from the right of 'bit_string' is 1 - :param bit_string: st - :param bit_id: + :param bit_string: Bit string to test + :param bit_id: Bit number, 0-indexed from the right (lsb :return: value of the requested bit (bool) """ return bit_string & (1 << bit_id) != 0 From eae57f71589538bb5c6544e1435bb2ea552506a7 Mon Sep 17 00:00:00 2001 From: jan Date: Sat, 14 Apr 2018 14:34:34 -0700 Subject: [PATCH 057/124] add set_bit --- masque/utils.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/masque/utils.py b/masque/utils.py index 0f5db05..61137c0 100644 --- a/masque/utils.py +++ b/masque/utils.py @@ -30,6 +30,22 @@ def get_bit(bit_string: Any, bit_id: int) -> bool: return bit_string & (1 << bit_id) != 0 +def set_bit(bit_string: Any, bit_id: int, value: bool) -> Any: + """ + Returns 'bit_string' with bit number 'bit_id' set to 'value'. + + :param bit_string: Bit string to alter + :param bit_id: Bit number, 0-indexed from right (lsb) + :param value: Boolean value to set bit to + :return: Altered 'bit_string' + """ + mask = (1 << bit_id) + bit_string &= ~mask + if value: + bit_string |= mask + return bit_string + + def rotation_matrix_2d(theta: float) -> numpy.ndarray: """ 2D rotation matrix for rotating counterclockwise around the origin. From 4c535e6564b99849bb7aaae9de344e54b49b9e1e Mon Sep 17 00:00:00 2001 From: jan Date: Sat, 14 Apr 2018 14:38:42 -0700 Subject: [PATCH 058/124] return self from Text's scale_by and rotate --- masque/shapes/text.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/masque/shapes/text.py b/masque/shapes/text.py index e645920..cd8340b 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -90,11 +90,13 @@ class Text(Shape): return all_polygons - def rotate(self, theta: float): + def rotate(self, theta: float) -> 'Text': self.rotation += theta + return self - def scale_by(self, c: float): + def scale_by(self, c: float) -> 'Text': self.height *= c + return self def normalized_form(self, norm_value: float) -> normalized_shape_tuple: return (type(self), self.string, self.font_path, self.layer), \ From 3e06214b7e77f27f7d290321c865075ad33b0955 Mon Sep 17 00:00:00 2001 From: jan Date: Sat, 14 Apr 2018 14:53:53 -0700 Subject: [PATCH 059/124] Add recursive "apply()" helper to pattern --- masque/pattern.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/masque/pattern.py b/masque/pattern.py index 7af9654..8b8575f 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -94,6 +94,22 @@ class Pattern: pat.subpatterns = [s for s in self.subpatterns if subpatterns_func(s)] return pat + def apply(self, + func: Callable[['Pattern'], 'Pattern'] + ) -> 'Pattern': + """ + Recursively apply func() to this pattern and its subpatterns. + func() is expected to take and return a Pattern. + func() is first applied to the pattern as a whole, then the subpatterns. + + :param func: Function which accepts a Pattern, and returns a pattern. + :return: The result of applying func() to this pattern and all subpatterns. + """ + pat = func(self) + for subpat in pat.subpatterns: + subpat.pattern = subpat.pattern.apply(func) + return pat + def polygonize(self, poly_num_points: int=None, poly_max_arclen: float=None From e4545bfa30de45ae1d32d1336b1a3aa0ecbb8c29 Mon Sep 17 00:00:00 2001 From: jan Date: Sat, 14 Apr 2018 15:02:13 -0700 Subject: [PATCH 060/124] use is_scalar for checking if layer is a tuple --- masque/file/gdsii.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 79a8f67..e218545 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -13,7 +13,7 @@ import numpy from .utils import mangle_name, make_dose_table from .. import Pattern, SubPattern, PatternError from ..shapes import Polygon -from ..utils import rotation_matrix_2d, get_bit, vector2 +from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar __author__ = 'Jan Petykiewicz' @@ -76,15 +76,15 @@ def write(patterns: Pattern or List[Pattern], 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, :])) - if hasattr(polygon.layer, '__len__'): + if is_scalar(polygon.layer): + layer = polygon.layer + data_type = 0 + else: layer = polygon.layer[0] if len(polygon.layer) > 1: data_type = polygon.layer[1] else: data_type = 0 - else: - layer = polygon.layer - data_type = 0 structure.append(gdsii.elements.Boundary(layer=layer, data_type=data_type, xy=xy_closed)) @@ -184,10 +184,10 @@ def write_dose2dtype(patterns: Pattern or List[Pattern], data_type = dose_vals_list.index(polygon.dose * pat_dose) xy_open = numpy.round(polygon.vertices + polygon.offset).astype(int) xy_closed = numpy.vstack((xy_open, xy_open[0, :])) - if hasattr(polygon.layer, '__len__'): - layer = polygon.layer[0] - else: + if is_scalar(polygon.layer): layer = polygon.layer + else: + layer = polygon.layer[0] structure.append(gdsii.elements.Boundary(layer=layer, data_type=data_type, xy=xy_closed)) From 37a55e0d9bbaa1cb386022d54dd8bcb12089e721 Mon Sep 17 00:00:00 2001 From: jan Date: Sat, 14 Apr 2018 15:02:35 -0700 Subject: [PATCH 061/124] Add deepcopy() convenience method to Pattern --- masque/pattern.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/masque/pattern.py b/masque/pattern.py index 8b8575f..a019a1b 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -367,6 +367,8 @@ class 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() + :return: A copy of the current Pattern. """ cp = copy.copy(self) @@ -374,6 +376,14 @@ class Pattern: cp.subpatterns = [copy.copy(subpat) for subpat in cp.subpatterns] return cp + def deepcopy(self) -> 'Pattern': + """ + Convenience method for copy.deepcopy(pattern) + + :return: A deep copy of the current Pattern. + """ + return copy.deepcopy(self) + @staticmethod def load(filename: str) -> 'Pattern': """ From 04ff11d3cb303d4d42b94d577eb4738590d4b5a7 Mon Sep 17 00:00:00 2001 From: jan Date: Sat, 14 Apr 2018 15:06:12 -0700 Subject: [PATCH 062/124] check for zero-length names --- masque/file/gdsii.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index e218545..e40191d 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -68,7 +68,10 @@ def write(patterns: Pattern or List[Pattern], # Now create a structure for each pattern, and add in any Boundary and SREF elements for pat in patterns_by_id.values(): sanitized_name = re.compile('[^A-Za-z0-9_\?\$]').sub('_', pat.name) - structure = gdsii.structure.Structure(name=sanitized_name.encode('ASCII')) + encoded_name = sanitized_name.encode('ASCII') + if len(encoded_name) == 0: + raise PatternError('Zero-length name after sanitize+encode, originally "{}"'.format(pat.name)) + structure = gdsii.structure.Structure(name=encoded_name) lib.append(structure) # Add a Boundary element for each shape @@ -92,7 +95,10 @@ def write(patterns: Pattern or List[Pattern], # strans must be set for angle and mag to take effect for subpat in pat.subpatterns: sanitized_name = re.compile('[^A-Za-z0-9_\?\$]').sub('_', subpat.pattern.name) - sref = gdsii.elements.SRef(struct_name=sanitized_name.encode('ASCII'), + 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 @@ -175,7 +181,10 @@ def write_dose2dtype(patterns: Pattern or List[Pattern], for pat_id, pat_dose in sd_table: pat = patterns_by_id[pat_id] - structure = gdsii.structure.Structure(name=mangle_name(pat, pat_dose).encode('ASCII')) + encoded_name = mangle_name(pat, pat_dose).encode('ASCII') + if len(encoded_name) == 0: + 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 @@ -195,7 +204,10 @@ def write_dose2dtype(patterns: Pattern or List[Pattern], # strans must be set for angle and mag to take effect for subpat in pat.subpatterns: dose_mult = subpat.dose * pat_dose - sref = gdsii.elements.SRef(struct_name=mangle_name(subpat.pattern, dose_mult).encode('ASCII'), + encoded_name = mangle_name(subpat.pattern, dose_mult).encode('ASCII') + 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 From 0170f45f759999708fc33626a53362cc6f93f481 Mon Sep 17 00:00:00 2001 From: jan Date: Sat, 14 Apr 2018 15:19:48 -0700 Subject: [PATCH 063/124] GDS stores rotation in degrees --- masque/file/gdsii.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index e40191d..17921a6 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -101,7 +101,7 @@ def write(patterns: Pattern or List[Pattern], sref = gdsii.elements.SRef(struct_name=encoded_name, xy=numpy.round([subpat.offset]).astype(int)) sref.strans = 0 - sref.angle = subpat.rotation + sref.angle = subpat.rotation * 180 / numpy.pi sref.mag = subpat.scale structure.append(sref) @@ -210,7 +210,7 @@ def write_dose2dtype(patterns: Pattern or List[Pattern], sref = gdsii.elements.SRef(struct_name=encoded_name, xy=numpy.round([subpat.offset]).astype(int)) sref.strans = 0 - sref.angle = subpat.rotation + sref.angle = subpat.rotation * 180 / numpy.pi sref.mag = subpat.scale structure.append(sref) @@ -270,7 +270,7 @@ def read(filename: str, if get_bit(element.strans, 13): subpat.offset *= subpat.scale if element.angle is not None: - subpat.rotation = element.angle + subpat.rotation = element.angle * numpy.pi / 180 # Bit 14 means absolute rotation if get_bit(element.strans, 14): subpat.offset = numpy.dot(rotation_matrix_2d(subpat.rotation), subpat.offset) From c14daf2e5e0b72e09556649415e64aebd1f25027 Mon Sep 17 00:00:00 2001 From: jan Date: Sat, 14 Apr 2018 15:20:39 -0700 Subject: [PATCH 064/124] Fix GDS AREF handling --- masque/file/gdsii.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 17921a6..6d1fcb0 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -301,8 +301,17 @@ def read(filename: str, pat.subpatterns.append(ref_element_to_subpat(element, element.xy)) elif isinstance(element, gdsii.elements.ARef): - for offset in element.xy: - pat.subpatterns.append(ref_element_to_subpat(element, offset)) + xy = numpy.array(element.xy) + origin = xy[0] + col_spacing = (xy[1] - origin) / element.cols + row_spacing = (xy[2] - origin) / element.rows + + print(element.xy) + 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) # Create a dict of {pattern.name: pattern, ...}, then fix up all subpattern.pattern entries From 358f45c5fd68820b4b6321ce815c329cc25d3709 Mon Sep 17 00:00:00 2001 From: jan Date: Sat, 14 Apr 2018 15:23:01 -0700 Subject: [PATCH 065/124] Error out when we see absolute positioning in GDS We don't support it (yet?) --- masque/file/gdsii.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 6d1fcb0..d586cf7 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -258,24 +258,28 @@ def read(filename: str, # 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: Figure out what "absolute" means in the context of elements and if the current - # behavior is correct # BUG: Need to check STRANS bit 0 to handle x-reflection + # 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, 13): - subpat.offset *= subpat.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, 14): - subpat.offset = numpy.dot(rotation_matrix_2d(subpat.rotation), subpat.offset) + 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!') return subpat + patterns = [] for structure in lib: pat = Pattern(name=structure.name.decode('ASCII')) From d5a255a9d7db3d0ceae8ac2a5027d0f3d050c3cb Mon Sep 17 00:00:00 2001 From: jan Date: Sat, 14 Apr 2018 15:27:56 -0700 Subject: [PATCH 066/124] Add mirror() to shapes Might need to fix ordering on Text.to_polygons() --- masque/shapes/arc.py | 6 ++++++ masque/shapes/circle.py | 4 ++++ masque/shapes/ellipse.py | 5 +++++ masque/shapes/polygon.py | 4 ++++ masque/shapes/shape.py | 10 ++++++++++ masque/shapes/text.py | 25 +++++++++++++++++++++++-- 6 files changed, 52 insertions(+), 2 deletions(-) diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index b4c233d..17693a1 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -280,6 +280,12 @@ class Arc(Shape): self.rotation += theta return self + def mirror(self, axis: int) -> 'Arc': + self.offset[axis - 1] *= -1 + self.rotation *= -1 + self.angles *= -1 + return self + def scale_by(self, c: float) -> 'Arc': self.radii *= c self.width *= c diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index f1ddb38..489a608 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -82,6 +82,10 @@ class Circle(Shape): def rotate(self, theta: float) -> 'Circle': return self + def mirror(self, axis: int) -> 'Circle': + self.offset *= -1 + return self + def scale_by(self, c: float) -> 'Circle': self.radius *= c return self diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index ea6e3bf..6b7317f 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -142,6 +142,11 @@ class Ellipse(Shape): self.rotation += theta return self + def mirror(self, axis: int) -> 'Ellipse': + self.offset[axis - 1] *= -1 + self.rotation *= -1 + return self + def scale_by(self, c: float) -> 'Ellipse': self.radii *= c return self diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 34f6285..f8e968a 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -146,6 +146,10 @@ class Polygon(Shape): self.vertices = numpy.dot(rotation_matrix_2d(theta), self.vertices.T).T return self + def mirror(self, axis: int) -> 'Polygon': + self.vertices[:, axis - 1] *= -1 + return self + def scale_by(self, c: float) -> 'Polygon': self.vertices *= c return self diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index 4c6ab05..73bebae 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -71,6 +71,16 @@ class Shape(metaclass=ABCMeta): """ pass + @abstractmethod + def mirror(self, axis: int) -> 'Shape': + """ + Mirror the shape across an axis. + + :param axis: Axis to mirror across. + :return: self + """ + pass + @abstractmethod def scale_by(self, c: float) -> 'Shape': """ diff --git a/masque/shapes/text.py b/masque/shapes/text.py index cd8340b..64b7468 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -18,6 +18,7 @@ class Text(Shape): _string = '' _height = 1.0 _rotation = 0.0 + _mirrored = None font_path = '' # vertices property @@ -51,10 +52,22 @@ class Text(Shape): raise PatternError('Height must be a scalar') self._height = val + # 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 + def __init__(self, string: str, height: float, font_path: str, + mirrored: List[bool]=None, rotation: float=0.0, offset: vector2=(0.0, 0.0), layer: int=0, @@ -66,6 +79,9 @@ class Text(Shape): self.height = height self.rotation = rotation self.font_path = font_path + if mirrored is None: + mirrored = [False, False] + self.mirrored = mirrored def to_polygons(self, _poly_num_points: int=None, @@ -79,9 +95,9 @@ class Text(Shape): # Move these polygons to the right of the previous letter for xys in raw_polys: poly = Polygon(xys, dose=self.dose, layer=self.layer) + [poly.mirror(ax) for ax, do in enumerate(self.mirrored) if do] poly.scale_by(self.height) poly.offset = self.offset + [total_advance, 0] - # poly.scale_by(self.height) poly.rotate_around(self.offset, self.rotation) all_polygons += [poly] @@ -94,16 +110,21 @@ class Text(Shape): self.rotation += theta return self + def mirror(self, axis: int) -> 'Text': + self.mirrored[axis] = not self.mirrored[axis] + return self + def scale_by(self, c: float) -> 'Text': self.height *= c return self def normalized_form(self, norm_value: float) -> normalized_shape_tuple: - return (type(self), self.string, self.font_path, self.layer), \ + return (type(self), self.string, self.font_path, self.mirrored, self.layer), \ (self.offset, self.height / norm_value, self.rotation, self.dose), \ lambda: Text(string=self.string, height=self.height * norm_value, font_path=self.font_path, + mirrored=self.mirrored, layer=self.layer) def get_bounds(self) -> numpy.ndarray: From f580e784f71c642780b2437b791fc701f28c8ac8 Mon Sep 17 00:00:00 2001 From: jan Date: Sat, 14 Apr 2018 15:29:19 -0700 Subject: [PATCH 067/124] Add mirroring functions to patterns/subpatterns --- masque/pattern.py | 34 ++++++++++++++++++++++++++++++++++ masque/subpattern.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/masque/pattern.py b/masque/pattern.py index a019a1b..f61ee4e 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -351,6 +351,40 @@ class Pattern: entry.rotate(rotation) return self + def mirror_element_centers(self, axis: int) -> 'Pattern': + """ + Mirror the offsets of all shapes and subpatterns across an axis + + :param axis: Axis to mirror across + :return: self + """ + for entry in self.shapes + self.subpatterns: + entry.offset[axis] *= -1 + return self + + def mirror_elements(self, axis: int) -> 'Pattern': + """ + Mirror each shape and subpattern across an axis, relative to its + center (offset) + + :param axis: Axis to mirror across + :return: self + """ + for entry in self.shapes + self.subpatterns: + entry.mirror(axis) + return self + + def mirror(self, axis: int) -> 'Pattern': + """ + Mirror the Pattern across an axis + + :param axis: Axis to mirror across + :return: self + """ + self.mirror_elements(axis) + self.mirror_element_centers(axis) + return self + def scale_element_doses(self, factor: float) -> 'Pattern': """ Multiply all shape and subpattern doses by a factor diff --git a/masque/subpattern.py b/masque/subpattern.py index 6242468..231d4e3 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -3,7 +3,7 @@ offset, rotation, scaling, and other such properties to the reference. """ -from typing import Union +from typing import Union, List import numpy from numpy import pi @@ -26,11 +26,13 @@ class SubPattern: _rotation = 0.0 # type: float _dose = 1.0 # type: float _scale = 1.0 # type: float + _mirrored = None # type: List[bool] def __init__(self, pattern: 'Pattern', offset: vector2=(0.0, 0.0), rotation: float=0.0, + mirrored: List[bool]=None, dose: float=1.0, scale: float=1.0): self.pattern = pattern @@ -38,6 +40,9 @@ class SubPattern: self.rotation = rotation self.dose = dose self.scale = scale + if mirrored is None: + mirrored = [False, False] + self.mirrored = mirrored # offset property @property @@ -90,6 +95,17 @@ class SubPattern: 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 + def as_pattern(self) -> 'Pattern': """ Returns a copy of self.pattern which has been scaled, rotated, etc. according to this @@ -98,6 +114,7 @@ class SubPattern: """ pattern = self.pattern.copy() pattern.scale_by(self.scale) + [pattern.mirror(ax) for ax, do in enumerate(self.mirrored) if do] pattern.rotate_around((0.0, 0.0), self.rotation) pattern.translate_elements(self.offset) pattern.scale_element_doses(self.dose) @@ -138,6 +155,16 @@ class SubPattern: self.rotation += rotation return self + def mirror(self, axis: int) -> 'SubPattern': + """ + 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 From 70f3ea9304686eebac462d2ac5cb09d85fd79d7e Mon Sep 17 00:00:00 2001 From: jan Date: Sat, 14 Apr 2018 15:29:44 -0700 Subject: [PATCH 068/124] Handle mirrored gds shapes --- masque/file/gdsii.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index d586cf7..d57680d 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -102,6 +102,14 @@ def write(patterns: Pattern or List[Pattern], 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) @@ -212,6 +220,14 @@ def write_dose2dtype(patterns: Pattern or List[Pattern], 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: @@ -258,7 +274,6 @@ def read(filename: str, # 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: Need to check STRANS bit 0 to handle x-reflection # 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. @@ -277,6 +292,9 @@ def read(filename: str, 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 From 79ac6a59e4e3e517242e7485a6e6a15e34293d85 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 15 Apr 2018 15:25:38 -0700 Subject: [PATCH 069/124] Fix mirror axis in Pattern.mirror() --- masque/pattern.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/pattern.py b/masque/pattern.py index f61ee4e..3256abb 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -359,7 +359,7 @@ class Pattern: :return: self """ for entry in self.shapes + self.subpatterns: - entry.offset[axis] *= -1 + entry.offset[axis - 1] *= -1 return self def mirror_elements(self, axis: int) -> 'Pattern': From 1f17c07befeca1a02cec28dee4f94974a6ea2af2 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 15 Apr 2018 15:27:19 -0700 Subject: [PATCH 070/124] remove errant print --- masque/file/gdsii.py | 1 - 1 file changed, 1 deletion(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index d57680d..e49ed85 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -328,7 +328,6 @@ def read(filename: str, col_spacing = (xy[1] - origin) / element.cols row_spacing = (xy[2] - origin) / element.rows - print(element.xy) for c in range(element.cols): for r in range(element.rows): offset = origin + c * col_spacing + r * row_spacing From e38a530dee97bd13b9a2b0abf681be2e52187844 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 15 Apr 2018 15:39:38 -0700 Subject: [PATCH 071/124] close paren in docstring --- masque/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/utils.py b/masque/utils.py index 61137c0..5d99835 100644 --- a/masque/utils.py +++ b/masque/utils.py @@ -24,7 +24,7 @@ def get_bit(bit_string: Any, bit_id: int) -> bool: Returns true iff bit number 'bit_id' from the right of 'bit_string' is 1 :param bit_string: Bit string to test - :param bit_id: Bit number, 0-indexed from the right (lsb + :param bit_id: Bit number, 0-indexed from the right (lsb) :return: value of the requested bit (bool) """ return bit_string & (1 << bit_id) != 0 From 52adb582dc1adc4b028d72fa1ad616709dc20eac Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 15 Apr 2018 16:14:27 -0700 Subject: [PATCH 072/124] copy name when using Pattern.subset() --- masque/pattern.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/pattern.py b/masque/pattern.py index 3256abb..191ba5f 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -87,7 +87,7 @@ class Pattern: :return: A Pattern containing all the shapes and subpatterns for which the parameter functions return True """ - pat = Pattern() + pat = Pattern(name=self.name) if shapes_func is not None: pat.shapes = [s for s in self.shapes if shapes_func(s)] if subpatterns_func is not None: From f875ae89d744059a36383f47c1e60faa115a9bdc Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 15 Apr 2018 16:34:52 -0700 Subject: [PATCH 073/124] make sure apply() only hits each pattern one --- masque/pattern.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/masque/pattern.py b/masque/pattern.py index 191ba5f..927c6b5 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -98,16 +98,30 @@ class Pattern: func: Callable[['Pattern'], 'Pattern'] ) -> 'Pattern': """ - Recursively apply func() to this pattern and its subpatterns. + 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 the subpatterns. + func() is first applied to the pattern as a whole, then the referenced patterns. + It is only applied to any given pattern once, regardless of how many times it is + referenced. :param func: Function which accepts a Pattern, and returns a pattern. :return: The result of applying func() to this pattern and all subpatterns. + :raises: PatternError if called on a pattern containing a circular reference. """ + pat_map = {id(self): None} pat = func(self) + pat_map[id(self)] = pat + for subpat in pat.subpatterns: - subpat.pattern = subpat.pattern.apply(func) + ref_pat_id = id(subpat.pattern) + if ref_pat_id not in pat_map: + pat_map[ref_pat_id] = None + subpat.pattern = subpat.pattern.apply(func) + pat_map[ref_pat_id] = subpat.pattern + elif pat_map[ref_pat_id] is None: + raise PatternError('.apply() called on pattern with circular reference') + else: + subpat.pattern = pat_map[ref_pat_id] return pat def polygonize(self, From 4c3250a2a1b69f649e3b8cde60f258fa7998a0e2 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 15 Apr 2018 16:41:31 -0700 Subject: [PATCH 074/124] add recursive option to subset --- masque/pattern.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/masque/pattern.py b/masque/pattern.py index 927c6b5..e730e51 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -81,17 +81,26 @@ class Pattern: 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 + of the subset. Default always returns False. :param subpatterns_func: Given a subpattern, returns a boolean denoting if it is a member - of the subset + of the subset. Default always returns False. + :param recursive: If True, also calls .subset() recursively on patterns referenced by this + pattern. :return: A Pattern containing all the shapes and subpatterns for which the parameter - functions return True + functions return True """ - pat = Pattern(name=self.name) - 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)] + def do_subset(self): + pat = Pattern(name=self.name) + 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 + + if recursive: + pat = self.apply(do_subset) + else: + pat = do_subset(self) return pat def apply(self, From cc35ff802e70168bc33b1f792439a3844ee579e9 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 15 Apr 2018 16:42:00 -0700 Subject: [PATCH 075/124] clean up comments --- masque/pattern.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/masque/pattern.py b/masque/pattern.py index e730e51..11269e0 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -143,10 +143,10 @@ class Pattern: 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. + 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. + segment. Optional, defaults to shapes' internal defaults. :return: self """ old_shapes = self.shapes @@ -193,7 +193,7 @@ class Pattern: :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) + 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 From 082236b6fdeeb29cf98a95089520cccef62c1db5 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 15 Apr 2018 16:43:26 -0700 Subject: [PATCH 076/124] add missing arg --- masque/pattern.py | 1 + 1 file changed, 1 insertion(+) diff --git a/masque/pattern.py b/masque/pattern.py index 11269e0..1ee77da 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -74,6 +74,7 @@ class Pattern: def subset(self, shapes_func: Callable[[Shape], bool]=None, subpatterns_func: Callable[[SubPattern], bool]=None, + recursive: bool=False, ) -> 'Pattern': """ Returns a Pattern containing only the shapes and subpatterns for which shapes_func or From 6fda9917002dacd1ec114147318eb5b2daa1e12a Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 15 Apr 2018 19:25:42 -0700 Subject: [PATCH 077/124] Rewrite/fix apply() implementation --- masque/pattern.py | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/masque/pattern.py b/masque/pattern.py index 1ee77da..5f9605b 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -14,7 +14,7 @@ import numpy from .subpattern import SubPattern from .shapes import Shape, Polygon from .utils import rotation_matrix_2d, vector2 - +from .error import PatternError __author__ = 'Jan Petykiewicz' @@ -105,33 +105,37 @@ class Pattern: return pat def apply(self, - func: Callable[['Pattern'], 'Pattern'] + func: Callable[['Pattern'], 'Pattern'], + memo: Dict[int, 'Pattern']=None, ) -> '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 the referenced patterns. + 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. :param func: Function which accepts a Pattern, and returns a pattern. + :param 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). :return: The result of applying func() to this pattern and all subpatterns. :raises: PatternError if called on a pattern containing a circular reference. """ - pat_map = {id(self): None} - pat = func(self) - pat_map[id(self)] = pat + if memo is None: + memo = {} - for subpat in pat.subpatterns: - ref_pat_id = id(subpat.pattern) - if ref_pat_id not in pat_map: - pat_map[ref_pat_id] = None - subpat.pattern = subpat.pattern.apply(func) - pat_map[ref_pat_id] = subpat.pattern - elif pat_map[ref_pat_id] is None: - raise PatternError('.apply() called on pattern with circular reference') - else: - subpat.pattern = pat_map[ref_pat_id] + pat_id = id(self) + if pat_id not in memo: + memo[pat_id] = None + pat = func(self) + for subpat in pat.subpatterns: + 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 polygonize(self, From 4840c321c555b3c75bc2ad17df634fdae00bb4b0 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 15 Apr 2018 19:27:59 -0700 Subject: [PATCH 078/124] rename param for do_subset --- masque/pattern.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/masque/pattern.py b/masque/pattern.py index 5f9605b..f9d41f6 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -90,12 +90,12 @@ class Pattern: :return: A Pattern containing all the shapes and subpatterns for which the parameter functions return True """ - def do_subset(self): - pat = Pattern(name=self.name) + def do_subset(src): + pat = Pattern(name=src.name) if shapes_func is not None: - pat.shapes = [s for s in self.shapes if shapes_func(s)] + pat.shapes = [s for s in src.shapes if shapes_func(s)] if subpatterns_func is not None: - pat.subpatterns = [s for s in self.subpatterns if subpatterns_func(s)] + pat.subpatterns = [s for s in src.subpatterns if subpatterns_func(s)] return pat if recursive: From 8623dbbeac2a9a487eb00c7a9b405e6ab9644af1 Mon Sep 17 00:00:00 2001 From: jan Date: Thu, 30 Aug 2018 23:05:30 -0700 Subject: [PATCH 079/124] Put masque-layer to gds-layer conversion into a private function, and only call once per shape --- masque/file/gdsii.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index e49ed85..6bb95e2 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -76,18 +76,10 @@ def write(patterns: Pattern or List[Pattern], # Add a Boundary element for each shape for shape in 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, :])) - if is_scalar(polygon.layer): - layer = polygon.layer - data_type = 0 - else: - layer = polygon.layer[0] - if len(polygon.layer) > 1: - data_type = polygon.layer[1] - else: - data_type = 0 structure.append(gdsii.elements.Boundary(layer=layer, data_type=data_type, xy=xy_closed)) @@ -344,3 +336,16 @@ def read(filename: str, del sp.ref_name return patterns_dict, library_info + + +def _mlayer2gds(mlayer): + if is_scalar(mlayer): + layer = mlayer + data_type = 0 + else: + layer = mlayer[0] + if len(mlayer) > 1: + data_type = mlayer[1] + else: + data_type = 0 + return layer, data_type From 108694551bb094d83b8c54389e1a4bcdc3c6b348 Mon Sep 17 00:00:00 2001 From: jan Date: Thu, 30 Aug 2018 23:06:31 -0700 Subject: [PATCH 080/124] add support for labels (e.g. GDS TEXT) --- masque/__init__.py | 1 + masque/file/gdsii.py | 25 ++++++++++++++++++++++++- masque/pattern.py | 44 ++++++++++++++++++++++++++++++++------------ 3 files changed, 57 insertions(+), 13 deletions(-) diff --git a/masque/__init__.py b/masque/__init__.py index 53eab35..1555475 100644 --- a/masque/__init__.py +++ b/masque/__init__.py @@ -26,6 +26,7 @@ from .error import PatternError from .shapes import Shape +from .label import Label from .subpattern import SubPattern from .pattern import Pattern diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 6bb95e2..61f20ab 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -11,7 +11,7 @@ import re import numpy from .utils import mangle_name, make_dose_table -from .. import Pattern, SubPattern, PatternError +from .. import Pattern, SubPattern, PatternError, Label from ..shapes import Polygon from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar @@ -74,6 +74,7 @@ def write(patterns: Pattern or List[Pattern], structure = gdsii.structure.Structure(name=encoded_name) lib.append(structure) + # Add a Boundary element for each shape for shape in pat.shapes: layer, data_type = _mlayer2gds(shape.layer) @@ -83,6 +84,14 @@ def write(patterns: Pattern or List[Pattern], 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_closed = numpy.round([label.offset, label.offset]).astype(int) + structure.append(gdsii.elements.Text(layer=layer, + text_type=text_type, + xy=xy_closed, + string=label.string.encode('ASCII'))) + # Add an SREF for each subpattern entry # strans must be set for angle and mag to take effect for subpat in pat.subpatterns: @@ -200,6 +209,14 @@ def write_dose2dtype(patterns: Pattern or List[Pattern], 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_closed = numpy.round([label.offset, label.offset]).astype(int) + structure.append(gdsii.elements.Text(layer=layer, + text_type=text_type, + xy=xy_closed, + string=label.string.encode('ASCII'))) + # Add an SREF for each subpattern entry # strans must be set for angle and mag to take effect for subpat in pat.subpatterns: @@ -311,6 +328,12 @@ def read(filename: str, pat.shapes.append(shape) + elif isinstance(element, gdsii.elements.Text): + label = Label(offset=element.xy, + layer=(element.layer, element.text_type), + string=element.string.decode('ASCII')) + pat.labels.append(label) + elif isinstance(element, gdsii.elements.SRef): pat.subpatterns.append(ref_element_to_subpat(element, element.xy)) diff --git a/masque/pattern.py b/masque/pattern.py index f9d41f6..4c2807a 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -13,6 +13,7 @@ import numpy from .subpattern import SubPattern from .shapes import Shape, Polygon +from .label import Label from .utils import rotation_matrix_2d, vector2 from .error import PatternError @@ -32,11 +33,13 @@ class Pattern: :var name: An identifier for this object. Not necessarily unique. """ shapes = None # type: List[Shape] + labels = None # type: List[Labels] subpatterns = None # type: List[SubPattern] name = None # type: str def __init__(self, shapes: List[Shape]=(), + labels: List[Label]=(), subpatterns: List[SubPattern]=(), name: str='', ): @@ -45,6 +48,7 @@ class Pattern: Non-list inputs for shapes and subpatterns get converted to lists. :param shapes: Initial shapes in the Pattern + :param labels: Initial labels in the Pattern :param subpatterns: Initial subpatterns in the Pattern :param name: An identifier for the Pattern """ @@ -53,6 +57,11 @@ class Pattern: 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: @@ -62,27 +71,32 @@ class Pattern: def append(self, other_pattern: 'Pattern') -> 'Pattern': """ - Appends all shapes and subpatterns from other_pattern to self's shapes and subpatterns. + Appends all shapes, labels and subpatterns from other_pattern to self's shapes, + labels, and supbatterns. :param other_pattern: The Pattern to append :return: 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 shapes and subpatterns for which shapes_func or - subpatterns_func returns True. - Self is _not_ altered, but shapes and subpatterns are _not_ copied. + 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. :param shapes_func: Given a shape, returns a boolean denoting whether the shape is a member of the subset. Default always returns False. + :param labels_func: Given a label, returns a boolean denoting whether the label is a member + of the subset. Default always returns False. :param subpatterns_func: Given a subpattern, returns a boolean denoting if it is a member of the subset. Default always returns False. :param recursive: If True, also calls .subset() recursively on patterns referenced by this @@ -94,6 +108,8 @@ class Pattern: 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 @@ -281,7 +297,7 @@ class Pattern: :return: [[x_min, y_min], [x_max, y_max]] or None """ - entries = self.shapes + self.subpatterns + entries = self.shapes + self.subpatterns + self.labels if not entries: return None @@ -304,17 +320,19 @@ class Pattern: self.subpatterns = [] for subpat in subpatterns: subpat.pattern.flatten() - self.shapes += subpat.as_pattern().shapes + p = subpat.as_pattern() + self.shapes += p.shapes + self.labels += p.labels return self def translate_elements(self, offset: vector2) -> 'Pattern': """ - Translates all shapes and subpatterns by the given offset. + Translates all shapes, label, and subpatterns by the given offset. :param offset: Offset to translate by :return: self """ - for entry in self.shapes + self.subpatterns: + for entry in self.shapes + self.subpatterns + self.labels: entry.translate(offset) return self @@ -359,12 +377,12 @@ class Pattern: def rotate_element_centers(self, rotation: float) -> 'Pattern': """ - Rotate the offsets of all shapes and subpatterns around (0, 0) + Rotate the offsets of all shapes, labels, and subpatterns around (0, 0) :param rotation: Angle to rotate by (counter-clockwise, radians) :return: self """ - for entry in self.shapes + self.subpatterns: + for entry in self.shapes + self.subpatterns + self.labels: entry.offset = numpy.dot(rotation_matrix_2d(rotation), entry.offset) return self @@ -381,12 +399,12 @@ class Pattern: def mirror_element_centers(self, axis: int) -> 'Pattern': """ - Mirror the offsets of all shapes and subpatterns across an axis + Mirror the offsets of all shapes, labels, and subpatterns across an axis :param axis: Axis to mirror across :return: self """ - for entry in self.shapes + self.subpatterns: + for entry in self.shapes + self.subpatterns + self.labels: entry.offset[axis - 1] *= -1 return self @@ -435,6 +453,7 @@ class Pattern: """ cp = copy.copy(self) cp.shapes = copy.deepcopy(cp.shapes) + cp.labels = copy.deepcopy(cp.labels) cp.subpatterns = [copy.copy(subpat) for subpat in cp.subpatterns] return cp @@ -487,6 +506,7 @@ class Pattern: :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 """ + # TODO: add text labels to visualize() from matplotlib import pyplot import matplotlib.collections From 6bafaaf4fc20d4d9d536972a614a4eb5fac26d12 Mon Sep 17 00:00:00 2001 From: jan Date: Thu, 30 Aug 2018 23:06:45 -0700 Subject: [PATCH 081/124] Fix typo in arc docs --- masque/shapes/arc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 17693a1..c1dadea 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -18,7 +18,7 @@ class Arc(Shape): The radii define an ellipse; the ring is formed with radii +/- width/2. The rotation gives the angle from x-axis, counterclockwise, to the first (x) radius. - The start and stop angle are measure counterclockwise from the first (x) radius. + The start and stop angle are measured counterclockwise from the first (x) radius. """ _radii = None # type: numpy.ndarray From 86068102d926696c7bf997ea5a6144c6bef2e8b5 Mon Sep 17 00:00:00 2001 From: jan Date: Thu, 30 Aug 2018 23:06:56 -0700 Subject: [PATCH 082/124] Clarify that layer can be a tuple --- masque/shapes/shape.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index 73bebae..3384353 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -31,8 +31,8 @@ class Shape(metaclass=ABCMeta): # [x_offset, y_offset] _offset = numpy.array([0.0, 0.0]) # type: numpy.ndarray - # Layer (integer >= 0) - _layer = 0 # type: int + # Layer (integer >= 0 or tuple) + _layer = 0 # type: int or Tuple # Dose _dose = 1.0 # type: float From 4fea49edef55d324784707dffeba1adfbb9843ef Mon Sep 17 00:00:00 2001 From: jan Date: Thu, 30 Aug 2018 23:07:14 -0700 Subject: [PATCH 083/124] Add missing file (support for labels) --- masque/label.py | 129 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 masque/label.py diff --git a/masque/label.py b/masque/label.py new file mode 100644 index 0000000..99f4bb3 --- /dev/null +++ b/masque/label.py @@ -0,0 +1,129 @@ +from typing import List, Tuple +import copy +import numpy +from numpy import pi + +from . import PatternError +from .utils import is_scalar, vector2 + + +__author__ = 'Jan Petykiewicz' + + +class Label: + """ + A circle, which has a position and radius. + """ + + # [x_offset, y_offset] + _offset = numpy.array([0.0, 0.0]) # type: numpy.ndarray + + # Layer (integer >= 0) + _layer = 0 # type: int or Tuple + + # Label string + _string = None # type: str + + # ---- Properties + # offset property + @property + def offset(self) -> numpy.ndarray: + """ + [x, y] offset + + :return: [x_offset, y_offset] + """ + 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() + + # layer property + @property + def layer(self) -> int or Tuple[int]: + """ + Layer number (int or tuple of ints) + + :return: Layer + """ + return self._layer + + @layer.setter + def layer(self, val: int or List[int]): + self._layer = val + + # string property + @property + def string(self) -> str: + """ + Label string (str) + + :return: string + """ + return self._string + + @string.setter + def string(self, val: str): + self._string = val + + def __init__(self, + string: str, + offset: vector2=(0.0, 0.0), + layer: int=0): + self.string = string + self.offset = numpy.array(offset, dtype=float) + self.layer = layer + + + # ---- Non-abstract methods + def copy(self) -> 'Label': + """ + Returns a deep copy of the shape. + + :return: Deep copy of self + """ + return copy.deepcopy(self) + + def translate(self, offset: vector2) -> 'Label': + """ + Translate the shape by the given offset + + :param offset: [x_offset, y,offset] + :return: self + """ + self.offset += offset + return self + + def rotate_around(self, pivot: vector2, rotation: float) -> 'Label': + """ + Rotate the shape around a point. + + :param pivot: Point (x, y) 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.translate(+pivot) + return self + + def get_bounds(self) -> numpy.ndarray: + """ + Return the bounds of the label. + + Labels are assumed to take up 0 area, i.e. + bounds = [self.offset, + self.offset] + + :return: Bounds [[xmin, xmax], [ymin, ymax]] + """ + return numpy.array([self.offset, self.offset]) + + From 44989905457cb022a48c1955088ac90360038dec Mon Sep 17 00:00:00 2001 From: jan Date: Thu, 30 Aug 2018 23:12:01 -0700 Subject: [PATCH 084/124] Move version string into __init__ --- masque/__init__.py | 2 ++ setup.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/masque/__init__.py b/masque/__init__.py index 1555475..c629ba2 100644 --- a/masque/__init__.py +++ b/masque/__init__.py @@ -32,3 +32,5 @@ from .pattern import Pattern __author__ = 'Jan Petykiewicz' + +version = '0.3' diff --git a/setup.py b/setup.py index 9e2b5ba..6405718 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,10 @@ #!/usr/bin/env python from setuptools import setup, find_packages +import masque setup(name='masque', - version='0.3', + version=masque.version, description='Lithography mask library', author='Jan Petykiewicz', author_email='anewusername@gmail.com', From d92645e413e75b53e17a2dc31f3ec06c64e3b92b Mon Sep 17 00:00:00 2001 From: jan Date: Thu, 30 Aug 2018 23:12:15 -0700 Subject: [PATCH 085/124] Add long description to setup.py --- setup.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/setup.py b/setup.py index 6405718..6772b91 100644 --- a/setup.py +++ b/setup.py @@ -3,9 +3,14 @@ from setuptools import setup, find_packages import masque +with open('README.md', 'r') as f: + long_description = f.read() + setup(name='masque', version=masque.version, description='Lithography mask library', + long_description=long_description, + long_description_content_type='text/markdown', author='Jan Petykiewicz', author_email='anewusername@gmail.com', url='https://mpxd.net/code/jan/masque', From f3aa27a7c400fbdda0ba2f0508a245b394aa2664 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 2 Sep 2018 20:01:25 -0700 Subject: [PATCH 086/124] add missing import --- masque/__init__.py | 2 +- masque/label.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/masque/__init__.py b/masque/__init__.py index c629ba2..e77b1e7 100644 --- a/masque/__init__.py +++ b/masque/__init__.py @@ -33,4 +33,4 @@ from .pattern import Pattern __author__ = 'Jan Petykiewicz' -version = '0.3' +version = '0.4' diff --git a/masque/label.py b/masque/label.py index 99f4bb3..b3bbb6f 100644 --- a/masque/label.py +++ b/masque/label.py @@ -4,7 +4,7 @@ import numpy from numpy import pi from . import PatternError -from .utils import is_scalar, vector2 +from .utils import is_scalar, vector2, rotation_matrix_2d __author__ = 'Jan Petykiewicz' From 7eda7ea873c73f1a5188db74741cae44dc16132b Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 16 Sep 2018 20:18:04 -0700 Subject: [PATCH 087/124] Clarify docs -- read returns a dict --- masque/file/gdsii.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 61f20ab..a62e437 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -255,9 +255,9 @@ def read_dtype2dose(filename: str) -> (List[Pattern], Dict[str, Any]): def read(filename: str, use_dtype_as_dose: bool = False, clean_vertices: bool = True, - ) -> (List[Pattern], Dict[str, Any]): + ) -> (Dict[str, Pattern], Dict[str, Any]): """ - Read a gdsii file and translate it into a list of Pattern objects. GDSII structures are + Read a gdsii file and translate it into a dict of Pattern objects. GDSII structures are translated into Pattern objects; boundaries are translated into polygons, and srefs and arefs are translated into SubPattern objects. @@ -268,7 +268,7 @@ def read(filename: str, :param clean_vertices: If true, remove any redundant vertices when loading polygons. The cleaning process removes any polygons with zero area or <3 vertices. Default True. - :return: Tuple: (List of Patterns generated GDSII structures, Dict of GDSII library info) + :return: Tuple: (Dict of pattern_name:Patterns generated from GDSII structures, Dict of GDSII library info) """ with open(filename, mode='rb') as stream: From 4323d81abda478c4fa589a5aa30d9fae7699e80e Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 16 Sep 2018 20:18:21 -0700 Subject: [PATCH 088/124] Change default written library name --- masque/file/gdsii.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index a62e437..c2a15e3 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -53,7 +53,7 @@ def write(patterns: Pattern or List[Pattern], """ # Create library lib = gdsii.library.Library(version=600, - name='masque-write_dose2dtype'.encode('ASCII'), + name='masque-gdsii-write'.encode('ASCII'), logical_unit=logical_units_per_unit, physical_unit=meters_per_unit) From 48b8087d0c043f0f41f6c3a985587fe115d98212 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 16 Sep 2018 20:19:28 -0700 Subject: [PATCH 089/124] Make read output consistent with write args --- masque/file/gdsii.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index c2a15e3..7fe3738 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -261,6 +261,12 @@ def read(filename: str, translated into Pattern objects; boundaries are translated into polygons, and srefs and arefs are translated into SubPattern objects. + Additional library info is returned in a dict, containing: + 'name': name of the library + 'meters_per_unit': number of meters per database unit (all values are in database units) + 'logical_units_per_unit': number of "logical" units displayed by layout tools (typically microns) + per database unit + :param filename: Filename specifying a GDSII file to read from. :param use_dtype_as_dose: If false, set each polygon's layer to (gds_layer, gds_datatype). If true, set the layer to gds_layer and the dose to gds_datatype. @@ -275,8 +281,8 @@ def read(filename: str, lib = gdsii.library.Library.load(stream) library_info = {'name': lib.name.decode('ASCII'), - 'physical_unit': lib.physical_unit, - 'logical_unit': lib.logical_unit, + 'meters_per_unit': lib.physical_unit, + 'logical_units_per_unit': lib.logical_unit, } def ref_element_to_subpat(element, offset: vector2) -> SubPattern: From 8b9d0fa2c97711e176c8ebf528fbd75f2b157875 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 16 Sep 2018 20:20:56 -0700 Subject: [PATCH 090/124] use python3 for setup --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6772b91..b12a255 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 from setuptools import setup, find_packages import masque From 832e3b46fa6f18c76d3c0cf7faa3f1820066742f Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 2 Sep 2018 21:05:18 -0700 Subject: [PATCH 091/124] Add general angle-to-parameter helper function and improve accuracy of to_polygons --- masque/shapes/arc.py | 57 ++++++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index c1dadea..5c81b35 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -170,14 +170,12 @@ class Arc(Shape): r0, r1 = self.radii # Convert from polar angle to ellipse parameter (for [rx*cos(t), ry*sin(t)] representation) - a0, a1 = (numpy.arctan2(r0*numpy.sin(a), r1*numpy.cos(a)) for a in self.angles) - sign = numpy.sign(self.angles[1] - self.angles[0]) - if sign != numpy.sign(a1 - a0): - a1 += sign * 2 * pi + a_ranges = self._angles_to_parameters() # Approximate perimeter # Ramanujan, S., "Modular Equations and Approximations to ," # Quart. J. Pure. Appl. Math., vol. 45 (1913-1914), pp. 350-372 + a0, a1 = a_ranges[1] # use outer arc h = ((r1 - r0) / (r1 + r0)) ** 2 ellipse_perimeter = pi * (r1 + r0) * (1 + 3 * h / (10 + math.sqrt(4 - 3 * h))) perimeter = abs(a0 - a1) / (2 * pi) * ellipse_perimeter # TODO: make this more accurate @@ -187,18 +185,20 @@ class Arc(Shape): n += [poly_num_points] if poly_max_arclen is not None: n += [perimeter / poly_max_arclen] - thetas = numpy.linspace(a1, a0, max(n), endpoint=True) + thetas_inner = numpy.linspace(a_ranges[0][1], a_ranges[0][0], max(n), endpoint=True) + thetas_outer = numpy.linspace(a_ranges[1][0], a_ranges[1][1], max(n), endpoint=True) - sin_th, cos_th = (numpy.sin(thetas), numpy.cos(thetas)) + sin_th_i, cos_th_i = (numpy.sin(thetas_inner), numpy.cos(thetas_inner)) + sin_th_o, cos_th_o = (numpy.sin(thetas_outer), numpy.cos(thetas_outer)) wh = self.width / 2.0 - xs1 = (r0 + wh) * cos_th - ys1 = (r1 + wh) * sin_th - xs2 = (r0 - wh) * cos_th - ys2 = (r1 - wh) * sin_th + xs1 = (r0 + wh) * cos_th_o + ys1 = (r1 + wh) * sin_th_o + xs2 = (r0 - wh) * cos_th_i + ys2 = (r1 - wh) * sin_th_i - xs = numpy.hstack((xs1, xs2[::-1])) - ys = numpy.hstack((ys1, ys2[::-1])) + xs = numpy.hstack((xs1, xs2)) + ys = numpy.hstack((ys1, ys2)) xys = numpy.vstack((xs, ys)).T poly = Polygon(xys, dose=self.dose, layer=self.layer, offset=self.offset) @@ -218,25 +218,20 @@ class Arc(Shape): If the extrema are innaccessible due to arc constraints, check the arc endpoints instead. ''' + a_ranges = self._angles_to_parameters() + mins = [] maxs = [] - for sgn in (+1, -1): + for a, sgn in zip(a_ranges, (-1, +1)): wh = sgn * self.width/2 rx = self.radius_x + wh ry = self.radius_y + wh - # Create paremeter 'a' for parametrized ellipse - a0, a1 = (numpy.arctan2(rx*numpy.sin(a), ry*numpy.cos(a)) for a in self.angles) - sign = numpy.sign(self.angles[1] - self.angles[0]) - if sign != numpy.sign(a1 - a0): - a1 += sign * 2 * pi - - a = numpy.array((a0, a1)) + a0, a1 = a a0_offset = a0 - (a0 % (2 * pi)) sin_r = numpy.sin(self.rotation) cos_r = numpy.cos(self.rotation) - tan_r = numpy.tan(self.rotation) sin_a = numpy.sin(a) cos_a = numpy.cos(a) @@ -316,3 +311,23 @@ class Arc(Shape): return (type(self), radii, angles, width, self.layer), \ (self.offset, scale/norm_value, rotation, self.dose), \ lambda: Arc(radii=radii*norm_value, angles=angles, width=width, layer=self.layer) + + def _angles_to_parameters(self) -> numpy.ndarray: + ''' + :return: "Eccentric anomaly" parameter ranges for the inner and outer edges, in the form + [[a_min_inner, a_max_inner], [a_min_outer, a_max_outer]] + ''' + a = [] + for sgn in (-1, +1): + wh = sgn * self.width/2 + rx = self.radius_x + wh + ry = self.radius_y + wh + + # create paremeter 'a' for parametrized ellipse + a0, a1 = (numpy.arctan2(rx*numpy.sin(a), ry*numpy.cos(a)) for a in self.angles) + sign = numpy.sign(self.angles[1] - self.angles[0]) + if sign != numpy.sign(a1 - a0): + a1 += sign * 2 * pi + + a.append((a0, a1)) + return numpy.array(a) From f3115baabedc0a84191ebe4591ae4e5e3a571598 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 2 Sep 2018 22:40:20 -0700 Subject: [PATCH 092/124] Add get_cap_edges() --- masque/shapes/arc.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 5c81b35..701c7cc 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -312,6 +312,33 @@ class Arc(Shape): (self.offset, scale/norm_value, rotation, self.dose), \ lambda: Arc(radii=radii*norm_value, angles=angles, width=width, layer=self.layer) + def get_cap_edges(self) -> numpy.ndarray: + ''' + :returns: [[[x0, y0], [x1, y1]], array of 4 points, specifying the two cuts which + [[x2, y2], [x3, y3]]], would create this arc from its corresponding ellipse. + ''' + a_ranges = self._angles_to_parameters() + + mins = [] + maxs = [] + for a, sgn in zip(a_ranges, (-1, +1)): + wh = sgn * self.width/2 + rx = self.radius_x + wh + ry = self.radius_y + wh + + sin_r = numpy.sin(self.rotation) + cos_r = numpy.cos(self.rotation) + sin_a = numpy.sin(a) + cos_a = numpy.cos(a) + + # arc endpoints + xn, xp = sorted(rx * cos_r * cos_a - ry * sin_r * sin_a) + yn, yp = sorted(rx * sin_r * cos_a + ry * cos_r * sin_a) + + mins.append([xn, yn]) + maxs.append([xp, yp]) + return numpy.array([mins, maxs]) + self.offset + def _angles_to_parameters(self) -> numpy.ndarray: ''' :return: "Eccentric anomaly" parameter ranges for the inner and outer edges, in the form From d17cf5b6be33b1b89eceae2b38f26aec85e2e7c4 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 2 Sep 2018 22:46:11 -0700 Subject: [PATCH 093/124] label should have a list of 1 point --- masque/file/gdsii.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 61f20ab..7ed3352 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -86,10 +86,10 @@ def write(patterns: Pattern or List[Pattern], xy=xy_closed)) for label in pat.labels: layer, text_type = _mlayer2gds(label.layer) - xy_closed = numpy.round([label.offset, label.offset]).astype(int) + xy= numpy.round([label.offset]).astype(int) structure.append(gdsii.elements.Text(layer=layer, text_type=text_type, - xy=xy_closed, + xy=xy, string=label.string.encode('ASCII'))) # Add an SREF for each subpattern entry @@ -211,10 +211,10 @@ def write_dose2dtype(patterns: Pattern or List[Pattern], xy=xy_closed)) for label in pat.labels: layer, text_type = _mlayer2gds(label.layer) - xy_closed = numpy.round([label.offset, label.offset]).astype(int) + xy = numpy.round([label.offset]).astype(int) structure.append(gdsii.elements.Text(layer=layer, text_type=text_type, - xy=xy_closed, + xy=xy, string=label.string.encode('ASCII'))) # Add an SREF for each subpattern entry From 64cb1ced346aa439d095d4a9d1edd336024ccd4a Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 28 Oct 2018 13:31:23 -0700 Subject: [PATCH 094/124] fix spacing (cosmetic) --- masque/file/gdsii.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 37559b7..0bc9656 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -86,7 +86,7 @@ def write(patterns: Pattern or List[Pattern], xy=xy_closed)) for label in pat.labels: layer, text_type = _mlayer2gds(label.layer) - xy= numpy.round([label.offset]).astype(int) + xy = numpy.round([label.offset]).astype(int) structure.append(gdsii.elements.Text(layer=layer, text_type=text_type, xy=xy, @@ -211,7 +211,7 @@ def write_dose2dtype(patterns: Pattern or List[Pattern], xy=xy_closed)) for label in pat.labels: layer, text_type = _mlayer2gds(label.layer) - xy = numpy.round([label.offset]).astype(int) + xy = numpy.round([label.offset]).astype(int) structure.append(gdsii.elements.Text(layer=layer, text_type=text_type, xy=xy, From b295c318a9d9556c90b1186c6eacf72afd1bcfb1 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 28 Oct 2018 13:32:04 -0700 Subject: [PATCH 095/124] Fix arc bounding box calculation --- masque/shapes/arc.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 701c7cc..74f0ec0 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -237,33 +237,31 @@ class Arc(Shape): # Cutoff angles xpt = (-self.rotation) % (2 * pi) + a0_offset - ypt = self.rotation % (2 * pi) + a0_offset + ypt = (pi/2 - self.rotation) % (2 * pi) + a0_offset xnt = (xpt - pi) % (2 * pi) + a0_offset ynt = (ypt - pi) % (2 * pi) + a0_offset # Points along coordinate axes - xs = rx * sin_r - yc = ry * cos_r - sin_ax = yc / (yc * yc + xs * xs) - cos_ax = xs / (yc * yc + xs * xs) - xr = rx * cos_r * cos_ax - ry * sin_r * sin_ax - yr = ry * sin_r * cos_ax + ry * cos_r * sin_ax + rx2_inv = 1 / (rx * rx) + ry2_inv = 1 / (ry * ry) + xr = numpy.abs(cos_r * cos_r * rx2_inv + sin_r * sin_r * ry2_inv) ** -0.5 + yr = numpy.abs(-sin_r * -sin_r * rx2_inv + cos_r * cos_r * ry2_inv) ** -0.5 # Arc endpoints xn, xp = sorted(rx * cos_r * cos_a - ry * sin_r * sin_a) yn, yp = sorted(rx * sin_r * cos_a + ry * cos_r * sin_a) - # If - if a0 < xpt < a1: + # If our arc subtends a coordinate axis, use the extremum along that axis + if a0 < xpt < a1 or a0 < xpt + 2 * pi < a1: xp = xr - if a0 < xnt < a1: + if a0 < xnt < a1 or a0 < xnt + 2 * pi < a1: xn = -xr - if a0 < ypt < a1: + if a0 < ypt < a1 or a0 < ypt + 2 * pi < a1: yp = yr - if a0 < ynt < a1: + if a0 < ynt < a1 or a0 < ynt + 2 * pi < a1: yn = -yr mins.append([xn, yn]) From df5c61f1d4280444552a78be060b37d0331c35fe Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 28 Oct 2018 13:34:09 -0700 Subject: [PATCH 096/124] Clarify that rectangle/square is centered on the origin Eventually, should allow arbitrary limit combination (rather than center + width) --- masque/shapes/polygon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index f8e968a..85282a4 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -87,7 +87,7 @@ class Polygon(Shape): dose: float=1.0 ) -> 'Polygon': """ - Draw a square given side_length + Draw a square given side_length, centered on the origin. :param side_length: Length of one side :param rotation: Rotation counterclockwise, in radians @@ -114,7 +114,7 @@ class Polygon(Shape): dose: float=1.0 ) -> 'Polygon': """ - Draw a rectangle with side lengths lx and ly + Draw a rectangle with side lengths lx and ly, centered on the origin. :param lx: Length along x (before rotation) :param ly: Length along y (before rotation) From 8dfd856e18561ec10942614773f096a894c78f60 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 28 Oct 2018 13:34:37 -0700 Subject: [PATCH 097/124] Subtattern .as_pattern() should deepcopy --- masque/subpattern.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/subpattern.py b/masque/subpattern.py index 231d4e3..2ff0033 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -112,7 +112,7 @@ class SubPattern: SubPattern's properties. :return: Copy of self.pattern that has been altered to reflect the SubPattern's properties. """ - pattern = self.pattern.copy() + pattern = self.pattern.deepcopy() pattern.scale_by(self.scale) [pattern.mirror(ax) for ax, do in enumerate(self.mirrored) if do] pattern.rotate_around((0.0, 0.0), self.rotation) From ab483fc9d46eee2a5f4f10be91fd6190b0353a2b Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 13 Nov 2018 23:32:26 -0800 Subject: [PATCH 098/124] Remove pyclipper dependency; remove shape.cut() --- README.md | 1 - masque/__init__.py | 1 - masque/shapes/polygon.py | 39 ++++++++++++++++++++++++++------ masque/shapes/shape.py | 49 ---------------------------------------- setup.py | 1 - 5 files changed, 32 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index d2b7e35..6851d70 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,6 @@ E-beam doses, and the ability to output to multiple formats. Requirements: * python >= 3.5 (written and tested with 3.6) * numpy -* pyclipper * matplotlib (optional, used for visualization functions and text) * python-gdsii (optional, used for gdsii i/o) * svgwrite (optional, used for svg output) diff --git a/masque/__init__.py b/masque/__init__.py index e77b1e7..a2a744c 100644 --- a/masque/__init__.py +++ b/masque/__init__.py @@ -18,7 +18,6 @@ Dependencies: - numpy - - pyclipper - matplotlib [Pattern.visualize(...)] - python-gdsii [masque.file.gdsii] - svgwrite [masque.file.svgwrite] diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 85282a4..9fd10be 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -2,8 +2,6 @@ from typing import List import copy import numpy from numpy import pi -import pyclipper -from pyclipper import scale_to_clipper, scale_from_clipper from . import Shape, normalized_shape_tuple from .. import PatternError @@ -185,11 +183,38 @@ class Polygon(Shape): :returns: self """ - self.vertices = scale_from_clipper( - pyclipper.CleanPolygon( - scale_to_clipper( - self.vertices - ))) + self.remove_colinear_vertices() return self + def remove_duplicate_vertices(self) -> 'Polygon' + ''' + Removes all consecutive duplicate (repeated) vertices. + + :returns: self + ''' + duplicates = (self.vertices == numpy.roll(self.vertices, 1, axis=0)).all(axis=1) + self.vertices = self.vertices[~duplicates] + return self + + def remove_colinear_vertices(self) -> 'Polygon' + ''' + Removes consecutive co-linear vertices. + + :returns: self + ''' + dv0 = numpy.roll(self.vertices, 1, axis=0) - self.vertices + dv1 = numpy.roll(dv0, -1, axis=0) + + # find cases where at least one coordinate is 0 in successive dv's + eq = dv1 == dv0 + aa_colinear = numpy.logical_and(eq, dv0 == 0).any(axis=1) + + # find cases where slope is equal + with numpy.errstate(divide='ignore', invalid='ignore'): # don't care about zeroes + slope_quotient = (dv0[:, 0] * dv1[:, 1]) / (dv1[:, 0] * dv0[:, 1]) + slopes_equal = numpy.abs(slope_quotient - 1) < 1e-14 + + colinear = numpy.logical_or(aa_colinear, slopes_equal) + self.vertices = self.vertices[~colinear] + return self diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index 3384353..13ffa28 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -3,9 +3,6 @@ from abc import ABCMeta, abstractmethod import copy import numpy -import pyclipper -from pyclipper import scale_to_clipper, scale_from_clipper - from .. import PatternError from ..utils import is_scalar, rotation_matrix_2d, vector2 @@ -369,49 +366,3 @@ class Shape(metaclass=ABCMeta): return manhattan_polygons - def cut(self, - cut_xs: numpy.ndarray = None, - cut_ys: numpy.ndarray = None - ) -> List['Polygon']: - """ - Decomposes the shape into a list of constituent polygons by polygonizing and - then cutting along the specified x and/or y coordinates. - - :param cut_xs: list of x-coordinates to cut along (e.g., [1, 1.4, 6]) - :param cut_ys: list of y-coordinates to cut along (e.g., [1, 3, 5.4]) - :return: List of Polygon objects - """ - from . import Polygon - - clipped_shapes = [] - for polygon in self.to_polygons(): - min_x, min_y = numpy.min(polygon.vertices, axis=0) - max_x, max_y = numpy.max(polygon.vertices, axis=0) - range_x = max_x - min_x - range_y = max_y - min_y - - edge_xs = (min_x - range_x - 1,) + tuple(cut_xs) + (max_x + range_x + 1,) - edge_ys = (min_y - range_y - 1,) + tuple(cut_ys) + (max_y + range_y + 1,) - - for i in range(2): - for j in range(2): - clipper = pyclipper.Pyclipper() - clipper.AddPath(scale_to_clipper(polygon.vertices), pyclipper.PT_SUBJECT, True) - - for start_x, stop_x in zip(edge_xs[i::2], edge_xs[(i+1)::2]): - for start_y, stop_y in zip(edge_ys[j::2], edge_ys[(j+1)::2]): - clipper.AddPath(scale_to_clipper(( - (start_x, start_y), - (start_x, stop_y), - (stop_x, stop_y), - (stop_x, start_y), - )), pyclipper.PT_CLIP, True) - - clipped_parts = scale_from_clipper(clipper.Execute(pyclipper.CT_INTERSECTION, - pyclipper.PFT_EVENODD, - pyclipper.PFT_EVENODD)) - for part in clipped_parts: - poly = polygon.copy() - poly.vertices = part - clipped_shapes.append(poly) - return clipped_shapes diff --git a/setup.py b/setup.py index b12a255..e3af502 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,6 @@ setup(name='masque', packages=find_packages(), install_requires=[ 'numpy', - 'pyclipper', ], extras_require={ 'visualization': ['matplotlib'], From 5bce2005b63e54b78c3ac5fad5c0f92511c79804 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 13 Nov 2018 23:32:44 -0800 Subject: [PATCH 099/124] Add docstring for manhattanize_fast --- masque/shapes/shape.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index 13ffa28..45eb35f 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -196,6 +196,19 @@ class Shape(metaclass=ABCMeta): return self def manhattanize_fast(self, grid_x: numpy.ndarray, grid_y: numpy.ndarray) -> List['Polygon']: + """ + Returns a list of polygons with grid-aligned ("Manhattan") edges approximating the shape. + + This function works by + 1) Converting the shape to polygons using .to_polygons() + 2) Approximating each edge with an equivalent Manhattan edge + This process results in a reasonable Manhattan representation of the shape, but is + imprecise near non-Manhattan or off-grid corners. + + :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: List of Polygon objects with grid-aligned edges. + """ from . import Polygon grid_x = numpy.unique(grid_x) From 3ebb87bfc704ab7ce5cab43e1e172859f7d519fe Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 13 Nov 2018 23:33:16 -0800 Subject: [PATCH 100/124] fix location of svgwrite dependency --- masque/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/__init__.py b/masque/__init__.py index a2a744c..0112287 100644 --- a/masque/__init__.py +++ b/masque/__init__.py @@ -20,7 +20,7 @@ - numpy - matplotlib [Pattern.visualize(...)] - python-gdsii [masque.file.gdsii] - - svgwrite [masque.file.svgwrite] + - svgwrite [masque.file.svg] """ from .error import PatternError From ef305cbac920c976c30b2285a1339cfdb2f31f2a Mon Sep 17 00:00:00 2001 From: jan Date: Fri, 23 Nov 2018 18:09:14 -0800 Subject: [PATCH 101/124] Fix syntax --- masque/shapes/polygon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 9fd10be..554d71c 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -186,7 +186,7 @@ class Polygon(Shape): self.remove_colinear_vertices() return self - def remove_duplicate_vertices(self) -> 'Polygon' + def remove_duplicate_vertices(self) -> 'Polygon': ''' Removes all consecutive duplicate (repeated) vertices. @@ -196,7 +196,7 @@ class Polygon(Shape): self.vertices = self.vertices[~duplicates] return self - def remove_colinear_vertices(self) -> 'Polygon' + def remove_colinear_vertices(self) -> 'Polygon': ''' Removes consecutive co-linear vertices. From eb6a5d8e8ccb52c6ef56f835049a6812c59cca76 Mon Sep 17 00:00:00 2001 From: jan Date: Fri, 23 Nov 2018 18:31:56 -0800 Subject: [PATCH 102/124] Add shapes.Polygon.rect() for simpler construction of various axis-aligned rectangles --- masque/shapes/polygon.py | 75 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 554d71c..4bc2a14 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -130,6 +130,81 @@ class Polygon(Shape): poly.rotate(rotation) return poly + @staticmethod + def rect(xmin: float = None, + xctr: float = None, + xmax: float = None, + lx: float = None, + ymin: float = None, + yctr: float = None, + ymax: float = None, + ly: float = None, + layer: int = 0, + dose: float = 1.0 + ) -> 'Polygon': + """ + Draw a rectangle by specifying side/center positions. + + Must provide 2 of (xmin, xctr, xmax, lx), + and 2 of (ymin, yctr, ymax, ly). + + :param xmin: Minimum x coordinate + :param xctr: Center x coordinate + :param xmax: Maximum x coordinate + :param lx: Length along x direction + :param ymin: Minimum y coordinate + :param yctr: Center y coordinate + :param ymax: Maximum y coordinate + :param yx: Length along y direction + :param layer: Layer, default 0 + :param dose: Dose, default 1.0 + :return: A Polygon object containing the requested rectangle + """ + if lx is None: + if xctr is None: + xctr = 0.5 * (xmax + xmin) + lx = xmax - xmin + elif xmax is None: + lx = 2 * (xctr - xmin) + elif xmin is None: + lx = 2 * (xmax - xctr) + else: + raise PatternError('Two of xmin, xctr, xmax, lx must be None!') + else: + if xctr is not None: + pass + elif xmax is None: + xctr = xmin + 0.5 * lx + elif xmin is None: + xctr = xmax - 0.5 * lx + else: + raise PatternError('Two of xmin, xctr, xmax, lx must be None!') + + if ly is None: + if yctr is None: + yctr = 0.5 * (ymax + ymin) + ly = ymax - ymin + elif ymax is None: + ly = 2 * (yctr - ymin) + elif ymin is None: + ly = 2 * (ymax - yctr) + else: + raise PatternError('Two of ymin, yctr, ymax, ly must be None!') + else: + if yctr is not None: + pass + elif ymax is None: + yctr = ymin + 0.5 * ly + elif ymin is None: + yctr = ymax - 0.5 * ly + else: + raise PatternError('Two of ymin, yctr, ymax, ly must be None!') + + poly = Polygon.rectangle(lx, ly, offset=(xctr, yctr), + layer=layer, dose=dose) + return poly + + def to_polygons(self, _poly_num_points: int=None, _poly_max_arclen: float=None, From 539198435cbd007a0acae95e86cadc726636d291 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 31 Mar 2019 14:13:12 -0700 Subject: [PATCH 103/124] Add .copy() and .deepcopy() convenience methods --- masque/subpattern.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/masque/subpattern.py b/masque/subpattern.py index 2ff0033..811c670 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -4,6 +4,7 @@ """ from typing import Union, List +import copy import numpy from numpy import pi @@ -183,3 +184,19 @@ class SubPattern: """ self.scale *= c return self + + def copy(self) -> 'SubPattern': + """ + Return a shallow copy of the subpattern. + + :return: copy.copy(self) + """ + return copy.copy(self) + + def deepcopy(self) -> 'SubPattern': + """ + Return a deep copy of the subpattern. + + :return: copy.copy(self) + """ + return copy.deepcopy(self) From c50bd8e148c07e68e33e7edcd173befe28b4d86e Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 31 Mar 2019 20:57:10 -0700 Subject: [PATCH 104/124] Add GridRepetition: a SubPattern-like object which implements regular spatial arrays. Also rework masque.file.gdsii to consolidate write() and write_dose2dtype() --- masque/__init__.py | 1 + masque/file/gdsii.py | 329 ++++++++++++++++++++++++++----------------- masque/pattern.py | 3 +- masque/repetition.py | 291 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 492 insertions(+), 132 deletions(-) create mode 100644 masque/repetition.py diff --git a/masque/__init__.py b/masque/__init__.py index 0112287..ead84c5 100644 --- a/masque/__init__.py +++ b/masque/__init__.py @@ -27,6 +27,7 @@ from .error import PatternError from .shapes import Shape from .label import Label from .subpattern import SubPattern +from .repetition import GridRepetition from .pattern import Pattern diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 0bc9656..7e512c0 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -6,12 +6,12 @@ import gdsii.library import gdsii.structure import gdsii.elements -from typing import List, Any, Dict +from typing import List, Any, Dict, Tuple import re import numpy 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 ..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) lib.append(structure) - # Add a Boundary element for each shape - for shape in 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'))) + structure += _shapes_to_boundaries(pat.shapes) - # Add an SREF for each subpattern entry - # strans must be set for angle and mag to take effect - for subpat in pat.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)) - 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) + structure += _labels_to_texts(pat.labels) + + # Add an SREF / AREF for each subpattern entry + structure += _subpatterns_to_refs(pat.subpatterns) with open(filename, mode='wb') as 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) and dose (float, list entry). """ - # Create library - lib = gdsii.library.Library(version=600, - name='masque-write_dose2dtype'.encode('ASCII'), - logical_unit=logical_units_per_unit, - physical_unit=meters_per_unit) + patterns, dose_vals = dose2dtype(patterns) + write(patterns, filename, meters_per_unit, logical_units_per_unit) + return dose_vals + +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): patterns = [patterns] @@ -183,66 +170,36 @@ def write_dose2dtype(patterns: Pattern or List[Pattern], if len(dose_vals) > 256: raise PatternError('Too many dose values: {}, maximum 256 when using dtypes.'.format(len(dose_vals))) - dose_vals_list = list(dose_vals) - - # Now create a structure for each row in sd_table (ie, each pattern + dose combination) - # and add in any Boundary and SREF elements + # Create a new pattern for each non-1-dose entry in the dose table + # and update the shapes to reflect their new dose + new_pats = {} # (id, dose) -> new_pattern mapping 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') if len(encoded_name) == 0: 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 polygon in shape.to_polygons(): - data_type = dose_vals_list.index(polygon.dose * pat_dose) - xy_open = numpy.round(polygon.vertices + polygon.offset).astype(int) - xy_closed = numpy.vstack((xy_open, xy_open[0, :])) - if is_scalar(polygon.layer): - 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'))) + data_type = dose_vals_list.index(shape.dose * pat_dose) + if is_scalar(shape.layer): + layer = (shape.layer, data_type) + else: + layer = (shape.layer[0], data_type) - # Add an SREF for each subpattern entry - # strans must be set for angle and mag to take effect + new_pats[(pat_id, pat_dose)] = pat + + # 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: dose_mult = subpat.dose * pat_dose - encoded_name = mangle_name(subpat.pattern, dose_mult).encode('ASCII') - 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) + subpat.pattern = new_pats[(id(subpat.pattern), dose_mult)] - with open(filename, mode='wb') as stream: - lib.save(stream) - - return dose_vals_list + return patterns, list(dose_vals) 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, } - 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 = [] for structure in lib: pat = Pattern(name=structure.name.decode('ASCII')) @@ -341,18 +270,10 @@ def read(filename: str, pat.labels.append(label) 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): - xy = numpy.array(element.xy) - 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)) + pat.subpatterns.append(_aref_to_gridrep(element)) patterns.append(pat) @@ -378,3 +299,149 @@ def _mlayer2gds(mlayer): else: data_type = 0 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 diff --git a/masque/pattern.py b/masque/pattern.py index 4c2807a..36c083b 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -12,6 +12,7 @@ import numpy # .visualize imports matplotlib and matplotlib.collections from .subpattern import SubPattern +from .repetition import GridRepetition from .shapes import Shape, Polygon from .label import Label from .utils import rotation_matrix_2d, vector2 @@ -34,7 +35,7 @@ class Pattern: """ shapes = None # type: List[Shape] labels = None # type: List[Labels] - subpatterns = None # type: List[SubPattern] + subpatterns = None # type: List[SubPattern or GridRepetition] name = None # type: str def __init__(self, diff --git a/masque/repetition.py b/masque/repetition.py new file mode 100644 index 0000000..cc30d20 --- /dev/null +++ b/masque/repetition.py @@ -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) + From bc557a54b7a93d563a7697c19d630d4595c35ae4 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 31 Mar 2019 20:57:18 -0700 Subject: [PATCH 105/124] fix typo in comment --- masque/shapes/polygon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 4bc2a14..9f52af8 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -155,7 +155,7 @@ class Polygon(Shape): :param ymin: Minimum y coordinate :param yctr: Center y coordinate :param ymax: Maximum y coordinate - :param yx: Length along y direction + :param ly: Length along y direction :param layer: Layer, default 0 :param dose: Dose, default 1.0 :return: A Polygon object containing the requested rectangle From 023aea15e33920e115c4ca4e2364dda6c513eae5 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 7 Apr 2019 17:24:53 -0700 Subject: [PATCH 106/124] add classifiers --- setup.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/setup.py b/setup.py index e3af502..e06360a 100644 --- a/setup.py +++ b/setup.py @@ -24,5 +24,20 @@ setup(name='masque', 'svg': ['svgwrite'], 'text': ['freetype-py', 'matplotlib'], }, + classifiers=[ + 'Programming Language :: Python :: 3', + 'Development Status :: 4 - Beta', + 'Environment :: Other Environment', + 'Intended Audience :: Developers', + 'Intended Audience :: Information Technology', + 'Intended Audience :: Manufacturing', + 'Intended Audience :: Science/Research', + 'License :: OSI Approved :: GNU Affero General Public License v3', + 'Operating System :: POSIX :: Linux', + 'Operating System :: Microsoft :: Windows', + 'Topic :: Scientific/Engineering', + 'Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], ) From f25c090bc4a40872f2cc1f417c4363b3856ba47f Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 7 Apr 2019 17:26:22 -0700 Subject: [PATCH 107/124] add MANIFEST.in --- MANIFEST.in | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..c28ab72 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include README.md +include LICENSE.md From 628845ca4b952d08cecbefcb29e5e2b5df4eaf7a Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 7 Apr 2019 17:52:44 -0700 Subject: [PATCH 108/124] Bump version to 0.5 --- masque/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/__init__.py b/masque/__init__.py index ead84c5..acf7cbc 100644 --- a/masque/__init__.py +++ b/masque/__init__.py @@ -33,4 +33,4 @@ from .pattern import Pattern __author__ = 'Jan Petykiewicz' -version = '0.4' +version = '0.5' From 57bdb00d8888002ae01f77f836a85077fbf6b6fe Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 7 Apr 2019 17:53:16 -0700 Subject: [PATCH 109/124] Update README to reflect upload to pypi --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6851d70..a75f3d5 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,14 @@ Requirements: * freetype (optional, used for text) -Install with pip, via git: +Install with pip: ```bash -pip install git+https://mpxd.net/code/jan/masque.git@release +pip3 install masque +``` + +Alternatively, install from git +```bash +pip3 install git+https://mpxd.net/code/jan/masque.git@release ``` ## TODO From 877add3e45f63aa5a3052edc031fed53097e7248 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 7 Apr 2019 17:53:22 -0700 Subject: [PATCH 110/124] Add classifiers --- setup.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.py b/setup.py index e06360a..6cda402 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,6 @@ setup(name='masque', classifiers=[ 'Programming Language :: Python :: 3', 'Development Status :: 4 - Beta', - 'Environment :: Other Environment', 'Intended Audience :: Developers', 'Intended Audience :: Information Technology', 'Intended Audience :: Manufacturing', @@ -35,9 +34,8 @@ setup(name='masque', 'License :: OSI Approved :: GNU Affero General Public License v3', 'Operating System :: POSIX :: Linux', 'Operating System :: Microsoft :: Windows', - 'Topic :: Scientific/Engineering', 'Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)', - 'Topic :: Software Development :: Libraries :: Python Modules', + 'Topic :: Scientific/Engineering :: Visualization', ], ) From cd1a1173c1768d87955e810f259864c496bb44e0 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 7 Apr 2019 17:58:11 -0700 Subject: [PATCH 111/124] add pypi link to readme --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index a75f3d5..150481f 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ The general idea is to implement something resembling the GDSII file-format, but with some vectorized element types (eg. circles, not just polygons), better support for E-beam doses, and the ability to output to multiple formats. +- [Source repository](https://mpxd.net/code/jan/masque) +- [PyPi](https://pypi.org/project/masque) + + ## Installation Requirements: @@ -28,9 +32,12 @@ pip3 install git+https://mpxd.net/code/jan/masque.git@release ``` ## TODO + * Mirroring * Polygon de-embedding + ### Maybe + * Construct from bitmap * Boolean operations on polygons (using pyclipper) * Output to OASIS (using fatamorgana) From b1ac39094e791ed17780c7e8b27b05c66b91b262 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 11 Apr 2019 18:40:09 -0700 Subject: [PATCH 112/124] gitignore build artifacts --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 715503a..3ef4b5d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ *.pyc __pycache__ *.idea +build/ +dist/ +*.egg-info/ From 90a068da809fd44d233fbc73533b8e9ae2b2d71c Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 13 Apr 2019 17:33:04 -0700 Subject: [PATCH 113/124] tabs-to-spaces --- masque/pattern.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/pattern.py b/masque/pattern.py index 36c083b..26d7293 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -448,7 +448,7 @@ class 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() + See also: Pattern.deepcopy() :return: A copy of the current Pattern. """ From bc723d96f396628173963ac73aa2ec6f0a430d93 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 13 Apr 2019 17:33:15 -0700 Subject: [PATCH 114/124] Counts should be ints --- masque/file/gdsii.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 7e512c0..427e516 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -398,8 +398,8 @@ def _subpatterns_to_refs(subpatterns: List[SubPattern or GridRepetition] ] ref = gdsii.elements.ARef(struct_name=encoded_name, xy=numpy.round(xy).astype(int), - cols=subpat.a_count, - rows=subpat.b_count) + cols=numpy.round(subpat.a_count).astype(int), + rows=numpy.round(subpat.b_count).astype(int)) else: ref = gdsii.elements.SRef(struct_name=encoded_name, xy=numpy.round([subpat.offset]).astype(int)) From f7ce17c29395aa24e6b85adb21db8db09c1e9f5d Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 13 Apr 2019 17:34:01 -0700 Subject: [PATCH 115/124] Fix mirroring --- masque/file/gdsii.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 427e516..8be0539 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -407,7 +407,7 @@ def _subpatterns_to_refs(subpatterns: List[SubPattern or GridRepetition] ref.strans = 0 ref.angle = subpat.rotation * 180 / numpy.pi mirror_x, mirror_y = subpat.mirrored - if mirror_y and mirror_y: + if mirror_x and mirror_y: ref.angle += 180 elif mirror_x: ref.strans = set_bit(ref.strans, 15 - 0, True) From 3815069136a87b821eeed98de7b5b4e1875d0dd6 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 13 Apr 2019 17:34:28 -0700 Subject: [PATCH 116/124] Fix out-of-range angles --- masque/file/gdsii.py | 1 + 1 file changed, 1 insertion(+) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 8be0539..a9b7e98 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -414,6 +414,7 @@ def _subpatterns_to_refs(subpatterns: List[SubPattern or GridRepetition] elif mirror_y: ref.angle += 180 ref.strans = set_bit(ref.strans, 15 - 0, True) + ref.angle %= 360 ref.mag = subpat.scale refs.append(ref) From 3094aa4043b786f78edd2dae362376c835e1573e Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 13 Apr 2019 21:10:08 -0700 Subject: [PATCH 117/124] Automatically disambiguate repeated pattern names. Also check for >32 char names --- masque/file/gdsii.py | 45 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index a9b7e98..1e618ca 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -9,6 +9,9 @@ import gdsii.elements from typing import List, Any, Dict, Tuple import re import numpy +import base64 +import struct +import logging from .utils import mangle_name, make_dose_table from .. import Pattern, SubPattern, GridRepetition, PatternError, Label, Shape @@ -19,6 +22,9 @@ from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar __author__ = 'Jan Petykiewicz' +logger = logging.getLogger(__name__) + + def write(patterns: Pattern or List[Pattern], filename: str, meters_per_unit: float, @@ -65,13 +71,11 @@ def write(patterns: Pattern or List[Pattern], for pattern in patterns: patterns_by_id.update(pattern.referenced_patterns_by_id()) + _disambiguate_pattern_names(patterns_by_id.values()) + # Now create a structure for each pattern, and add in any Boundary and SREF elements for pat in patterns_by_id.values(): - sanitized_name = re.compile('[^A-Za-z0-9_\?\$]').sub('_', pat.name) - encoded_name = sanitized_name.encode('ASCII') - if len(encoded_name) == 0: - raise PatternError('Zero-length name after sanitize+encode, originally "{}"'.format(pat.name)) - structure = gdsii.structure.Structure(name=encoded_name) + structure = gdsii.structure.Structure(name=pat.name) lib.append(structure) # Add a Boundary element for each shape @@ -384,10 +388,7 @@ def _subpatterns_to_refs(subpatterns: List[SubPattern or GridRepetition] # 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)) + encoded_name = subpat.pattern.name if isinstance(subpat, GridRepetition): mirror_signs = (-1) ** numpy.array(subpat.mirrored) @@ -446,3 +447,29 @@ def _labels_to_texts(labels: List[Label]) -> List[gdsii.elements.Text]: xy=xy, string=label.string.encode('ASCII'))) return texts + + +def _disambiguate_pattern_names(patterns): + used_names = [] + for pat in patterns: + sanitized_name = re.compile('[^A-Za-z0-9_\?\$]').sub('_', pat.name) + + i = 0 + suffixed_name = sanitized_name + while suffixed_name in used_names: + suffix = base64.b64encode(struct.pack('>Q', i), b'$?').decode('ASCII') + + suffixed_name = sanitized_name + '$' + suffix[:-1].lstrip('A') + i += 1 + + if suffixed_name != sanitized_name: + logger.warning('Pattern name "{}" appears multiple times; renaming to "{}"'.format(pat.name, suffixed_name)) + + encoded_name = sanitized_name.encode('ASCII') + if len(encoded_name) == 0: + raise PatternError('Zero-length name after sanitize+encode, originally "{}"'.format(pat.name)) + if len(encoded_name) > 32: + raise PatternError('Pattern name "{}" length > 32 after encode, originally "{}"'.format(encoded_name, pat.name)) + + pat.name = encoded_name + used_names.append(suffixed_name) From 5e6485f410809a886088d560c0d6d76434dcf5ce Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 15 Apr 2019 22:43:03 -0700 Subject: [PATCH 118/124] allow setting library name --- masque/file/gdsii.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 1e618ca..85d77ea 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -28,7 +28,8 @@ logger = logging.getLogger(__name__) def write(patterns: Pattern or List[Pattern], filename: str, meters_per_unit: float, - logical_units_per_unit: float = 1): + logical_units_per_unit: float = 1, + library_name: str = 'masque-gdsii-write'): """ Write a Pattern or list of patterns to a GDSII file, by first calling .polygonize() to change the shapes into polygons, and then writing patterns @@ -56,10 +57,12 @@ def write(patterns: Pattern or List[Pattern], :param logical_units_per_unit: Written into the GDSII file. Allows the GDSII to specify a "logical" unit which is different from the "database" unit, for display purposes. Default 1. + :param library_name: Library name written into the GDSII file. + Default 'masque-gdsii-write'. """ # Create library lib = gdsii.library.Library(version=600, - name='masque-gdsii-write'.encode('ASCII'), + name=library_name.encode('ASCII'), logical_unit=logical_units_per_unit, physical_unit=meters_per_unit) @@ -93,7 +96,8 @@ def write(patterns: Pattern or List[Pattern], def write_dose2dtype(patterns: Pattern or List[Pattern], filename: str, meters_per_unit: float, - logical_units_per_unit: float = 1 + *args, + **kwargs, ) -> List[float]: """ Write a Pattern or list of patterns to a GDSII file, by first calling @@ -121,14 +125,13 @@ def write_dose2dtype(patterns: Pattern or List[Pattern], :param filename: Filename to write to. :param meters_per_unit: Written into the GDSII file, meters per (database) length unit. All distances are assumed to be an integer multiple of this unit, and are stored as such. - :param logical_units_per_unit: Written into the GDSII file. Allows the GDSII to specify a - "logical" unit which is different from the "database" unit, for display purposes. - Default 1. + :param args: passed to masque.file.gdsii.write(). + :param kwargs: passed to masque.file.gdsii.write(). :returns: A list of doses, providing a mapping between datatype (int, list index) and dose (float, list entry). """ patterns, dose_vals = dose2dtype(patterns) - write(patterns, filename, meters_per_unit, logical_units_per_unit) + write(patterns, filename, meters_per_unit, *args, **kwargs) return dose_vals From 11bbf6aa0b402bfafeb44491bc18c8a49db0d2e4 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 16 Apr 2019 00:41:18 -0700 Subject: [PATCH 119/124] Fix auto-renaming for structures --- masque/file/gdsii.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 85d77ea..fdf7086 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -468,7 +468,7 @@ def _disambiguate_pattern_names(patterns): if suffixed_name != sanitized_name: logger.warning('Pattern name "{}" appears multiple times; renaming to "{}"'.format(pat.name, suffixed_name)) - encoded_name = sanitized_name.encode('ASCII') + encoded_name = suffixed_name.encode('ASCII') if len(encoded_name) == 0: raise PatternError('Zero-length name after sanitize+encode, originally "{}"'.format(pat.name)) if len(encoded_name) > 32: From c6830abe22c1e15931339d83cfe424f400dfeb0f Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 16 Apr 2019 00:42:48 -0700 Subject: [PATCH 120/124] Fix corners in manhattanize_fast --- masque/shapes/shape.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index 45eb35f..abd0103 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -223,14 +223,26 @@ class Shape(metaclass=ABCMeta): for v, v_next in zip(p_verts, numpy.roll(p_verts, -1, axis=0)): dv = v_next - v + # Find x-index bounds for the line # TODO: fix this and err_xmin/xmax for grids smaller than the line / shape + gxi_range = numpy.digitize([v[0], v_next[0]], grid_x) + gxi_min = numpy.min(gxi_range - 1).clip(0, len(grid_x) - 1) + gxi_max = numpy.max(gxi_range).clip(0, len(grid_x)) + + err_xmin = (min(v[0], v_next[0]) - grid_x[gxi_min]) / (grid_x[gxi_min + 1] - grid_x[gxi_min]) + err_xmax = (max(v[0], v_next[0]) - grid_x[gxi_max - 1]) / (grid_x[gxi_max] - grid_x[gxi_max - 1]) + + if err_xmin >= 0.5: + gxi_xmin += 1 + if err_xmax >= 0.5: + gxi_xmax += 1 + + if abs(dv[0]) < 1e-20: - xs = numpy.array([v[0], v[0]]) # TODO maybe pick between v[0] and v_next[0]? + # Vertical line, don't calculate slope + xi = [gxi_min, gxi_max - 1] ys = numpy.array([v[1], v_next[1]]) - xi = numpy.digitize(xs, grid_x).clip(1, len(grid_x) - 1) yi = numpy.digitize(ys, grid_y).clip(1, len(grid_y) - 1) - err_x = (xs - grid_x[xi]) / (grid_x[xi] - grid_x[xi - 1]) err_y = (ys - grid_y[yi]) / (grid_y[yi] - grid_y[yi - 1]) - xi[err_y < 0.5] -= 1 yi[err_y < 0.5] -= 1 segment = numpy.column_stack((grid_x[xi], grid_y[yi])) @@ -250,20 +262,13 @@ class Shape(metaclass=ABCMeta): # now set inds to the index of the nearest y-grid line inds[err < 0.5] -= 1 - #if dv[0] >= 0: - # inds[err <= 0.5] -= 1 - #else: - # inds[err < 0.5] -= 1 return inds - gxi_range = numpy.digitize([v[0], v_next[0]], grid_x) - gxi_min = numpy.min(gxi_range - 1).clip(0, len(grid_x)) - gxi_max = numpy.max(gxi_range).clip(0, len(grid_x)) - + # Find the y indices on all x gridlines xs = grid_x[gxi_min:gxi_max] inds = get_grid_inds(xs) - # Find intersections for midpoints + # Find y-intersections for x-midpoints xs2 = (xs[:-1] + xs[1:]) / 2 inds2 = get_grid_inds(xs2) From e3586a45745eb594b005f03a4612f43aaf5d658d Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 16 Apr 2019 22:24:43 -0700 Subject: [PATCH 121/124] fix variable names (manhattanize_fast fixes) --- masque/shapes/shape.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index abd0103..981e2ce 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -232,9 +232,9 @@ class Shape(metaclass=ABCMeta): err_xmax = (max(v[0], v_next[0]) - grid_x[gxi_max - 1]) / (grid_x[gxi_max] - grid_x[gxi_max - 1]) if err_xmin >= 0.5: - gxi_xmin += 1 + gxi_min += 1 if err_xmax >= 0.5: - gxi_xmax += 1 + gxi_max += 1 if abs(dv[0]) < 1e-20: From 8987cf8c4470f6215fcb1526cb37d40e58b46104 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 18 Apr 2019 01:12:33 -0700 Subject: [PATCH 122/124] mirror_signs are per-coordinate, not per-vector --- masque/file/gdsii.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index fdf7086..cb40616 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -368,8 +368,8 @@ def _aref_to_gridrep(element: gdsii.elements.ARef) -> GridRepetition: 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] + a_vector = numpy.dot(rotation_matrix_2d(-rotation), vec_a0 / scale / counts[0]) * mirror_signs + b_vector = numpy.dot(rotation_matrix_2d(-rotation), vec_b0 / scale / counts[1]) * mirror_signs gridrep = GridRepetition(pattern=None, From 9ecd34b2f7c7ae4f066caed6567a63b9982893d2 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 18 Apr 2019 01:12:51 -0700 Subject: [PATCH 123/124] Cast offsets to float --- masque/repetition.py | 2 +- masque/subpattern.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/masque/repetition.py b/masque/repetition.py index cc30d20..9742733 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -98,7 +98,7 @@ class GridRepetition: if val.size != 2: raise PatternError('Offset must be convertible to size-2 ndarray') - self._offset = val.flatten() + self._offset = val.flatten().astype(float) # dose property @property diff --git a/masque/subpattern.py b/masque/subpattern.py index 811c670..0415894 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -57,7 +57,7 @@ class SubPattern: if val.size != 2: raise PatternError('Offset must be convertible to size-2 ndarray') - self._offset = val.flatten() + self._offset = val.flatten().astype(float) # dose property @property From 79c89b2a4b2a1cb5e5c3fbcd251e0bbd11d28626 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 18 Apr 2019 01:14:08 -0700 Subject: [PATCH 124/124] Rename empty-named patterns on gdsii save --- masque/file/gdsii.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index cb40616..b164561 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -459,17 +459,20 @@ def _disambiguate_pattern_names(patterns): i = 0 suffixed_name = sanitized_name - while suffixed_name in used_names: + while suffixed_name in used_names or suffixed_name == '': suffix = base64.b64encode(struct.pack('>Q', i), b'$?').decode('ASCII') suffixed_name = sanitized_name + '$' + suffix[:-1].lstrip('A') i += 1 - if suffixed_name != sanitized_name: + if sanitized_name == '': + logger.warning('Empty pattern name saved as "{}"'.format(suffixed_name)) + elif suffixed_name != sanitized_name: logger.warning('Pattern name "{}" appears multiple times; renaming to "{}"'.format(pat.name, suffixed_name)) encoded_name = suffixed_name.encode('ASCII') if len(encoded_name) == 0: + # Should never happen since zero-length names are replaced raise PatternError('Zero-length name after sanitize+encode, originally "{}"'.format(pat.name)) if len(encoded_name) > 32: raise PatternError('Pattern name "{}" length > 32 after encode, originally "{}"'.format(encoded_name, pat.name))