From c6fac19fe0050213c409385e93a2db862ee8d0e7 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 20 Apr 2019 14:18:25 -0700 Subject: [PATCH 001/109] Set standard constructor arg order and add `rotation` and `mirrored` args where reasonable --- masque/shapes/arc.py | 15 ++++++++------- masque/shapes/ellipse.py | 13 +++++++------ masque/shapes/polygon.py | 11 ++++++++--- masque/shapes/text.py | 8 +++----- 4 files changed, 26 insertions(+), 21 deletions(-) diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 74f0ec0..c468564 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Tuple import math import numpy from numpy import pi @@ -141,19 +141,21 @@ class Arc(Shape): radii: vector2, angles: vector2, width: float, - rotation: float=0, poly_num_points: int=DEFAULT_POLY_NUM_POINTS, poly_max_arclen: float=None, offset: vector2=(0.0, 0.0), + rotation: float=0, + mirrored: Tuple[bool] = (False, False), layer: int=0, dose: float=1.0): - self.offset = offset - self.layer = layer - self.dose = dose self.radii = radii self.angles = angles self.width = width + self.offset = offset self.rotation = rotation + [self.mirror(a) for a, do in enumerate(mirrored) if do] + self.layer = layer + self.dose = dose self.poly_num_points = poly_num_points self.poly_max_arclen = poly_max_arclen @@ -201,8 +203,7 @@ class Arc(Shape): ys = numpy.hstack((ys1, ys2)) xys = numpy.vstack((xs, ys)).T - poly = Polygon(xys, dose=self.dose, layer=self.layer, offset=self.offset) - poly.rotate(self.rotation) + poly = Polygon(xys, dose=self.dose, layer=self.layer, offset=self.offset, rotation=self.rotation) return [poly] def get_bounds(self) -> numpy.ndarray: diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index 6b7317f..724c11d 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Tuple import math import numpy from numpy import pi @@ -82,17 +82,19 @@ class Ellipse(Shape): 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), + rotation: float=0, + mirrored: Tuple[bool] = (False, False), layer: int=0, dose: float=1.0): + self.radii = radii self.offset = offset + self.rotation = rotation + [self.mirror(a) for a, do in enumerate(mirrored) if do] 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 @@ -129,8 +131,7 @@ class Ellipse(Shape): ys = r1 * sin_th xys = numpy.vstack((xs, ys)).T - poly = Polygon(xys, dose=self.dose, layer=self.layer, offset=self.offset) - poly.rotate(self.rotation) + poly = Polygon(xys, dose=self.dose, layer=self.layer, offset=self.offset, rotation=self.rotation) return [poly] def get_bounds(self) -> numpy.ndarray: diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index a0b214b..0f97e89 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Tuple import copy import numpy from numpy import pi @@ -71,12 +71,17 @@ class Polygon(Shape): def __init__(self, vertices: numpy.ndarray, offset: vector2=(0.0, 0.0), + rotation: float=0.0, + mirrored: Tuple[bool] = (False, False), layer: int=0, - dose: float=1.0): - self.offset = offset + dose: float=1.0, + ): self.layer = layer self.dose = dose self.vertices = vertices + self.offset = offset + self.rotate(rotation) + [self.mirror(a) for a, do in enumerate(mirrored) if do] @staticmethod def square(side_length: float, diff --git a/masque/shapes/text.py b/masque/shapes/text.py index 64b7468..167e2c2 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -61,15 +61,15 @@ class Text(Shape): def mirrored(self, val: List[bool]): if is_scalar(val): raise PatternError('Mirrored must be a 2-element list of booleans') - self._mirrored = val + self._mirrored = list(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), + rotation: float=0.0, + mirrored: Tuple[bool]=(False, False), layer: int=0, dose: float=1.0): self.offset = offset @@ -79,8 +79,6 @@ 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, From 8fe8bbd655a86c2091a813fb8b27ecf07add2650 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 20 Apr 2019 14:18:52 -0700 Subject: [PATCH 002/109] Normalization-related fixes --- masque/shapes/arc.py | 4 ++-- masque/shapes/shape.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index c468564..bb2da3a 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -307,9 +307,9 @@ class Arc(Shape): rotation %= 2 * pi width = self.width - return (type(self), radii, angles, width, self.layer), \ + return (type(self), radii, angles, width/norm_value, self.layer), \ (self.offset, scale/norm_value, rotation, self.dose), \ - lambda: Arc(radii=radii*norm_value, angles=angles, width=width, layer=self.layer) + lambda: Arc(radii=radii*norm_value, angles=angles, width=width*norm_value, layer=self.layer) def get_cap_edges(self) -> numpy.ndarray: ''' diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index 981e2ce..7e26c87 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -94,16 +94,16 @@ class Shape(metaclass=ABCMeta): 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 + :param norm_value: This value is used to normalize lengths intrinsic to the shape; + eg. for a circle, the returned intrinsic radius 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). + extrinsic: ([x_offset, y_offset], scale, rotation, dose) constructor: A callable (no arguments) which returns an instance of type(self) with internal state equivalent to 'intrinsic'. """ From d53c9487ffcbd76e0d093821ab2b7bbe1b839c89 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 20 Apr 2019 14:19:18 -0700 Subject: [PATCH 003/109] Comment and error-checking fixes --- masque/shapes/polygon.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 0f97e89..eb1bdf8 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -13,7 +13,8 @@ __author__ = 'Jan Petykiewicz' class Polygon(Shape): """ - A polygon, consisting of a bunch of vertices (Nx2 ndarray) along with an offset. + A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an + implicitly-closed boundary, and an offset. A normalized_form(...) is available, but can be quite slow with lots of vertices. """ @@ -35,14 +36,14 @@ class Polygon(Shape): 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)') + raise PatternError('Must have at least 3 vertices (Nx2 where N>2)') self._vertices = val # xs property @property def xs(self) -> numpy.ndarray: """ - All x vertices in a 1D ndarray + All vertex x coords as a 1D ndarray """ return self.vertices[:, 0] @@ -57,7 +58,7 @@ class Polygon(Shape): @property def ys(self) -> numpy.ndarray: """ - All y vertices in a 1D ndarray + All vertex y coords as a 1D ndarray """ return self.vertices[:, 1] From 783c0c0844d2c3bdd0b3fb44bfa84da5214d76ab Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 20 Apr 2019 15:25:19 -0700 Subject: [PATCH 004/109] Add support for gds paths --- masque/file/gdsii.py | 91 +++++++--- masque/shapes/__init__.py | 1 + masque/shapes/path.py | 344 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 414 insertions(+), 22 deletions(-) create mode 100644 masque/shapes/path.py diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index b164561..7eec1c1 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -15,8 +15,9 @@ import logging from .utils import mangle_name, make_dose_table from .. import Pattern, SubPattern, GridRepetition, PatternError, Label, Shape -from ..shapes import Polygon +from ..shapes import Polygon, Path from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar +from ..utils import remove_colinear_vertices __author__ = 'Jan Petykiewicz' @@ -25,6 +26,13 @@ __author__ = 'Jan Petykiewicz' logger = logging.getLogger(__name__) +path_cap_map = {0: Path.Cap.Flush, + 1: Path.Cap.Circle, + 2: Path.Cap.Square, + #3: custom? + } + + def write(patterns: Pattern or List[Pattern], filename: str, meters_per_unit: float, @@ -81,12 +89,8 @@ def write(patterns: Pattern or List[Pattern], structure = gdsii.structure.Structure(name=pat.name) lib.append(structure) - # Add a Boundary element for each shape - structure += _shapes_to_boundaries(pat.shapes) - + structure += _shapes_to_elements(pat.shapes) 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: @@ -255,13 +259,17 @@ def read(filename: str, for element in structure: # Switch based on element type: if isinstance(element, gdsii.elements.Boundary): + args = {'vertices': element.xy[:-1], + } + if use_dtype_as_dose: - shape = Polygon(vertices=element.xy[:-1], - dose=element.data_type, - layer=element.layer) + args['dose'] = element.data_type + args['layer'] = element.layer else: - shape = Polygon(vertices=element.xy[:-1], - layer=(element.layer, element.data_type)) + args['layer'] = (element.layer, element.data_type) + + shape = Polygon(**args) + if clean_vertices: try: shape.clean_vertices() @@ -270,6 +278,33 @@ def read(filename: str, pat.shapes.append(shape) + if isinstance(element, gdsii.elements.Path): + if element.path_type in path_cap_map: + cap = path_cap_map[element.path_type] + else: + raise PatternError('Unrecognized path type: {}'.format(element.path_type)) + + args = {'vertices': element.xy, + 'width': element.width, + 'cap': cap, + } + + if use_dtype_as_dose: + args['dose'] = element.data_type + args['layer'] = element.layer + else: + args['layer'] = (element.layer, element.data_type) + + shape = Path(**args) + + if clean_vertices: + try: + shape.clean_vertices() + except PatternError as err: + continue + + pat.shapes.append(shape) + elif isinstance(element, gdsii.elements.Text): label = Label(offset=element.xy, layer=(element.layer, element.text_type), @@ -425,19 +460,31 @@ def _subpatterns_to_refs(subpatterns: List[SubPattern or GridRepetition] return refs -def _shapes_to_boundaries(shapes: List[Shape] - ) -> List[gdsii.elements.Boundary]: - # Add a Boundary element for each shape - boundaries = [] +def _shapes_to_elements(shapes: List[Shape], + polygonize_paths: bool = False + ) -> List[gdsii.elements.Boundary]: + elements = [] + # Add a Boundary element for each shape, and Path elements if necessary 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 + if isinstance(shape, Path) and not polygonize_paths: + xy = numpy.round(shape.vertices + shape.offset).astype(int) + width = numpy.round(shape.width).astype(int) + path_type = next(k for k, v in path_cap_map.items() if v == shape.cap) #reverse lookup + path = gdsii.elements.Path(layer=layer, + data_type=data_type, + xy=xy) + path.path_type = path_type + path.width = width + elements.append(path) + else: + 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, :])) + elements.append(gdsii.elements.Boundary(layer=layer, + data_type=data_type, + xy=xy_closed)) + return elements def _labels_to_texts(labels: List[Label]) -> List[gdsii.elements.Text]: diff --git a/masque/shapes/__init__.py b/masque/shapes/__init__.py index 4c64204..4a513e9 100644 --- a/masque/shapes/__init__.py +++ b/masque/shapes/__init__.py @@ -10,3 +10,4 @@ from .circle import Circle from .ellipse import Ellipse from .arc import Arc from .text import Text +from .path import Path diff --git a/masque/shapes/path.py b/masque/shapes/path.py new file mode 100644 index 0000000..c43d847 --- /dev/null +++ b/masque/shapes/path.py @@ -0,0 +1,344 @@ +from typing import List, Tuple +import copy +from enum import Enum +import numpy +from numpy import pi + +from . import Shape, normalized_shape_tuple, Polygon, Circle +from .. import PatternError +from ..utils import is_scalar, rotation_matrix_2d, vector2 +from ..utils import remove_colinear_vertices, remove_duplicate_vertices + +__author__ = 'Jan Petykiewicz' + + +class Path(Shape): + """ + A path, consisting of a bunch of vertices (Nx2 ndarray), a width, an end-cap shape, + and an offset. + + A normalized_form(...) is available, but can be quite slow with lots of vertices. + """ + _vertices = None # type: numpy.ndarray + _width = None # type: float + _cap = None # type: Path.Cap + + class Cap(Enum): + Flush = 0 # Path ends at final vertices + Circle = 1 # Path extends past final vertices with a semicircle of radius width/2 + Square = 2 # Path extends past final vertices with a width-by-width/2 rectangle + + + # width property + @property + def width(self) -> float: + """ + Path width (float, >= 0) + + :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 non-negative') + self._width = val + + # cap property + @property + def cap(self) -> 'Path.Cap': + """ + Path end-cap + + :return: Path.Cap enum + """ + return self._cap + + @cap.setter + def cap(self, val: 'Path.Cap'): + self._cap = Path.Cap(val) + + # vertices property + @property + def vertices(self) -> numpy.ndarray: + """ + Vertices of the path (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 len(val.shape) < 2 or val.shape[1] != 2: + raise PatternError('Vertices must be an Nx2 array') + if val.shape[0] < 2: + raise PatternError('Must have at least 2 vertices (Nx2 where N>1)') + self._vertices = val + + # xs property + @property + def xs(self) -> numpy.ndarray: + """ + All vertex x coords as 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 vertex y coords as 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, + width: float = 0.0, + cap: 'Path.Cap' = Cap.Flush, + offset: vector2=(0.0, 0.0), + rotation: float = 0, + mirrored: Tuple[bool] = (False, False), + layer: int=0, + dose: float=1.0, + ) -> 'Path': + self.offset = offset + self.layer = layer + self.dose = dose + self.vertices = vertices + self.width = width + self.cap = cap + self.rotate(rotation) + [self.mirror(a) for a, do in enumerate(mirrored) if do] + + @staticmethod + def travel(travel_pairs: Tuple[Tuple[float, float]], + width: float = 0.0, + cap: 'Path.Cap' = Cap.Flush, + offset: vector2=(0.0, 0.0), + rotation: float = 0, + mirrored: Tuple[bool] = (False, False), + layer: int=0, + dose: float=1.0, + ) -> 'Path': + """ + Build a path by specifying the turn angles and travel distances + rather than setting the distances directly. + + :param travel_pairs: A list of (angle, distance) pairs that define + the path. Angles are counterclockwise, in radians, and are relative + to the previous segment's direction (the initial angle is relative + to the +x axis). + :param width: Path width, default 0 + :param cap: End-cap type, default Path.Cap.Flush (no end-cap) + :param offset: Offset, default (0, 0) + :param rotation: Rotation counterclockwise, in radians. Default 0 + :param mirrored: Whether to mirror across the x or y axes. For example, + mirrored=(True, False) results in a reflection across the x-axis, + multiplying the path's y-coordinates by -1. Default (False, False) + :param layer: Layer, default 0 + :param dose: Dose, default 1.0 + :return: The resulting Path object + """ + #TODO: needs testing + direction = numpy.array([1, 0]) + + verts = [[0, 0]] + for angle, distance in travel_pairs: + direction = numpy.dot(rotation_matrix_2d(angle), direction.T).T + verts.append(verts[-1] + direction * distance) + + return Path(vertices=verts, width=width, cap=cap, + offset=offset, rotation=rotation, mirrored=mirrored, + layer=layer, dose=dose) + + def to_polygons(self, + poly_num_points: int=None, + poly_max_arclen: float=None, + ) -> List['Polygon']: + if self.cap in (Path.Cap.Flush, Path.Cap.Circle): + extension = 0.0 + elif self.cap == Path.Cap.Square: + extension = self.width / 2 + else: + raise PatternError('Unrecognized path endcap: {}'.format(self.cap)) + + v = remove_colinear_vertices(self.vertices, closed_path=False) + dv = numpy.diff(v, axis=0) + dvdir = dv / numpy.sqrt((dv * dv).sum(axis=1))[:, None] + + if self.width == 0: + verts = numpy.vstack((v, v[::-1])) + return [Polygon(offset=self.offset, vertices=verts, dose=self.dose, layer=self.layer)] + + perp = dvdir[:, ::-1] * [[1, -1]] * self.width / 2 + + # add extension + if extension != 0: + v[0] -= dvdir[0] * extension + v[-1] += dvdir[-1] * extension + dv = numpy.diff(v, axis=0) # recalculate dv; dvdir and perp should stay the same + + + # Find intersections of expanded sides + As = numpy.stack((dv[:-1], -dv[1:]), axis=2) + bs = v[1:-1] - v[:-2] + perp[1:] - perp[:-1] + ds = v[1:-1] - v[:-2] - perp[1:] + perp[:-1] + + rp = numpy.linalg.solve(As, bs)[:, 0, None] + rn = numpy.linalg.solve(As, ds)[:, 0, None] + + intersection_p = v[:-2] + rp * dv[:-1] + perp[:-1] + intersection_n = v[:-2] + rn * dv[:-1] - perp[:-1] + + towards_perp = (dv[1:] * perp[:-1]).sum(axis=1) > 0 # path bends towards previous perp? +# straight = (dv[1:] * perp[:-1]).sum(axis=1) == 0 # path is straight + acute = (dv[1:] * dv[:-1]).sum(axis=1) < 0 # angle is acute? + + # Build vertices + o0 = [v[0] + perp[0]] + o1 = [v[0] - perp[0]] + for i in range(dv.shape[0] - 1): + if towards_perp[i]: + o0.append(intersection_p[i]) + if acute[i]: + o1.append(intersection_n[i]) + else: + # Opposite is >270 + pt0 = v[i + 1] - perp[i + 0] + dvdir[i + 0] * self.width / 2 + pt1 = v[i + 1] - perp[i + 1] - dvdir[i + 1] * self.width / 2 + o1 += [pt0, pt1] + else: + o1.append(intersection_n[i]) + if acute[i]: + # > 270 + pt0 = v[i + 1] + perp[i + 0] + dvdir[i + 0] * self.width / 2 + pt1 = v[i + 1] + perp[i + 1] - dvdir[i + 1] * self.width / 2 + o0 += [pt0, pt1] + else: + o0.append(intersection_p[i]) + o0.append(v[-1] + perp[-1]) + o1.append(v[-1] - perp[-1]) + verts = numpy.vstack((o0, o1[::-1])) + + polys = [Polygon(offset=self.offset, vertices=verts, dose=self.dose, layer=self.layer)] + + if self.cap == Path.Cap.Circle: + #for vert in v: # not sure if every vertex, or just ends? + for vert in [v[0], v[-1]]: + circ = Circle(offset=vert, radius=self.width / 2, dose=self.dose, layer=self.layer) + polys += circ.to_polygons(poly_num_points=poly_num_points, poly_max_arclen=poly_max_arclen) + + return polys + + def get_bounds(self) -> numpy.ndarray: + if self.cap == Path.Cap.Circle: + bounds = self.offset + numpy.vstack((numpy.min(self.vertices, axis=0) - self.width / 2, + numpy.max(self.vertices, axis=0) + self.width / 2)) + elif self.cap in (Path.Cap.Flush, + Path.Cap.Square): + if self.cap == Path.Cap.Flush: + extension = 0 + elif self.cap == Path.Cap.Square: + extension = self.width / 2 + + v = remove_colinear_vertices(self.vertices, closed_path=False) + dv = numpy.diff(v, axis=0) + dvdir = dv / numpy.sqrt((dv * dv).sum(axis=1))[:, None] + perp = dvdir[:, ::-1] * [[1, -1]] * self.width / 2 + + v[0] -= dvdir * extension + v[-1] += dvdir * extension + + bounds = self.offset + numpy.vstack((numpy.min(v - numpy.abs(perp), axis=0), + numpy.max(v + numpy.abs(perp), axis=0))) + else: + raise PatternError('get_bounds() not implemented for endcaps: {}'.format(self.cap)) + + return bounds + + def rotate(self, theta: float) -> 'Path': + self.vertices = numpy.dot(rotation_matrix_2d(theta), self.vertices.T).T + return self + + def mirror(self, axis: int) -> 'Path': + self.vertices[:, axis - 1] *= -1 + return self + + def scale_by(self, c: float) -> 'Path': + self.vertices *= c + self.width *= 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 paths, 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) + + width0 = self.width / norm_value + + return (type(self), reordered_vertices.data.tobytes(), width0, self.cap, self.layer), \ + (offset, scale/norm_value, rotation, self.dose), \ + lambda: Polygon(reordered_vertices*norm_value, width=self.width*norm_value, + cap=self.cap, layer=self.layer) + + def clean_vertices(self) -> 'Path': + """ + Removes duplicate, co-linear and otherwise redundant vertices. + + :returns: self + """ + self.remove_colinear_vertices() + return self + + def remove_duplicate_vertices(self) -> 'Path': + ''' + Removes all consecutive duplicate (repeated) vertices. + + :returns: self + ''' + self.vertices = remove_duplicate_vertices(self.vertices, closed_path=False) + return self + + def remove_colinear_vertices(self) -> 'Path': + ''' + Removes consecutive co-linear vertices. + + :returns: self + ''' + self.vertices = remove_colinear_vertices(self.vertices, closed_path=False) + return self From d6d26b4e465c170033f3b0edd1d4b50ef79917bd Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 20 Apr 2019 15:26:27 -0700 Subject: [PATCH 005/109] Add modify_originals param and don't modify the pattern by default --- masque/file/gdsii.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 7eec1c1..6951ce3 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -8,6 +8,7 @@ import gdsii.elements from typing import List, Any, Dict, Tuple import re +import copy import numpy import base64 import struct @@ -37,7 +38,8 @@ def write(patterns: Pattern or List[Pattern], filename: str, meters_per_unit: float, logical_units_per_unit: float = 1, - library_name: str = 'masque-gdsii-write'): + library_name: str = 'masque-gdsii-write', + modify_originals: bool = False): """ 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 @@ -50,8 +52,6 @@ def write(patterns: Pattern or List[Pattern], 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. @@ -67,16 +67,22 @@ def write(patterns: Pattern or List[Pattern], Default 1. :param library_name: Library name written into the GDSII file. Default 'masque-gdsii-write'. + :param modify_originals: If True, the original pattern is modified as part of the writing + process. Otherwise, a copy is made. + Default False. """ + if not modify_originals: + patterns = copy.deepcopy(patterns) + + if isinstance(patterns, Pattern): + patterns = [patterns] + # Create library lib = gdsii.library.Library(version=600, name=library_name.encode('ASCII'), 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 = {id(pattern): pattern for pattern in patterns} for pattern in patterns: From 485a7bc29dab667c78ebd27fb870fd408f4e5309 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 20 Apr 2019 15:29:56 -0700 Subject: [PATCH 006/109] General overhaul of gdsii read/write functions - read() and write() now take streams instead of filenames - readfile() and writefile() were added to handle filenames and can detect and handle '.gz' suffixed/compressed files. - write_dose2dtype() and and read_dtype2dose() were removed in favor of read(use_dtype_as_dose=True) and dose2dtype() --- masque/file/gdsii.py | 91 +++++++++++++++++++++----------------------- 1 file changed, 43 insertions(+), 48 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 6951ce3..8a0ff7a 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -8,11 +8,14 @@ import gdsii.elements from typing import List, Any, Dict, Tuple import re +import io import copy import numpy import base64 import struct import logging +import pathlib +import gzip from .utils import mangle_name, make_dose_table from .. import Pattern, SubPattern, GridRepetition, PatternError, Label, Shape @@ -35,7 +38,7 @@ path_cap_map = {0: Path.Cap.Flush, def write(patterns: Pattern or List[Pattern], - filename: str, + stream: io.BufferedIOBase, meters_per_unit: float, logical_units_per_unit: float = 1, library_name: str = 'masque-gdsii-write', @@ -58,8 +61,8 @@ def write(patterns: Pattern or List[Pattern], If you want pattern polygonized with non-default arguments, just call pattern.polygonize() prior to calling 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 patterns: A Pattern or list of patterns to write to file. + :param file: Filename or stream object 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 @@ -99,50 +102,29 @@ def write(patterns: Pattern or List[Pattern], structure += _labels_to_texts(pat.labels) structure += _subpatterns_to_refs(pat.subpatterns) - with open(filename, mode='wb') as stream: - lib.save(stream) + lib.save(stream) + return -def write_dose2dtype(patterns: Pattern or List[Pattern], - filename: str, - meters_per_unit: float, - *args, - **kwargs, - ) -> List[float]: +def writefile(patterns: List[Pattern] or Pattern, + filename: str or pathlib.Path, + *args, + **kwargs, + ): """ - 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). + Wrapper for gdsii.write() that takes a filename or path instead of a stream. - 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(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. - - If you want pattern polygonized with non-default arguments, just call pattern.polygonize() - prior to calling 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. - :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). + Will automatically compress the file if it has a .gz suffix. """ - patterns, dose_vals = dose2dtype(patterns) - write(patterns, filename, meters_per_unit, *args, **kwargs) - return dose_vals + path = pathlib.Path(filename) + if path.suffix == 'gz': + open_func = gzip.open + else: + open_func = open + + with open_func(path, mode='wb') as stream: + results = write(patterns, stream, *args, **kwargs) + return results def dose2dtype(patterns: Pattern or List[Pattern], @@ -219,14 +201,27 @@ def dose2dtype(patterns: Pattern or List[Pattern], return patterns, list(dose_vals) -def read_dtype2dose(filename: str) -> (List[Pattern], Dict[str, Any]): +def readfile(filename: str or pathlib.Path, + *args, + **kwargs, + ) -> (Dict[str, Pattern], Dict[str, Any]): """ - Alias for read(filename, use_dtype_as_dose=True) + Wrapper for gdsii.read() that takes a filename or path instead of a stream. + + Tries to autodetermine file type based on suffixes """ - return read(filename, use_dtype_as_dose=True) + path = pathlib.Path(filename) + if path.suffix == 'gz': + open_func = gzip.open + else: + open_func = open + + with open_func(path, mode='rb') as stream: + results = read(stream, *args, **kwargs) + return results -def read(filename: str, +def read(stream: io.BufferedIOBase, use_dtype_as_dose: bool = False, clean_vertices: bool = True, ) -> (Dict[str, Pattern], Dict[str, Any]): @@ -251,8 +246,7 @@ def read(filename: str, :return: Tuple: (Dict of pattern_name:Patterns generated from GDSII structures, Dict of GDSII library info) """ - with open(filename, mode='rb') as stream: - lib = gdsii.library.Library.load(stream) + lib = gdsii.library.Library.load(stream) library_info = {'name': lib.name.decode('ASCII'), 'meters_per_unit': lib.physical_unit, @@ -532,3 +526,4 @@ def _disambiguate_pattern_names(patterns): pat.name = encoded_name used_names.append(suffixed_name) + From 00188912001b0b945a9719ed830e7d5ce02f1cb9 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 20 Apr 2019 15:42:42 -0700 Subject: [PATCH 007/109] handle defaults for path params --- masque/file/gdsii.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 8a0ff7a..58b40ec 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -30,7 +30,9 @@ __author__ = 'Jan Petykiewicz' logger = logging.getLogger(__name__) -path_cap_map = {0: Path.Cap.Flush, +path_cap_map = { + None: Path.Cap.Flush, + 0: Path.Cap.Flush, 1: Path.Cap.Circle, 2: Path.Cap.Square, #3: custom? @@ -285,7 +287,7 @@ def read(stream: io.BufferedIOBase, raise PatternError('Unrecognized path type: {}'.format(element.path_type)) args = {'vertices': element.xy, - 'width': element.width, + 'width': element.width if element.width is not None else 0.0, 'cap': cap, } From ee6699a7de9ad8ca62edbbe00e7628cba2f0af50 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 20 Apr 2019 15:44:45 -0700 Subject: [PATCH 008/109] fix alternate polygon constructors --- 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 eb1bdf8..afb47a6 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -106,7 +106,7 @@ class Polygon(Shape): [+1, +1], [+1, -1]], dtype=float) vertices = 0.5 * side_length * norm_square - poly = Polygon(vertices, offset, layer, dose) + poly = Polygon(vertices, offset=offset, layer=layer, dose=dose) poly.rotate(rotation) return poly @@ -133,7 +133,7 @@ class Polygon(Shape): [-lx, +ly], [+lx, +ly], [+lx, -ly]], dtype=float) - poly = Polygon(vertices, offset, layer, dose) + poly = Polygon(vertices, offset=offset, layer=layer, dose=dose) poly.rotate(rotation) return poly From 5741b38ca6f8bccfbab60670b6e879b506900ad0 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 6 May 2019 21:07:53 -0700 Subject: [PATCH 009/109] fixup remove_colinear_vertices --- masque/utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/masque/utils.py b/masque/utils.py index b1faa5f..bf82cba 100644 --- a/masque/utils.py +++ b/masque/utils.py @@ -74,10 +74,12 @@ def remove_colinear_vertices(vertices: numpy.ndarray, closed_path: bool = True) closed path. If False, the path is assumed to be open. Default True. :return: ''' + vertices = numpy.array(vertices) + # Check for dx0/dy0 == dx1/dy1 - dv = numpy.roll(vertices, 1, axis=0) - vertices #[y0 - yn1, y1-y0, ...] - dxdy = dv * numpy.roll(dv, 1, axis=0)[:, ::-1] # [[dx1*dy0, dx1*dy0], ...] + dv = numpy.roll(vertices, -1, axis=0) - vertices # [y1-y0, y2-y1, ...] + dxdy = dv * numpy.roll(dv, 1, axis=0)[:, ::-1] #[[dx0*(dy_-1), (dx_-1)*dy0], dx1*dy0, dy1*dy0]] dxdy_diff = numpy.abs(numpy.diff(dxdy, axis=1))[:, 0] err_mult = 2 * numpy.abs(dxdy).sum(axis=1) + 1e-40 From 380393dbc35febc482d9aeed2a6d8a40c52027f6 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 7 May 2019 00:58:39 -0700 Subject: [PATCH 010/109] Fix mirror axis for arrays --- 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 58b40ec..bda7753 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -399,7 +399,7 @@ def _aref_to_gridrep(element: gdsii.elements.ARef) -> GridRepetition: 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 + mirror_signs[1] = -1 counts = [element.cols, element.rows] vec_a0 = element.xy[1] - offset @@ -417,7 +417,7 @@ def _aref_to_gridrep(element: gdsii.elements.ARef) -> GridRepetition: offset=offset, rotation=rotation, scale=scale, - mirrored=(mirror_signs == -1)) + mirrored=(mirror_signs[::-1] == -1)) gridrep.ref_name = element.struct_name return gridrep From 9ab1372c7bbf86dec7016f38676bd12a26243acd Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 13 May 2019 23:55:14 -0700 Subject: [PATCH 011/109] Allow shapes to carry an arbitrary identifier (Tuple) --- masque/shapes/shape.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index 7e26c87..46123a1 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -34,6 +34,9 @@ class Shape(metaclass=ABCMeta): # Dose _dose = 1.0 # type: float + # An arbitrary identifier for the shape, usually not set but used by Pattern.flatten() + identifier = () # type: Tuple + # --- Abstract methods @abstractmethod def to_polygons(self, num_vertices: int, max_arclen: float) -> List['Polygon']: From 347716ec6e9e036b3b670630e9f4467b5cc58109 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 13 May 2019 23:55:42 -0700 Subject: [PATCH 012/109] Give shapes unique path-like identifiers when flattening --- masque/pattern.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/masque/pattern.py b/masque/pattern.py index 26d7293..eea1af8 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -315,15 +315,31 @@ class Pattern: """ Removes all subpatterns and adds equivalent shapes. + Shape identifiers are changed to represent their original position in the + pattern hierarchy: + (L1_name (str), L1_index (int), L2_name, L2_index, ..., *original_shape_identifier) + where L1_name is the first-level subpattern's name (e.g. self.subpatterns[0].pattern.name), + L2_name is the next-level subpattern's name (e.g. + self.subpatterns[0].pattern.subpatterns[0].pattern.name) and L1_index is an integer + used to differentiate between multiple instance of the same (or same-named) subpatterns. + :return: self """ subpatterns = copy.deepcopy(self.subpatterns) self.subpatterns = [] + shape_counts = {} for subpat in subpatterns: subpat.pattern.flatten() p = subpat.as_pattern() - self.shapes += p.shapes - self.labels += p.labels + + # Update identifiers so each shape has a unique one + for shape in p.shapes: + combined_identifier = (subpat.pattern.name,) + shape.identifier + shape_count = shape_counts.get(combined_identifier, 0) + shape.identifier = (subpat.pattern.name, shape_count) + shape.identifier + shape_counts[combined_identifier] = shape_count + 1 + + self.append(p) return self def translate_elements(self, offset: vector2) -> 'Pattern': From 0bbcf3823ae309dd9580805fca3f6f6cc9bb3296 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 15 May 2019 00:11:28 -0700 Subject: [PATCH 013/109] Fix .gz suffix detection --- 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 bda7753..61c9f57 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -119,7 +119,7 @@ def writefile(patterns: List[Pattern] or Pattern, Will automatically compress the file if it has a .gz suffix. """ path = pathlib.Path(filename) - if path.suffix == 'gz': + if path.suffix == '.gz': open_func = gzip.open else: open_func = open @@ -213,7 +213,7 @@ def readfile(filename: str or pathlib.Path, Tries to autodetermine file type based on suffixes """ path = pathlib.Path(filename) - if path.suffix == 'gz': + if path.suffix == '.gz': open_func = gzip.open else: open_func = open From b8b848c28f15ccdc06a74d8218f2a2f171ef56e1 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 15 May 2019 00:11:44 -0700 Subject: [PATCH 014/109] add Pattern.is_empty() --- masque/pattern.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/masque/pattern.py b/masque/pattern.py index eea1af8..a63bb6d 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -482,6 +482,14 @@ class Pattern: """ return copy.deepcopy(self) + def is_empty(self) -> bool: + """ + Returns true if the Pattern contains no shapes, labels, or subpatterns. + + :return: True if the pattern is empty. + """ + return (len(self.subpatterns) == 0 and len(self.shapes) == 0 and len(self.labels) == 0) + @staticmethod def load(filename: str) -> 'Pattern': """ From d5665f54a7f0764495c4edf684c7bcae3c217f27 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 15 May 2019 00:12:34 -0700 Subject: [PATCH 015/109] Comment and type hint updates --- masque/repetition.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/masque/repetition.py b/masque/repetition.py index 9742733..2d153e3 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -33,7 +33,7 @@ class GridRepetition: _mirrored = None # type: List[bool] _a_vector = None # type: numpy.ndarray - _b_vector = None # type: numpy.ndarray + _b_vector = None # type: numpy.ndarray or None a_count = None # type: int b_count = 1 # type: int @@ -179,15 +179,11 @@ class GridRepetition: 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. + Returns a copy of self.pattern which has been scaled, rotated, repeated, etc. + etc. according to this GridRepetitions's properties. + :return: Copy of self.pattern that has been repeated / altered as implied by + this object's other 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): @@ -246,7 +242,7 @@ class GridRepetition: def mirror(self, axis: int) -> 'GridRepetition': """ - Mirror the subpattern across an axis. + Mirror the GridRepetition across an axis. :param axis: Axis to mirror across. :return: self @@ -257,7 +253,7 @@ class GridRepetition: 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. + extent of the GridRepetition in each dimension. Returns None if the contained Pattern is empty. :return: [[x_min, y_min], [x_max, y_max]] or None @@ -266,7 +262,7 @@ class GridRepetition: def scale_by(self, c: float) -> 'GridRepetition': """ - Scale the subpattern by a factor + Scale the GridRepetition by a factor :param c: scaling factor """ @@ -281,7 +277,7 @@ class GridRepetition: """ return copy.copy(self) - def deepcopy(self) -> 'SubPattern': + def deepcopy(self) -> 'GridRepetition': """ Return a deep copy of the repetition. From bc43be48bce4690d5b51a6ec0640b7f3f3456892 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 15 May 2019 00:14:17 -0700 Subject: [PATCH 016/109] Implement SquareCustom end-caps and gds output --- masque/file/gdsii.py | 9 ++++- masque/shapes/path.py | 78 ++++++++++++++++++++++++++++++++----------- 2 files changed, 66 insertions(+), 21 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 61c9f57..8b04bec 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -35,7 +35,7 @@ path_cap_map = { 0: Path.Cap.Flush, 1: Path.Cap.Circle, 2: Path.Cap.Square, - #3: custom? + 4: Path.Cap.SquareCustom, } @@ -291,6 +291,13 @@ def read(stream: io.BufferedIOBase, 'cap': cap, } + if cap == Path.Cap.SquareCustom: + args['cap_extensions'] = numpy.zeros(2) + if element.bgn_extn is not None: + args['cap_extensions'][0] = element.bgn_extn + if element.end_extn is not None: + args['cap_extensions'][1] = element.end_extn + if use_dtype_as_dose: args['dose'] = element.data_type args['layer'] = element.layer diff --git a/masque/shapes/path.py b/masque/shapes/path.py index c43d847..675029a 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -22,12 +22,14 @@ class Path(Shape): _vertices = None # type: numpy.ndarray _width = None # type: float _cap = None # type: Path.Cap + _cap_extensions = None # type: numpy.ndarray or None class Cap(Enum): Flush = 0 # Path ends at final vertices Circle = 1 # Path extends past final vertices with a semicircle of radius width/2 Square = 2 # Path extends past final vertices with a width-by-width/2 rectangle - + SquareCustom = 4 # Path extends past final vertices with a rectangle of length + # defined by path.cap_extensions # width property @property @@ -59,7 +61,35 @@ class Path(Shape): @cap.setter def cap(self, val: 'Path.Cap'): + # TODO: Document that setting cap can change cap_extensions self._cap = Path.Cap(val) + if self.cap != Path.Cap.SquareCustom: + self.cap_extensions = None + elif self.cap_extensions is None: + # just got set to SquareCustom + self.cap_extensions = numpy.zeros(2) + + # cap_extensions property + @property + def cap_extensions(self) -> numpy.ndarray or None: + """ + Path end-cap extensionf + + :return: 2-element ndarray or None + """ + return self._cap_extensions + + @cap_extensions.setter + def cap_extensions(self, vals: numpy.ndarray or None): + custom_caps = (Path.Cap.SquareCustom,) + if self.cap in custom_caps: + if vals is None: + raise Exception('Tried to set cap extensions to None on path with custom cap type') + self._cap_extensions = numpy.array(vals, dtype=float) + else: + if vals is not None: + raise Exception('Tried to set custom cap extensions on path with non-custom cap type') + self._cap_extensions = vals # vertices property @property @@ -126,6 +156,8 @@ class Path(Shape): self.vertices = vertices self.width = width self.cap = cap + if cap_extensions is not None: + self.cap_extensions = cap_extensions self.rotate(rotation) [self.mirror(a) for a, do in enumerate(mirrored) if do] @@ -133,6 +165,7 @@ class Path(Shape): def travel(travel_pairs: Tuple[Tuple[float, float]], width: float = 0.0, cap: 'Path.Cap' = Cap.Flush, + cap_extensions = None, offset: vector2=(0.0, 0.0), rotation: float = 0, mirrored: Tuple[bool] = (False, False), @@ -149,6 +182,8 @@ class Path(Shape): to the +x axis). :param width: Path width, default 0 :param cap: End-cap type, default Path.Cap.Flush (no end-cap) + :param cap_extensions: End-cap extension distances, when using Path.Cap.CustomSquare. + Default (0, 0) or None, depending on cap type :param offset: Offset, default (0, 0) :param rotation: Rotation counterclockwise, in radians. Default 0 :param mirrored: Whether to mirror across the x or y axes. For example, @@ -166,7 +201,7 @@ class Path(Shape): direction = numpy.dot(rotation_matrix_2d(angle), direction.T).T verts.append(verts[-1] + direction * distance) - return Path(vertices=verts, width=width, cap=cap, + return Path(vertices=verts, width=width, cap=cap, cap_extensions=cap_extensions, offset=offset, rotation=rotation, mirrored=mirrored, layer=layer, dose=dose) @@ -174,12 +209,7 @@ class Path(Shape): poly_num_points: int=None, poly_max_arclen: float=None, ) -> List['Polygon']: - if self.cap in (Path.Cap.Flush, Path.Cap.Circle): - extension = 0.0 - elif self.cap == Path.Cap.Square: - extension = self.width / 2 - else: - raise PatternError('Unrecognized path endcap: {}'.format(self.cap)) + extensions = self._calculate_cap_extensions() v = remove_colinear_vertices(self.vertices, closed_path=False) dv = numpy.diff(v, axis=0) @@ -191,13 +221,12 @@ class Path(Shape): perp = dvdir[:, ::-1] * [[1, -1]] * self.width / 2 - # add extension - if extension != 0: - v[0] -= dvdir[0] * extension - v[-1] += dvdir[-1] * extension + # add extensions + if (extensions != 0).any(): + v[0] -= dvdir[0] * extensions[0] + v[-1] += dvdir[-1] * extensions[1] dv = numpy.diff(v, axis=0) # recalculate dv; dvdir and perp should stay the same - # Find intersections of expanded sides As = numpy.stack((dv[:-1], -dv[1:]), axis=2) bs = v[1:-1] - v[:-2] + perp[1:] - perp[:-1] @@ -254,19 +283,17 @@ class Path(Shape): bounds = self.offset + numpy.vstack((numpy.min(self.vertices, axis=0) - self.width / 2, numpy.max(self.vertices, axis=0) + self.width / 2)) elif self.cap in (Path.Cap.Flush, - Path.Cap.Square): - if self.cap == Path.Cap.Flush: - extension = 0 - elif self.cap == Path.Cap.Square: - extension = self.width / 2 + Path.Cap.Square, + Path.Cap.SquareCustom): + extensions = self._calculate_cap_extensions() v = remove_colinear_vertices(self.vertices, closed_path=False) dv = numpy.diff(v, axis=0) dvdir = dv / numpy.sqrt((dv * dv).sum(axis=1))[:, None] perp = dvdir[:, ::-1] * [[1, -1]] * self.width / 2 - v[0] -= dvdir * extension - v[-1] += dvdir * extension + v[0] -= dvdir * extensions[0] + v[-1] += dvdir * extensions[1] bounds = self.offset + numpy.vstack((numpy.min(v - numpy.abs(perp), axis=0), numpy.max(v + numpy.abs(perp), axis=0))) @@ -342,3 +369,14 @@ class Path(Shape): ''' self.vertices = remove_colinear_vertices(self.vertices, closed_path=False) return self + + def _calculate_cap_extensions(self) -> numpy.ndarray: + if self.cap == Path.Cap.Square: + extensions = numpy.full(2, self.width / 2) + elif self.cap == Path.Cap.SquareCustom: + extensions = self.cap_extensions + else: + # Flush or Circle + extensions = numpy.zeros(2) + return extensions + From a44a7c176e413fe4d849f84204c87ec588f09348 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 15 May 2019 00:14:35 -0700 Subject: [PATCH 017/109] Add type hints --- masque/shapes/text.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/masque/shapes/text.py b/masque/shapes/text.py index 167e2c2..8da2a65 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -15,11 +15,11 @@ __author__ = 'Jan Petykiewicz' class Text(Shape): - _string = '' - _height = 1.0 - _rotation = 0.0 - _mirrored = None - font_path = '' + _string = '' # type: str + _height = 1.0 # type: float + _rotation = 0.0 # type: float + _mirrored = None # type: List[str] + font_path = '' # type: str # vertices property @property From 2909b25fd7206f3161185c817a8133a32cac1a25 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 15 May 2019 00:17:40 -0700 Subject: [PATCH 018/109] Missing args for commit bc43be48bce4690d5b51a6ec0640b7f3f3456892 (cap_extensions) --- masque/shapes/path.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/masque/shapes/path.py b/masque/shapes/path.py index 675029a..3ce8f8c 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -144,7 +144,8 @@ class Path(Shape): vertices: numpy.ndarray, width: float = 0.0, cap: 'Path.Cap' = Cap.Flush, - offset: vector2=(0.0, 0.0), + cap_extensions: numpy.ndarray = None, + offset: vector2 = (0.0, 0.0), rotation: float = 0, mirrored: Tuple[bool] = (False, False), layer: int=0, From 94410dffc9678a1e500cd7998fd9f740e45ea52a Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 15 May 2019 00:18:43 -0700 Subject: [PATCH 019/109] Fix clockwise (to-perp) path bends --- masque/shapes/path.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/masque/shapes/path.py b/masque/shapes/path.py index 3ce8f8c..8edcf21 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -250,12 +250,12 @@ class Path(Shape): if towards_perp[i]: o0.append(intersection_p[i]) if acute[i]: - o1.append(intersection_n[i]) - else: # Opposite is >270 pt0 = v[i + 1] - perp[i + 0] + dvdir[i + 0] * self.width / 2 pt1 = v[i + 1] - perp[i + 1] - dvdir[i + 1] * self.width / 2 o1 += [pt0, pt1] + else: + o1.append(intersection_n[i]) else: o1.append(intersection_n[i]) if acute[i]: From a4614460590878033159fea99b490956a343dc30 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 15 May 2019 00:19:37 -0700 Subject: [PATCH 020/109] Custom deepcopy() implementations to help speed things up --- masque/pattern.py | 9 +++++++++ masque/repetition.py | 12 +++++++++++- masque/shapes/arc.py | 10 +++++++++- masque/shapes/circle.py | 8 +++++++- masque/shapes/ellipse.py | 9 ++++++++- masque/shapes/path.py | 11 ++++++++++- masque/shapes/polygon.py | 9 ++++++++- masque/shapes/text.py | 10 +++++++++- masque/subpattern.py | 10 +++++++++- 9 files changed, 80 insertions(+), 8 deletions(-) diff --git a/masque/pattern.py b/masque/pattern.py index a63bb6d..fe75fb6 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -70,6 +70,15 @@ class Pattern: self.name = name + def __deepcopy__(self, memo: Dict = None) -> 'Pattern': + memo = {} if memo is None else memo + new = copy.copy(self) + new.name = self.name + new.shapes = copy.deepcopy(self.shapes, memo) + new.labels = copy.deepcopy(self.labels, memo) + new.subpatterns = copy.deepcopy(self.subpatterns, memo) + return new + def append(self, other_pattern: 'Pattern') -> 'Pattern': """ Appends all shapes, labels and subpatterns from other_pattern to self's shapes, diff --git a/masque/repetition.py b/masque/repetition.py index 2d153e3..efa91a7 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -3,7 +3,7 @@ instances of a Pattern in the same parent Pattern. """ -from typing import Union, List +from typing import Union, List, Dict import copy import numpy @@ -86,6 +86,16 @@ class GridRepetition: mirrored = [False, False] self.mirrored = mirrored + def __deepcopy__(self, memo: Dict = None) -> 'GridReptition': + memo = {} if memo is None else memo + new = copy.copy(self) + new.pattern = copy.deepcopy(self.pattern, memo) + new._offset = self._offset.copy() + new._mirrored = copy.deepcopy(self._mirrored, memo) + new._a_vector = self._a_vector.copy() + new._b_vector = copy.copy(self._b_vector) # ndarray or None so don't need deepcopy + return new + # offset property @property def offset(self) -> numpy.ndarray: diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index bb2da3a..ee6bf15 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -1,4 +1,4 @@ -from typing import List, Tuple +from typing import List, Tuple, Dict import math import numpy from numpy import pi @@ -159,6 +159,14 @@ class Arc(Shape): self.poly_num_points = poly_num_points self.poly_max_arclen = poly_max_arclen + def __deepcopy__(self, memo: Dict = None) -> 'Arc': + memo = {} if memo is None else memo + new = copy.copy(self) + new._offset = self._offset.copy() + new._radii = self._radii.copy() + new._angles = self._angles.copy() + return new + 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 diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index 489a608..785a382 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Dict import numpy from numpy import pi @@ -53,6 +53,12 @@ class Circle(Shape): self.poly_num_points = poly_num_points self.poly_max_arclen = poly_max_arclen + def __deepcopy__(self, memo: Dict = None) -> 'Circle': + memo = {} if memo is None else memo + new = copy.copy(self) + new._offset = self._offset.copy() + return new + 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 diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index 724c11d..7064bd5 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -1,4 +1,4 @@ -from typing import List, Tuple +from typing import List, Tuple, Dict import math import numpy from numpy import pi @@ -98,6 +98,13 @@ class Ellipse(Shape): self.poly_num_points = poly_num_points self.poly_max_arclen = poly_max_arclen + def __deepcopy__(self, memo: Dict = None) -> 'Ellipse': + memo = {} if memo is None else memo + new = copy.copy(self) + new._offset = self._offset.copy() + new._radii = self._radii.copy() + return new + def to_polygons(self, poly_num_points: int=None, poly_max_arclen: float=None diff --git a/masque/shapes/path.py b/masque/shapes/path.py index 8edcf21..a09bae1 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -1,4 +1,4 @@ -from typing import List, Tuple +from typing import List, Tuple, Dict import copy from enum import Enum import numpy @@ -162,6 +162,15 @@ class Path(Shape): self.rotate(rotation) [self.mirror(a) for a, do in enumerate(mirrored) if do] + def __deepcopy__(self, memo: Dict = None) -> 'Path': + memo = {} if memo is None else memo + new = copy.copy(self) + new._offset = self._offset.copy() + new._vertices = self._vertices.copy() + new._cap = copy.deepcopy(self._cap, memo) + new._cap_extensions = copy.deepcopy(self._cap_extensions, memo) + return new + @staticmethod def travel(travel_pairs: Tuple[Tuple[float, float]], width: float = 0.0, diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index afb47a6..403689c 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -1,4 +1,4 @@ -from typing import List, Tuple +from typing import List, Tuple, Dict import copy import numpy from numpy import pi @@ -84,6 +84,13 @@ class Polygon(Shape): self.rotate(rotation) [self.mirror(a) for a, do in enumerate(mirrored) if do] + def __deepcopy__(self, memo: Dict = None) -> 'Polygon': + memo = {} if memo is None else memo + new = copy.copy(self) + new._offset = self._offset.copy() + new._vertices = self._vertices.copy() + return new + @staticmethod def square(side_length: float, rotation: float=0.0, diff --git a/masque/shapes/text.py b/masque/shapes/text.py index 8da2a65..d265d64 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -1,4 +1,5 @@ -from typing import List, Tuple +from typing import List, Tuple, Dict +import copy import numpy from numpy import pi, inf @@ -81,6 +82,13 @@ class Text(Shape): self.font_path = font_path self.mirrored = mirrored + def __deepcopy__(self, memo: Dict = None) -> 'Text': + memo = {} if memo is None else memo + new = copy.copy(self) + new._offset = self._offset.copy() + new._mirrored = copy.deepcopy(self._mirrored, memo) + return new + def to_polygons(self, _poly_num_points: int=None, _poly_max_arclen: float=None diff --git a/masque/subpattern.py b/masque/subpattern.py index 0415894..88fe2ee 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, List +from typing import Union, List, Dict import copy import numpy @@ -45,6 +45,14 @@ class SubPattern: mirrored = [False, False] self.mirrored = mirrored + def __deepcopy__(self, memo: Dict = None) -> 'SubPattern': + memo = {} if memo is None else memo + new = copy.copy(self) + new.pattern = copy.deepcopy(self.pattern, memo) + new._offset = self._offset.copy() + new._mirrored = copy.deepcopy(self._mirrored, memo) + return new + # offset property @property def offset(self) -> numpy.ndarray: From 376cbcce263a67ecb9c28a08234a13a916d4cf42 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 15 May 2019 23:50:31 -0700 Subject: [PATCH 021/109] Use buffered IO free ~10% speedup on read, probably similar on write --- 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 8b04bec..37b1a22 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -124,7 +124,7 @@ def writefile(patterns: List[Pattern] or Pattern, else: open_func = open - with open_func(path, mode='wb') as stream: + with io.BufferedWriter(open_func(path, mode='wb')) as stream: results = write(patterns, stream, *args, **kwargs) return results @@ -218,7 +218,7 @@ def readfile(filename: str or pathlib.Path, else: open_func = open - with open_func(path, mode='rb') as stream: + with io.BufferedReader(open_func(path, mode='rb')) as stream: results = read(stream, *args, **kwargs) return results From 3ff9a8a506b10946cb78221c4b11f8925c543b77 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 15 May 2019 23:51:00 -0700 Subject: [PATCH 022/109] Shorten long names when writing Maybe turn this into an option later? --- 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 37b1a22..d57810b 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -511,7 +511,14 @@ def _labels_to_texts(labels: List[Label]) -> List[gdsii.elements.Text]: def _disambiguate_pattern_names(patterns): used_names = [] for pat in patterns: - sanitized_name = re.compile('[^A-Za-z0-9_\?\$]').sub('_', pat.name) + if len(pat.name) > 32: + shortened_name = pat.name[:26] + logger.warning('Pattern name "{}" is too long ({}/32 chars),'.format(pat.name, len(pat.name)) + \ + ' shortening to "{}" before generating suffix'.format(shortened_name)) + else: + shortened_name = pat.name + + sanitized_name = re.compile('[^A-Za-z0-9_\?\$]').sub('_', shortened_name) i = 0 suffixed_name = sanitized_name From 4bd5542ce95db9463a837316abb437c0e6ea83d0 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 15 May 2019 23:51:17 -0700 Subject: [PATCH 023/109] Clarify warning when adding suffix --- masque/file/gdsii.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index d57810b..bc5570b 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -531,7 +531,8 @@ def _disambiguate_pattern_names(patterns): 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)) + logger.warning('Pattern name "{}" ({}) appears multiple times; renaming to "{}"'.format( + pat.name, sanitized_name, suffixed_name)) encoded_name = suffixed_name.encode('ASCII') if len(encoded_name) == 0: From a6a3ae797c85f22f5bbbf5ab78f9e366c67ffaf5 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 15 May 2019 23:51:51 -0700 Subject: [PATCH 024/109] Fix Pattern.scale_by() for labels Label offsets weren't being scaled. --- masque/pattern.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/masque/pattern.py b/masque/pattern.py index fe75fb6..fb89f26 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -384,6 +384,8 @@ class Pattern: for entry in self.shapes + self.subpatterns: entry.offset *= c entry.scale_by(c) + for entry in self.labels: + entry.offset *= c return self def rotate_around(self, pivot: vector2, rotation: float) -> 'Pattern': From 1ef8518c60d0f290010b8254d8b21c19ea9063de Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 May 2019 00:28:46 -0700 Subject: [PATCH 025/109] Fix dose2dtype Was broken during gdsii refactor --- masque/file/gdsii.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index bc5570b..108d72d 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -171,6 +171,8 @@ def 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) + # 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 @@ -179,18 +181,22 @@ def dose2dtype(patterns: Pattern or List[Pattern], new_pats[(pat_id, pat_dose)] = patterns_by_id[pat_id] continue - pat = patterns_by_id[pat_id].deepcopy() + old_pat = patterns_by_id[pat_id] + pat = old_pat.copy() # keep old subpatterns + pat.shapes = copy.deepcopy(old_pat.shapes) + pat.labels = copy.deepcopy(old_pat.labels) - encoded_name = mangle_name(pat, pat_dose).encode('ASCII') + encoded_name = mangle_name(pat, pat_dose) if len(encoded_name) == 0: raise PatternError('Zero-length name after mangle+encode, originally "{}"'.format(pat.name)) + pat.name = encoded_name for shape in pat.shapes: data_type = dose_vals_list.index(shape.dose * pat_dose) if is_scalar(shape.layer): - layer = (shape.layer, data_type) + shape.layer = (shape.layer, data_type) else: - layer = (shape.layer[0], data_type) + shape.layer = (shape.layer[0], data_type) new_pats[(pat_id, pat_dose)] = pat From 17451cd9956861ee9d58e8ebd4100894f2da4cee Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 May 2019 00:29:17 -0700 Subject: [PATCH 026/109] Force dose2dtype to accept only a list of patterns Backwards incompatible, just like the rest of the gdsii rework --- masque/file/gdsii.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 108d72d..6932469 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -129,8 +129,8 @@ def writefile(patterns: List[Pattern] or Pattern, return results -def dose2dtype(patterns: Pattern or List[Pattern], - ) -> Tuple[List[Pattern], List[float]]: +def dose2dtype(patterns: List[Pattern], + ) -> Tuple[List[Pattern], List[float]]: """ For each shape in each pattern, set shape.layer to the tuple (base_layer, datatype), where: @@ -149,9 +149,6 @@ def dose2dtype(patterns: Pattern or List[Pattern], 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] - # Get a dict of id(pattern) -> pattern patterns_by_id = {id(pattern): pattern for pattern in patterns} for pattern in patterns: @@ -206,7 +203,7 @@ def dose2dtype(patterns: Pattern or List[Pattern], dose_mult = subpat.dose * pat_dose subpat.pattern = new_pats[(id(subpat.pattern), dose_mult)] - return patterns, list(dose_vals) + return patterns, dose_vals_list def readfile(filename: str or pathlib.Path, From 703c1aa6d8d5c655dc4483beebf0929351049b31 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 May 2019 00:31:07 -0700 Subject: [PATCH 027/109] Custom deepcopy for labels --- masque/label.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/masque/label.py b/masque/label.py index b3bbb6f..738ab20 100644 --- a/masque/label.py +++ b/masque/label.py @@ -1,4 +1,4 @@ -from typing import List, Tuple +from typing import List, Tuple, Dict import copy import numpy from numpy import pi @@ -80,6 +80,11 @@ class Label: self.offset = numpy.array(offset, dtype=float) self.layer = layer + def __deepcopy__(self, memo: Dict = None) -> 'Label': + memo = {} if memo is None else memo + new = copy.copy(self) + new._offset = self._offset.copy() + return new # ---- Non-abstract methods def copy(self) -> 'Label': From 0b962999b2909b51b87d94fbb7c7e9588abf6c48 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 May 2019 00:34:01 -0700 Subject: [PATCH 028/109] Add arbitrary identifier for labels and subpattern/repetition --- masque/label.py | 4 ++++ masque/repetition.py | 5 ++++- masque/subpattern.py | 4 +++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/masque/label.py b/masque/label.py index 738ab20..2a2b56d 100644 --- a/masque/label.py +++ b/masque/label.py @@ -24,6 +24,9 @@ class Label: # Label string _string = None # type: str + # Arbitrary identifier tuple + identifier: Tuple + # ---- Properties # offset property @property @@ -76,6 +79,7 @@ class Label: string: str, offset: vector2=(0.0, 0.0), layer: int=0): + self.identifier = () self.string = string self.offset = numpy.array(offset, dtype=float) self.layer = layer diff --git a/masque/repetition.py b/masque/repetition.py index efa91a7..8618ba6 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -3,7 +3,7 @@ instances of a Pattern in the same parent Pattern. """ -from typing import Union, List, Dict +from typing import Union, List, Dict, Tuple import copy import numpy @@ -37,6 +37,8 @@ class GridRepetition: a_count = None # type: int b_count = 1 # type: int + identifier: Tuple + def __init__(self, pattern: 'Pattern', a_vector: numpy.ndarray, @@ -77,6 +79,7 @@ class GridRepetition: self.a_count = a_count self.b_count = b_count + self.identifier = () self.pattern = pattern self.offset = offset self.rotation = rotation diff --git a/masque/subpattern.py b/masque/subpattern.py index 88fe2ee..c84305c 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, List, Dict +from typing import Union, List, Dict, Tuple import copy import numpy @@ -28,6 +28,7 @@ class SubPattern: _dose = 1.0 # type: float _scale = 1.0 # type: float _mirrored = None # type: List[bool] + identifier: Tuple def __init__(self, pattern: 'Pattern', @@ -36,6 +37,7 @@ class SubPattern: mirrored: List[bool]=None, dose: float=1.0, scale: float=1.0): + self.identifier = () self.pattern = pattern self.offset = offset self.rotation = rotation From 38f64f7c622c7f2f06955533c678ffd71391cd96 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 May 2019 00:37:56 -0700 Subject: [PATCH 029/109] Use __slots__ for class members Also use the other sort of type hints for instance variables --- masque/label.py | 10 +++++----- masque/pattern.py | 9 +++++---- masque/repetition.py | 31 +++++++++++++++++++++---------- masque/shapes/arc.py | 18 +++++++++--------- masque/shapes/circle.py | 11 +++++------ masque/shapes/ellipse.py | 14 +++++++------- masque/shapes/path.py | 12 ++++++++---- masque/shapes/polygon.py | 4 +++- masque/shapes/shape.py | 17 ++++++----------- masque/shapes/text.py | 15 ++++++++++----- masque/subpattern.py | 14 +++++++------- 11 files changed, 86 insertions(+), 69 deletions(-) diff --git a/masque/label.py b/masque/label.py index 2a2b56d..2dcf6f2 100644 --- a/masque/label.py +++ b/masque/label.py @@ -14,15 +14,15 @@ class Label: """ A circle, which has a position and radius. """ - + __slots__ = ('_offset', '_layer', '_string', 'identifier') # [x_offset, y_offset] - _offset = numpy.array([0.0, 0.0]) # type: numpy.ndarray + _offset: numpy.ndarray - # Layer (integer >= 0) - _layer = 0 # type: int or Tuple + # Layer (integer >= 0) or 2-Tuple of integers + _layer: int or Tuple # Label string - _string = None # type: str + _string: str # Arbitrary identifier tuple identifier: Tuple diff --git a/masque/pattern.py b/masque/pattern.py index fb89f26..aa6c459 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -33,10 +33,11 @@ class Pattern: may reference the same Pattern object. :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 or GridRepetition] - name = None # type: str + __slots__ = ('shapes', 'labels', 'subpatterns', 'name') + shapes: List[Shape] + labels: List[Label] + subpatterns: List[SubPattern or GridRepetition] + name: str def __init__(self, shapes: List[Shape]=(), diff --git a/masque/repetition.py b/masque/repetition.py index 8618ba6..ccdf529 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -23,19 +23,30 @@ class GridRepetition: GridRepetition provides support for efficiently embedding multiple copies of a Pattern into another Pattern at regularly-spaced offsets. """ + __slots__ = ('pattern', + '_offset', + '_rotation', + '_dose', + '_scale', + '_mirrored', + '_a_vector', + '_b_vector', + 'a_count', + 'b_count', + 'identifier') - pattern = None # type: Pattern + pattern: '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] + _offset: numpy.ndarray + _rotation: float + _dose: float + _scale: float + _mirrored: List[bool] - _a_vector = None # type: numpy.ndarray - _b_vector = None # type: numpy.ndarray or None - a_count = None # type: int - b_count = 1 # type: int + _a_vector: numpy.ndarray + _b_vector: numpy.ndarray or None + a_count: int + b_count: int identifier: Tuple diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index ee6bf15..b8e8547 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -20,15 +20,14 @@ class Arc(Shape): The rotation gives the angle from x-axis, counterclockwise, to the first (x) radius. The start and stop angle are measured 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 + __slots__ = ('_radii', '_angles', '_width', '_rotation', + 'poly_num_points', 'poly_max_arclen') + _radii: numpy.ndarray + _angles: numpy.ndarray + _width: float + _rotation: float + poly_num_points: int + poly_max_arclen: float # radius properties @property @@ -148,6 +147,7 @@ class Arc(Shape): mirrored: Tuple[bool] = (False, False), layer: int=0, dose: float=1.0): + self.identifier = () self.radii = radii self.angles = angles self.width = width diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index 785a382..97df5c0 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -14,12 +14,10 @@ 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 + __slots__ = ('_radius', 'poly_num_points', 'poly_max_arclen') + _radius: float + poly_num_points: int + poly_max_arclen: float # radius property @property @@ -46,6 +44,7 @@ class Circle(Shape): offset: vector2=(0.0, 0.0), layer: int=0, dose: float=1.0): + self.identifier = () self.offset = numpy.array(offset, dtype=float) self.layer = layer self.dose = dose diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index 7064bd5..7915007 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -16,13 +16,12 @@ 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 + __slots__ = ('_radii', '_rotation', + 'poly_num_points', 'poly_max_arclen') + _radii: numpy.ndarray + _rotation: float + poly_num_points: int + poly_max_arclen: float # radius properties @property @@ -89,6 +88,7 @@ class Ellipse(Shape): mirrored: Tuple[bool] = (False, False), layer: int=0, dose: float=1.0): + self.identifier = () self.radii = radii self.offset = offset self.rotation = rotation diff --git a/masque/shapes/path.py b/masque/shapes/path.py index a09bae1..8e3b385 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -19,10 +19,11 @@ class Path(Shape): A normalized_form(...) is available, but can be quite slow with lots of vertices. """ - _vertices = None # type: numpy.ndarray - _width = None # type: float - _cap = None # type: Path.Cap - _cap_extensions = None # type: numpy.ndarray or None + __slots__ = ('_vertices', '_width', '_cap', '_cap_extensions') + _vertices: numpy.ndarray + _width: float + _cap: 'Path.Cap' + _cap_extensions: numpy.ndarray or None class Cap(Enum): Flush = 0 # Path ends at final vertices @@ -151,6 +152,9 @@ class Path(Shape): layer: int=0, dose: float=1.0, ) -> 'Path': + self._cap_extensions = None # Since .cap setter might access it + + self.identifier = () self.offset = offset self.layer = layer self.dose = dose diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 403689c..fda6cd7 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -18,7 +18,8 @@ class Polygon(Shape): A normalized_form(...) is available, but can be quite slow with lots of vertices. """ - _vertices = None # type: numpy.ndarray + __slots__ = ('_vertices',) + _vertices: numpy.ndarray # vertices property @property @@ -77,6 +78,7 @@ class Polygon(Shape): layer: int=0, dose: float=1.0, ): + self.identifier = () self.layer = layer self.dose = dose self.vertices = vertices diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index 46123a1..4dca5ae 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -24,18 +24,13 @@ class Shape(metaclass=ABCMeta): """ Abstract class specifying functions common to all shapes. """ + __slots__ = ('_offset', '_layer', '_dose', 'identifier') - # [x_offset, y_offset] - _offset = numpy.array([0.0, 0.0]) # type: numpy.ndarray - - # Layer (integer >= 0 or tuple) - _layer = 0 # type: int or Tuple - - # Dose - _dose = 1.0 # type: float - - # An arbitrary identifier for the shape, usually not set but used by Pattern.flatten() - identifier = () # type: Tuple + _offset: numpy.ndarray # [x_offset, y_offset] + _layer: int or Tuple # Layer (integer >= 0 or tuple) + _dose: float # Dose + identifier: Tuple # An arbitrary identifier for the shape, + # usually empty but used by Pattern.flatten() # --- Abstract methods @abstractmethod diff --git a/masque/shapes/text.py b/masque/shapes/text.py index d265d64..5824a62 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -16,11 +16,16 @@ __author__ = 'Jan Petykiewicz' class Text(Shape): - _string = '' # type: str - _height = 1.0 # type: float - _rotation = 0.0 # type: float - _mirrored = None # type: List[str] - font_path = '' # type: str + """ + Text (to be printed e.g. as a set of polygons). + This is distinct from non-printed Label objects. + """ + __slots__ = ('_string', '_height', '_rotation', '_mirrored', 'font_path') + _string: str + _height: float + _rotation: float + _mirrored: List[str] + font_path: str # vertices property @property diff --git a/masque/subpattern.py b/masque/subpattern.py index c84305c..745184b 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -21,13 +21,13 @@ class SubPattern: 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 - _mirrored = None # type: List[bool] + __slots__ = ('pattern', '_offset', '_rotation', '_dose', '_scale', '_mirrored', 'identifier') + pattern: 'Pattern' + _offset: numpy.ndarray + _rotation: float + _dose: float + _scale: float + _mirrored: List[bool] identifier: Tuple def __init__(self, From 58353b7884f3a46fa7db14efa160aa7ae6b67908 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 May 2019 00:38:20 -0700 Subject: [PATCH 030/109] Remove orphaned comment --- masque/label.py | 1 - 1 file changed, 1 deletion(-) diff --git a/masque/label.py b/masque/label.py index 2dcf6f2..3e2e8c7 100644 --- a/masque/label.py +++ b/masque/label.py @@ -90,7 +90,6 @@ class Label: new._offset = self._offset.copy() return new - # ---- Non-abstract methods def copy(self) -> 'Label': """ Returns a deep copy of the shape. From 16c6bfc70a0612bc3ca13625e98ed3bb5f9297f1 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 May 2019 00:39:46 -0700 Subject: [PATCH 031/109] Cosmetic changes to argument lists --- masque/pattern.py | 30 +++++++++++++++--------------- masque/shapes/arc.py | 17 ++++++++++------- masque/shapes/circle.py | 15 +++++++++------ masque/shapes/ellipse.py | 16 ++++++++-------- masque/shapes/path.py | 14 +++++++------- masque/shapes/polygon.py | 30 +++++++++++++++--------------- masque/shapes/shape.py | 10 ++++++++-- masque/shapes/text.py | 16 ++++++++-------- masque/subpattern.py | 10 +++++----- 9 files changed, 85 insertions(+), 73 deletions(-) diff --git a/masque/pattern.py b/masque/pattern.py index aa6c459..4e9d730 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -94,10 +94,10 @@ class Pattern: 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, + shapes_func: Callable[[Shape], bool] = None, + labels_func: Callable[[Label], bool] = None, + subpatterns_func: Callable[[SubPattern], bool] = None, + recursive: bool = False, ) -> 'Pattern': """ Returns a Pattern containing only the entities (e.g. shapes) for which the @@ -133,7 +133,7 @@ class Pattern: def apply(self, func: Callable[['Pattern'], 'Pattern'], - memo: Dict[int, 'Pattern']=None, + memo: Dict[int, 'Pattern'] = None, ) -> 'Pattern': """ Recursively apply func() to this pattern and any pattern it references. @@ -166,8 +166,8 @@ class Pattern: return pat def polygonize(self, - poly_num_points: int=None, - poly_max_arclen: float=None + 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, @@ -191,7 +191,7 @@ class Pattern: def manhattanize(self, grid_x: numpy.ndarray, - grid_y: numpy.ndarray + grid_y: numpy.ndarray, ) -> 'Pattern': """ Calls .polygonize() and .flatten on the pattern, then calls .manhattanize() on all the @@ -209,9 +209,9 @@ class Pattern: return self def subpatternize(self, - recursive: bool=True, - norm_value: int=1e6, - exclude_types: Tuple[Shape]=(Polygon,) + recursive: bool = True, + norm_value: int = int(1e6), + exclude_types: Tuple[Shape] = (Polygon,) ) -> 'Pattern': """ Iterates through this Pattern and all referenced Patterns. Within each Pattern, it iterates @@ -529,10 +529,10 @@ class Pattern: return self def visualize(self, - offset: vector2=(0., 0.), - line_color: str='k', - fill_color: str='none', - overdraw: bool=False): + 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 diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index b8e8547..6972e3d 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -140,13 +140,13 @@ class Arc(Shape): radii: vector2, angles: vector2, width: float, - poly_num_points: int=DEFAULT_POLY_NUM_POINTS, - poly_max_arclen: float=None, - offset: vector2=(0.0, 0.0), - rotation: float=0, + poly_num_points: int = DEFAULT_POLY_NUM_POINTS, + poly_max_arclen: float = None, + offset: vector2 = (0.0, 0.0), + rotation: float = 0, mirrored: Tuple[bool] = (False, False), - layer: int=0, - dose: float=1.0): + layer: int = 0, + dose: float = 1.0): self.identifier = () self.radii = radii self.angles = angles @@ -167,7 +167,10 @@ class Arc(Shape): new._angles = self._angles.copy() return new - def to_polygons(self, poly_num_points: int=None, poly_max_arclen: float=None) -> List[Polygon]: + 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: diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index 97df5c0..247aa78 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -39,11 +39,11 @@ class Circle(Shape): 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): + 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.identifier = () self.offset = numpy.array(offset, dtype=float) self.layer = layer @@ -58,7 +58,10 @@ class Circle(Shape): new._offset = self._offset.copy() return new - def to_polygons(self, poly_num_points: int=None, poly_max_arclen: float=None) -> List[Polygon]: + 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: diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index 7915007..6be05c8 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -81,13 +81,13 @@ class Ellipse(Shape): def __init__(self, radii: vector2, - poly_num_points: int=DEFAULT_POLY_NUM_POINTS, - poly_max_arclen: float=None, - offset: vector2=(0.0, 0.0), - rotation: float=0, + poly_num_points: int = DEFAULT_POLY_NUM_POINTS, + poly_max_arclen: float = None, + offset: vector2 = (0.0, 0.0), + rotation: float = 0, mirrored: Tuple[bool] = (False, False), - layer: int=0, - dose: float=1.0): + layer: int = 0, + dose: float = 1.0): self.identifier = () self.radii = radii self.offset = offset @@ -106,8 +106,8 @@ class Ellipse(Shape): return new def to_polygons(self, - poly_num_points: int=None, - poly_max_arclen: float=None + 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 diff --git a/masque/shapes/path.py b/masque/shapes/path.py index 8e3b385..6525ec1 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -149,8 +149,8 @@ class Path(Shape): offset: vector2 = (0.0, 0.0), rotation: float = 0, mirrored: Tuple[bool] = (False, False), - layer: int=0, - dose: float=1.0, + layer: int = 0, + dose: float = 1.0, ) -> 'Path': self._cap_extensions = None # Since .cap setter might access it @@ -180,11 +180,11 @@ class Path(Shape): width: float = 0.0, cap: 'Path.Cap' = Cap.Flush, cap_extensions = None, - offset: vector2=(0.0, 0.0), + offset: vector2 = (0.0, 0.0), rotation: float = 0, mirrored: Tuple[bool] = (False, False), - layer: int=0, - dose: float=1.0, + layer: int = 0, + dose: float = 1.0, ) -> 'Path': """ Build a path by specifying the turn angles and travel distances @@ -220,8 +220,8 @@ class Path(Shape): layer=layer, dose=dose) def to_polygons(self, - poly_num_points: int=None, - poly_max_arclen: float=None, + poly_num_points: int = None, + poly_max_arclen: float = None, ) -> List['Polygon']: extensions = self._calculate_cap_extensions() diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index fda6cd7..503d9a5 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -72,11 +72,11 @@ class Polygon(Shape): def __init__(self, vertices: numpy.ndarray, - offset: vector2=(0.0, 0.0), - rotation: float=0.0, + offset: vector2 = (0.0, 0.0), + rotation: float = 0.0, mirrored: Tuple[bool] = (False, False), - layer: int=0, - dose: float=1.0, + layer: int = 0, + dose: float = 1.0, ): self.identifier = () self.layer = layer @@ -95,10 +95,10 @@ class Polygon(Shape): @staticmethod def square(side_length: float, - rotation: float=0.0, - offset: vector2=(0.0, 0.0), - layer: int=0, - dose: float=1.0 + rotation: float = 0.0, + offset: vector2 = (0.0, 0.0), + layer: int = 0, + dose: float = 1.0, ) -> 'Polygon': """ Draw a square given side_length, centered on the origin. @@ -122,10 +122,10 @@ class Polygon(Shape): @staticmethod def rectangle(lx: float, ly: float, - rotation: float=0, - offset: vector2=(0.0, 0.0), - layer: int=0, - dose: float=1.0 + 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, centered on the origin. @@ -156,7 +156,7 @@ class Polygon(Shape): ymax: float = None, ly: float = None, layer: int = 0, - dose: float = 1.0 + dose: float = 1.0, ) -> 'Polygon': """ Draw a rectangle by specifying side/center positions. @@ -222,8 +222,8 @@ class Polygon(Shape): def to_polygons(self, - _poly_num_points: int=None, - _poly_max_arclen: float=None, + _poly_num_points: int = None, + _poly_max_arclen: float = None, ) -> List['Polygon']: return [copy.deepcopy(self)] diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index 4dca5ae..00d9c18 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -193,7 +193,10 @@ class Shape(metaclass=ABCMeta): self.translate(+pivot) return self - def manhattanize_fast(self, grid_x: numpy.ndarray, grid_y: numpy.ndarray) -> List['Polygon']: + 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. @@ -295,7 +298,10 @@ class Shape(metaclass=ABCMeta): return manhattan_polygons - def manhattanize(self, grid_x: numpy.ndarray, grid_y: numpy.ndarray) -> List['Polygon']: + 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. diff --git a/masque/shapes/text.py b/masque/shapes/text.py index 5824a62..a579e9b 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -73,11 +73,11 @@ class Text(Shape): string: str, height: float, font_path: str, - offset: vector2=(0.0, 0.0), - rotation: float=0.0, - mirrored: Tuple[bool]=(False, False), - layer: int=0, - dose: float=1.0): + offset: vector2 = (0.0, 0.0), + rotation: float = 0.0, + mirrored: Tuple[bool] = (False, False), + layer: int = 0, + dose: float = 1.0): self.offset = offset self.layer = layer self.dose = dose @@ -95,8 +95,8 @@ class Text(Shape): return new def to_polygons(self, - _poly_num_points: int=None, - _poly_max_arclen: float=None + _poly_num_points: int = None, + _poly_max_arclen: float = None, ) -> List[Polygon]: all_polygons = [] total_advance = 0 @@ -153,7 +153,7 @@ class Text(Shape): def get_char_as_polygons(font_path: str, char: str, - resolution: float=48*64, + resolution: float = 48*64, ) -> Tuple[List[List[List[float]]], float]: from freetype import Face from matplotlib.path import Path diff --git a/masque/subpattern.py b/masque/subpattern.py index 745184b..f76605b 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -32,11 +32,11 @@ class SubPattern: 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): + offset: vector2 = (0.0, 0.0), + rotation: float = 0.0, + mirrored: List[bool] = None, + dose: float = 1.0, + scale: float = 1.0): self.identifier = () self.pattern = pattern self.offset = offset From 2e54cf10804811c542d010b63474ba171b55dc3c Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 May 2019 00:39:56 -0700 Subject: [PATCH 032/109] initialize identifier for text shapes --- masque/shapes/text.py | 1 + 1 file changed, 1 insertion(+) diff --git a/masque/shapes/text.py b/masque/shapes/text.py index a579e9b..5c6b115 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -78,6 +78,7 @@ class Text(Shape): mirrored: Tuple[bool] = (False, False), layer: int = 0, dose: float = 1.0): + self.identifier = () self.offset = offset self.layer = layer self.dose = dose From 032c410b430c60c132ad0f026c46c82a4f1d987f Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 May 2019 00:41:26 -0700 Subject: [PATCH 033/109] Add mirror_x to extrinsic properties It's not implemented for Polygon shapes, since I haven't thought about how to normalize those for reflection yet --- masque/pattern.py | 4 ++-- masque/shapes/arc.py | 2 +- masque/shapes/circle.py | 2 +- masque/shapes/ellipse.py | 2 +- masque/shapes/path.py | 2 +- masque/shapes/polygon.py | 2 +- masque/shapes/shape.py | 4 ++-- masque/shapes/text.py | 15 ++++++++++++--- 8 files changed, 21 insertions(+), 12 deletions(-) diff --git a/masque/pattern.py b/masque/pattern.py index 4e9d730..785b6d4 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -261,9 +261,9 @@ class Pattern: pat = Pattern(shapes=[shape]) for i, values in shape_table[label][1]: - (offset, scale, rotation, dose) = values + (offset, scale, rotation, mirror_x, dose) = values subpat = SubPattern(pattern=pat, offset=offset, scale=scale, - rotation=rotation, dose=dose) + rotation=rotation, dose=dose, mirrored=(mirror_x, False)) self.subpatterns.append(subpat) shapes_to_remove.append(i) diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 6972e3d..c47840a 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -319,7 +319,7 @@ class Arc(Shape): width = self.width return (type(self), radii, angles, width/norm_value, self.layer), \ - (self.offset, scale/norm_value, rotation, self.dose), \ + (self.offset, scale/norm_value, rotation, False, self.dose), \ lambda: Arc(radii=radii*norm_value, angles=angles, width=width*norm_value, layer=self.layer) def get_cap_edges(self) -> numpy.ndarray: diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index 247aa78..c706856 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -102,6 +102,6 @@ class Circle(Shape): rotation = 0.0 magnitude = self.radius / norm_value return (type(self), self.layer), \ - (self.offset, magnitude, rotation, self.dose), \ + (self.offset, magnitude, rotation, False, self.dose), \ lambda: Circle(radius=norm_value, layer=self.layer) diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index 6be05c8..498d281 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -169,6 +169,6 @@ class Ellipse(Shape): scale = self.radius_y angle = (self.rotation + pi / 2) % pi return (type(self), radii, self.layer), \ - (self.offset, scale/norm_value, angle, self.dose), \ + (self.offset, scale/norm_value, angle, False, self.dose), \ lambda: Ellipse(radii=radii*norm_value, layer=self.layer) diff --git a/masque/shapes/path.py b/masque/shapes/path.py index 6525ec1..5bd612c 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -353,7 +353,7 @@ class Path(Shape): width0 = self.width / norm_value return (type(self), reordered_vertices.data.tobytes(), width0, self.cap, self.layer), \ - (offset, scale/norm_value, rotation, self.dose), \ + (offset, scale/norm_value, rotation, False, self.dose), \ lambda: Polygon(reordered_vertices*norm_value, width=self.width*norm_value, cap=self.cap, layer=self.layer) diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 503d9a5..e42d71b 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -265,7 +265,7 @@ class Polygon(Shape): 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), \ + (offset, scale/norm_value, rotation, False, self.dose), \ lambda: Polygon(reordered_vertices*norm_value, layer=self.layer) def clean_vertices(self) -> 'Polygon': diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index 00d9c18..497e7e9 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -12,7 +12,7 @@ __author__ = 'Jan Petykiewicz' # Type definitions normalized_shape_tuple = Tuple[Tuple, - Tuple[numpy.ndarray, float, float, float], + Tuple[numpy.ndarray, float, float, bool, float], Callable[[], 'Shape']] # ## Module-wide defaults @@ -101,7 +101,7 @@ class Shape(metaclass=ABCMeta): (intrinsic, extrinsic, constructor). These are further broken down as: intrinsic: A tuple of basic types containing all information about the instance that is not contained in 'extrinsic'. Usually, intrinsic[0] == type(self). - extrinsic: ([x_offset, y_offset], scale, rotation, dose) + extrinsic: ([x_offset, y_offset], scale, rotation, mirror_across_x_axis, dose) constructor: A callable (no arguments) which returns an instance of type(self) with internal state equivalent to 'intrinsic'. """ diff --git a/masque/shapes/text.py b/masque/shapes/text.py index 5c6b115..0349f4b 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -131,12 +131,21 @@ class Text(Shape): return self def normalized_form(self, norm_value: float) -> normalized_shape_tuple: - return (type(self), self.string, self.font_path, self.mirrored, self.layer), \ - (self.offset, self.height / norm_value, self.rotation, self.dose), \ + mirror_x, mirror_y = self.mirrored + rotation = self.rotation + if mirror_x and mirror_y: + rotation += pi + elif mirror_y: + rotation += pi + mirror_x = True + rotation %= 2 * pi + return (type(self), self.string, self.font_path, self.layer), \ + (self.offset, self.height / norm_value, rotation, mirror_x, self.dose), \ lambda: Text(string=self.string, height=self.height * norm_value, font_path=self.font_path, - mirrored=self.mirrored, + rotation=rotation, + mirrored=(mirror_x, False), layer=self.layer) def get_bounds(self) -> numpy.ndarray: From f00d283c56da8a5506626582da559403bee4d59d Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 May 2019 00:41:43 -0700 Subject: [PATCH 034/109] Add missing imports --- masque/shapes/arc.py | 1 + masque/shapes/circle.py | 1 + masque/shapes/ellipse.py | 1 + 3 files changed, 3 insertions(+) diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index c47840a..92204a4 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -1,4 +1,5 @@ from typing import List, Tuple, Dict +import copy import math import numpy from numpy import pi diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index c706856..5ee901f 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -1,4 +1,5 @@ from typing import List, Dict +import copy import numpy from numpy import pi diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index 498d281..fba194a 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -1,4 +1,5 @@ from typing import List, Tuple, Dict +import copy import math import numpy from numpy import pi From a1545559025dca692ed87efd3f650c0eb568cf2a Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 May 2019 00:42:14 -0700 Subject: [PATCH 035/109] Add TODO noting that polygon mirroring could be normalized --- masque/shapes/polygon.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index e42d71b..04b91c7 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -264,6 +264,8 @@ class Polygon(Shape): x_min = x_min[y_min] reordered_vertices = numpy.roll(rotated_vertices, -x_min, axis=0) + # TODO: normalize mirroring? + return (type(self), reordered_vertices.data.tobytes(), self.layer), \ (offset, scale/norm_value, rotation, False, self.dose), \ lambda: Polygon(reordered_vertices*norm_value, layer=self.layer) From 28084dfe456ca37fb80ba15eaa2c20f889a172dc Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 May 2019 00:42:55 -0700 Subject: [PATCH 036/109] Make name the first argument to Pattern() init Major incompatibility with previous versions!! --- masque/pattern.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/masque/pattern.py b/masque/pattern.py index 785b6d4..e47be49 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -40,10 +40,10 @@ class Pattern: name: str def __init__(self, - shapes: List[Shape]=(), - labels: List[Label]=(), - subpatterns: List[SubPattern]=(), - name: str='', + name: str = '', + shapes: List[Shape] = (), + labels: List[Label] = (), + subpatterns: List[SubPattern] = (), ): """ Basic init; arguments get assigned to member variables. From 503a77925e68e8243a89693050100a5f1dc16033 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 May 2019 00:44:10 -0700 Subject: [PATCH 037/109] Move away from __dict__ style save/load Incompatible with previous versions, but necessary given the move to __slots__. Also use pickle.HIGHEST_PROTOCOL --- masque/pattern.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/masque/pattern.py b/masque/pattern.py index e47be49..f120c52 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -511,10 +511,8 @@ class Pattern: :return: Loaded Pattern """ with open(filename, 'rb') as f: - tmp_dict = pickle.load(f) + pattern = pickle.load(f) - pattern = Pattern() - pattern.__dict__.update(tmp_dict) return pattern def save(self, filename: str) -> 'Pattern': @@ -525,7 +523,7 @@ class Pattern: :return: self """ with open(filename, 'wb') as f: - pickle.dump(self.__dict__, f, protocol=2) + pickle.dump(self, f, protocol=pickle.HIGHEST_PROTOCOL) return self def visualize(self, From 67c9b510cbb1d285313d798825bfa89c177bf76b Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 May 2019 01:16:18 -0700 Subject: [PATCH 038/109] Rename svg functions to reflect their gds counterparts Backwards incompatible! --- masque/file/svg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/masque/file/svg.py b/masque/file/svg.py index 6a28c25..6e5ff75 100644 --- a/masque/file/svg.py +++ b/masque/file/svg.py @@ -12,7 +12,7 @@ from .. import Pattern __author__ = 'Jan Petykiewicz' -def write(pattern: Pattern, +def writefile(pattern: Pattern, filename: str, custom_attributes: bool=False): """ @@ -83,7 +83,7 @@ def write(pattern: Pattern, svg.save() -def write_inverted(pattern: Pattern, filename: str): +def writefile_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 From ca80b14ee49db6a6585f7816b8cb7b73a86c5495 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 May 2019 01:20:10 -0700 Subject: [PATCH 039/109] Fix text get_bounds was addressing list using numpy slices --- masque/shapes/text.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/shapes/text.py b/masque/shapes/text.py index 0349f4b..96b9db1 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -151,7 +151,7 @@ class Text(Shape): 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]] + bounds = numpy.array([[+inf, +inf], [-inf, -inf]]) polys = self.to_polygons() for poly in polys: poly_bounds = poly.get_bounds() From 76f213a7ce75575d5216dcfaef71e10b0ee341d0 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 17 May 2019 20:51:53 -0700 Subject: [PATCH 040/109] use .identifier instead of the dynamically-created .ref_name --- masque/file/gdsii.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 6932469..7fb7ae2 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -332,12 +332,12 @@ def read(stream: io.BufferedIOBase, 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). + # according to the subpattern.identifier (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 + sp.pattern = patterns_dict[sp.identifier.decode('ASCII')] + del sp.identifier return patterns_dict, library_info @@ -357,13 +357,13 @@ def _mlayer2gds(mlayer): 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. + # and sets the instance .identifier 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 + subpat.identifier = element.struct_name if element.strans is not None: if element.mag is not None: subpat.scale = element.mag @@ -385,7 +385,7 @@ def _sref_to_subpat(element: gdsii.elements.SRef) -> SubPattern: 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. + # and sets the instance .identifier 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 @@ -428,7 +428,7 @@ def _aref_to_gridrep(element: gdsii.elements.ARef) -> GridRepetition: rotation=rotation, scale=scale, mirrored=(mirror_signs[::-1] == -1)) - gridrep.ref_name = element.struct_name + gridrep.identifier = element.struct_name return gridrep From 194a90fe7a5682c4f3a43661ff235b5f6c69dc97 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 May 2019 15:04:33 -0700 Subject: [PATCH 041/109] Add Pattern.dfs() Depth-first traversal with hierarchy and transform tracking --- masque/pattern.py | 77 ++++++++++++++++++++++++++++++++++++++++++++++- masque/utils.py | 14 +++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/masque/pattern.py b/masque/pattern.py index f120c52..7b39d97 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -15,12 +15,15 @@ from .subpattern import SubPattern from .repetition import GridRepetition from .shapes import Shape, Polygon from .label import Label -from .utils import rotation_matrix_2d, vector2 +from .utils import rotation_matrix_2d, vector2, normalize_mirror from .error import PatternError __author__ = 'Jan Petykiewicz' +visitor_function_t = Callable[['Pattern', Tuple['Pattern'], Dict, numpy.ndarray], 'Pattern'] + + class Pattern: """ 2D layout consisting of some set of shapes and references to other Pattern objects @@ -165,6 +168,78 @@ class Pattern: pat = memo[pat_id] return pat + def dfs(self, + visit_before: visitor_function_t = None, + visit_after: visitor_function_t = None, + transform: numpy.ndarray or bool or None = False , + memo: Dict = None, + hierarchy: Tuple['Pattern'] = (), + ) -> 'Pattern': + """ + Experimental convenience function. + Performs a depth-first traversal of this pattern and its subpatterns. + At each pattern in the tree, the following sequence is called: + ``` + current_pattern = visit_before(current_pattern, **vist_args) + for sp in current_pattern.subpatterns] + sp.pattern = sp.pattern.df(visit_before, visit_after, updated_transform, + memo, (current_pattern,) + hierarchy) + current_pattern = visit_after(current_pattern, **visit_args) + ``` + where `visit_args` are + `hierarchy`: (top_pattern, L1_pattern, L2_pattern, ..., parent_pattern) + tuple of all parent-and-higher patterns + `transform`: numpy.ndarray containing cumulative + [x_offset, y_offset, rotation (rad), mirror_x (0 or 1)] + for the instance being visited + `memo`: Arbitrary dict (not altered except by visit_*()) + + :param visit_before: Function to call before traversing subpatterns. + Should accept a Pattern and **visit_args, and return the (possibly modified) + pattern. Default None (not called). + :param visit_after: Function to call after traversing subpatterns. + Should accept a Pattern and **visit_args, and return the (possibly modified) + pattern. Default None (not called). + :param transform: Initial value for `visit_args['transform']`. + Can be `False`, in which case the transform is not calculated. + `True` or `None` is interpreted as [0, 0, 0, 0]. + :param memo: Arbitrary dict for use by visit_*() functions. Default None (empty dict). + :param hierarchy: Tuple of patterns specifying the hierarchy above the current pattern. + Appended to the start of the generated `visit_args['hierarchy']`. + Default is an empty tuple. + """ + if memo is None: + memo = {} + + if transform is None or transform is True: + transform = numpy.zeros(4) + + if self in hierarchy: + raise PatternError('.dfs() called on pattern with circular reference') + + pat = self + if visit_before is not None: + pat = visit_before(pat, hierarchy=hierarchy, memo=memo, transform=transform) + + for subpattern in self.subpatterns: + if transform is not False: + mirror_x, angle = normalize_mirror(subpattern.mirrored) + angle += subpattern.rotation + sp_transform = transform + numpy.hstack((subpattern.offset, angle, mirror_x)) + sp_transform[3] %= 2 + else: + sp_transform = False + + subpattern.pattern = subpattern.pattern.dfs(visit_before=visit_before, + visit_after=visit_after, + transform=sp_transform, + memo=memo, + hierarchy=hierarchy + (self,)) + + if visit_after is not None: + pat = visit_after(pat, hierarchy=hierarchy, memo=memo, transform=transform) + return pat + def polygonize(self, poly_num_points: int = None, poly_max_arclen: float = None, diff --git a/masque/utils.py b/masque/utils.py index bf82cba..aa99bf2 100644 --- a/masque/utils.py +++ b/masque/utils.py @@ -57,6 +57,20 @@ def rotation_matrix_2d(theta: float) -> numpy.ndarray: [numpy.sin(theta), +numpy.cos(theta)]]) +def normalize_mirror(mirrored: Tuple[bool, bool]) -> Tuple[bool, float]: + mirrored_x, mirrored_y = mirrored + if mirrored_x and mirrored_y: + angle = numpy.pi + mirror_x = False + elif mirrored_x: + angle = 0 + mirror_x = True + elif mirror_y: + angle = numpy.pi + mirror_x = True + return mirror_x, angle + + def remove_duplicate_vertices(vertices: numpy.ndarray, closed_path: bool = True) -> numpy.ndarray: duplicates = (vertices == numpy.roll(vertices, 1, axis=0)).all(axis=1) if not closed_path: From 75b42f6b63cb5a8e3a84cd6fd4a03b64c9a9b069 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 May 2019 15:05:30 -0700 Subject: [PATCH 042/109] Use normalize_mirror for all 2d-to-1d mirroring --- masque/file/gdsii.py | 16 ++++------------ masque/shapes/text.py | 11 +++-------- 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 7fb7ae2..cfb3f36 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -21,7 +21,7 @@ from .utils import mangle_name, make_dose_table from .. import Pattern, SubPattern, GridRepetition, PatternError, Label, Shape from ..shapes import Polygon, Path from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar -from ..utils import remove_colinear_vertices +from ..utils import remove_colinear_vertices, normalize_mirror __author__ = 'Jan Petykiewicz' @@ -455,17 +455,9 @@ def _subpatterns_to_refs(subpatterns: List[SubPattern or GridRepetition] 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_x 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.angle %= 360 + mirror_x, extra_angle = normalize_mirror(subpat.mirrored) + ref.angle = ((subpat.rotation + extra_angle) * 180 / numpy.pi) % 360 + ref.strans = set_bit(ref.strans, 15 - 0, True) if mirror_x else 0 ref.mag = subpat.scale refs.append(ref) diff --git a/masque/shapes/text.py b/masque/shapes/text.py index 96b9db1..f6c4aac 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -5,7 +5,7 @@ from numpy import pi, inf from . import Shape, Polygon, normalized_shape_tuple from .. import PatternError -from ..utils import is_scalar, vector2, get_bit +from ..utils import is_scalar, vector2, get_bit, normalize_mirror # Loaded on use: # from freetype import Face @@ -131,13 +131,8 @@ class Text(Shape): return self def normalized_form(self, norm_value: float) -> normalized_shape_tuple: - mirror_x, mirror_y = self.mirrored - rotation = self.rotation - if mirror_x and mirror_y: - rotation += pi - elif mirror_y: - rotation += pi - mirror_x = True + mirror_x, rotation = normalize_mirror(self.mirrored) + rotation += self.rotation rotation %= 2 * pi return (type(self), self.string, self.font_path, self.layer), \ (self.offset, self.height / norm_value, rotation, mirror_x, self.dose), \ From 02c3d300e08985bed88386bdf9b82bc274fff695 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 May 2019 15:05:38 -0700 Subject: [PATCH 043/109] Wrap long lines (cosmetic) --- masque/pattern.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/masque/pattern.py b/masque/pattern.py index 7b39d97..801964e 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -548,8 +548,8 @@ class Pattern: 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 Pattern, deep-copying shapes and copying subpattern + entries, but not deep-copying any referenced patterns. See also: Pattern.deepcopy() @@ -575,7 +575,9 @@ class Pattern: :return: True if the pattern is empty. """ - return (len(self.subpatterns) == 0 and len(self.shapes) == 0 and len(self.labels) == 0) + return (len(self.subpatterns) == 0 and + len(self.shapes) == 0 and + len(self.labels) == 0) @staticmethod def load(filename: str) -> 'Pattern': From 0ab5674fb91006934235e302bc22ecf40fd2c762 Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 20 May 2019 21:04:07 -0700 Subject: [PATCH 044/109] strans needs starting value (0) --- 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 cfb3f36..82a3a6f 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -435,7 +435,6 @@ def _aref_to_gridrep(element: gdsii.elements.ARef) -> GridRepetition: 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: encoded_name = subpat.pattern.name @@ -457,7 +456,8 @@ def _subpatterns_to_refs(subpatterns: List[SubPattern or GridRepetition] mirror_x, extra_angle = normalize_mirror(subpat.mirrored) ref.angle = ((subpat.rotation + extra_angle) * 180 / numpy.pi) % 360 - ref.strans = set_bit(ref.strans, 15 - 0, True) if mirror_x else 0 + # strans must be non-None for angle and mag to take effect + ref.strans = set_bit(0, 15 - 0, mirror_x) ref.mag = subpat.scale refs.append(ref) From 2c9af3f63f7e21b877affce31d044618c34079d9 Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 20 May 2019 21:04:31 -0700 Subject: [PATCH 045/109] Simplify normalize_mirror --- masque/utils.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/masque/utils.py b/masque/utils.py index aa99bf2..b7c7b05 100644 --- a/masque/utils.py +++ b/masque/utils.py @@ -59,15 +59,8 @@ def rotation_matrix_2d(theta: float) -> numpy.ndarray: def normalize_mirror(mirrored: Tuple[bool, bool]) -> Tuple[bool, float]: mirrored_x, mirrored_y = mirrored - if mirrored_x and mirrored_y: - angle = numpy.pi - mirror_x = False - elif mirrored_x: - angle = 0 - mirror_x = True - elif mirror_y: - angle = numpy.pi - mirror_x = True + mirror_x = (mirrored_x != mirrored_y) #XOR + angle = numpy.pi if mirrored_y else 0 return mirror_x, angle From dcd8324eb40bcdef91291cfea63b01c15ba220e4 Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 20 May 2019 21:06:28 -0700 Subject: [PATCH 046/109] Use to_polygons() for bounds calculation for simple path shapes --- masque/shapes/path.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/masque/shapes/path.py b/masque/shapes/path.py index 5bd612c..d7ede07 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -2,7 +2,7 @@ from typing import List, Tuple, Dict import copy from enum import Enum import numpy -from numpy import pi +from numpy import pi, inf from . import Shape, normalized_shape_tuple, Polygon, Circle from .. import PatternError @@ -299,18 +299,12 @@ class Path(Shape): elif self.cap in (Path.Cap.Flush, Path.Cap.Square, Path.Cap.SquareCustom): - extensions = self._calculate_cap_extensions() - - v = remove_colinear_vertices(self.vertices, closed_path=False) - dv = numpy.diff(v, axis=0) - dvdir = dv / numpy.sqrt((dv * dv).sum(axis=1))[:, None] - perp = dvdir[:, ::-1] * [[1, -1]] * self.width / 2 - - v[0] -= dvdir * extensions[0] - v[-1] += dvdir * extensions[1] - - bounds = self.offset + numpy.vstack((numpy.min(v - numpy.abs(perp), axis=0), - numpy.max(v + numpy.abs(perp), axis=0))) + bounds = numpy.array([[+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, :]) else: raise PatternError('get_bounds() not implemented for endcaps: {}'.format(self.cap)) From 49982f1207df027ad71973fedfc3ef802118d4b4 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 25 May 2019 12:40:17 -0700 Subject: [PATCH 047/109] Add newlines to long log messages --- 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 82a3a6f..b5b5a13 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -508,7 +508,7 @@ def _disambiguate_pattern_names(patterns): for pat in patterns: if len(pat.name) > 32: shortened_name = pat.name[:26] - logger.warning('Pattern name "{}" is too long ({}/32 chars),'.format(pat.name, len(pat.name)) + \ + logger.warning('Pattern name "{}" is too long ({}/32 chars),\n'.format(pat.name, len(pat.name)) + ' shortening to "{}" before generating suffix'.format(shortened_name)) else: shortened_name = pat.name @@ -526,15 +526,15 @@ def _disambiguate_pattern_names(patterns): 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( + logger.warning('Pattern name "{}" ({}) appears multiple times;\n renaming to "{}"'.format( pat.name, sanitized_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)) + raise PatternError('Zero-length name after sanitize+encode,\n originally "{}"'.format(pat.name)) if len(encoded_name) > 32: - raise PatternError('Pattern name "{}" length > 32 after encode, originally "{}"'.format(encoded_name, pat.name)) + raise PatternError('Pattern name "{}" length > 32 after encode,\n originally "{}"'.format(encoded_name, pat.name)) pat.name = encoded_name used_names.append(suffixed_name) From b5bd7cd9c8d690ccfe7d90d9aded5f83273ee9d4 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 25 May 2019 12:40:59 -0700 Subject: [PATCH 048/109] Pattern.get_bounds() should return None if it's empty s --- masque/pattern.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/masque/pattern.py b/masque/pattern.py index 801964e..c47d933 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -9,6 +9,7 @@ import pickle from collections import defaultdict import numpy +from numpy import inf # .visualize imports matplotlib and matplotlib.collections from .subpattern import SubPattern @@ -387,14 +388,18 @@ class Pattern: 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:]: + min_bounds = numpy.array((+inf, +inf)) + max_bounds = numpy.array((+inf, +inf)) + for entry in entries: bounds = entry.get_bounds() + if bounds is None: + continue min_bounds = numpy.minimum(min_bounds, bounds[0, :]) max_bounds = numpy.maximum(max_bounds, bounds[1, :]) - return numpy.vstack((min_bounds, max_bounds)) + if (max_bounds < min_bounds).any(): + return None + else: + return numpy.vstack((min_bounds, max_bounds)) def flatten(self) -> 'Pattern': """ From 6801ee7fc0737f5744d286109731d2fa7d5d493a Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 25 May 2019 12:41:51 -0700 Subject: [PATCH 049/109] Account for rotation/mirror in Pattern.dfs() --- masque/pattern.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/masque/pattern.py b/masque/pattern.py index c47d933..6b1ff2e 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -224,9 +224,13 @@ class Pattern: for subpattern in self.subpatterns: if transform is not False: + sign = numpy.ones(2) + if transform[3]: + sign[1] = -1 + xy = numpy.dot(rotation_matrix_2d(transform[2]), subpattern.offset * sign) mirror_x, angle = normalize_mirror(subpattern.mirrored) angle += subpattern.rotation - sp_transform = transform + numpy.hstack((subpattern.offset, angle, mirror_x)) + sp_transform = transform + (xy[0], xy[1], angle, mirror_x) sp_transform[3] %= 2 else: sp_transform = False From e057f6e4a27f84cc28f1fcfd4f561bbebcfc710a Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 25 May 2019 12:46:17 -0700 Subject: [PATCH 050/109] Add binary files and vim buffers into gitignore --- .gitignore | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.gitignore b/.gitignore index 3ef4b5d..98547db 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,16 @@ *.pyc __pycache__ + *.idea + build/ dist/ *.egg-info/ + + +*.swp +*.swo + +*.gds +*.svg +*.oas From e5f28a7a92f0bbe36b4b9f13dea5c7d116b6a186 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 9 Jun 2019 23:57:36 -0700 Subject: [PATCH 051/109] Fix Pattern.get_bounds() --- masque/pattern.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/pattern.py b/masque/pattern.py index 6b1ff2e..1de2f28 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -393,7 +393,7 @@ class Pattern: return None min_bounds = numpy.array((+inf, +inf)) - max_bounds = numpy.array((+inf, +inf)) + max_bounds = numpy.array((-inf, -inf)) for entry in entries: bounds = entry.get_bounds() if bounds is None: From 2259e7987bbd86c26230f794a3493a82bf138a66 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 9 Jun 2019 23:59:55 -0700 Subject: [PATCH 052/109] don't rename shape.to_polygons() args --- masque/shapes/polygon.py | 4 ++-- masque/shapes/text.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 04b91c7..a899229 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -222,8 +222,8 @@ class Polygon(Shape): def to_polygons(self, - _poly_num_points: int = None, - _poly_max_arclen: float = None, + poly_num_points: int = None, # unused + poly_max_arclen: float = None, # unused ) -> List['Polygon']: return [copy.deepcopy(self)] diff --git a/masque/shapes/text.py b/masque/shapes/text.py index f6c4aac..f05e7bd 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -96,8 +96,8 @@ class Text(Shape): return new def to_polygons(self, - _poly_num_points: int = None, - _poly_max_arclen: float = None, + poly_num_points: int = None, # unused + poly_max_arclen: float = None, # unused ) -> List[Polygon]: all_polygons = [] total_advance = 0 From f471374b689a9758585501a1d9b40ff689b7a4cb Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 27 Sep 2019 21:03:52 -0700 Subject: [PATCH 053/109] Use separate VERSION file --- MANIFEST.in | 1 + masque/VERSION | 1 + masque/__init__.py | 6 +++++- setup.py | 9 +++++++-- 4 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 masque/VERSION diff --git a/MANIFEST.in b/MANIFEST.in index c28ab72..8120ce7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,3 @@ include README.md include LICENSE.md +include masque/VERSION diff --git a/masque/VERSION b/masque/VERSION new file mode 100644 index 0000000..2eb3c4f --- /dev/null +++ b/masque/VERSION @@ -0,0 +1 @@ +0.5 diff --git a/masque/__init__.py b/masque/__init__.py index acf7cbc..919a183 100644 --- a/masque/__init__.py +++ b/masque/__init__.py @@ -23,6 +23,8 @@ - svgwrite [masque.file.svg] """ +import pathlib + from .error import PatternError from .shapes import Shape from .label import Label @@ -33,4 +35,6 @@ from .pattern import Pattern __author__ = 'Jan Petykiewicz' -version = '0.5' +with open(pathlib.Path(__file__).parent / 'VERSION', 'r') as f: + __version__ = f.read().strip() +version = __version__ diff --git a/setup.py b/setup.py index 6cda402..739b5f3 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,15 @@ #!/usr/bin/env python3 from setuptools import setup, find_packages -import masque with open('README.md', 'r') as f: long_description = f.read() +with open('masque/VERSION', 'r') as f: + version = f.read().strip() + setup(name='masque', - version=masque.version, + version=version, description='Lithography mask library', long_description=long_description, long_description_content_type='text/markdown', @@ -15,6 +17,9 @@ setup(name='masque', author_email='anewusername@gmail.com', url='https://mpxd.net/code/jan/masque', packages=find_packages(), + package_data={ + 'masque': ['VERSION'] + }, install_requires=[ 'numpy', ], From 7a3738bd0c3fc841b3ff89d4ca03066677780c94 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 30 Sep 2019 23:50:12 -0700 Subject: [PATCH 054/109] update TODO list --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 150481f..57ffb6d 100644 --- a/README.md +++ b/README.md @@ -33,11 +33,7 @@ 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 7fd64e0abf9d41973372a3cdf6431e8ca2c119df Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 30 Sep 2019 23:50:40 -0700 Subject: [PATCH 055/109] give better instructions for extras --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 57ffb6d..fd16875 100644 --- a/README.md +++ b/README.md @@ -15,15 +15,15 @@ E-beam doses, and the ability to output to multiple formats. Requirements: * python >= 3.5 (written and tested with 3.6) * numpy -* 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) +* 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: ```bash -pip3 install masque +pip3 install 'masque[visualization,gdsii,svg,text]' ``` Alternatively, install from git From bece3136bea9195cd7332007a17c77249ce421f7 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 30 Sep 2019 23:52:24 -0700 Subject: [PATCH 056/109] Bump version number to 1.0 Note MAJOR incompatibilities with previous version, including - order of Pattern() args - save format - i/o functions --- masque/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/VERSION b/masque/VERSION index 2eb3c4f..d3827e7 100644 --- a/masque/VERSION +++ b/masque/VERSION @@ -1 +1 @@ -0.5 +1.0 From 7b0a5676259f72f76c1492c570a9b2bb9d266472 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 4 Dec 2019 23:30:46 -0800 Subject: [PATCH 057/109] .mirrored should be an ndarray --- 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 ccdf529..7885067 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -170,7 +170,7 @@ class GridRepetition: def mirrored(self, val: List[bool]): if is_scalar(val): raise PatternError('Mirrored must be a 2-element list of booleans') - self._mirrored = val + self._mirrored = numpy.array(val, dtype=bool) # a_vector property @property diff --git a/masque/subpattern.py b/masque/subpattern.py index f76605b..a8089cd 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -115,7 +115,7 @@ class SubPattern: def mirrored(self, val: List[bool]): if is_scalar(val): raise PatternError('Mirrored must be a 2-element list of booleans') - self._mirrored = val + self._mirrored = numpy.array(val, dtype=bool) def as_pattern(self) -> 'Pattern': """ From 4bc51f765ffa4ad2c462ee74c71a60398706ffae Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 4 Dec 2019 23:34:57 -0800 Subject: [PATCH 058/109] custom __copy__ for SubPattern and GridRepetition the difference between copy/deepcopy should be whether a copy is made of the contained pattern --- masque/repetition.py | 17 +++++++++++++---- masque/subpattern.py | 11 +++++++++-- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/masque/repetition.py b/masque/repetition.py index 7885067..faea08f 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -100,14 +100,23 @@ class GridRepetition: mirrored = [False, False] self.mirrored = mirrored + def __copy__(self) -> 'GridRepetition': + new = GridRepetition(pattern=self.pattern, + a_vector=self.a_vector.copy(), + b_vector=copy.copy(self.b_vector), + a_count=self.a_count, + b_count=self.b_count, + offset=self.offset.copy(), + rotation=self.rotation, + dose=self.dose, + scale=self.scale, + mirrored=self.mirrored.copy()) + return new + def __deepcopy__(self, memo: Dict = None) -> 'GridReptition': memo = {} if memo is None else memo new = copy.copy(self) new.pattern = copy.deepcopy(self.pattern, memo) - new._offset = self._offset.copy() - new._mirrored = copy.deepcopy(self._mirrored, memo) - new._a_vector = self._a_vector.copy() - new._b_vector = copy.copy(self._b_vector) # ndarray or None so don't need deepcopy return new # offset property diff --git a/masque/subpattern.py b/masque/subpattern.py index a8089cd..d18bc36 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -47,12 +47,19 @@ class SubPattern: mirrored = [False, False] self.mirrored = mirrored + def __copy__(self) -> 'SubPattern': + new = SubPattern(pattern=self.pattern, + offset=self.offset.copy(), + rotation=self.rotation, + dose=self.dose, + scale=self.scale, + mirrored=self.mirrored.copy()) + return new + def __deepcopy__(self, memo: Dict = None) -> 'SubPattern': memo = {} if memo is None else memo new = copy.copy(self) new.pattern = copy.deepcopy(self.pattern, memo) - new._offset = self._offset.copy() - new._mirrored = copy.deepcopy(self._mirrored, memo) return new # offset property From f3669f2dfd77e24314ab0d0a7a3f1e09b3f63f89 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 5 Dec 2019 23:18:18 -0800 Subject: [PATCH 059/109] fixup mirroring for subpatterns --- masque/file/gdsii.py | 5 +++-- masque/repetition.py | 4 ++++ masque/subpattern.py | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index b5b5a13..5a891ef 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -439,8 +439,10 @@ def _subpatterns_to_refs(subpatterns: List[SubPattern or GridRepetition] for subpat in subpatterns: encoded_name = subpat.pattern.name + # Note: GDS mirrors first and rotates second + mirror_x, extra_angle = normalize_mirror(subpat.mirrored) if isinstance(subpat, GridRepetition): - mirror_signs = (-1) ** numpy.array(subpat.mirrored) + mirror_signs = [(-1 if mirror_x else 1), 1] xy = numpy.array(subpat.offset) + [ [0, 0], numpy.dot(rotation_matrix_2d(subpat.rotation), subpat.a_vector * mirror_signs) * subpat.scale * subpat.a_count, @@ -454,7 +456,6 @@ def _subpatterns_to_refs(subpatterns: List[SubPattern or GridRepetition] ref = gdsii.elements.SRef(struct_name=encoded_name, xy=numpy.round([subpat.offset]).astype(int)) - mirror_x, extra_angle = normalize_mirror(subpat.mirrored) ref.angle = ((subpat.rotation + extra_angle) * 180 / numpy.pi) % 360 # strans must be non-None for angle and mag to take effect ref.strans = set_bit(0, 15 - 0, mirror_x) diff --git a/masque/repetition.py b/masque/repetition.py index faea08f..6349662 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -281,6 +281,10 @@ class GridRepetition: :return: self """ self.mirrored[axis] = not self.mirrored[axis] + self.rotation *= -1 + self.a_vector[axis] *= -1 + if self.b_vector is not None: + self.b_vector[axis] *= -1 return self def get_bounds(self) -> numpy.ndarray or None: diff --git a/masque/subpattern.py b/masque/subpattern.py index d18bc36..b8441d5 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -181,6 +181,7 @@ class SubPattern: :return: self """ self.mirrored[axis] = not self.mirrored[axis] + self.rotation *= -1 return self def get_bounds(self) -> numpy.ndarray or None: From 97b7eda21a4674444a203f6fcd9f890c1597db25 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 6 Dec 2019 22:28:11 -0800 Subject: [PATCH 060/109] Force repetition counts to be integers --- masque/repetition.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/masque/repetition.py b/masque/repetition.py index 6349662..2a9fa27 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -31,8 +31,8 @@ class GridRepetition: '_mirrored', '_a_vector', '_b_vector', - 'a_count', - 'b_count', + '_a_count', + '_b_count', 'identifier') pattern: 'Pattern' @@ -45,8 +45,8 @@ class GridRepetition: _a_vector: numpy.ndarray _b_vector: numpy.ndarray or None - a_count: int - b_count: int + _a_count: int + _b_count: int identifier: Tuple @@ -209,6 +209,27 @@ class GridRepetition: raise PatternError('b_vector must be convertible to size-2 ndarray') self._b_vector = val.flatten() + # a_count property + @property + def a_count(self) -> int: + return self._a_count + + @a_count.setter + def a_count(self, val: int): + if val != int(val): + raise PatternError('a_count must be convertable to an int!') + self._a_count = int(val) + + # b_count property + @property + def b_count(self) -> int: + return self._b_count + + @b_count.setter + def b_count(self, val: int): + if val != int(val): + raise PatternError('b_count must be convertable to an int!') + self._b_count = int(val) def as_pattern(self) -> 'Pattern': """ From fcc47705344e9aa3784d52bdb8f27dc88b79c934 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 11 Dec 2019 21:16:31 -0800 Subject: [PATCH 061/109] Mirroring fix for gdsii import --- 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 5a891ef..3766872 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -379,7 +379,7 @@ def _sref_to_subpat(element: gdsii.elements.SRef) -> SubPattern: 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) + subpat.mirrored[0] = 1 return subpat From df9c4b52f4a22bcfc602adaac378c833bf546f57 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 12 Dec 2019 01:48:24 -0800 Subject: [PATCH 062/109] add some todos --- masque/file/gdsii.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 3766872..31a287f 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -23,6 +23,8 @@ from ..shapes import Polygon, Path from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar from ..utils import remove_colinear_vertices, normalize_mirror +#TODO document how GDS rotation / mirror works +#TODO absolute positioning __author__ = 'Jan Petykiewicz' From 0118bf0eb33c643172d926982748a209be6cd189 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 12 Dec 2019 00:46:07 -0800 Subject: [PATCH 063/109] update ellip_grating.py example to use new gdsii interface --- examples/ellip_grating.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ellip_grating.py b/examples/ellip_grating.py index 0a4ce43..e6fe438 100644 --- a/examples/ellip_grating.py +++ b/examples/ellip_grating.py @@ -21,7 +21,7 @@ def main(): pat2 = pat.copy() pat2.name = 'grating2' - masque.file.gdsii.write_dose2dtype((pat, pat2), 'out.gds', 1e-9, 1e-3) + masque.file.gdsii.writefile((pat, pat2), 'out.gds.gz', 1e-9, 1e-3) if __name__ == '__main__': From 09711116a739369470ed35cfb7c86895479fca59 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 12 Dec 2019 01:19:07 -0800 Subject: [PATCH 064/109] allow more freedom in pattern names (e.g. names which violate spec, longer suffixes, filter warning, etc) --- masque/file/gdsii.py | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 31a287f..ad63711 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -6,7 +6,7 @@ import gdsii.library import gdsii.structure import gdsii.elements -from typing import List, Any, Dict, Tuple +from typing import List, Any, Dict, Tuple, Callable import re import io import copy @@ -46,7 +46,8 @@ def write(patterns: Pattern or List[Pattern], meters_per_unit: float, logical_units_per_unit: float = 1, library_name: str = 'masque-gdsii-write', - modify_originals: bool = False): + modify_originals: bool = False, + disambiguate_func: Callable[[List[Pattern]], None] = None): """ 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 @@ -77,13 +78,20 @@ def write(patterns: Pattern or List[Pattern], :param modify_originals: If True, the original pattern is modified as part of the writing process. Otherwise, a copy is made. Default False. + :param disambiguate_func: Function which takes a list of patterns and alters them + to make their names valid and unique. Default is `disambiguate_pattern_names`, which + attempts to adhere to the GDSII standard as well as possible. + WARNING: No additional error checking is performed on the results. """ - if not modify_originals: - patterns = copy.deepcopy(patterns) - if isinstance(patterns, Pattern): patterns = [patterns] + if disambiguate_func is None: + disambiguate_func = disambiguate_pattern_names + + if not modify_originals: + patterns = copy.deepcopy(patterns) + # Create library lib = gdsii.library.Library(version=600, name=library_name.encode('ASCII'), @@ -95,7 +103,7 @@ 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()) + disambiguate_func(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(): @@ -506,12 +514,16 @@ def _labels_to_texts(labels: List[Label]) -> List[gdsii.elements.Text]: return texts -def _disambiguate_pattern_names(patterns): +def disambiguate_pattern_names(patterns, + max_name_length: int = 32, + suffix_length: int = 6, + dup_warn_filter: Callable[[str,], bool] = None, # If returns False, don't warn about this name + ): used_names = [] for pat in patterns: - if len(pat.name) > 32: - shortened_name = pat.name[:26] - logger.warning('Pattern name "{}" is too long ({}/32 chars),\n'.format(pat.name, len(pat.name)) + + if len(pat.name) > max_name_length: + shortened_name = pat.name[:max_name_length - suffix_length] + logger.warning('Pattern name "{}" is too long ({}/{} chars),\n'.format(pat.name, len(pat.name), max_name_length) + ' shortening to "{}" before generating suffix'.format(shortened_name)) else: shortened_name = pat.name @@ -529,15 +541,16 @@ def _disambiguate_pattern_names(patterns): 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;\n renaming to "{}"'.format( - pat.name, sanitized_name, suffixed_name)) + if dup_warn_filter is None or dup_warn_filter(pat.name): + logger.warning('Pattern name "{}" ({}) appears multiple times;\n renaming to "{}"'.format( + pat.name, sanitized_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,\n originally "{}"'.format(pat.name)) - if len(encoded_name) > 32: - raise PatternError('Pattern name "{}" length > 32 after encode,\n originally "{}"'.format(encoded_name, pat.name)) + if len(encoded_name) > max_name_length: + raise PatternError('Pattern name "{}" length > {} after encode,\n originally "{}"'.format(encoded_name, max_name_length, pat.name)) pat.name = encoded_name used_names.append(suffixed_name) From e0db621595788c52e4290648974e206a953e8e6c Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 12 Dec 2019 00:38:11 -0800 Subject: [PATCH 065/109] allow locking of all objects --- masque/__init__.py | 2 +- masque/error.py | 8 ++++ masque/file/gdsii.py | 4 +- masque/label.py | 40 +++++++++++++++++-- masque/pattern.py | 85 ++++++++++++++++++++++++++++++++++------ masque/repetition.py | 64 +++++++++++++++++++++++++++--- masque/shapes/arc.py | 5 ++- masque/shapes/circle.py | 5 ++- masque/shapes/ellipse.py | 5 ++- masque/shapes/path.py | 3 ++ masque/shapes/polygon.py | 3 ++ masque/shapes/shape.py | 34 +++++++++++++++- masque/shapes/text.py | 6 ++- masque/subpattern.py | 62 ++++++++++++++++++++++++++--- 14 files changed, 290 insertions(+), 36 deletions(-) diff --git a/masque/__init__.py b/masque/__init__.py index 919a183..bd6908a 100644 --- a/masque/__init__.py +++ b/masque/__init__.py @@ -25,7 +25,7 @@ import pathlib -from .error import PatternError +from .error import PatternError, PatternLockedError from .shapes import Shape from .label import Label from .subpattern import SubPattern diff --git a/masque/error.py b/masque/error.py index 8a67b6e..4a5c21a 100644 --- a/masque/error.py +++ b/masque/error.py @@ -7,3 +7,11 @@ class PatternError(Exception): def __str__(self): return repr(self.value) + + +class PatternLockedError(PatternError): + """ + Exception raised when trying to modify a locked pattern + """ + def __init__(self): + PatternError.__init__(self, 'Tried to modify a locked Pattern, subpattern, or shape') diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index ad63711..f5ecc83 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -76,7 +76,7 @@ def write(patterns: Pattern or List[Pattern], :param library_name: Library name written into the GDSII file. Default 'masque-gdsii-write'. :param modify_originals: If True, the original pattern is modified as part of the writing - process. Otherwise, a copy is made. + process. Otherwise, a copy is made and deepunlock()-ed. Default False. :param disambiguate_func: Function which takes a list of patterns and alters them to make their names valid and unique. Default is `disambiguate_pattern_names`, which @@ -90,7 +90,7 @@ def write(patterns: Pattern or List[Pattern], disambiguate_func = disambiguate_pattern_names if not modify_originals: - patterns = copy.deepcopy(patterns) + patterns = [p.deepcopy().deepunlock() for p in patterns] # Create library lib = gdsii.library.Library(version=600, diff --git a/masque/label.py b/masque/label.py index 3e2e8c7..d9fcd3e 100644 --- a/masque/label.py +++ b/masque/label.py @@ -3,7 +3,7 @@ import copy import numpy from numpy import pi -from . import PatternError +from .error import PatternError, PatternLockedError from .utils import is_scalar, vector2, rotation_matrix_2d @@ -12,9 +12,9 @@ __author__ = 'Jan Petykiewicz' class Label: """ - A circle, which has a position and radius. + A text annotation with a position and layer (but no size; it is not drawn) """ - __slots__ = ('_offset', '_layer', '_string', 'identifier') + __slots__ = ('_offset', '_layer', '_string', 'identifier', 'locked') # [x_offset, y_offset] _offset: numpy.ndarray @@ -27,6 +27,13 @@ class Label: # Arbitrary identifier tuple identifier: Tuple + locked: bool # If True, any changes to the label will raise a PatternLockedError + + def __setattr__(self, name, value): + if self.locked and name != 'locked': + raise PatternLockedError() + object.__setattr__(self, name, value) + # ---- Properties # offset property @property @@ -78,11 +85,20 @@ class Label: def __init__(self, string: str, offset: vector2=(0.0, 0.0), - layer: int=0): + layer: int=0, + locked: bool = False): + self.unlock() self.identifier = () self.string = string self.offset = numpy.array(offset, dtype=float) self.layer = layer + self.locked = locked + + def __copy__(self) -> 'Label': + return Label(string=self.string, + offset=self.offset.copy(), + layer=self.layer, + locked=self.locked) def __deepcopy__(self, memo: Dict = None) -> 'Label': memo = {} if memo is None else memo @@ -134,4 +150,20 @@ class Label: """ return numpy.array([self.offset, self.offset]) + def lock(self) -> 'Label': + """ + Lock the Label + :return: self + """ + object.__setattr__(self, 'locked', True) + return self + + def unlock(self) -> 'Label': + """ + Unlock the Label + + :return: self + """ + object.__setattr__(self, 'locked', False) + return self diff --git a/masque/pattern.py b/masque/pattern.py index 1de2f28..50d4e34 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -17,7 +17,7 @@ from .repetition import GridRepetition from .shapes import Shape, Polygon from .label import Label from .utils import rotation_matrix_2d, vector2, normalize_mirror -from .error import PatternError +from .error import PatternError, PatternLockedError __author__ = 'Jan Petykiewicz' @@ -37,17 +37,19 @@ class Pattern: may reference the same Pattern object. :var name: An identifier for this object. Not necessarily unique. """ - __slots__ = ('shapes', 'labels', 'subpatterns', 'name') + __slots__ = ('shapes', 'labels', 'subpatterns', 'name', 'locked') shapes: List[Shape] labels: List[Label] subpatterns: List[SubPattern or GridRepetition] name: str + locked: bool def __init__(self, name: str = '', shapes: List[Shape] = (), labels: List[Label] = (), subpatterns: List[SubPattern] = (), + locked: bool = False, ): """ Basic init; arguments get assigned to member variables. @@ -57,7 +59,9 @@ class Pattern: :param labels: Initial labels in the Pattern :param subpatterns: Initial subpatterns in the Pattern :param name: An identifier for the Pattern + :param locked: Whether to lock the pattern after construction """ + self.unlock() if isinstance(shapes, list): self.shapes = shapes else: @@ -74,14 +78,27 @@ class Pattern: self.subpatterns = list(subpatterns) self.name = name + self.locked = locked + + def __setattr__(self, name, value): + if self.locked and name != 'locked': + raise PatternLockedError() + object.__setattr__(self, name, value) + + def __copy__(self, memo: Dict = None) -> 'Pattern': + return Pattern(name=self.name, + shapes=copy.deepcopy(self.shapes), + labels=copy.deepcopy(self.labels), + subpatterns=[copy.copy(sp) for sp in self.subpatterns], + locked=self.locked) def __deepcopy__(self, memo: Dict = None) -> 'Pattern': memo = {} if memo is None else memo - new = copy.copy(self) - new.name = self.name - new.shapes = copy.deepcopy(self.shapes, memo) - new.labels = copy.deepcopy(self.labels, memo) - new.subpatterns = copy.deepcopy(self.subpatterns, memo) + new = Pattern(name=self.name, + shapes=copy.deepcopy(self.shapes, memo), + labels=copy.deepcopy(self.labels, memo), + subpatterns=copy.deepcopy(self.subpatterns, memo), + locked=self.locked) return new def append(self, other_pattern: 'Pattern') -> 'Pattern': @@ -363,7 +380,7 @@ class Pattern: :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() + pat = self.deepcopy().deepunlock().polygonize().flatten() return [shape.vertices + shape.offset for shape in pat.shapes] def referenced_patterns_by_id(self) -> Dict[int, 'Pattern']: @@ -564,11 +581,7 @@ class Pattern: :return: A copy of the current 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 + return copy.copy(self) def deepcopy(self) -> 'Pattern': """ @@ -588,6 +601,52 @@ class Pattern: len(self.shapes) == 0 and len(self.labels) == 0) + def lock(self) -> 'Pattern': + """ + Lock the pattern + + :return: self + """ + object.__setattr__(self, 'locked', True) + return self + + def unlock(self) -> 'Pattern': + """ + Unlock the pattern + + :return: self + """ + object.__setattr__(self, 'locked', False) + return self + + def deeplock(self) -> 'Pattern': + """ + Recursively lock the pattern, all referenced shapes, subpatterns, and labels + + :return: self + """ + self.lock() + for ss in self.shapes + self.labels: + ss.lock() + for sp in self.subpatterns: + sp.deeplock() + return self + + def deepunlock(self) -> 'Pattern': + """ + Recursively unlock the pattern, all referenced shapes, subpatterns, and labels + + This is dangerous unless you have just performed a deepcopy! + + :return: self + """ + self.unlock() + for ss in self.shapes + self.labels: + ss.unlock() + for sp in self.subpatterns: + sp.deepunlock() + return self + @staticmethod def load(filename: str) -> 'Pattern': """ diff --git a/masque/repetition.py b/masque/repetition.py index 2a9fa27..0c3234a 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -9,7 +9,7 @@ import copy import numpy from numpy import pi -from .error import PatternError +from .error import PatternError, PatternLockedError from .utils import is_scalar, rotation_matrix_2d, vector2 @@ -33,7 +33,8 @@ class GridRepetition: '_b_vector', '_a_count', '_b_count', - 'identifier') + 'identifier', + 'locked') pattern: 'Pattern' @@ -49,6 +50,7 @@ class GridRepetition: _b_count: int identifier: Tuple + locked: bool def __init__(self, pattern: 'Pattern', @@ -60,7 +62,8 @@ class GridRepetition: rotation: float = 0.0, mirrored: List[bool] = None, dose: float = 1.0, - scale: float = 1.0): + scale: float = 1.0, + locked: bool = False): """ :param a_vector: First lattice vector, of the form [x, y]. Specifies center-to-center spacing between adjacent elements. @@ -70,6 +73,7 @@ class GridRepetition: 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. + :param locked: Whether the subpattern is locked after initialization. :raises: InvalidDataError if b_* inputs conflict with each other or a_count < 1. """ @@ -85,6 +89,7 @@ class GridRepetition: if b_count < 1: raise InvalidDataError('Repetition has too-small b_count: ' '{}'.format(b_count)) + self.unlock() self.a_vector = a_vector self.b_vector = b_vector self.a_count = a_count @@ -99,6 +104,12 @@ class GridRepetition: if mirrored is None: mirrored = [False, False] self.mirrored = mirrored + self.locked = locked + + def __setattr__(self, name, value): + if self.locked and name != 'locked': + raise PatternLockedError() + object.__setattr__(self, name, value) def __copy__(self) -> 'GridRepetition': new = GridRepetition(pattern=self.pattern, @@ -110,7 +121,8 @@ class GridRepetition: rotation=self.rotation, dose=self.dose, scale=self.scale, - mirrored=self.mirrored.copy()) + mirrored=self.mirrored.copy(), + locked=self.locked) return new def __deepcopy__(self, memo: Dict = None) -> 'GridReptition': @@ -126,6 +138,9 @@ class GridRepetition: @offset.setter def offset(self, val: vector2): + if self.locked: + raise PatternLockedError() + if not isinstance(val, numpy.ndarray): val = numpy.array(val, dtype=float) @@ -243,7 +258,7 @@ class GridRepetition: 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 = self.pattern.deepcopy().deepunlock() newPat.translate_elements(offset) patterns.append(newPat) @@ -343,3 +358,42 @@ class GridRepetition: """ return copy.deepcopy(self) + def lock(self) -> 'GridRepetition': + """ + Lock the GridRepetition + + :return: self + """ + object.__setattr__(self, 'locked', True) + return self + + def unlock(self) -> 'GridRepetition': + """ + Unlock the GridRepetition + + :return: self + """ + object.__setattr__(self, 'locked', False) + return self + + def deeplock(self) -> 'GridRepetition': + """ + Recursively lock the GridRepetition and its contained pattern + + :return: self + """ + self.lock() + self.pattern.deeplock() + return self + + def deepunlock(self) -> 'GridRepetition': + """ + Recursively unlock the GridRepetition and its contained pattern + + This is dangerous unless you have just performed a deepcopy! + + :return: self + """ + self.unlock() + self.pattern.deepunlock() + return self diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 92204a4..49c8fbe 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -147,7 +147,9 @@ class Arc(Shape): rotation: float = 0, mirrored: Tuple[bool] = (False, False), layer: int = 0, - dose: float = 1.0): + dose: float = 1.0, + locked: bool = False): + self.unlock() self.identifier = () self.radii = radii self.angles = angles @@ -159,6 +161,7 @@ class Arc(Shape): self.dose = dose self.poly_num_points = poly_num_points self.poly_max_arclen = poly_max_arclen + self.locked = locked def __deepcopy__(self, memo: Dict = None) -> 'Arc': memo = {} if memo is None else memo diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index 5ee901f..f9b2192 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -44,7 +44,9 @@ class Circle(Shape): poly_max_arclen: float = None, offset: vector2 = (0.0, 0.0), layer: int = 0, - dose: float = 1.0): + dose: float = 1.0, + locked: bool = False): + self.unlock() self.identifier = () self.offset = numpy.array(offset, dtype=float) self.layer = layer @@ -52,6 +54,7 @@ class Circle(Shape): self.radius = radius self.poly_num_points = poly_num_points self.poly_max_arclen = poly_max_arclen + self.locked = locked def __deepcopy__(self, memo: Dict = None) -> 'Circle': memo = {} if memo is None else memo diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index fba194a..7fe2c66 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -88,7 +88,9 @@ class Ellipse(Shape): rotation: float = 0, mirrored: Tuple[bool] = (False, False), layer: int = 0, - dose: float = 1.0): + dose: float = 1.0, + locked: bool = False): + self.unlock() self.identifier = () self.radii = radii self.offset = offset @@ -98,6 +100,7 @@ class Ellipse(Shape): self.dose = dose self.poly_num_points = poly_num_points self.poly_max_arclen = poly_max_arclen + self.locked = locked def __deepcopy__(self, memo: Dict = None) -> 'Ellipse': memo = {} if memo is None else memo diff --git a/masque/shapes/path.py b/masque/shapes/path.py index d7ede07..7062fd5 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -151,7 +151,9 @@ class Path(Shape): mirrored: Tuple[bool] = (False, False), layer: int = 0, dose: float = 1.0, + locked: bool = False, ) -> 'Path': + self.unlock() self._cap_extensions = None # Since .cap setter might access it self.identifier = () @@ -165,6 +167,7 @@ class Path(Shape): self.cap_extensions = cap_extensions self.rotate(rotation) [self.mirror(a) for a, do in enumerate(mirrored) if do] + self.locked = locked def __deepcopy__(self, memo: Dict = None) -> 'Path': memo = {} if memo is None else memo diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index a899229..02ac891 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -77,7 +77,9 @@ class Polygon(Shape): mirrored: Tuple[bool] = (False, False), layer: int = 0, dose: float = 1.0, + locked: bool = False, ): + self.unlock() self.identifier = () self.layer = layer self.dose = dose @@ -85,6 +87,7 @@ class Polygon(Shape): self.offset = offset self.rotate(rotation) [self.mirror(a) for a, do in enumerate(mirrored) if do] + self.locked = locked def __deepcopy__(self, memo: Dict = None) -> 'Polygon': memo = {} if memo is None else memo diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index 497e7e9..429e9dd 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -3,7 +3,7 @@ from abc import ABCMeta, abstractmethod import copy import numpy -from .. import PatternError +from ..error import PatternError, PatternLockedError from ..utils import is_scalar, rotation_matrix_2d, vector2 @@ -24,13 +24,26 @@ class Shape(metaclass=ABCMeta): """ Abstract class specifying functions common to all shapes. """ - __slots__ = ('_offset', '_layer', '_dose', 'identifier') + __slots__ = ('_offset', '_layer', '_dose', 'identifier', 'locked') _offset: numpy.ndarray # [x_offset, y_offset] _layer: int or Tuple # Layer (integer >= 0 or tuple) _dose: float # Dose identifier: Tuple # An arbitrary identifier for the shape, # usually empty but used by Pattern.flatten() + locked: bool # If True, any changes to the shape will raise a PatternLockedError + + def __setattr__(self, name, value): + if self.locked and name != 'locked': + raise PatternLockedError() + object.__setattr__(self, name, value) + + def __copy__(self) -> 'Shape': + cls = self.__class__ + new = cls.__new__(cls) + for name in Shape.__slots__ + self.__slots__: + object.__setattr__(new, name, getattr(self, name)) + return new # --- Abstract methods @abstractmethod @@ -388,3 +401,20 @@ class Shape(metaclass=ABCMeta): return manhattan_polygons + def lock(self) -> 'Shape': + """ + Lock the Shape + + :return: self + """ + object.__setattr__(self, 'locked', True) + return self + + def unlock(self) -> 'Shape': + """ + Unlock the Shape + + :return: self + """ + object.__setattr__(self, 'locked', False) + return self diff --git a/masque/shapes/text.py b/masque/shapes/text.py index f05e7bd..2d082d5 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -77,7 +77,10 @@ class Text(Shape): rotation: float = 0.0, mirrored: Tuple[bool] = (False, False), layer: int = 0, - dose: float = 1.0): + dose: float = 1.0, + locked: bool = False, + ): + self.unlock() self.identifier = () self.offset = offset self.layer = layer @@ -87,6 +90,7 @@ class Text(Shape): self.rotation = rotation self.font_path = font_path self.mirrored = mirrored + self.locked = locked def __deepcopy__(self, memo: Dict = None) -> 'Text': memo = {} if memo is None else memo diff --git a/masque/subpattern.py b/masque/subpattern.py index b8441d5..b2da81f 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -9,7 +9,7 @@ import copy import numpy from numpy import pi -from .error import PatternError +from .error import PatternError, PatternLockedError from .utils import is_scalar, rotation_matrix_2d, vector2 @@ -21,7 +21,8 @@ class SubPattern: SubPattern provides basic support for nesting Pattern objects within each other, by adding offset, rotation, scaling, and associated methods. """ - __slots__ = ('pattern', '_offset', '_rotation', '_dose', '_scale', '_mirrored', 'identifier') + __slots__ = ('pattern', '_offset', '_rotation', '_dose', '_scale', '_mirrored', + 'identifier', 'locked') pattern: 'Pattern' _offset: numpy.ndarray _rotation: float @@ -29,14 +30,18 @@ class SubPattern: _scale: float _mirrored: List[bool] identifier: Tuple + locked: bool + #TODO more documentation? 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): + scale: float = 1.0, + locked: bool = False): + self.unlock() self.identifier = () self.pattern = pattern self.offset = offset @@ -46,6 +51,12 @@ class SubPattern: if mirrored is None: mirrored = [False, False] self.mirrored = mirrored + self.locked = locked + + def __setattr__(self, name, value): + if self.locked and name != 'locked': + raise PatternLockedError() + object.__setattr__(self, name, value) def __copy__(self) -> 'SubPattern': new = SubPattern(pattern=self.pattern, @@ -53,7 +64,8 @@ class SubPattern: rotation=self.rotation, dose=self.dose, scale=self.scale, - mirrored=self.mirrored.copy()) + mirrored=self.mirrored.copy(), + locked=self.locked) return new def __deepcopy__(self, memo: Dict = None) -> 'SubPattern': @@ -130,7 +142,7 @@ class SubPattern: SubPattern's properties. :return: Copy of self.pattern that has been altered to reflect the SubPattern's properties. """ - pattern = self.pattern.deepcopy() + pattern = self.pattern.deepcopy().deepunlock() 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) @@ -218,3 +230,43 @@ class SubPattern: :return: copy.copy(self) """ return copy.deepcopy(self) + + def lock(self) -> 'SubPattern': + """ + Lock the SubPattern + + :return: self + """ + object.__setattr__(self, 'locked', True) + return self + + def unlock(self) -> 'SubPattern': + """ + Unlock the SubPattern + + :return: self + """ + object.__setattr__(self, 'locked', False) + return self + + def deeplock(self) -> 'SubPattern': + """ + Recursively lock the SubPattern and its contained pattern + + :return: self + """ + self.lock() + self.pattern.deeplock() + return self + + def deepunlock(self) -> 'SubPattern': + """ + Recursively unlock the SubPattern and its contained pattern + + This is dangerous unless you have just performed a deepcopy! + + :return: self + """ + self.unlock() + self.pattern.deepunlock() + return self From f2e6548d21bab7ee33bfe4a7b511ca4da3e3f0bc Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 13 Dec 2019 01:25:38 -0800 Subject: [PATCH 066/109] fix locking issue in __deepcopy__ --- masque/label.py | 3 ++- masque/repetition.py | 3 ++- masque/subpattern.py | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/masque/label.py b/masque/label.py index d9fcd3e..5dd6e57 100644 --- a/masque/label.py +++ b/masque/label.py @@ -102,8 +102,9 @@ class Label: def __deepcopy__(self, memo: Dict = None) -> 'Label': memo = {} if memo is None else memo - new = copy.copy(self) + new = copy.copy(self).unlock() new._offset = self._offset.copy() + new.locked = self.locked return new def copy(self) -> 'Label': diff --git a/masque/repetition.py b/masque/repetition.py index 0c3234a..fb40270 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -127,8 +127,9 @@ class GridRepetition: def __deepcopy__(self, memo: Dict = None) -> 'GridReptition': memo = {} if memo is None else memo - new = copy.copy(self) + new = copy.copy(self).unlock() new.pattern = copy.deepcopy(self.pattern, memo) + new.locked = self.locked return new # offset property diff --git a/masque/subpattern.py b/masque/subpattern.py index b2da81f..78df1b0 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -70,8 +70,9 @@ class SubPattern: def __deepcopy__(self, memo: Dict = None) -> 'SubPattern': memo = {} if memo is None else memo - new = copy.copy(self) + new = copy.copy(self).unlock() new.pattern = copy.deepcopy(self.pattern, memo) + new.locked = self.locked return new # offset property From 25c9e591cb2da05f2948394bf1919f65a3676af6 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 13 Dec 2019 21:16:43 -0800 Subject: [PATCH 067/109] shapes: unlock before modifying during deepcopy --- masque/shapes/arc.py | 3 ++- masque/shapes/circle.py | 3 ++- masque/shapes/ellipse.py | 3 ++- masque/shapes/path.py | 3 ++- masque/shapes/polygon.py | 3 ++- masque/shapes/text.py | 3 ++- 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 49c8fbe..467db98 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -165,10 +165,11 @@ class Arc(Shape): def __deepcopy__(self, memo: Dict = None) -> 'Arc': memo = {} if memo is None else memo - new = copy.copy(self) + new = copy.copy(self).unlock() new._offset = self._offset.copy() new._radii = self._radii.copy() new._angles = self._angles.copy() + new.locked = self.locked return new def to_polygons(self, diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index f9b2192..8816787 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -58,8 +58,9 @@ class Circle(Shape): def __deepcopy__(self, memo: Dict = None) -> 'Circle': memo = {} if memo is None else memo - new = copy.copy(self) + new = copy.copy(self).unlock() new._offset = self._offset.copy() + new.locked = self.locked return new def to_polygons(self, diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index 7fe2c66..0d8f084 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -104,9 +104,10 @@ class Ellipse(Shape): def __deepcopy__(self, memo: Dict = None) -> 'Ellipse': memo = {} if memo is None else memo - new = copy.copy(self) + new = copy.copy(self).unlock() new._offset = self._offset.copy() new._radii = self._radii.copy() + new.locked = self.locked return new def to_polygons(self, diff --git a/masque/shapes/path.py b/masque/shapes/path.py index 7062fd5..b31c5c5 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -171,11 +171,12 @@ class Path(Shape): def __deepcopy__(self, memo: Dict = None) -> 'Path': memo = {} if memo is None else memo - new = copy.copy(self) + new = copy.copy(self).unlock() new._offset = self._offset.copy() new._vertices = self._vertices.copy() new._cap = copy.deepcopy(self._cap, memo) new._cap_extensions = copy.deepcopy(self._cap_extensions, memo) + new.locked = self.locked return new @staticmethod diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 02ac891..3bbaeec 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -91,9 +91,10 @@ class Polygon(Shape): def __deepcopy__(self, memo: Dict = None) -> 'Polygon': memo = {} if memo is None else memo - new = copy.copy(self) + new = copy.copy(self).unlock() new._offset = self._offset.copy() new._vertices = self._vertices.copy() + new.locked = self.locked return new @staticmethod diff --git a/masque/shapes/text.py b/masque/shapes/text.py index 2d082d5..63ff2e1 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -94,9 +94,10 @@ class Text(Shape): def __deepcopy__(self, memo: Dict = None) -> 'Text': memo = {} if memo is None else memo - new = copy.copy(self) + new = copy.copy(self).unlock() new._offset = self._offset.copy() new._mirrored = copy.deepcopy(self._mirrored, memo) + new.locked = self.locked return new def to_polygons(self, From 5ba67af8161cba631db43150947f3e1f736f62ab Mon Sep 17 00:00:00 2001 From: jan Date: Tue, 31 Dec 2019 13:56:18 -0800 Subject: [PATCH 068/109] bump version to 1.1 --- masque/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/VERSION b/masque/VERSION index d3827e7..9459d4b 100644 --- a/masque/VERSION +++ b/masque/VERSION @@ -1 +1 @@ -1.0 +1.1 From 64b8e4dec8945eb48be96b1f20d3d781e85fb975 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 7 Jan 2020 22:17:00 -0800 Subject: [PATCH 069/109] Fix duplicates in gds with multiple top-level cells Since the different patterns we are handed might reference the same Patterns, we have to deepcopy the entire list at once so that we don't make multiple copies of them. --- 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 f5ecc83..e05b4ff 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -90,7 +90,7 @@ def write(patterns: Pattern or List[Pattern], disambiguate_func = disambiguate_pattern_names if not modify_originals: - patterns = [p.deepcopy().deepunlock() for p in patterns] + patterns = [p.deepunlock() for p in copy.deepcopy(patterns)] # Create library lib = gdsii.library.Library(version=600, From 0db70d2c50759fb68530abf5dc81ee45a2ac5a7f Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 7 Feb 2020 23:01:14 -0800 Subject: [PATCH 070/109] Change GridRepetition rotation/mirrored/scale properties so that they consistently apply only to the individual instances; the rotate()/mirror()/scale_by() functions apply to the full array. Add *_elements() versions of the functions --- masque/file/gdsii.py | 23 +++++++++------------- masque/repetition.py | 47 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 52 insertions(+), 18 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index e05b4ff..8b4373f 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -404,7 +404,7 @@ def _aref_to_gridrep(element: gdsii.elements.ARef) -> GridRepetition: rotation = 0 offset = numpy.array(element.xy[0]) scale = 1 - mirror_signs = numpy.ones(2) + mirror_across_x = False if element.strans is not None: if element.mag is not None: @@ -419,15 +419,11 @@ def _aref_to_gridrep(element: gdsii.elements.ARef) -> GridRepetition: raise PatternError('Absolute rotation is not implemented yet!') # Bit 0 means mirror x-axis if get_bit(element.strans, 15 - 0): - mirror_signs[1] = -1 + mirror_across_x = True 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 - b_vector = numpy.dot(rotation_matrix_2d(-rotation), vec_b0 / scale / counts[1]) * mirror_signs - + a_vector = (element.xy[1] - offset) / counts[0] + b_vector = (element.xy[2] - offset) / counts[1] gridrep = GridRepetition(pattern=None, a_vector=a_vector, @@ -437,7 +433,7 @@ def _aref_to_gridrep(element: gdsii.elements.ARef) -> GridRepetition: offset=offset, rotation=rotation, scale=scale, - mirrored=(mirror_signs[::-1] == -1)) + mirrored=(mirror_across_x, False)) gridrep.identifier = element.struct_name return gridrep @@ -450,13 +446,12 @@ def _subpatterns_to_refs(subpatterns: List[SubPattern or GridRepetition] encoded_name = subpat.pattern.name # Note: GDS mirrors first and rotates second - mirror_x, extra_angle = normalize_mirror(subpat.mirrored) + mirror_across_x, extra_angle = normalize_mirror(subpat.mirrored) if isinstance(subpat, GridRepetition): - mirror_signs = [(-1 if mirror_x else 1), 1] 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, + subpat.a_vector * subpat.a_count, + subpat.b_vector * subpat.b_count, ] ref = gdsii.elements.ARef(struct_name=encoded_name, xy=numpy.round(xy).astype(int), @@ -468,7 +463,7 @@ def _subpatterns_to_refs(subpatterns: List[SubPattern or GridRepetition] ref.angle = ((subpat.rotation + extra_angle) * 180 / numpy.pi) % 360 # strans must be non-None for angle and mag to take effect - ref.strans = set_bit(0, 15 - 0, mirror_x) + ref.strans = set_bit(0, 15 - 0, mirror_across_x) ref.mag = subpat.scale refs.append(ref) diff --git a/masque/repetition.py b/masque/repetition.py index fb40270..f118f02 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -39,10 +39,14 @@ class GridRepetition: pattern: 'Pattern' _offset: numpy.ndarray - _rotation: float _dose: float + + _rotation: float + ''' Applies to individual instances in the grid, not the grid vectors ''' _scale: float + ''' Applies to individual instances in the grid, not the grid vectors ''' _mirrored: List[bool] + ''' Applies to individual instances in the grid, not the grid vectors ''' _a_vector: numpy.ndarray _b_vector: numpy.ndarray or None @@ -287,7 +291,7 @@ class GridRepetition: def rotate_around(self, pivot: vector2, rotation: float) -> 'GridRepetition': """ - Rotate around a point + Rotate the array around a point :param pivot: Point to rotate around :param rotation: Angle to rotate by (counterclockwise, radians) @@ -304,6 +308,19 @@ class GridRepetition: """ Rotate around (0, 0) + :param rotation: Angle to rotate by (counterclockwise, radians) + :return: self + """ + self.rotate_elements(rotation) + self.a_vector = numpy.dot(rotation_matrix_2d(rotation), self.a_vector) + if self.b_vector is not None: + self.b_vector = numpy.dot(rotation_matrix_2d(rotation), self.b_vector) + return self + + def rotate_elements(self, rotation: float) -> 'GridRepetition': + """ + Rotate each element around its origin + :param rotation: Angle to rotate by (counterclockwise, radians) :return: self """ @@ -317,13 +334,23 @@ class GridRepetition: :param axis: Axis to mirror across. :return: self """ - self.mirrored[axis] = not self.mirrored[axis] - self.rotation *= -1 + self.mirror_elements(axis) self.a_vector[axis] *= -1 if self.b_vector is not None: self.b_vector[axis] *= -1 return self + def mirror_elements(self, axis: int) -> 'GridRepetition': + """ + Mirror each element across an axis relative to its origin. + + :param axis: Axis to mirror across. + :return: self + """ + self.mirrored[axis] = not self.mirrored[axis] + self.rotation *= -1 + 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 @@ -338,6 +365,18 @@ class GridRepetition: """ Scale the GridRepetition by a factor + :param c: scaling factor + """ + self.scale_elements_by(c) + self.a_vector *= c + if self.b_vector is not None: + self.b_vector *= c + return self + + def scale_elements_by(self, c: float) -> 'GridRepetition': + """ + Scale each element by a factor + :param c: scaling factor """ self.scale *= c From 2a53d8ef4d987bdacd9e32ed55dbdc3885e1cd79 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 7 Feb 2020 23:49:53 -0800 Subject: [PATCH 071/109] bump version to 1.2 --- masque/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/VERSION b/masque/VERSION index 9459d4b..5625e59 100644 --- a/masque/VERSION +++ b/masque/VERSION @@ -1 +1 @@ -1.1 +1.2 From a930c65d42830c898d1530a94256066f512e2c20 Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 10 Feb 2020 10:09:07 -0800 Subject: [PATCH 072/109] fix gridrepetition mirror --- masque/repetition.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/masque/repetition.py b/masque/repetition.py index f118f02..bf03d95 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -335,9 +335,9 @@ class GridRepetition: :return: self """ self.mirror_elements(axis) - self.a_vector[axis] *= -1 + self.a_vector[1-axis] *= -1 if self.b_vector is not None: - self.b_vector[axis] *= -1 + self.b_vector[1-axis] *= -1 return self def mirror_elements(self, axis: int) -> 'GridRepetition': From 20981f10b94da5e98700c5e2169a61fc78d14475 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 16 Feb 2020 18:17:28 -0800 Subject: [PATCH 073/109] Fix error name InvalidDataError -> PatternError --- masque/repetition.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/masque/repetition.py b/masque/repetition.py index bf03d95..5ed652e 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -78,7 +78,7 @@ class GridRepetition: :param b_count: Number of elements in the b_vector direction. Should be omitted if b_vector was omitted. :param locked: Whether the subpattern is locked after initialization. - :raises: InvalidDataError if b_* inputs conflict with each other + :raises: PatternError if b_* inputs conflict with each other or a_count < 1. """ if b_vector is None: @@ -88,11 +88,11 @@ class GridRepetition: b_vector = numpy.array([0.0, 0.0]) if a_count < 1: - raise InvalidDataError('Repetition has too-small a_count: ' - '{}'.format(a_count)) + raise PatternError('Repetition has too-small a_count: ' + '{}'.format(a_count)) if b_count < 1: - raise InvalidDataError('Repetition has too-small b_count: ' - '{}'.format(b_count)) + raise PatternError('Repetition has too-small b_count: ' + '{}'.format(b_count)) self.unlock() self.a_vector = a_vector self.b_vector = b_vector From 5adabfd25ab1a20f6746695a4a95abb817dc53de Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 17 Feb 2020 21:02:53 -0800 Subject: [PATCH 074/109] Docstring format change (new param and return format) Also some minor code formatting fixes in utils --- masque/__init__.py | 17 +- masque/file/gdsii.py | 123 ++++++++------ masque/file/svg.py | 37 ++-- masque/file/utils.py | 22 ++- masque/label.py | 55 +++--- masque/pattern.py | 359 ++++++++++++++++++++++++--------------- masque/repetition.py | 166 ++++++++++++------ masque/shapes/arc.py | 52 ++++-- masque/shapes/circle.py | 7 +- masque/shapes/ellipse.py | 14 +- masque/shapes/path.py | 57 ++++--- masque/shapes/polygon.py | 73 ++++---- masque/shapes/shape.py | 149 +++++++++------- masque/shapes/text.py | 15 +- masque/subpattern.py | 92 +++++++--- masque/utils.py | 104 ++++++++---- 16 files changed, 845 insertions(+), 497 deletions(-) diff --git a/masque/__init__.py b/masque/__init__.py index bd6908a..dfb324b 100644 --- a/masque/__init__.py +++ b/masque/__init__.py @@ -6,21 +6,24 @@ 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. + `Pattern` is a basic object containing a 2D lithography mask, composed of a list of `Shape` + objects, a list of `Label` objects, and a list of references to other `Patterns` (using + `SubPattern` and `GridRepetition`). - SubPattern provides basic support for nesting Pattern objects within each other, by adding + `SubPattern` provides basic support for nesting `Pattern` objects within each other, by adding offset, rotation, scaling, and other such properties to a Pattern reference. + `GridRepetition` provides support for nesting regular arrays of `Pattern` objects. + 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.svg] + - `numpy` + - `matplotlib` [Pattern.visualize(...)] + - `python-gdsii` [masque.file.gdsii] + - `svgwrite` [masque.file.svg] """ import pathlib diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 8b4373f..ca80cc6 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -49,39 +49,40 @@ def write(patterns: Pattern or List[Pattern], modify_originals: bool = False, disambiguate_func: Callable[[List[Pattern]], None] = None): """ - 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 + 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, - 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 + 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` - 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. + 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() + If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()` prior to calling this function. - :param patterns: A Pattern or list of patterns to write to file. - :param file: Filename or stream object 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 library_name: Library name written into the GDSII file. - Default 'masque-gdsii-write'. - :param modify_originals: If True, the original pattern is modified as part of the writing - process. Otherwise, a copy is made and deepunlock()-ed. - Default False. - :param disambiguate_func: Function which takes a list of patterns and alters them - to make their names valid and unique. Default is `disambiguate_pattern_names`, which - attempts to adhere to the GDSII standard as well as possible. - WARNING: No additional error checking is performed on the results. + Args: + patterns: A Pattern or list of patterns to write to file. + file: Filename or stream object to write to. + 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. + 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`. + library_name: Library name written into the GDSII file. + Default 'masque-gdsii-write'. + modify_originals: If `True`, the original pattern is modified as part of the writing + process. Otherwise, a copy is made and `deepunlock()`-ed. + Default `False`. + disambiguate_func: Function which takes a list of patterns and alters them + to make their names valid and unique. Default is `disambiguate_pattern_names`, which + attempts to adhere to the GDSII standard as well as possible. + WARNING: No additional error checking is performed on the results. """ if isinstance(patterns, Pattern): patterns = [patterns] @@ -124,9 +125,15 @@ def writefile(patterns: List[Pattern] or Pattern, **kwargs, ): """ - Wrapper for gdsii.write() that takes a filename or path instead of a stream. + Wrapper for `gdsii.write()` that takes a filename or path instead of a stream. Will automatically compress the file if it has a .gz suffix. + + Args: + patterns: `Pattern` or list of patterns to save + filename: Filename to save to. + *args: passed to `gdsii.write` + **kwargs: passed to `gdsii.write` """ path = pathlib.Path(filename) if path.suffix == '.gz': @@ -153,8 +160,11 @@ def dose2dtype(patterns: List[Pattern], 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) + Args: + 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). @@ -221,9 +231,14 @@ def readfile(filename: str or pathlib.Path, **kwargs, ) -> (Dict[str, Pattern], Dict[str, Any]): """ - Wrapper for gdsii.read() that takes a filename or path instead of a stream. + Wrapper for `gdsii.read()` that takes a filename or path instead of a stream. - Tries to autodetermine file type based on suffixes + Will automatically decompress files with a .gz suffix. + + Args: + filename: Filename to save to. + *args: passed to `gdsii.read` + **kwargs: passed to `gdsii.read` """ path = pathlib.Path(filename) if path.suffix == '.gz': @@ -251,14 +266,18 @@ def read(stream: io.BufferedIOBase, '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. - Default False. - :param clean_vertices: If true, remove any redundant vertices when loading polygons. + Args: + filename: Filename specifying a GDSII file to read from. + 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`. + 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: (Dict of pattern_name:Patterns generated from GDSII structures, Dict of GDSII library info) + Default `True`. + + Returns: + - Dict of pattern_name:Patterns generated from GDSII structures + - Dict of GDSII library info """ lib = gdsii.library.Library.load(stream) @@ -353,6 +372,7 @@ def read(stream: io.BufferedIOBase, def _mlayer2gds(mlayer): + """ Helper to turn a layer tuple-or-int into a layer and datatype""" if is_scalar(mlayer): layer = mlayer data_type = 0 @@ -366,12 +386,15 @@ def _mlayer2gds(mlayer): 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 .identifier 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. + """ + Helper function to create a SubPattern from an SREF. Sets subpat.pattern to None + and sets the instance .identifier 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.identifier = element.struct_name if element.strans is not None: @@ -394,13 +417,15 @@ def _sref_to_subpat(element: gdsii.elements.SRef) -> SubPattern: 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 .identifier 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 + """ + Helper function to create a GridRepetition from an AREF. Sets gridrep.pattern to None + and sets the instance .identifier 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. + """ rotation = 0 offset = numpy.array(element.xy[0]) scale = 1 diff --git a/masque/file/svg.py b/masque/file/svg.py index 6e5ff75..3b8276a 100644 --- a/masque/file/svg.py +++ b/masque/file/svg.py @@ -23,20 +23,21 @@ def writefile(pattern: Pattern, Note that this function modifies the Pattern. - If custom_attributes is True, non-standard pattern_layer and pattern_dose attributes + 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 + 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() + 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. + Args: + pattern: Pattern to write to file. Modified by this function. + filename: Filename to write to. + custom_attributes: Whether to write non-standard `pattern_layer` and + `pattern_dose` attributes to the SVG elements. """ # Polygonize pattern @@ -85,18 +86,19 @@ def writefile(pattern: Pattern, def writefile_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 + 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. + one `` element. Note that this function modifies the Pattern. - If you want pattern polygonized with non-default arguments, just call pattern.polygonize() + 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. + Args: + pattern: Pattern to write to file. Modified by this function. + filename: Filename to write to. """ # Polygonize and flatten pattern pattern.polygonize().flatten() @@ -129,8 +131,11 @@ 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. + Args: + vertices: Nx2 array of vertices. + + Returns: + SVG path-string. """ commands = 'M{:g},{:g} '.format(vertices[0][0], vertices[0][1]) for vertex in vertices[1:]: diff --git a/masque/file/utils.py b/masque/file/utils.py index 97e3d36..f8b2841 100644 --- a/masque/file/utils.py +++ b/masque/file/utils.py @@ -12,11 +12,14 @@ __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. + 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. + Args: + pattern: Pattern whose name we want to mangle. + dose_multiplier: Dose multiplier to mangle with. + + Returns: + Mangled name. """ expression = re.compile('[^A-Za-z0-9_\?\$]') full_name = '{}_{}_{}'.format(pattern.name, dose_multiplier, id(pattern)) @@ -26,11 +29,14 @@ def mangle_name(pattern: Pattern, dose_multiplier: float=1.0) -> str: def make_dose_table(patterns: List[Pattern], dose_multiplier: float=1.0) -> Set[Tuple[int, float]]: """ - Create a set containing (id(pat), written_dose) for each pattern (including subpatterns) + Create a set containing `(id(pat), written_dose)` for each pattern (including subpatterns) - :param pattern: Source Patterns. - :param dose_multiplier: Multiplier for all written_dose entries. - :return: {(id(subpat.pattern), written_dose), ...} + Args: + pattern: Source Patterns. + dose_multiplier: Multiplier for all written_dose entries. + + Returns: + `{(id(subpat.pattern), written_dose), ...}` """ dose_table = {(id(pattern), dose_multiplier) for pattern in patterns} for pattern in patterns: diff --git a/masque/label.py b/masque/label.py index 5dd6e57..1e46e6a 100644 --- a/masque/label.py +++ b/masque/label.py @@ -15,19 +15,21 @@ class Label: A text annotation with a position and layer (but no size; it is not drawn) """ __slots__ = ('_offset', '_layer', '_string', 'identifier', 'locked') - # [x_offset, y_offset] + _offset: numpy.ndarray + """ [x_offset, y_offset] """ - # Layer (integer >= 0) or 2-Tuple of integers _layer: int or Tuple + """ Layer (integer >= 0, or 2-Tuple of integers) """ - # Label string _string: str + """ Label string """ - # Arbitrary identifier tuple identifier: Tuple + """ Arbitrary identifier tuple, useful for keeping track of history when flattening """ - locked: bool # If True, any changes to the label will raise a PatternLockedError + locked: bool + """ If `True`, any changes to the label will raise a `PatternLockedError` """ def __setattr__(self, name, value): if self.locked and name != 'locked': @@ -40,8 +42,6 @@ class Label: def offset(self) -> numpy.ndarray: """ [x, y] offset - - :return: [x_offset, y_offset] """ return self._offset @@ -59,8 +59,6 @@ class Label: def layer(self) -> int or Tuple[int]: """ Layer number (int or tuple of ints) - - :return: Layer """ return self._layer @@ -73,8 +71,6 @@ class Label: def string(self) -> str: """ Label string (str) - - :return: string """ return self._string @@ -109,29 +105,33 @@ class Label: def copy(self) -> 'Label': """ - Returns a deep copy of the shape. - - :return: Deep copy of self + Returns a deep copy of the label. """ return copy.deepcopy(self) def translate(self, offset: vector2) -> 'Label': """ - Translate the shape by the given offset + Translate the label by the given offset - :param offset: [x_offset, y,offset] - :return: self + Args: + offset: [x_offset, y,offset] + + Returns: + self """ self.offset += offset return self def rotate_around(self, pivot: vector2, rotation: float) -> 'Label': """ - Rotate the shape around a point. + Rotate the label around a point. - :param pivot: Point (x, y) to rotate around - :param rotation: Angle to rotate by (counterclockwise, radians) - :return: self + Args: + pivot: Point (x, y) to rotate around + rotation: Angle to rotate by (counterclockwise, radians) + + Returns: + self """ pivot = numpy.array(pivot, dtype=float) self.translate(-pivot) @@ -147,24 +147,27 @@ class Label: bounds = [self.offset, self.offset] - :return: Bounds [[xmin, xmax], [ymin, ymax]] + Returns: + Bounds [[xmin, xmax], [ymin, ymax]] """ return numpy.array([self.offset, self.offset]) def lock(self) -> 'Label': """ - Lock the Label + Lock the Label, causing any modifications to raise an exception. - :return: self + Return: + self """ object.__setattr__(self, 'locked', True) return self def unlock(self) -> 'Label': """ - Unlock the Label + Unlock the Label, re-allowing changes. - :return: self + Return: + self """ object.__setattr__(self, 'locked', False) return self diff --git a/masque/pattern.py b/masque/pattern.py index 50d4e34..ab0e99f 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -27,22 +27,32 @@ visitor_function_t = Callable[['Pattern', Tuple['Pattern'], Dict, numpy.ndarray] class Pattern: """ - 2D layout consisting of some set of shapes and references to other Pattern objects - (via SubPattern). Shapes are assumed to inherit from .shapes.Shape or provide equivalent - functions. - - :var shapes: List of all shapes in this Pattern. Elements in this list are assumed to inherit - from Shape or provide equivalent functions. - :var subpatterns: List of all SubPattern objects in this Pattern. Multiple SubPattern objects - may reference the same Pattern object. - :var name: An identifier for this object. Not necessarily unique. + 2D layout consisting of some set of shapes, labels, and references to other Pattern objects + (via SubPattern and GridRepetition). Shapes are assumed to inherit from + masque.shapes.Shape or provide equivalent functions. """ __slots__ = ('shapes', 'labels', 'subpatterns', 'name', 'locked') + shapes: List[Shape] + """ List of all shapes in this Pattern. + Elements in this list are assumed to inherit from Shape or provide equivalent functions. + """ + labels: List[Label] + """ List of all labels in this Pattern. """ + subpatterns: List[SubPattern or GridRepetition] + """ List of all objects referencing other patterns in this Pattern. + Examples are SubPattern (gdsii "instances") or GridRepetition (gdsii "arrays") + Multiple objects in this list may reference the same Pattern object + (multiple instances of the same object). + """ + name: str + """ A name for this pattern """ + locked: bool + """ When the pattern is locked, no changes may be made. """ def __init__(self, name: str = '', @@ -55,11 +65,12 @@ class Pattern: 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 labels: Initial labels in the Pattern - :param subpatterns: Initial subpatterns in the Pattern - :param name: An identifier for the Pattern - :param locked: Whether to lock the pattern after construction + Args: + shapes: Initial shapes in the Pattern + labels: Initial labels in the Pattern + subpatterns: Initial subpatterns in the Pattern + name: An identifier for the Pattern + locked: Whether to lock the pattern after construction """ self.unlock() if isinstance(shapes, list): @@ -106,8 +117,11 @@ class Pattern: 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 + Args: + other_pattern: The Pattern to append + + Returns: + self """ self.subpatterns += other_pattern.subpatterns self.shapes += other_pattern.shapes @@ -125,16 +139,19 @@ class Pattern: 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 - pattern. - :return: A Pattern containing all the shapes and subpatterns for which the parameter - functions return True + Args: + shapes_func: Given a shape, returns a boolean denoting whether the shape is a member + of the subset. Default always returns False. + labels_func: Given a label, returns a boolean denoting whether the label is a member + of the subset. Default always returns False. + subpatterns_func: Given a subpattern, returns a boolean denoting if it is a member + of the subset. Default always returns False. + recursive: If True, also calls .subset() recursively on patterns referenced by this + pattern. + + Returns: + A Pattern containing all the shapes and subpatterns for which the parameter + functions return True """ def do_subset(src): pat = Pattern(name=src.name) @@ -163,12 +180,17 @@ class Pattern: 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. + Args: + func: Function which accepts a Pattern, and returns a pattern. + memo: Dictionary used to avoid re-running on multiply-referenced patterns. + Stores `{id(pattern): func(pattern)}` for patterns which have already been processed. + Default `None` (no already-processed patterns). + + Returns: + The result of applying func() to this pattern and all subpatterns. + + Raises: + PatternError if called on a pattern containing a circular reference. """ if memo is None: memo = {} @@ -212,19 +234,24 @@ class Pattern: for the instance being visited `memo`: Arbitrary dict (not altered except by visit_*()) - :param visit_before: Function to call before traversing subpatterns. + Args: + visit_before: Function to call before traversing subpatterns. + Should accept a `Pattern` and `**visit_args`, and return the (possibly modified) + pattern. Default `None` (not called). + visit_after: Function to call after traversing subpatterns. Should accept a Pattern and **visit_args, and return the (possibly modified) - pattern. Default None (not called). - :param visit_after: Function to call after traversing subpatterns. - Should accept a Pattern and **visit_args, and return the (possibly modified) - pattern. Default None (not called). - :param transform: Initial value for `visit_args['transform']`. + pattern. Default `None` (not called). + transform: Initial value for `visit_args['transform']`. Can be `False`, in which case the transform is not calculated. - `True` or `None` is interpreted as [0, 0, 0, 0]. - :param memo: Arbitrary dict for use by visit_*() functions. Default None (empty dict). - :param hierarchy: Tuple of patterns specifying the hierarchy above the current pattern. + `True` or `None` is interpreted as `[0, 0, 0, 0]`. + memo: Arbitrary dict for use by `visit_*()` functions. Default `None` (empty dict). + hierarchy: Tuple of patterns specifying the hierarchy above the current pattern. Appended to the start of the generated `visit_args['hierarchy']`. Default is an empty tuple. + + Returns: + The result, including `visit_before(self, ...)` and `visit_after(self, ...)`. + Note that `self` may also be altered! """ if memo is None: memo = {} @@ -267,16 +294,19 @@ class Pattern: poly_max_arclen: float = None, ) -> 'Pattern': """ - Calls .to_polygons(...) on all the shapes in this Pattern and any referenced patterns, + 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(...). + 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 + Args: + poly_num_points: Number of points to use for each polygon. Can be overridden by + `poly_max_arclen` if that results in more points. Optional, defaults to shapes' + internal defaults. + poly_max_arclen: Maximum arclength which can be approximated by a single line segment. Optional, defaults to shapes' internal defaults. - :return: self + + Returns: + self """ old_shapes = self.shapes self.shapes = list(itertools.chain.from_iterable( @@ -291,12 +321,15 @@ class Pattern: grid_y: numpy.ndarray, ) -> 'Pattern': """ - Calls .polygonize() and .flatten on the pattern, then calls .manhattanize() on all the + 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 + Args: + grid_x: List of allowed x-coordinates for the Manhattanized polygon edges. + grid_y: List of allowed y-coordinates for the Manhattanized polygon edges. + + Returns: + self """ self.polygonize().flatten() @@ -311,21 +344,25 @@ class Pattern: 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-, + Iterates through this `Pattern` and all referenced `Pattern`s. Within each `Pattern`, it iterates + over all shapes, calling `.normalized_form(norm_value)` on them to retrieve a scale-, offset-, dose-, and rotation-independent form. Each shape whose normalized form appears more than once is removed and re-added using subpattern objects referencing a newly-created - Pattern containing only the normalized form of the shape. + `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. + Note: + 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 + Args: + recursive: Whether to call recursively on self's subpatterns. Default `True`. + norm_value: Passed to `shape.normalized_form(norm_value)`. Default `1e6` (see function note about GDSII) - :param exclude_types: Shape types passed in this argument are always left untouched, for - speed or convenience. Default: (Shapes.Polygon,) - :return: self + exclude_types: Shape types passed in this argument are always left untouched, for + speed or convenience. Default: `(shapes.Polygon,)` + + Returns: + self """ if exclude_types is None: @@ -337,9 +374,9 @@ class Pattern: 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() + # 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)): @@ -348,9 +385,9 @@ class Pattern: 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 + # 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. + # `self.shapes` entries for which we made SubPatterns. shapes_to_remove = [] for label in shape_table: if len(shape_table[label][1]) > 1: @@ -374,21 +411,23 @@ class Pattern: """ Represents the pattern as a list of polygons. - Deep-copies the pattern, then calls .polygonize() and .flatten() on the copy in order to + 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],...]. + Returns: + A list of `(Ni, 2)` `numpy.ndarray`s specifying vertices of the polygons. Each ndarray + is of the form `[[x0, y0], [x1, y1],...]`. """ pat = self.deepcopy().deepunlock().polygonize().flatten() return [shape.vertices + shape.offset for shape in pat.shapes] def referenced_patterns_by_id(self) -> Dict[int, 'Pattern']: """ - Create a dictionary of {id(pat): pat} for all Pattern objects referenced by this + 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 + Returns: + Dictionary of `{id(pat): pat}` for all referenced Pattern objects """ ids = {} for subpat in self.subpatterns: @@ -399,11 +438,12 @@ class Pattern: def get_bounds(self) -> Union[numpy.ndarray, None]: """ - Return a numpy.ndarray containing [[x_min, y_min], [x_max, y_max]], corresponding to the + Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the extent of the Pattern's contents in each dimension. - Returns None if the Pattern is empty. + Returns `None` if the Pattern is empty. - :return: [[x_min, y_min], [x_max, y_max]] or None + Returns: + `[[x_min, y_min], [x_max, y_max]]` or `None` """ entries = self.shapes + self.subpatterns + self.labels if not entries: @@ -428,13 +468,16 @@ class Pattern: Shape identifiers are changed to represent their original position in the pattern hierarchy: - (L1_name (str), L1_index (int), L2_name, L2_index, ..., *original_shape_identifier) - where L1_name is the first-level subpattern's name (e.g. self.subpatterns[0].pattern.name), - L2_name is the next-level subpattern's name (e.g. - self.subpatterns[0].pattern.subpatterns[0].pattern.name) and L1_index is an integer - used to differentiate between multiple instance of the same (or same-named) subpatterns. + `(L1_name (str), L1_index (int), L2_name, L2_index, ..., *original_shape_identifier)` + where + `L1_name` is the first-level subpattern's name (e.g. `self.subpatterns[0].pattern.name`), + `L2_name` is the next-level subpattern's name (e.g. + `self.subpatterns[0].pattern.subpatterns[0].pattern.name`) and + `L1_index` is an integer used to differentiate between multiple instance ofi the same + (or same-named) subpatterns. - :return: self + Returns: + self """ subpatterns = copy.deepcopy(self.subpatterns) self.subpatterns = [] @@ -457,22 +500,28 @@ class Pattern: """ Translates all shapes, label, and subpatterns by the given offset. - :param offset: Offset to translate by - :return: self + Args: + offset: (x, y) to translate by + + Returns: + self """ for entry in self.shapes + self.subpatterns + self.labels: entry.translate(offset) return self - def scale_elements(self, scale: float) -> 'Pattern': + def scale_elements(self, c: float) -> 'Pattern': """" Scales all shapes and subpatterns by the given value. - :param scale: value to scale by - :return: self + Args: + c: factor to scale by + + Returns: + self """ for entry in self.shapes + self.subpatterns: - entry.scale(scale) + entry.scale(c) return self def scale_by(self, c: float) -> 'Pattern': @@ -480,8 +529,11 @@ class 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 + Args: + c: factor to scale by + + Returns: + self """ for entry in self.shapes + self.subpatterns: entry.offset *= c @@ -494,9 +546,12 @@ class 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 + Args: + pivot: (x, y) location to rotate around + rotation: Angle to rotate by (counter-clockwise, radians) + + Returns: + self """ pivot = numpy.array(pivot) self.translate_elements(-pivot) @@ -509,8 +564,11 @@ class Pattern: """ Rotate the offsets of all shapes, labels, and subpatterns around (0, 0) - :param rotation: Angle to rotate by (counter-clockwise, radians) - :return: self + Args: + rotation: Angle to rotate by (counter-clockwise, radians) + + Returns: + self """ for entry in self.shapes + self.subpatterns + self.labels: entry.offset = numpy.dot(rotation_matrix_2d(rotation), entry.offset) @@ -520,8 +578,11 @@ class Pattern: """ Rotate each shape and subpattern around its center (offset) - :param rotation: Angle to rotate by (counter-clockwise, radians) - :return: self + Args: + rotation: Angle to rotate by (counter-clockwise, radians) + + Returns: + self """ for entry in self.shapes + self.subpatterns: entry.rotate(rotation) @@ -531,8 +592,12 @@ class Pattern: """ Mirror the offsets of all shapes, labels, and subpatterns across an axis - :param axis: Axis to mirror across - :return: self + Args: + axis: Axis to mirror across + (0: mirror across x axis, 1: mirror across y axis) + + Returns: + self """ for entry in self.shapes + self.subpatterns + self.labels: entry.offset[axis - 1] *= -1 @@ -541,10 +606,14 @@ class Pattern: def mirror_elements(self, axis: int) -> 'Pattern': """ Mirror each shape and subpattern across an axis, relative to its - center (offset) + offset - :param axis: Axis to mirror across - :return: self + Args: + axis: Axis to mirror across + (0: mirror across x axis, 1: mirror across y axis) + + Returns: + self """ for entry in self.shapes + self.subpatterns: entry.mirror(axis) @@ -554,22 +623,29 @@ class Pattern: """ Mirror the Pattern across an axis - :param axis: Axis to mirror across - :return: self + Args: + axis: Axis to mirror across + (0: mirror across x axis, 1: mirror across y axis) + + Returns: + self """ self.mirror_elements(axis) self.mirror_element_centers(axis) return self - def scale_element_doses(self, factor: float) -> 'Pattern': + def scale_element_doses(self, c: float) -> 'Pattern': """ Multiply all shape and subpattern doses by a factor - :param factor: Factor to multiply doses by - :return: self + Args: + c: Factor to multiply doses by + + Return: + self """ for entry in self.shapes + self.subpatterns: - entry.dose *= factor + entry.dose *= c return self def copy(self) -> 'Pattern': @@ -577,25 +653,26 @@ 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. + Returns: + A copy of the current Pattern. """ return copy.copy(self) def deepcopy(self) -> 'Pattern': """ - Convenience method for copy.deepcopy(pattern) + Convenience method for `copy.deepcopy(pattern)` - :return: A deep copy of the current Pattern. + Returns: + A deep copy of the current Pattern. """ return copy.deepcopy(self) def is_empty(self) -> bool: """ - Returns true if the Pattern contains no shapes, labels, or subpatterns. - - :return: True if the pattern is empty. + Returns: + True if the pattern is contains no shapes, labels, or subpatterns. """ return (len(self.subpatterns) == 0 and len(self.shapes) == 0 and @@ -603,9 +680,11 @@ class Pattern: def lock(self) -> 'Pattern': """ - Lock the pattern + Lock the pattern, raising an exception if it is modified. + Also see `deeplock()`. - :return: self + Returns: + self """ object.__setattr__(self, 'locked', True) return self @@ -614,16 +693,18 @@ class Pattern: """ Unlock the pattern - :return: self + Returns: + self """ object.__setattr__(self, 'locked', False) return self def deeplock(self) -> 'Pattern': """ - Recursively lock the pattern, all referenced shapes, subpatterns, and labels + Recursively lock the pattern, all referenced shapes, subpatterns, and labels. - :return: self + Returns: + self """ self.lock() for ss in self.shapes + self.labels: @@ -634,11 +715,13 @@ class Pattern: def deepunlock(self) -> 'Pattern': """ - Recursively unlock the pattern, all referenced shapes, subpatterns, and labels + Recursively unlock the pattern, all referenced shapes, subpatterns, and labels. - This is dangerous unless you have just performed a deepcopy! + This is dangerous unless you have just performed a deepcopy, since anything + you change will be changed everywhere it is referenced! - :return: self + Return: + self """ self.unlock() for ss in self.shapes + self.labels: @@ -650,10 +733,13 @@ class Pattern: @staticmethod def load(filename: str) -> 'Pattern': """ - Load a Pattern from a file + Load a Pattern from a file using pickle - :param filename: Filename to load from - :return: Loaded Pattern + Args: + filename: Filename to load from + + Returns: + Loaded Pattern """ with open(filename, 'rb') as f: pattern = pickle.load(f) @@ -662,10 +748,13 @@ class Pattern: def save(self, filename: str) -> 'Pattern': """ - Save the Pattern to a file + Save the Pattern to a file using pickle - :param filename: Filename to save to - :return: self + Args: + filename: Filename to save to + + Returns: + self """ with open(filename, 'wb') as f: pickle.dump(self, f, protocol=pickle.HIGHEST_PROTOCOL) @@ -679,12 +768,16 @@ class Pattern: """ Draw a picture of the Pattern and wait for the user to inspect it - Imports matplotlib. + 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 + Note that this can be slow; it is often faster to export to GDSII and use + klayout or a different GDS viewer! + + Args: + offset: Coordinates to offset by before drawing + line_color: Outlines are drawn with this color (passed to `matplotlib.collections.PolyCollection`) + fill_color: Interiors are drawn with this color (passed to `matplotlib.collections.PolyCollection`) + overdraw: Whether to create a new figure or draw on a pre-existing one """ # TODO: add text labels to visualize() from matplotlib import pyplot diff --git a/masque/repetition.py b/masque/repetition.py index 5ed652e..924a756 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -20,8 +20,8 @@ __author__ = 'Jan Petykiewicz' class GridRepetition: """ - GridRepetition provides support for efficiently embedding multiple copies of a Pattern - into another Pattern at regularly-spaced offsets. + GridRepetition provides support for efficiently embedding multiple copies of a `Pattern` + into another `Pattern` at regularly-spaced offsets. """ __slots__ = ('pattern', '_offset', @@ -37,24 +37,49 @@ class GridRepetition: 'locked') pattern: 'Pattern' + """ The `Pattern` being instanced """ _offset: numpy.ndarray + """ (x, y) offset for the base instance """ + _dose: float + """ Dose factor """ _rotation: float - ''' Applies to individual instances in the grid, not the grid vectors ''' + """ Rotation of the individual instances in the grid (not the grid vectors). + Radians, counterclockwise. + """ + _scale: float - ''' Applies to individual instances in the grid, not the grid vectors ''' + """ Scaling factor applied to individual instances in the grid (not the grid vectors) """ + _mirrored: List[bool] - ''' Applies to individual instances in the grid, not the grid vectors ''' + """ Whether to mirror individual instances across the x and y axes + (Applies to individual instances in the grid, not the grid vectors) + """ _a_vector: numpy.ndarray - _b_vector: numpy.ndarray or None + """ Vector `[x, y]` specifying the first lattice vector of the grid. + Specifies center-to-center spacing between adjacent elements. + """ + _a_count: int + """ Number of instances along the direction specified by the `a_vector` """ + + _b_vector: numpy.ndarray or None + """ Vector `[x, y]` specifying a second lattice vector for the grid. + Specifies center-to-center spacing between adjacent elements. + Can be `None` for a 1D array. + """ + _b_count: int + """ Number of instances along the direction specified by the `b_vector` """ identifier: Tuple + """ Arbitrary identifier """ + locked: bool + """ If `True`, disallows changes to the GridRepetition """ def __init__(self, pattern: 'Pattern', @@ -69,17 +94,20 @@ class GridRepetition: scale: float = 1.0, locked: bool = False): """ - :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. - :param locked: Whether the subpattern is locked after initialization. - :raises: PatternError if b_* inputs conflict with each other - or a_count < 1. + Args: + a_vector: First lattice vector, of the form `[x, y]`. + Specifies center-to-center spacing between adjacent elements. + a_count: Number of elements in the a_vector direction. + 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. + b_count: Number of elements in the `b_vector` direction. + Should be omitted if `b_vector` was omitted. + locked: Whether the `GridRepetition` is locked after initialization. + + Raises: + PatternError if `b_*` inputs conflict with each other + or `a_count < 1`. """ if b_vector is None: if b_count > 1: @@ -254,9 +282,11 @@ class GridRepetition: def as_pattern(self) -> 'Pattern': """ Returns a copy of self.pattern which has been scaled, rotated, repeated, etc. - etc. according to this GridRepetitions's properties. - :return: Copy of self.pattern that has been repeated / altered as implied by - this object's other properties. + etc. according to this `GridRepetition`'s properties. + + Returns: + A copy of self.pattern which has been scaled, rotated, repeated, etc. + etc. according to this `GridRepetition`'s properties. """ patterns = [] @@ -283,8 +313,11 @@ class GridRepetition: """ Translate by the given offset - :param offset: Translate by this offset - :return: self + Args: + offset: `[x, y]` to translate by + + Returns: + self """ self.offset += offset return self @@ -293,9 +326,12 @@ class GridRepetition: """ Rotate the array around a point - :param pivot: Point to rotate around - :param rotation: Angle to rotate by (counterclockwise, radians) - :return: self + Args: + pivot: Point `[x, y]` to rotate around + rotation: Angle to rotate by (counterclockwise, radians) + + Returns: + self """ pivot = numpy.array(pivot, dtype=float) self.translate(-pivot) @@ -308,8 +344,11 @@ class GridRepetition: """ Rotate around (0, 0) - :param rotation: Angle to rotate by (counterclockwise, radians) - :return: self + Args: + rotation: Angle to rotate by (counterclockwise, radians) + + Returns: + self """ self.rotate_elements(rotation) self.a_vector = numpy.dot(rotation_matrix_2d(rotation), self.a_vector) @@ -321,8 +360,11 @@ class GridRepetition: """ Rotate each element around its origin - :param rotation: Angle to rotate by (counterclockwise, radians) - :return: self + Args: + rotation: Angle to rotate by (counterclockwise, radians) + + Returns: + self """ self.rotation += rotation return self @@ -331,8 +373,12 @@ class GridRepetition: """ Mirror the GridRepetition across an axis. - :param axis: Axis to mirror across. - :return: self + Args: + axis: Axis to mirror across. + (0: mirror across x-axis, 1: mirror across y-axis) + + Returns: + self """ self.mirror_elements(axis) self.a_vector[1-axis] *= -1 @@ -344,8 +390,12 @@ class GridRepetition: """ Mirror each element across an axis relative to its origin. - :param axis: Axis to mirror across. - :return: self + Args: + axis: Axis to mirror across. + (0: mirror across x-axis, 1: mirror across y-axis) + + Returns: + self """ self.mirrored[axis] = not self.mirrored[axis] self.rotation *= -1 @@ -353,11 +403,12 @@ class GridRepetition: 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 GridRepetition in each dimension. - Returns None if the contained Pattern is empty. + Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the + extent of the `GridRepetition` in each dimension. + Returns `None` if the contained `Pattern` is empty. - :return: [[x_min, y_min], [x_max, y_max]] or None + Returns: + `[[x_min, y_min], [x_max, y_max]]` or `None` """ return self.as_pattern().get_bounds() @@ -365,7 +416,11 @@ class GridRepetition: """ Scale the GridRepetition by a factor - :param c: scaling factor + Args: + c: scaling factor + + Returns: + self """ self.scale_elements_by(c) self.a_vector *= c @@ -377,7 +432,11 @@ class GridRepetition: """ Scale each element by a factor - :param c: scaling factor + Args: + c: scaling factor + + Returns: + self """ self.scale *= c return self @@ -386,7 +445,8 @@ class GridRepetition: """ Return a shallow copy of the repetition. - :return: copy.copy(self) + Returns: + `copy.copy(self)` """ return copy.copy(self) @@ -394,33 +454,37 @@ class GridRepetition: """ Return a deep copy of the repetition. - :return: copy.copy(self) + Returns: + `copy.deepcopy(self)` """ return copy.deepcopy(self) def lock(self) -> 'GridRepetition': """ - Lock the GridRepetition + Lock the `GridRepetition`, disallowing changes. - :return: self + Returns: + self """ object.__setattr__(self, 'locked', True) return self def unlock(self) -> 'GridRepetition': """ - Unlock the GridRepetition + Unlock the `GridRepetition` - :return: self + Returns: + self """ object.__setattr__(self, 'locked', False) return self def deeplock(self) -> 'GridRepetition': """ - Recursively lock the GridRepetition and its contained pattern + Recursively lock the `GridRepetition` and its contained pattern - :return: self + Returns: + self """ self.lock() self.pattern.deeplock() @@ -428,11 +492,13 @@ class GridRepetition: def deepunlock(self) -> 'GridRepetition': """ - Recursively unlock the GridRepetition and its contained pattern + Recursively unlock the `GridRepetition` and its contained pattern - This is dangerous unless you have just performed a deepcopy! + This is dangerous unless you have just performed a deepcopy, since + the component parts may be reused elsewhere. - :return: self + Returns: + self """ self.unlock() self.pattern.deepunlock() diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 467db98..25aff16 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -24,19 +24,28 @@ class Arc(Shape): __slots__ = ('_radii', '_angles', '_width', '_rotation', 'poly_num_points', 'poly_max_arclen') _radii: numpy.ndarray - _angles: numpy.ndarray - _width: float + """ Two radii for defining an ellipse """ + _rotation: float + """ Rotation (ccw, radians) from the x axis to the first radius """ + + _angles: numpy.ndarray + """ Start and stop angles (ccw, radians) for choosing an arc from the ellipse, measured from the first radius """ + + _width: float + """ Width of the arc """ + poly_num_points: int + """ Sets the default number of points for `.polygonize()` """ + poly_max_arclen: float + """ Sets the default max segement length for `.polygonize()` """ # radius properties @property def radii(self) -> numpy.ndarray: """ - Return the radii [rx, ry] - - :return: [rx, ry] + Return the radii `[rx, ry]` """ return self._radii @@ -73,10 +82,11 @@ class Arc(Shape): @property def angles(self) -> vector2: """ - Return the start and stop angles [a_start, a_stop]. + Return the start and stop angles `[a_start, a_stop]`. Angles are measured from x-axis after rotation - :return: [a_start, a_stop] + Returns: + `[a_start, a_stop]` """ return self._angles @@ -109,7 +119,8 @@ class Arc(Shape): """ Rotation of radius_x from x_axis, counterclockwise, in radians. Stored mod 2*pi - :return: rotation counterclockwise in radians + Returns: + rotation counterclockwise in radians """ return self._rotation @@ -125,7 +136,8 @@ class Arc(Shape): """ Width of the arc (difference between inner and outer radii) - :return: width + Returns: + width """ return self._width @@ -225,12 +237,12 @@ class Arc(Shape): 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. + `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) + 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. @@ -329,8 +341,11 @@ class Arc(Shape): 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. + 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() @@ -356,8 +371,9 @@ class Arc(Shape): 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]] + Returns: + "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): diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index 8816787..8e47912 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -17,16 +17,19 @@ class Circle(Shape): """ __slots__ = ('_radius', 'poly_num_points', 'poly_max_arclen') _radius: float + """ Circle radius """ + poly_num_points: int + """ Sets the default number of points for `.polygonize()` """ + poly_max_arclen: float + """ Sets the default max segement length for `.polygonize()` """ # radius property @property def radius(self) -> float: """ Circle's radius (float, >= 0) - - :return: radius """ return self._radius diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index 0d8f084..931cf95 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -20,17 +20,22 @@ class Ellipse(Shape): __slots__ = ('_radii', '_rotation', 'poly_num_points', 'poly_max_arclen') _radii: numpy.ndarray + """ Ellipse radii """ + _rotation: float + """ Angle from x-axis to first radius (ccw, radians) """ + poly_num_points: int + """ Sets the default number of points for `.polygonize()` """ + poly_max_arclen: float + """ Sets the default max segement length for `.polygonize()` """ # radius properties @property def radii(self) -> numpy.ndarray: """ - Return the radii [rx, ry] - - :return: [rx, ry] + Return the radii `[rx, ry]` """ return self._radii @@ -70,7 +75,8 @@ class Ellipse(Shape): Rotation of rx from the x axis. Uses the interval [0, pi) in radians (counterclockwise is positive) - :return: counterclockwise rotation in radians + Returns: + counterclockwise rotation in radians """ return self._rotation diff --git a/masque/shapes/path.py b/masque/shapes/path.py index b31c5c5..7b0ff9c 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -37,8 +37,6 @@ class Path(Shape): def width(self) -> float: """ Path width (float, >= 0) - - :return: width """ return self._width @@ -55,8 +53,6 @@ class Path(Shape): def cap(self) -> 'Path.Cap': """ Path end-cap - - :return: Path.Cap enum """ return self._cap @@ -74,9 +70,10 @@ class Path(Shape): @property def cap_extensions(self) -> numpy.ndarray or None: """ - Path end-cap extensionf + Path end-cap extension - :return: 2-element ndarray or None + Returns: + 2-element ndarray or `None` """ return self._cap_extensions @@ -96,9 +93,7 @@ class Path(Shape): @property def vertices(self) -> numpy.ndarray: """ - Vertices of the path (Nx2 ndarray: [[x0, y0], [x1, y1], ...] - - :return: vertices + Vertices of the path (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`) """ return self._vertices @@ -194,22 +189,25 @@ class Path(Shape): Build a path by specifying the turn angles and travel distances rather than setting the distances directly. - :param travel_pairs: A list of (angle, distance) pairs that define - the path. Angles are counterclockwise, in radians, and are relative - to the previous segment's direction (the initial angle is relative - to the +x axis). - :param width: Path width, default 0 - :param cap: End-cap type, default Path.Cap.Flush (no end-cap) - :param cap_extensions: End-cap extension distances, when using Path.Cap.CustomSquare. - Default (0, 0) or None, depending on cap type - :param offset: Offset, default (0, 0) - :param rotation: Rotation counterclockwise, in radians. Default 0 - :param mirrored: Whether to mirror across the x or y axes. For example, - mirrored=(True, False) results in a reflection across the x-axis, - multiplying the path's y-coordinates by -1. Default (False, False) - :param layer: Layer, default 0 - :param dose: Dose, default 1.0 - :return: The resulting Path object + Args: + travel_pairs: A list of (angle, distance) pairs that define + the path. Angles are counterclockwise, in radians, and are relative + to the previous segment's direction (the initial angle is relative + to the +x axis). + width: Path width, default `0` + cap: End-cap type, default `Path.Cap.Flush` (no end-cap) + cap_extensions: End-cap extension distances, when using `Path.Cap.CustomSquare`. + Default `(0, 0)` or `None`, depending on cap type + offset: Offset, default `(0, 0)` + rotation: Rotation counterclockwise, in radians. Default `0` + mirrored: Whether to mirror across the x or y axes. For example, + `mirrored=(True, False)` results in a reflection across the x-axis, + multiplying the path's y-coordinates by -1. Default `(False, False)` + layer: Layer, default `0` + dose: Dose, default `1.0` + + Returns: + The resulting Path object """ #TODO: needs testing direction = numpy.array([1, 0]) @@ -359,7 +357,8 @@ class Path(Shape): """ Removes duplicate, co-linear and otherwise redundant vertices. - :returns: self + Returns: + self """ self.remove_colinear_vertices() return self @@ -368,7 +367,8 @@ class Path(Shape): ''' Removes all consecutive duplicate (repeated) vertices. - :returns: self + Returns: + self ''' self.vertices = remove_duplicate_vertices(self.vertices, closed_path=False) return self @@ -377,7 +377,8 @@ class Path(Shape): ''' Removes consecutive co-linear vertices. - :returns: self + Returns: + self ''' self.vertices = remove_colinear_vertices(self.vertices, closed_path=False) return self diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 3bbaeec..daef7fc 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -16,18 +16,17 @@ class Polygon(Shape): A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an implicitly-closed boundary, and an offset. - A normalized_form(...) is available, but can be quite slow with lots of vertices. + A `normalized_form(...)` is available, but can be quite slow with lots of vertices. """ __slots__ = ('_vertices',) _vertices: numpy.ndarray + """ Nx2 ndarray of vertices `[[x0, y0], [x1, y1], ...]` """ # vertices property @property def vertices(self) -> numpy.ndarray: """ - Vertices of the polygon (Nx2 ndarray: [[x0, y0], [x1, y1], ...] - - :return: vertices + Vertices of the polygon (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`) """ return self._vertices @@ -107,12 +106,15 @@ class Polygon(Shape): """ Draw a square given side_length, centered on the origin. - :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 + Args: + side_length: Length of one side + rotation: Rotation counterclockwise, in radians + offset: Offset, default `(0, 0)` + layer: Layer, default `0` + dose: Dose, default `1.0` + + Returns: + A Polygon object containing the requested square """ norm_square = numpy.array([[-1, -1], [-1, +1], @@ -134,13 +136,16 @@ class Polygon(Shape): """ 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) - :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 + Args: + lx: Length along x (before rotation) + ly: Length along y (before rotation) + rotation: Rotation counterclockwise, in radians + offset: Offset, default `(0, 0)` + layer: Layer, default `0` + dose: Dose, default `1.0` + + Returns: + A Polygon object containing the requested rectangle """ vertices = 0.5 * numpy.array([[-lx, -ly], [-lx, +ly], @@ -168,17 +173,20 @@ class Polygon(Shape): 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 ly: Length along y direction - :param layer: Layer, default 0 - :param dose: Dose, default 1.0 - :return: A Polygon object containing the requested rectangle + Args: + xmin: Minimum x coordinate + xctr: Center x coordinate + xmax: Maximum x coordinate + lx: Length along x direction + ymin: Minimum y coordinate + yctr: Center y coordinate + ymax: Maximum y coordinate + ly: Length along y direction + layer: Layer, default `0` + dose: Dose, default `1.0` + + Returns: + A Polygon object containing the requested rectangle """ if lx is None: if xctr is None: @@ -278,7 +286,8 @@ class Polygon(Shape): """ Removes duplicate, co-linear and otherwise redundant vertices. - :returns: self + Returns: + self """ self.remove_colinear_vertices() return self @@ -287,7 +296,8 @@ class Polygon(Shape): ''' Removes all consecutive duplicate (repeated) vertices. - :returns: self + Returns: + self ''' self.vertices = remove_duplicate_vertices(self.vertices, closed_path=True) return self @@ -296,7 +306,8 @@ class Polygon(Shape): ''' Removes consecutive co-linear vertices. - :returns: self + Returns: + self ''' self.vertices = remove_colinear_vertices(self.vertices, closed_path=True) return self diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index 429e9dd..8c5b2cb 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -26,12 +26,20 @@ class Shape(metaclass=ABCMeta): """ __slots__ = ('_offset', '_layer', '_dose', 'identifier', 'locked') - _offset: numpy.ndarray # [x_offset, y_offset] - _layer: int or Tuple # Layer (integer >= 0 or tuple) - _dose: float # Dose - identifier: Tuple # An arbitrary identifier for the shape, - # usually empty but used by Pattern.flatten() - locked: bool # If True, any changes to the shape will raise a PatternLockedError + _offset: numpy.ndarray + """ `[x_offset, y_offset]` """ + + _layer: int or Tuple + """ Layer (integer >= 0 or tuple) """ + + _dose: float + """ Dose """ + + identifier: Tuple + """ An arbitrary identifier for the shape, usually empty but used by `Pattern.flatten()` """ + + locked: bool + """ If `True`, any changes to the shape will raise a `PatternLockedError` """ def __setattr__(self, name, value): if self.locked and name != 'locked': @@ -51,31 +59,35 @@ class Shape(metaclass=ABCMeta): """ 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 + Args: + 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. + max_arclen: Maximum arclength which can be approximated by a single line + segment. Optional, defaults to shapes' internal defaults. + + Returns: + 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]] + Returns `[[x_min, y_min], [x_max, y_max]]` which specify a minimal bounding box for the shape. """ pass @abstractmethod def rotate(self, theta: float) -> 'Shape': """ - Rotate the shape around its center (0, 0), ignoring its offset. + Rotate the shape around its origin (0, 0), ignoring its offset. - :param theta: Angle to rotate by (counterclockwise, radians) - :return: self + Args: + theta: Angle to rotate by (counterclockwise, radians) + + Returns: + self """ pass @@ -84,8 +96,12 @@ class Shape(metaclass=ABCMeta): """ Mirror the shape across an axis. - :param axis: Axis to mirror across. - :return: self + Args: + axis: Axis to mirror across. + (0: mirror across x axis, 1: mirror across y axis) + + Returns: + self """ pass @@ -94,8 +110,11 @@ class Shape(metaclass=ABCMeta): """ Scale the shape's size (eg. radius, for a circle) by a constant factor. - :param c: Factor to scale by - :return: self + Args: + c: Factor to scale by + + Returns: + self """ pass @@ -105,18 +124,21 @@ class Shape(metaclass=ABCMeta): 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 the shape; + Args: + norm_value: This value is used to normalize lengths intrinsic to the shape; eg. for a circle, the returned intrinsic radius value will be (radius / norm_value), and - the returned callable will create a Circle(radius=norm_value, ...). This is useful + 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: - intrinsic: A tuple of basic types containing all information about the instance that - is not contained in 'extrinsic'. Usually, intrinsic[0] == type(self). - extrinsic: ([x_offset, y_offset], scale, rotation, mirror_across_x_axis, dose) - constructor: A callable (no arguments) which returns an instance of type(self) with - internal state equivalent to 'intrinsic'. + + Returns: + The returned information takes the form of a 3-element tuple, + `(intrinsic, extrinsic, constructor)`. These are further broken down as: + `intrinsic`: A tuple of basic types containing all information about the instance that + is not contained in 'extrinsic'. Usually, `intrinsic[0] == type(self)`. + `extrinsic`: `([x_offset, y_offset], scale, rotation, mirror_across_x_axis, dose)` + `constructor`: A callable (no arguments) which returns an instance of `type(self)` with + internal state equivalent to `intrinsic`. """ pass @@ -126,8 +148,6 @@ class Shape(metaclass=ABCMeta): def offset(self) -> numpy.ndarray: """ [x, y] offset - - :return: [x_offset, y_offset] """ return self._offset @@ -145,8 +165,6 @@ class Shape(metaclass=ABCMeta): def layer(self) -> int or Tuple[int]: """ Layer number (int or tuple of ints) - - :return: Layer """ return self._layer @@ -159,8 +177,6 @@ class Shape(metaclass=ABCMeta): def dose(self) -> float: """ Dose (float >= 0) - - :return: Dose value """ return self._dose @@ -177,7 +193,8 @@ class Shape(metaclass=ABCMeta): """ Returns a deep copy of the shape. - :return: Deep copy of self + Returns: + copy.deepcopy(self) """ return copy.deepcopy(self) @@ -185,8 +202,11 @@ class Shape(metaclass=ABCMeta): """ Translate the shape by the given offset - :param offset: [x_offset, y,offset] - :return: self + Args: + offset: [x_offset, y,offset] + + Returns: + self """ self.offset += offset return self @@ -195,9 +215,12 @@ class Shape(metaclass=ABCMeta): """ Rotate the shape around a point. - :param pivot: Point (x, y) to rotate around - :param rotation: Angle to rotate by (counterclockwise, radians) - :return: self + Args: + pivot: Point (x, y) to rotate around + rotation: Angle to rotate by (counterclockwise, radians) + + Returns: + self """ pivot = numpy.array(pivot, dtype=float) self.translate(-pivot) @@ -214,14 +237,17 @@ class Shape(metaclass=ABCMeta): 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() + 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. + Args: + grid_x: List of allowed x-coordinates for the Manhattanized polygon edges. + grid_y: List of allowed y-coordinates for the Manhattanized polygon edges. + + Returns: + List of `Polygon` objects with grid-aligned edges. """ from . import Polygon @@ -319,7 +345,7 @@ class Shape(metaclass=ABCMeta): 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() + 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 @@ -328,7 +354,7 @@ class Shape(metaclass=ABCMeta): 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 + 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. @@ -336,19 +362,22 @@ class Shape(metaclass=ABCMeta): equidistant from allowed edge location. Implementation notes: - i) Rasterization is performed using float_raster, giving a high-precision anti-aliased + 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, + 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 + 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. + Args: + grid_x: List of allowed x-coordinates for the Manhattanized polygon edges. + grid_y: List of allowed y-coordinates for the Manhattanized polygon edges. + + Returns: + List of `Polygon` objects with grid-aligned edges. """ from . import Polygon import skimage.measure @@ -403,9 +432,10 @@ class Shape(metaclass=ABCMeta): def lock(self) -> 'Shape': """ - Lock the Shape + Lock the Shape, disallowing further changes - :return: self + Returns: + self """ object.__setattr__(self, 'locked', True) return self @@ -414,7 +444,8 @@ class Shape(metaclass=ABCMeta): """ Unlock the Shape - :return: self + Returns: + self """ object.__setattr__(self, 'locked', False) return self diff --git a/masque/shapes/text.py b/masque/shapes/text.py index 63ff2e1..53a9551 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -173,12 +173,15 @@ def get_char_as_polygons(font_path: str, 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) + Args: + font_path: File path specifying a font loadable by freetype + char: Character to convert to polygons + resolution: Internal resolution setting (used for freetype + `Face.set_font_size(resolution))`. Modify at your own peril! + + Returns: + 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') diff --git a/masque/subpattern.py b/masque/subpattern.py index 78df1b0..9887e83 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -24,13 +24,29 @@ class SubPattern: __slots__ = ('pattern', '_offset', '_rotation', '_dose', '_scale', '_mirrored', 'identifier', 'locked') pattern: 'Pattern' + """ The `Pattern` being instanced """ + _offset: numpy.ndarray + """ (x, y) offset for the instance """ + _rotation: float + """ rotation for the instance, radians counterclockwise """ + _dose: float + """ dose factor for the instance """ + _scale: float + """ scale factor for the instance """ + _mirrored: List[bool] + """ Whether to mirror the instanc across the x and/or y axes. """ + identifier: Tuple + """ An arbitrary identifier """ + locked: bool + """ If `True`, disallows changes to the GridRepetition """ + #TODO more documentation? def __init__(self, @@ -139,9 +155,9 @@ class SubPattern: 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. + Returns: + A copy of self.pattern which has been scaled, rotated, etc. according to this + `SubPattern`'s properties. """ pattern = self.pattern.deepcopy().deepunlock() pattern.scale_by(self.scale) @@ -155,8 +171,11 @@ class SubPattern: """ Translate by the given offset - :param offset: Translate by this offset - :return: self + Args: + offset: Offset `[x, y]` to translate by + + Returns: + self """ self.offset += offset return self @@ -165,9 +184,12 @@ class SubPattern: """ Rotate around a point - :param pivot: Point to rotate around - :param rotation: Angle to rotate by (counterclockwise, radians) - :return: self + Args: + pivot: Point `[x, y]` to rotate around + rotation: Angle to rotate by (counterclockwise, radians) + + Returns: + self """ pivot = numpy.array(pivot, dtype=float) self.translate(-pivot) @@ -178,10 +200,13 @@ class SubPattern: def rotate(self, rotation: float) -> 'SubPattern': """ - Rotate around (0, 0) + Rotate the instance around it's origin - :param rotation: Angle to rotate by (counterclockwise, radians) - :return: self + Args: + rotation: Angle to rotate by (counterclockwise, radians) + + Returns: + self """ self.rotation += rotation return self @@ -190,8 +215,11 @@ class SubPattern: """ Mirror the subpattern across an axis. - :param axis: Axis to mirror across. - :return: self + Args: + axis: Axis to mirror across. + + Returns: + self """ self.mirrored[axis] = not self.mirrored[axis] self.rotation *= -1 @@ -199,11 +227,12 @@ class SubPattern: 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 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 + Returns: + `[[x_min, y_min], [x_max, y_max]]` or `None` """ return self.as_pattern().get_bounds() @@ -211,7 +240,11 @@ class SubPattern: """ Scale the subpattern by a factor - :param c: scaling factor + Args: + c: scaling factor + + Returns: + self """ self.scale *= c return self @@ -220,7 +253,8 @@ class SubPattern: """ Return a shallow copy of the subpattern. - :return: copy.copy(self) + Returns: + `copy.copy(self)` """ return copy.copy(self) @@ -228,15 +262,17 @@ class SubPattern: """ Return a deep copy of the subpattern. - :return: copy.copy(self) + Returns: + `copy.deepcopy(self)` """ return copy.deepcopy(self) def lock(self) -> 'SubPattern': """ - Lock the SubPattern + Lock the SubPattern, disallowing changes - :return: self + Returns: + self """ object.__setattr__(self, 'locked', True) return self @@ -245,7 +281,8 @@ class SubPattern: """ Unlock the SubPattern - :return: self + Returns: + self """ object.__setattr__(self, 'locked', False) return self @@ -254,7 +291,8 @@ class SubPattern: """ Recursively lock the SubPattern and its contained pattern - :return: self + Returns: + self """ self.lock() self.pattern.deeplock() @@ -264,9 +302,11 @@ class SubPattern: """ Recursively unlock the SubPattern and its contained pattern - This is dangerous unless you have just performed a deepcopy! + This is dangerous unless you have just performed a deepcopy, since + the subpattern and its components may be used in more than one once! - :return: self + Returns: + self """ self.unlock() self.pattern.deepunlock() diff --git a/masque/utils.py b/masque/utils.py index b7c7b05..91ba8b2 100644 --- a/masque/utils.py +++ b/masque/utils.py @@ -14,30 +14,37 @@ def is_scalar(var: Any) -> bool: """ Alias for 'not hasattr(var, "__len__")' - :param var: Checks if var has a length. + Args: + 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 'bit_string' is 1 + Interprets bit number `bit_id` from the right (lsb) of `bit_string` as a boolean - :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) + Args: + bit_string: Bit string to test + bit_id: Bit number, 0-indexed from the right (lsb) + + Returns: + Boolean value of the requested bit """ 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'. + Returns `bit_string`, with bit number `bit_id` set to boolean `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' + Args: + bit_string: Bit string to alter + bit_id: Bit number, 0-indexed from right (lsb) + value: Boolean value to set bit to + + Returns: + Altered `bit_string` """ mask = (1 << bit_id) bit_string &= ~mask @@ -50,14 +57,29 @@ 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 + Args: + theta: Angle to rotate, in radians + + Returns: + rotation matrix """ return numpy.array([[numpy.cos(theta), -numpy.sin(theta)], [numpy.sin(theta), +numpy.cos(theta)]]) def normalize_mirror(mirrored: Tuple[bool, bool]) -> Tuple[bool, float]: + """ + Converts 0-2 mirror operations `(mirror_across_x_axis, mirror_across_y_axis)` + into 0-1 mirror operations and a rotation + + Args: + mirrored: `(mirror_across_x_axis, mirror_across_y_axis)` + + Returns: + `mirror_across_x_axis` (bool) and + `angle_to_rotate` in radians + """ + mirrored_x, mirrored_y = mirrored mirror_x = (mirrored_x != mirrored_y) #XOR angle = numpy.pi if mirrored_y else 0 @@ -65,34 +87,48 @@ def normalize_mirror(mirrored: Tuple[bool, bool]) -> Tuple[bool, float]: def remove_duplicate_vertices(vertices: numpy.ndarray, closed_path: bool = True) -> numpy.ndarray: - duplicates = (vertices == numpy.roll(vertices, 1, axis=0)).all(axis=1) - if not closed_path: - duplicates[0] = False - return vertices[~duplicates] + """ + Given a list of vertices, remove any consecutive duplicates. + + Args: + vertices: `[[x0, y0], [x1, y1], ...]` + closed_path: If True, `vertices` is interpreted as an implicity-closed path + (i.e. the last vertex will be removed if it is the same as the first) + + Returns: + `vertices` with no consecutive duplicates. + """ + duplicates = (vertices == numpy.roll(vertices, 1, axis=0)).all(axis=1) + if not closed_path: + duplicates[0] = False + return vertices[~duplicates] def remove_colinear_vertices(vertices: numpy.ndarray, closed_path: bool = True) -> numpy.ndarray: - ''' - Given a list of vertices, remove any superflous vertices (i.e. - those which lie along the line formed by their neighbors) + """ + Given a list of vertices, remove any superflous vertices (i.e. + those which lie along the line formed by their neighbors) - :param vertices: Nx2 ndarray of vertices - :param closed_path: If True, the vertices are assumed to represent an implicitly - closed path. If False, the path is assumed to be open. Default True. - :return: - ''' - vertices = numpy.array(vertices) + Args: + vertices: Nx2 ndarray of vertices + closed_path: If `True`, the vertices are assumed to represent an implicitly + closed path. If `False`, the path is assumed to be open. Default `True`. - # Check for dx0/dy0 == dx1/dy1 + Returns: + `vertices` with colinear (superflous) vertices removed. + """ + vertices = numpy.array(vertices) - dv = numpy.roll(vertices, -1, axis=0) - vertices # [y1-y0, y2-y1, ...] - dxdy = dv * numpy.roll(dv, 1, axis=0)[:, ::-1] #[[dx0*(dy_-1), (dx_-1)*dy0], dx1*dy0, dy1*dy0]] + # Check for dx0/dy0 == dx1/dy1 - dxdy_diff = numpy.abs(numpy.diff(dxdy, axis=1))[:, 0] - err_mult = 2 * numpy.abs(dxdy).sum(axis=1) + 1e-40 + dv = numpy.roll(vertices, -1, axis=0) - vertices # [y1-y0, y2-y1, ...] + dxdy = dv * numpy.roll(dv, 1, axis=0)[:, ::-1] #[[dx0*(dy_-1), (dx_-1)*dy0], dx1*dy0, dy1*dy0]] - slopes_equal = (dxdy_diff / err_mult) < 1e-15 - if not closed_path: - slopes_equal[[0, -1]] = False + dxdy_diff = numpy.abs(numpy.diff(dxdy, axis=1))[:, 0] + err_mult = 2 * numpy.abs(dxdy).sum(axis=1) + 1e-40 - return vertices[~slopes_equal] + slopes_equal = (dxdy_diff / err_mult) < 1e-15 + if not closed_path: + slopes_equal[[0, -1]] = False + + return vertices[~slopes_equal] From 9f27a5093ac3e5d01cc5b7d87241ab559b31d79f Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 14 Mar 2020 15:52:10 -0700 Subject: [PATCH 075/109] add Pattern.referenced_patterns_by_name() --- masque/pattern.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/masque/pattern.py b/masque/pattern.py index ab0e99f..c9fda68 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -436,6 +436,21 @@ class Pattern: ids.update(subpat.pattern.referenced_patterns_by_id()) return ids + def referenced_patterns_by_name(self) -> List[Tuple[str, 'Pattern']]: + """ + Create a list of `(pat.name, pat)` tuples for all Pattern objects referenced by this + Pattern (operates recursively on all referenced Patterns as well). + + Note that names are not necessarily unique, so a list of tuples is returned + rather than a dict. + + Returns: + List of `(pat.name, pat)` tuples for all referenced Pattern objects + """ + pats_by_id = self.referenced_patterns_by_id() + pat_list = [(p.name, p) for p in pats_by_id.values()] + return pat_list + def get_bounds(self) -> Union[numpy.ndarray, None]: """ Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the From e684bd0e4081595ef4bd6086026e6673df16b6ed Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 14 Mar 2020 15:52:49 -0700 Subject: [PATCH 076/109] add Pattern.find_toplevel() Topological sort for lists of Pattern objects, useful for finding top cell of gds --- masque/pattern.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/masque/pattern.py b/masque/pattern.py index c9fda68..cf1a372 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -2,7 +2,7 @@ Base object for containing a lithography mask. """ -from typing import List, Callable, Tuple, Dict, Union +from typing import List, Callable, Tuple, Dict, Union, Set import copy import itertools import pickle @@ -824,3 +824,35 @@ class Pattern: if not overdraw: pyplot.show() + + @staticmethod + def find_toplevel(patterns: List['Pattern']) -> List['Pattern']: + """ + Given a list of Pattern objects, return those that are not referenced by + any other pattern. + + Args: + patterns: A list of patterns to filter. + + Returns: + A filtered list in which no pattern is referenced by any other pattern. + """ + def get_children(pat: Pattern, memo: Set) -> Set: + if pat in memo: + return memo + + children = set(sp.pattern for sp in pat.subpatterns) + new_children = children - memo + memo |= children + + for child_pat in new_children: + memo |= get_children(child_pat, memo) + return memo + + patterns = set(patterns) + not_toplevel = set() + for pattern in patterns: + not_toplevel |= get_children(pattern, not_toplevel) + + toplevel = list(patterns - not_toplevel) + return toplevel From a4f556c3b30be967dd168e07156a3e7df835bd90 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 14 Mar 2020 15:53:05 -0700 Subject: [PATCH 077/109] clarify docs for referenced_patterns_by_id() --- masque/pattern.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/masque/pattern.py b/masque/pattern.py index cf1a372..bf8f06f 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -423,11 +423,11 @@ class Pattern: def referenced_patterns_by_id(self) -> Dict[int, 'Pattern']: """ - Create a dictionary of `{id(pat): pat}` for all Pattern objects referenced by this + Create a dictionary with `{id(pat): pat}` for all Pattern objects referenced by this Pattern (operates recursively on all referenced Patterns as well) Returns: - Dictionary of `{id(pat): pat}` for all referenced Pattern objects + Dictionary with `{id(pat): pat}` for all referenced Pattern objects """ ids = {} for subpat in self.subpatterns: From e9244055c642cf53ef4a8622e5fa8f1c60582cf4 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 14 Mar 2020 15:53:35 -0700 Subject: [PATCH 078/109] Add type check for SubPattern's pattern arg --- masque/subpattern.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/masque/subpattern.py b/masque/subpattern.py index 9887e83..8683bb5 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -23,7 +23,7 @@ class SubPattern: """ __slots__ = ('pattern', '_offset', '_rotation', '_dose', '_scale', '_mirrored', 'identifier', 'locked') - pattern: 'Pattern' + pattern: 'Pattern' or None """ The `Pattern` being instanced """ _offset: numpy.ndarray @@ -50,13 +50,17 @@ class SubPattern: #TODO more documentation? def __init__(self, - pattern: 'Pattern', + pattern: 'Pattern' or None, offset: vector2 = (0.0, 0.0), rotation: float = 0.0, mirrored: List[bool] = None, dose: float = 1.0, scale: float = 1.0, locked: bool = False): + if pattern is not None and not hasattr(pattern, 'lock'): + raise PatternError('Provided pattern has no "lock()" method.\n' + 'Maybe it''s not a Pattern instance?') + self.unlock() self.identifier = () self.pattern = pattern From 95ab0934b77ca356753028757e87fb7c55bc65b0 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 14 Mar 2020 15:54:20 -0700 Subject: [PATCH 079/109] bump version to v1.3 --- masque/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/VERSION b/masque/VERSION index 5625e59..7e32cd5 100644 --- a/masque/VERSION +++ b/masque/VERSION @@ -1 +1 @@ -1.2 +1.3 From df179c9233b019039758c62b8d4165f910695868 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 11 May 2020 18:39:02 -0700 Subject: [PATCH 080/109] Improve type annotations for layer --- masque/file/gdsii.py | 6 +++--- masque/label.py | 10 +++++----- masque/shapes/arc.py | 4 ++-- masque/shapes/circle.py | 4 ++-- masque/shapes/ellipse.py | 4 ++-- masque/shapes/path.py | 6 +++--- masque/shapes/polygon.py | 10 +++++----- masque/shapes/shape.py | 8 ++++---- masque/shapes/text.py | 4 ++-- masque/utils.py | 1 + 10 files changed, 29 insertions(+), 28 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index ca80cc6..7606b1f 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -20,7 +20,7 @@ import gzip from .utils import mangle_name, make_dose_table from .. import Pattern, SubPattern, GridRepetition, PatternError, Label, Shape from ..shapes import Polygon, Path -from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar +from ..utils import rotation_matrix_2d, get_bit, set_bit, vector2, is_scalar, layer_t from ..utils import remove_colinear_vertices, normalize_mirror #TODO document how GDS rotation / mirror works @@ -210,7 +210,7 @@ def dose2dtype(patterns: List[Pattern], for shape in pat.shapes: data_type = dose_vals_list.index(shape.dose * pat_dose) - if is_scalar(shape.layer): + if isinstance(shape.layer, int): shape.layer = (shape.layer, data_type) else: shape.layer = (shape.layer[0], data_type) @@ -371,7 +371,7 @@ def read(stream: io.BufferedIOBase, return patterns_dict, library_info -def _mlayer2gds(mlayer): +def _mlayer2gds(mlayer: layer_t) -> Tuple[int, int]: """ Helper to turn a layer tuple-or-int into a layer and datatype""" if is_scalar(mlayer): layer = mlayer diff --git a/masque/label.py b/masque/label.py index 1e46e6a..a5f8c5e 100644 --- a/masque/label.py +++ b/masque/label.py @@ -4,10 +4,10 @@ import numpy from numpy import pi from .error import PatternError, PatternLockedError -from .utils import is_scalar, vector2, rotation_matrix_2d __author__ = 'Jan Petykiewicz' +from .utils import is_scalar, vector2, rotation_matrix_2d, layer_t class Label: @@ -19,7 +19,7 @@ class Label: _offset: numpy.ndarray """ [x_offset, y_offset] """ - _layer: int or Tuple + _layer: layer_t """ Layer (integer >= 0, or 2-Tuple of integers) """ _string: str @@ -56,14 +56,14 @@ class Label: # layer property @property - def layer(self) -> int or Tuple[int]: + def layer(self) -> layer_t: """ Layer number (int or tuple of ints) """ return self._layer @layer.setter - def layer(self, val: int or List[int]): + def layer(self, val: layer_t): self._layer = val # string property @@ -81,7 +81,7 @@ class Label: def __init__(self, string: str, offset: vector2=(0.0, 0.0), - layer: int=0, + layer: layer_t = 0, locked: bool = False): self.unlock() self.identifier = () diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 25aff16..f8b049c 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -6,10 +6,10 @@ 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' +from ..utils import is_scalar, vector2, layer_t class Arc(Shape): @@ -158,7 +158,7 @@ class Arc(Shape): offset: vector2 = (0.0, 0.0), rotation: float = 0, mirrored: Tuple[bool] = (False, False), - layer: int = 0, + layer: layer_t = 0, dose: float = 1.0, locked: bool = False): self.unlock() diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index 8e47912..a306796 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -5,10 +5,10 @@ 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' +from ..utils import is_scalar, vector2, layer_t class Circle(Shape): @@ -46,7 +46,7 @@ class Circle(Shape): poly_num_points: int = DEFAULT_POLY_NUM_POINTS, poly_max_arclen: float = None, offset: vector2 = (0.0, 0.0), - layer: int = 0, + layer: layer_t = 0, dose: float = 1.0, locked: bool = False): self.unlock() diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index 931cf95..af936d2 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -6,10 +6,10 @@ 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' +from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t class Ellipse(Shape): @@ -93,7 +93,7 @@ class Ellipse(Shape): offset: vector2 = (0.0, 0.0), rotation: float = 0, mirrored: Tuple[bool] = (False, False), - layer: int = 0, + layer: layer_t = 0, dose: float = 1.0, locked: bool = False): self.unlock() diff --git a/masque/shapes/path.py b/masque/shapes/path.py index 7b0ff9c..7d0ed17 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -6,7 +6,7 @@ from numpy import pi, inf from . import Shape, normalized_shape_tuple, Polygon, Circle from .. import PatternError -from ..utils import is_scalar, rotation_matrix_2d, vector2 +from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t from ..utils import remove_colinear_vertices, remove_duplicate_vertices __author__ = 'Jan Petykiewicz' @@ -144,7 +144,7 @@ class Path(Shape): offset: vector2 = (0.0, 0.0), rotation: float = 0, mirrored: Tuple[bool] = (False, False), - layer: int = 0, + layer: layer_t = 0, dose: float = 1.0, locked: bool = False, ) -> 'Path': @@ -182,7 +182,7 @@ class Path(Shape): offset: vector2 = (0.0, 0.0), rotation: float = 0, mirrored: Tuple[bool] = (False, False), - layer: int = 0, + layer: layer_t = 0, dose: float = 1.0, ) -> 'Path': """ diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index daef7fc..ab588aa 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -5,7 +5,7 @@ from numpy import pi from . import Shape, normalized_shape_tuple from .. import PatternError -from ..utils import is_scalar, rotation_matrix_2d, vector2 +from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t from ..utils import remove_colinear_vertices, remove_duplicate_vertices __author__ = 'Jan Petykiewicz' @@ -74,7 +74,7 @@ class Polygon(Shape): offset: vector2 = (0.0, 0.0), rotation: float = 0.0, mirrored: Tuple[bool] = (False, False), - layer: int = 0, + layer: layer_t = 0, dose: float = 1.0, locked: bool = False, ): @@ -100,7 +100,7 @@ class Polygon(Shape): def square(side_length: float, rotation: float = 0.0, offset: vector2 = (0.0, 0.0), - layer: int = 0, + layer: layer_t = 0, dose: float = 1.0, ) -> 'Polygon': """ @@ -130,7 +130,7 @@ class Polygon(Shape): ly: float, rotation: float = 0, offset: vector2 = (0.0, 0.0), - layer: int = 0, + layer: layer_t = 0, dose: float = 1.0, ) -> 'Polygon': """ @@ -164,7 +164,7 @@ class Polygon(Shape): yctr: float = None, ymax: float = None, ly: float = None, - layer: int = 0, + layer: layer_t = 0, dose: float = 1.0, ) -> 'Polygon': """ diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index 8c5b2cb..3f40841 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -4,7 +4,7 @@ import copy import numpy from ..error import PatternError, PatternLockedError -from ..utils import is_scalar, rotation_matrix_2d, vector2 +from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t __author__ = 'Jan Petykiewicz' @@ -29,7 +29,7 @@ class Shape(metaclass=ABCMeta): _offset: numpy.ndarray """ `[x_offset, y_offset]` """ - _layer: int or Tuple + _layer: layer_t """ Layer (integer >= 0 or tuple) """ _dose: float @@ -162,14 +162,14 @@ class Shape(metaclass=ABCMeta): # layer property @property - def layer(self) -> int or Tuple[int]: + def layer(self) -> layer_t: """ Layer number (int or tuple of ints) """ return self._layer @layer.setter - def layer(self, val: int or List[int]): + def layer(self, val: layer_t): self._layer = val # dose property diff --git a/masque/shapes/text.py b/masque/shapes/text.py index 53a9551..8741e3d 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -5,7 +5,7 @@ from numpy import pi, inf from . import Shape, Polygon, normalized_shape_tuple from .. import PatternError -from ..utils import is_scalar, vector2, get_bit, normalize_mirror +from ..utils import is_scalar, vector2, get_bit, normalize_mirror, layer_t # Loaded on use: # from freetype import Face @@ -76,7 +76,7 @@ class Text(Shape): offset: vector2 = (0.0, 0.0), rotation: float = 0.0, mirrored: Tuple[bool] = (False, False), - layer: int = 0, + layer: layer_t = 0, dose: float = 1.0, locked: bool = False, ): diff --git a/masque/utils.py b/masque/utils.py index 91ba8b2..bdb06a2 100644 --- a/masque/utils.py +++ b/masque/utils.py @@ -8,6 +8,7 @@ import numpy # Type definitions vector2 = Union[numpy.ndarray, Tuple[float, float]] +layer_t = Union[int, Tuple[int, int]] def is_scalar(var: Any) -> bool: From 334633662e96c553b4e084557755db4b9c04fd1d Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 11 May 2020 18:41:13 -0700 Subject: [PATCH 081/109] Remove extra __author__ declarations --- masque/file/gdsii.py | 2 -- masque/file/svg.py | 3 --- masque/file/utils.py | 3 --- masque/label.py | 3 --- masque/pattern.py | 2 -- masque/repetition.py | 2 -- masque/shapes/arc.py | 3 --- masque/shapes/circle.py | 3 --- masque/shapes/ellipse.py | 3 --- masque/shapes/path.py | 1 - masque/shapes/polygon.py | 2 -- masque/shapes/shape.py | 2 -- masque/shapes/text.py | 3 --- masque/subpattern.py | 2 -- 14 files changed, 34 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 7606b1f..4f3b46e 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -26,8 +26,6 @@ from ..utils import remove_colinear_vertices, normalize_mirror #TODO document how GDS rotation / mirror works #TODO absolute positioning -__author__ = 'Jan Petykiewicz' - logger = logging.getLogger(__name__) diff --git a/masque/file/svg.py b/masque/file/svg.py index 3b8276a..f63af3b 100644 --- a/masque/file/svg.py +++ b/masque/file/svg.py @@ -9,9 +9,6 @@ from .utils import mangle_name from .. import Pattern -__author__ = 'Jan Petykiewicz' - - def writefile(pattern: Pattern, filename: str, custom_attributes: bool=False): diff --git a/masque/file/utils.py b/masque/file/utils.py index f8b2841..5b1e658 100644 --- a/masque/file/utils.py +++ b/masque/file/utils.py @@ -7,9 +7,6 @@ from typing import Set, Tuple, List 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. diff --git a/masque/label.py b/masque/label.py index a5f8c5e..fe2c8cb 100644 --- a/masque/label.py +++ b/masque/label.py @@ -4,9 +4,6 @@ import numpy from numpy import pi from .error import PatternError, PatternLockedError - - -__author__ = 'Jan Petykiewicz' from .utils import is_scalar, vector2, rotation_matrix_2d, layer_t diff --git a/masque/pattern.py b/masque/pattern.py index bf8f06f..e16af5b 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -19,8 +19,6 @@ from .label import Label from .utils import rotation_matrix_2d, vector2, normalize_mirror from .error import PatternError, PatternLockedError -__author__ = 'Jan Petykiewicz' - visitor_function_t = Callable[['Pattern', Tuple['Pattern'], Dict, numpy.ndarray], 'Pattern'] diff --git a/masque/repetition.py b/masque/repetition.py index 924a756..11833c5 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -13,8 +13,6 @@ from .error import PatternError, PatternLockedError 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 diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index f8b049c..c6cc24a 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -6,9 +6,6 @@ from numpy import pi from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS from .. import PatternError - - -__author__ = 'Jan Petykiewicz' from ..utils import is_scalar, vector2, layer_t diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index a306796..96c46e4 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -5,9 +5,6 @@ from numpy import pi from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS from .. import PatternError - - -__author__ = 'Jan Petykiewicz' from ..utils import is_scalar, vector2, layer_t diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index af936d2..48cd9af 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -6,9 +6,6 @@ from numpy import pi from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_POINTS from .. import PatternError - - -__author__ = 'Jan Petykiewicz' from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t diff --git a/masque/shapes/path.py b/masque/shapes/path.py index 7d0ed17..bd24bdf 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -9,7 +9,6 @@ from .. import PatternError from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t from ..utils import remove_colinear_vertices, remove_duplicate_vertices -__author__ = 'Jan Petykiewicz' class Path(Shape): diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index ab588aa..67c9fdd 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -8,8 +8,6 @@ from .. import PatternError from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t from ..utils import remove_colinear_vertices, remove_duplicate_vertices -__author__ = 'Jan Petykiewicz' - class Polygon(Shape): """ diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index 3f40841..3aae0bb 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -7,8 +7,6 @@ from ..error import PatternError, PatternLockedError from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t -__author__ = 'Jan Petykiewicz' - # Type definitions normalized_shape_tuple = Tuple[Tuple, diff --git a/masque/shapes/text.py b/masque/shapes/text.py index 8741e3d..8db7c4a 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -12,9 +12,6 @@ from ..utils import is_scalar, vector2, get_bit, normalize_mirror, layer_t # from matplotlib.path import Path -__author__ = 'Jan Petykiewicz' - - class Text(Shape): """ Text (to be printed e.g. as a set of polygons). diff --git a/masque/subpattern.py b/masque/subpattern.py index 8683bb5..55d1ef1 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -13,8 +13,6 @@ from .error import PatternError, PatternLockedError from .utils import is_scalar, rotation_matrix_2d, vector2 -__author__ = 'Jan Petykiewicz' - class SubPattern: """ From 9fa527ea1184893123c5cf826b491b8cc89f7abb Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 11 May 2020 18:42:31 -0700 Subject: [PATCH 082/109] improve handling of patterns with no bounding box --- masque/file/svg.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/masque/file/svg.py b/masque/file/svg.py index f63af3b..8752ca2 100644 --- a/masque/file/svg.py +++ b/masque/file/svg.py @@ -4,6 +4,7 @@ SVG file format readers and writers import svgwrite import numpy +import warnings from .utils import mangle_name from .. import Pattern @@ -40,7 +41,12 @@ def writefile(pattern: Pattern, # Polygonize pattern pattern.polygonize() - [bounds_min, bounds_max] = pattern.get_bounds() + bounds = pattern.get_bounds() + if bounds is None: + bounds_min, bounds_max = numpy.array([[-1, -1], [1, 1]]) + warnings.warn('Pattern had no bounds (empty?); setting arbitrary viewbox') + else: + bounds_min, bounds_max = bounds viewbox = numpy.hstack((bounds_min - 1, (bounds_max - bounds_min) + 2)) viewbox_string = '{:g} {:g} {:g} {:g}'.format(*viewbox) @@ -100,7 +106,12 @@ def writefile_inverted(pattern: Pattern, filename: str): # Polygonize and flatten pattern pattern.polygonize().flatten() - [bounds_min, bounds_max] = pattern.get_bounds() + bounds = pattern.get_bounds() + if bounds is None: + bounds_min, bounds_max = numpy.array([[-1, -1], [1, 1]]) + warnings.warn('Pattern had no bounds (empty?); setting arbitrary viewbox') + else: + bounds_min, bounds_max = bounds viewbox = numpy.hstack((bounds_min - 1, (bounds_max - bounds_min) + 2)) viewbox_string = '{:g} {:g} {:g} {:g}'.format(*viewbox) From fa8fc52dd67c012bb2ac45f616421fac73576f5b Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 11 May 2020 18:49:30 -0700 Subject: [PATCH 083/109] Improve consistency of ndarray members --- masque/label.py | 4 ++-- masque/repetition.py | 6 +++--- masque/shapes/path.py | 2 +- masque/shapes/polygon.py | 2 +- masque/shapes/text.py | 2 +- masque/subpattern.py | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/masque/label.py b/masque/label.py index fe2c8cb..390ca84 100644 --- a/masque/label.py +++ b/masque/label.py @@ -49,7 +49,7 @@ class Label: if val.size != 2: raise PatternError('Offset must be convertible to size-2 ndarray') - self._offset = val.flatten() + self._offset = val.flatten().astype(float) # layer property @property @@ -83,7 +83,7 @@ class Label: self.unlock() self.identifier = () self.string = string - self.offset = numpy.array(offset, dtype=float) + self.offset = numpy.array(offset, dtype=float, copy=True) self.layer = layer self.locked = locked diff --git a/masque/repetition.py b/masque/repetition.py index 11833c5..47c4370 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -225,7 +225,7 @@ class GridRepetition: def mirrored(self, val: List[bool]): if is_scalar(val): raise PatternError('Mirrored must be a 2-element list of booleans') - self._mirrored = numpy.array(val, dtype=bool) + self._mirrored = numpy.array(val, dtype=bool, copy=True) # a_vector property @property @@ -239,7 +239,7 @@ class GridRepetition: if val.size != 2: raise PatternError('a_vector must be convertible to size-2 ndarray') - self._a_vector = val.flatten() + self._a_vector = val.flatten().astype(float) # b_vector property @property @@ -249,7 +249,7 @@ class GridRepetition: @b_vector.setter def b_vector(self, val: vector2): if not isinstance(val, numpy.ndarray): - val = numpy.array(val, dtype=float) + val = numpy.array(val, dtype=float, copy=True) if val.size != 2: raise PatternError('b_vector must be convertible to size-2 ndarray') diff --git a/masque/shapes/path.py b/masque/shapes/path.py index bd24bdf..85aafc3 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -98,7 +98,7 @@ class Path(Shape): @vertices.setter def vertices(self, val: numpy.ndarray): - val = numpy.array(val, dtype=float) + val = numpy.array(val, dtype=float) #TODO document that these might not be copied if len(val.shape) < 2 or val.shape[1] != 2: raise PatternError('Vertices must be an Nx2 array') if val.shape[0] < 2: diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 67c9fdd..4bd8384 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -30,7 +30,7 @@ class Polygon(Shape): @vertices.setter def vertices(self, val: numpy.ndarray): - val = numpy.array(val, dtype=float) + val = numpy.array(val, dtype=float) #TODO document that these might not be copied if len(val.shape) < 2 or val.shape[1] != 2: raise PatternError('Vertices must be an Nx2 array') if val.shape[0] < 3: diff --git a/masque/shapes/text.py b/masque/shapes/text.py index 8db7c4a..684c637 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -64,7 +64,7 @@ class Text(Shape): def mirrored(self, val: List[bool]): if is_scalar(val): raise PatternError('Mirrored must be a 2-element list of booleans') - self._mirrored = list(val) + self._mirrored = numpy.ndarray(val, dtype=bool, copy=True) def __init__(self, string: str, diff --git a/masque/subpattern.py b/masque/subpattern.py index 55d1ef1..1bb926c 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -153,7 +153,7 @@ class SubPattern: def mirrored(self, val: List[bool]): if is_scalar(val): raise PatternError('Mirrored must be a 2-element list of booleans') - self._mirrored = numpy.array(val, dtype=bool) + self._mirrored = numpy.array(val, dtype=bool, copy=True) def as_pattern(self) -> 'Pattern': """ From f8c49cdb5e4bc4472cfa78d26ef1f77172ae0376 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 11 May 2020 18:52:17 -0700 Subject: [PATCH 084/109] Add setter/getter for .pattern to catch wrong types --- masque/repetition.py | 18 +++++++++++++++--- masque/subpattern.py | 31 +++++++++++++++++++++++-------- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/masque/repetition.py b/masque/repetition.py index 47c4370..5b0c23c 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -3,7 +3,7 @@ instances of a Pattern in the same parent Pattern. """ -from typing import Union, List, Dict, Tuple +from typing import Union, List, Dict, Tuple, Optional, Sequence, TYPE_CHECKING import copy import numpy @@ -21,7 +21,7 @@ class GridRepetition: GridRepetition provides support for efficiently embedding multiple copies of a `Pattern` into another `Pattern` at regularly-spaced offsets. """ - __slots__ = ('pattern', + __slots__ = ('_pattern', '_offset', '_rotation', '_dose', @@ -34,7 +34,7 @@ class GridRepetition: 'identifier', 'locked') - pattern: 'Pattern' + _pattern: Optional['Pattern'] """ The `Pattern` being instanced """ _offset: numpy.ndarray @@ -162,6 +162,18 @@ class GridRepetition: new.locked = self.locked return new + # pattern property + @property + def pattern(self) -> Optional['Pattern']: + return self._pattern + + @pattern.setter + def pattern(self, val: Optional['Pattern']): + from .pattern import Pattern + if val is not None and not isinstance(val, Pattern): + raise PatternError('Provided pattern {} is not a Pattern object or None!'.format(val)) + self._pattern = val + # offset property @property def offset(self) -> numpy.ndarray: diff --git a/masque/subpattern.py b/masque/subpattern.py index 1bb926c..2577ce9 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, List, Dict, Tuple +from typing import Union, List, Dict, Tuple, Optional, Sequence, TYPE_CHECKING import copy import numpy @@ -19,9 +19,16 @@ class SubPattern: SubPattern provides basic support for nesting Pattern objects within each other, by adding offset, rotation, scaling, and associated methods. """ - __slots__ = ('pattern', '_offset', '_rotation', '_dose', '_scale', '_mirrored', - 'identifier', 'locked') - pattern: 'Pattern' or None + __slots__ = ('_pattern', + '_offset', + '_rotation', + '_dose', + '_scale', + '_mirrored', + 'identifier', + 'locked') + + _pattern: Optional['Pattern'] """ The `Pattern` being instanced """ _offset: numpy.ndarray @@ -55,10 +62,6 @@ class SubPattern: dose: float = 1.0, scale: float = 1.0, locked: bool = False): - if pattern is not None and not hasattr(pattern, 'lock'): - raise PatternError('Provided pattern has no "lock()" method.\n' - 'Maybe it''s not a Pattern instance?') - self.unlock() self.identifier = () self.pattern = pattern @@ -93,6 +96,18 @@ class SubPattern: new.locked = self.locked return new + # pattern property + @property + def pattern(self) -> Optional['Pattern']: + return self._pattern + + @pattern.setter + def pattern(self, val: Optional['Pattern']): + from .pattern import Pattern + if val is not None and not isinstance(val, Pattern): + raise PatternError('Provided pattern {} is not a Pattern object or None!'.format(val)) + self._pattern = val + # offset property @property def offset(self) -> numpy.ndarray: From 00394a62f0843625d5bba5b1cca9d70efdc84448 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 11 May 2020 18:58:57 -0700 Subject: [PATCH 085/109] Formally allow instances to point to None (i.e. an 'empty' pattern) --- masque/file/gdsii.py | 10 ++++++++-- masque/file/svg.py | 4 ++++ masque/file/utils.py | 2 ++ masque/pattern.py | 39 +++++++++++++++++++++++++++------------ masque/repetition.py | 7 ++++++- masque/subpattern.py | 9 +++++++-- 6 files changed, 54 insertions(+), 17 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 4f3b46e..0040006 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -100,7 +100,9 @@ def write(patterns: Pattern or List[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()) + for i, p in pattern.referenced_patterns_by_id().items(): + if p is not None: + patterns_by_id[i] = p disambiguate_func(patterns_by_id.values()) @@ -170,7 +172,9 @@ def dose2dtype(patterns: List[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()) + for i, p in pattern.referenced_patterns_by_id().items(): + if p is not None: + patterns_by_id[i] = p # Get a table of (id(pat), written_dose) for each pattern and subpattern sd_table = make_dose_table(patterns) @@ -466,6 +470,8 @@ def _subpatterns_to_refs(subpatterns: List[SubPattern or GridRepetition] ) -> List[gdsii.elements.ARef or gdsii.elements.SRef]: refs = [] for subpat in subpatterns: + if subpat.pattern is None: + continue encoded_name = subpat.pattern.name # Note: GDS mirrors first and rotates second diff --git a/masque/file/svg.py b/masque/file/svg.py index 8752ca2..2251f54 100644 --- a/masque/file/svg.py +++ b/masque/file/svg.py @@ -61,6 +61,8 @@ def writefile(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(): + if pat is None: + continue svg_group = svg.g(id=mangle_name(pat), fill='blue', stroke='red') for shape in pat.shapes: @@ -75,6 +77,8 @@ def writefile(pattern: Pattern, svg_group.add(path) for subpat in pat.subpatterns: + if subpat.pattern is None: + continue 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) diff --git a/masque/file/utils.py b/masque/file/utils.py index 5b1e658..9092ab9 100644 --- a/masque/file/utils.py +++ b/masque/file/utils.py @@ -38,6 +38,8 @@ def make_dose_table(patterns: List[Pattern], dose_multiplier: float=1.0) -> Set[ dose_table = {(id(pattern), dose_multiplier) for pattern in patterns} for pattern in patterns: for subpat in pattern.subpatterns: + if subpat.pattern is None: + continue 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) diff --git a/masque/pattern.py b/masque/pattern.py index e16af5b..1849946 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -151,7 +151,9 @@ class Pattern: A Pattern containing all the shapes and subpatterns for which the parameter functions return True """ - def do_subset(src): + def do_subset(src: Optional['Pattern']) -> Optional['Pattern']: + if src is None: + return None pat = Pattern(name=src.name) if shapes_func is not None: pat.shapes = [s for s in src.shapes if shapes_func(s)] @@ -165,6 +167,8 @@ class Pattern: pat = self.apply(do_subset) else: pat = do_subset(self) + + assert(pat is not None) return pat def apply(self, @@ -197,8 +201,12 @@ class Pattern: 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) + if pat is not None: + for subpat in pat.subpatterns: + if subpat.pattern is None: + subpat.pattern = func(None) + else: + subpat.pattern = subpat.pattern.apply(func, memo) memo[pat_id] = pat elif memo[pat_id] is None: raise PatternError('.apply() called on pattern with circular reference') @@ -277,11 +285,12 @@ class Pattern: else: sp_transform = False - subpattern.pattern = subpattern.pattern.dfs(visit_before=visit_before, - visit_after=visit_after, - transform=sp_transform, - memo=memo, - hierarchy=hierarchy + (self,)) + if subpattern.pattern is not None: + subpattern.pattern = subpattern.pattern.dfs(visit_before=visit_before, + visit_after=visit_after, + transform=sp_transform, + memo=memo, + hierarchy=hierarchy + (self,)) if visit_after is not None: pat = visit_after(pat, hierarchy=hierarchy, memo=memo, transform=transform) @@ -311,7 +320,8 @@ class Pattern: (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) + if subpat.pattern is not None: + subpat.pattern.polygonize(poly_num_points, poly_max_arclen) return self def manhattanize(self, @@ -368,6 +378,8 @@ class Pattern: if recursive: for subpat in self.subpatterns: + if subpat.pattern is None: + continue subpat.pattern.subpatternize(recursive=True, norm_value=norm_value, exclude_types=exclude_types) @@ -431,7 +443,8 @@ class Pattern: 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()) + if subpat.pattern is not None: + ids.update(subpat.pattern.referenced_patterns_by_id()) return ids def referenced_patterns_by_name(self) -> List[Tuple[str, 'Pattern']]: @@ -446,7 +459,7 @@ class Pattern: List of `(pat.name, pat)` tuples for all referenced Pattern objects """ pats_by_id = self.referenced_patterns_by_id() - pat_list = [(p.name, p) for p in pats_by_id.values()] + pat_list = [(p.name if p is not None else None, p) for p in pats_by_id.values()] return pat_list def get_bounds(self) -> Union[numpy.ndarray, None]: @@ -496,6 +509,8 @@ class Pattern: self.subpatterns = [] shape_counts = {} for subpat in subpatterns: + if subpat.pattern is None: + continue subpat.pattern.flatten() p = subpat.as_pattern() @@ -839,7 +854,7 @@ class Pattern: if pat in memo: return memo - children = set(sp.pattern for sp in pat.subpatterns) + children = set(sp.pattern for sp in pat.subpatterns if sp.pattern is not None) new_children = children - memo memo |= children diff --git a/masque/repetition.py b/masque/repetition.py index 5b0c23c..79cc38d 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -298,6 +298,7 @@ class GridRepetition: A copy of self.pattern which has been scaled, rotated, repeated, etc. etc. according to this `GridRepetition`'s properties. """ + assert(self.pattern is not None) patterns = [] for a in range(self.a_count): @@ -411,7 +412,7 @@ class GridRepetition: self.rotation *= -1 return self - def get_bounds(self) -> numpy.ndarray or None: + def get_bounds(self) -> Optional[numpy.ndarray]: """ Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the extent of the `GridRepetition` in each dimension. @@ -420,6 +421,8 @@ class GridRepetition: Returns: `[[x_min, y_min], [x_max, y_max]]` or `None` """ + if self.pattern is None: + return None return self.as_pattern().get_bounds() def scale_by(self, c: float) -> 'GridRepetition': @@ -496,6 +499,7 @@ class GridRepetition: Returns: self """ + assert(self.pattern is not None) self.lock() self.pattern.deeplock() return self @@ -510,6 +514,7 @@ class GridRepetition: Returns: self """ + assert(self.pattern is not None) self.unlock() self.pattern.deepunlock() return self diff --git a/masque/subpattern.py b/masque/subpattern.py index 2577ce9..16c3c7c 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -55,7 +55,7 @@ class SubPattern: #TODO more documentation? def __init__(self, - pattern: 'Pattern' or None, + pattern: Optional['Pattern'], offset: vector2 = (0.0, 0.0), rotation: float = 0.0, mirrored: List[bool] = None, @@ -176,6 +176,7 @@ class SubPattern: A copy of self.pattern which has been scaled, rotated, etc. according to this `SubPattern`'s properties. """ + assert(self.pattern is not None) pattern = self.pattern.deepcopy().deepunlock() pattern.scale_by(self.scale) [pattern.mirror(ax) for ax, do in enumerate(self.mirrored) if do] @@ -242,7 +243,7 @@ class SubPattern: self.rotation *= -1 return self - def get_bounds(self) -> numpy.ndarray or None: + def get_bounds(self) -> Optional[numpy.ndarray]: """ Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the extent of the `SubPattern` in each dimension. @@ -251,6 +252,8 @@ class SubPattern: Returns: `[[x_min, y_min], [x_max, y_max]]` or `None` """ + if self.pattern is None: + return None return self.as_pattern().get_bounds() def scale_by(self, c: float) -> 'SubPattern': @@ -311,6 +314,7 @@ class SubPattern: Returns: self """ + assert(self.pattern is not None) self.lock() self.pattern.deeplock() return self @@ -325,6 +329,7 @@ class SubPattern: Returns: self """ + assert(self.pattern is not None) self.unlock() self.pattern.deepunlock() return self From 8493364e9c026fd9b18cf80eaa1cdf1f01eec058 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 11 May 2020 18:59:47 -0700 Subject: [PATCH 086/109] Use a tuple for .identifier --- masque/file/gdsii.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 0040006..a835c05 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -367,7 +367,7 @@ def read(stream: io.BufferedIOBase, 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.identifier.decode('ASCII')] + sp.pattern = patterns_dict[sp.identifier[0].decode('ASCII')] del sp.identifier return patterns_dict, library_info @@ -390,7 +390,7 @@ def _mlayer2gds(mlayer: layer_t) -> Tuple[int, int]: 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 .identifier to the struct_name. + and sets the instance .identifier to (struct_name,). BUG: "Absolute" means not affected by parent elements. @@ -398,7 +398,7 @@ def _sref_to_subpat(element: gdsii.elements.SRef) -> SubPattern: undo the parent transformations, or implement it in masque. """ subpat = SubPattern(pattern=None, offset=element.xy) - subpat.identifier = element.struct_name + subpat.identifier = (element.struct_name,) if element.strans is not None: if element.mag is not None: subpat.scale = element.mag @@ -421,7 +421,7 @@ def _sref_to_subpat(element: gdsii.elements.SRef) -> SubPattern: 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 .identifier to the struct_name. + and sets the instance .identifier to (struct_name,). BUG: "Absolute" means not affected by parent elements. @@ -461,7 +461,7 @@ def _aref_to_gridrep(element: gdsii.elements.ARef) -> GridRepetition: rotation=rotation, scale=scale, mirrored=(mirror_across_x, False)) - gridrep.identifier = element.struct_name + gridrep.identifier = (element.struct_name,) return gridrep From f8e347c9978909caddcaea173cf4f20416675f76 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 11 May 2020 19:00:25 -0700 Subject: [PATCH 087/109] fix scale -> scale_by --- masque/pattern.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/pattern.py b/masque/pattern.py index 1849946..2c6fd96 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -549,7 +549,7 @@ class Pattern: self """ for entry in self.shapes + self.subpatterns: - entry.scale(c) + entry.scale_by(c) return self def scale_by(self, c: float) -> 'Pattern': From d33afc2bfd86a27420a80be49ec1d0a6468ca117 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 11 May 2020 19:01:02 -0700 Subject: [PATCH 088/109] disambiguate some variable names --- masque/file/gdsii.py | 12 ++++++------ masque/pattern.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index a835c05..b28333b 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -304,15 +304,15 @@ def read(stream: io.BufferedIOBase, else: args['layer'] = (element.layer, element.data_type) - shape = Polygon(**args) + poly = Polygon(**args) if clean_vertices: try: - shape.clean_vertices() + poly.clean_vertices() except PatternError: continue - pat.shapes.append(shape) + pat.shapes.append(poly) if isinstance(element, gdsii.elements.Path): if element.path_type in path_cap_map: @@ -338,15 +338,15 @@ def read(stream: io.BufferedIOBase, else: args['layer'] = (element.layer, element.data_type) - shape = Path(**args) + path = Path(**args) if clean_vertices: try: - shape.clean_vertices() + path.clean_vertices() except PatternError as err: continue - pat.shapes.append(shape) + pat.shapes.append(path) elif isinstance(element, gdsii.elements.Text): label = Label(offset=element.xy, diff --git a/masque/pattern.py b/masque/pattern.py index 2c6fd96..2f5b769 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -566,8 +566,8 @@ class Pattern: for entry in self.shapes + self.subpatterns: entry.offset *= c entry.scale_by(c) - for entry in self.labels: - entry.offset *= c + for label in self.labels: + label.offset *= c return self def rotate_around(self, pivot: vector2, rotation: float) -> 'Pattern': From 9bfb3bef9f65d521b5ba743fb5781657ddcc3655 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 11 May 2020 19:03:55 -0700 Subject: [PATCH 089/109] avoid unnecessarily nested classes Still provide a class variable for convenience --- masque/shapes/path.py | 46 ++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/masque/shapes/path.py b/masque/shapes/path.py index 85aafc3..2ffdd35 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -10,6 +10,13 @@ from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t from ..utils import remove_colinear_vertices, remove_duplicate_vertices +class PathCap(Enum): + Flush = 0 # Path ends at final vertices + Circle = 1 # Path extends past final vertices with a semicircle of radius width/2 + Square = 2 # Path extends past final vertices with a width-by-width/2 rectangle + SquareCustom = 4 # Path extends past final vertices with a rectangle of length + # defined by path.cap_extensions + class Path(Shape): """ @@ -21,15 +28,10 @@ class Path(Shape): __slots__ = ('_vertices', '_width', '_cap', '_cap_extensions') _vertices: numpy.ndarray _width: float - _cap: 'Path.Cap' _cap_extensions: numpy.ndarray or None + _cap: PathCap - class Cap(Enum): - Flush = 0 # Path ends at final vertices - Circle = 1 # Path extends past final vertices with a semicircle of radius width/2 - Square = 2 # Path extends past final vertices with a width-by-width/2 rectangle - SquareCustom = 4 # Path extends past final vertices with a rectangle of length - # defined by path.cap_extensions + Cap = PathCap # width property @property @@ -49,17 +51,17 @@ class Path(Shape): # cap property @property - def cap(self) -> 'Path.Cap': + def cap(self) -> PathCap: """ Path end-cap """ return self._cap @cap.setter - def cap(self, val: 'Path.Cap'): + def cap(self, val: PathCap): # TODO: Document that setting cap can change cap_extensions - self._cap = Path.Cap(val) - if self.cap != Path.Cap.SquareCustom: + self._cap = PathCap(val) + if self.cap != PathCap.SquareCustom: self.cap_extensions = None elif self.cap_extensions is None: # just got set to SquareCustom @@ -77,8 +79,8 @@ class Path(Shape): return self._cap_extensions @cap_extensions.setter - def cap_extensions(self, vals: numpy.ndarray or None): - custom_caps = (Path.Cap.SquareCustom,) + def cap_extensions(self, vals: Optional[numpy.ndarray]): + custom_caps = (PathCap.SquareCustom,) if self.cap in custom_caps: if vals is None: raise Exception('Tried to set cap extensions to None on path with custom cap type') @@ -138,7 +140,7 @@ class Path(Shape): def __init__(self, vertices: numpy.ndarray, width: float = 0.0, - cap: 'Path.Cap' = Cap.Flush, + cap: PathCap = PathCap.Flush, cap_extensions: numpy.ndarray = None, offset: vector2 = (0.0, 0.0), rotation: float = 0, @@ -176,7 +178,7 @@ class Path(Shape): @staticmethod def travel(travel_pairs: Tuple[Tuple[float, float]], width: float = 0.0, - cap: 'Path.Cap' = Cap.Flush, + cap: PathCap = PathCap.Flush, cap_extensions = None, offset: vector2 = (0.0, 0.0), rotation: float = 0, @@ -285,7 +287,7 @@ class Path(Shape): polys = [Polygon(offset=self.offset, vertices=verts, dose=self.dose, layer=self.layer)] - if self.cap == Path.Cap.Circle: + if self.cap == PathCap.Circle: #for vert in v: # not sure if every vertex, or just ends? for vert in [v[0], v[-1]]: circ = Circle(offset=vert, radius=self.width / 2, dose=self.dose, layer=self.layer) @@ -294,12 +296,12 @@ class Path(Shape): return polys def get_bounds(self) -> numpy.ndarray: - if self.cap == Path.Cap.Circle: + if self.cap == PathCap.Circle: bounds = self.offset + numpy.vstack((numpy.min(self.vertices, axis=0) - self.width / 2, numpy.max(self.vertices, axis=0) + self.width / 2)) - elif self.cap in (Path.Cap.Flush, - Path.Cap.Square, - Path.Cap.SquareCustom): + elif self.cap in (PathCap.Flush, + PathCap.Square, + PathCap.SquareCustom): bounds = numpy.array([[+inf, +inf], [-inf, -inf]]) polys = self.to_polygons() for poly in polys: @@ -383,9 +385,9 @@ class Path(Shape): return self def _calculate_cap_extensions(self) -> numpy.ndarray: - if self.cap == Path.Cap.Square: + if self.cap == PathCap.Square: extensions = numpy.full(2, self.width / 2) - elif self.cap == Path.Cap.SquareCustom: + elif self.cap == PathCap.SquareCustom: extensions = self.cap_extensions else: # Flush or Circle From bd4085365f48f6bd2e3f644eb61613d8240a0a49 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 11 May 2020 19:04:25 -0700 Subject: [PATCH 090/109] Path should create a Path, not a Polygon --- masque/shapes/path.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/masque/shapes/path.py b/masque/shapes/path.py index 2ffdd35..e320f4b 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -351,8 +351,8 @@ class Path(Shape): return (type(self), reordered_vertices.data.tobytes(), width0, self.cap, self.layer), \ (offset, scale/norm_value, rotation, False, self.dose), \ - lambda: Polygon(reordered_vertices*norm_value, width=self.width*norm_value, - cap=self.cap, layer=self.layer) + lambda: Path(reordered_vertices*norm_value, width=self.width*norm_value, + cap=self.cap, layer=self.layer) def clean_vertices(self) -> 'Path': """ From 157df47884c64ca74bfe535e1556f7ad6ca91097 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 11 May 2020 19:09:35 -0700 Subject: [PATCH 091/109] Improve type annotations based on mypy errors --- masque/file/gdsii.py | 29 ++++++++++++------------ masque/file/svg.py | 4 ++-- masque/pattern.py | 49 ++++++++++++++++++++-------------------- masque/repetition.py | 18 ++++++++------- masque/shapes/arc.py | 18 +++++++-------- masque/shapes/circle.py | 14 ++++++------ masque/shapes/ellipse.py | 16 ++++++------- masque/shapes/path.py | 12 +++++----- masque/shapes/polygon.py | 42 +++++++++++++++++++++++++--------- masque/shapes/shape.py | 30 +++++++++++++++--------- masque/shapes/text.py | 18 +++++++-------- masque/subpattern.py | 12 ++++++---- masque/utils.py | 6 ++--- 13 files changed, 151 insertions(+), 117 deletions(-) diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index b28333b..c0dcfc6 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -6,7 +6,7 @@ import gdsii.library import gdsii.structure import gdsii.elements -from typing import List, Any, Dict, Tuple, Callable +from typing import List, Any, Dict, Tuple, Callable, Union, Sequence, Iterable, Optional import re import io import copy @@ -39,13 +39,13 @@ path_cap_map = { } -def write(patterns: Pattern or List[Pattern], +def write(patterns: Union[Pattern, List[Pattern]], stream: io.BufferedIOBase, meters_per_unit: float, logical_units_per_unit: float = 1, library_name: str = 'masque-gdsii-write', modify_originals: bool = False, - disambiguate_func: Callable[[List[Pattern]], None] = None): + disambiguate_func: Callable[[Iterable[Pattern]], None] = None): """ 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 @@ -119,8 +119,8 @@ def write(patterns: Pattern or List[Pattern], return -def writefile(patterns: List[Pattern] or Pattern, - filename: str or pathlib.Path, +def writefile(patterns: Union[List[Pattern], Pattern], + filename: Union[str, pathlib.Path], *args, **kwargs, ): @@ -137,7 +137,7 @@ def writefile(patterns: List[Pattern] or Pattern, """ path = pathlib.Path(filename) if path.suffix == '.gz': - open_func = gzip.open + open_func: Callable = gzip.open else: open_func = open @@ -185,7 +185,8 @@ def dose2dtype(patterns: List[Pattern], 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] + for shape in pat.shapes: + dose_vals.add(shape.dose * pat_dose) if len(dose_vals) > 256: raise PatternError('Too many dose values: {}, maximum 256 when using dtypes.'.format(len(dose_vals))) @@ -228,10 +229,10 @@ def dose2dtype(patterns: List[Pattern], return patterns, dose_vals_list -def readfile(filename: str or pathlib.Path, +def readfile(filename: Union[str, pathlib.Path], *args, **kwargs, - ) -> (Dict[str, Pattern], Dict[str, Any]): + ) -> Tuple[Dict[str, Pattern], Dict[str, Any]]: """ Wrapper for `gdsii.read()` that takes a filename or path instead of a stream. @@ -244,7 +245,7 @@ def readfile(filename: str or pathlib.Path, """ path = pathlib.Path(filename) if path.suffix == '.gz': - open_func = gzip.open + open_func: Callable = gzip.open else: open_func = open @@ -256,7 +257,7 @@ def readfile(filename: str or pathlib.Path, def read(stream: io.BufferedIOBase, use_dtype_as_dose: bool = False, clean_vertices: bool = True, - ) -> (Dict[str, Pattern], Dict[str, Any]): + ) -> Tuple[Dict[str, Pattern], Dict[str, Any]]: """ 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 @@ -466,8 +467,8 @@ def _aref_to_gridrep(element: gdsii.elements.ARef) -> GridRepetition: return gridrep -def _subpatterns_to_refs(subpatterns: List[SubPattern or GridRepetition] - ) -> List[gdsii.elements.ARef or gdsii.elements.SRef]: +def _subpatterns_to_refs(subpatterns: List[Union[SubPattern, GridRepetition]] + ) -> List[Union[gdsii.elements.ARef, gdsii.elements.SRef]]: refs = [] for subpat in subpatterns: if subpat.pattern is None: @@ -574,7 +575,7 @@ def disambiguate_pattern_names(patterns, # Should never happen since zero-length names are replaced raise PatternError('Zero-length name after sanitize+encode,\n originally "{}"'.format(pat.name)) if len(encoded_name) > max_name_length: - raise PatternError('Pattern name "{}" length > {} after encode,\n originally "{}"'.format(encoded_name, max_name_length, pat.name)) + raise PatternError('Pattern name "{!r}" length > {} after encode,\n originally "{}"'.format(encoded_name, max_name_length, pat.name)) pat.name = encoded_name used_names.append(suffixed_name) diff --git a/masque/file/svg.py b/masque/file/svg.py index 2251f54..deef59a 100644 --- a/masque/file/svg.py +++ b/masque/file/svg.py @@ -1,7 +1,7 @@ """ SVG file format readers and writers """ - +from typing import Dict, Optional import svgwrite import numpy import warnings @@ -56,7 +56,7 @@ def writefile(pattern: Pattern, debug=(not custom_attributes)) # Get a dict of id(pattern) -> pattern - patterns_by_id = {**(pattern.referenced_patterns_by_id()), id(pattern): pattern} + patterns_by_id = {**(pattern.referenced_patterns_by_id()), id(pattern): pattern} # type: Dict[int, Optional[Pattern]] # Now create a group for each row in sd_table (ie, each pattern + dose combination) # and add in any Boundary and Use elements diff --git a/masque/pattern.py b/masque/pattern.py index 2f5b769..cefcdbd 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -2,7 +2,8 @@ Base object for containing a lithography mask. """ -from typing import List, Callable, Tuple, Dict, Union, Set +from typing import List, Callable, Tuple, Dict, Union, Set, Sequence, Optional, Type +from typing import MutableMapping, Iterable import copy import itertools import pickle @@ -39,7 +40,7 @@ class Pattern: labels: List[Label] """ List of all labels in this Pattern. """ - subpatterns: List[SubPattern or GridRepetition] + subpatterns: List[Union[SubPattern, GridRepetition]] """ List of all objects referencing other patterns in this Pattern. Examples are SubPattern (gdsii "instances") or GridRepetition (gdsii "arrays") Multiple objects in this list may reference the same Pattern object @@ -54,9 +55,9 @@ class Pattern: def __init__(self, name: str = '', - shapes: List[Shape] = (), - labels: List[Label] = (), - subpatterns: List[SubPattern] = (), + shapes: Sequence[Shape] = (), + labels: Sequence[Label] = (), + subpatterns: Sequence[Union[SubPattern, GridRepetition]] = (), locked: bool = False, ): """ @@ -129,7 +130,7 @@ class Pattern: def subset(self, shapes_func: Callable[[Shape], bool] = None, labels_func: Callable[[Label], bool] = None, - subpatterns_func: Callable[[SubPattern], bool] = None, + subpatterns_func: Callable[[Union[SubPattern, GridRepetition]], bool] = None, recursive: bool = False, ) -> 'Pattern': """ @@ -172,9 +173,9 @@ class Pattern: return pat def apply(self, - func: Callable[['Pattern'], 'Pattern'], - memo: Dict[int, 'Pattern'] = None, - ) -> 'Pattern': + func: Callable[[Optional['Pattern']], Optional['Pattern']], + memo: Optional[Dict[int, Optional['Pattern']]] = None, + ) -> Optional['Pattern']: """ Recursively apply func() to this pattern and any pattern it references. func() is expected to take and return a Pattern. @@ -217,9 +218,9 @@ class Pattern: def dfs(self, visit_before: visitor_function_t = None, visit_after: visitor_function_t = None, - transform: numpy.ndarray or bool or None = False , - memo: Dict = None, - hierarchy: Tuple['Pattern'] = (), + transform: Union[numpy.ndarray, bool, None] = False, + memo: Optional[Dict] = None, + hierarchy: Tuple['Pattern', ...] = (), ) -> 'Pattern': """ Experimental convenience function. @@ -270,7 +271,7 @@ class Pattern: pat = self if visit_before is not None: - pat = visit_before(pat, hierarchy=hierarchy, memo=memo, transform=transform) + pat = visit_before(pat, hierarchy=hierarchy, memo=memo, transform=transform) # type: ignore for subpattern in self.subpatterns: if transform is not False: @@ -293,12 +294,12 @@ class Pattern: hierarchy=hierarchy + (self,)) if visit_after is not None: - pat = visit_after(pat, hierarchy=hierarchy, memo=memo, transform=transform) + pat = visit_after(pat, hierarchy=hierarchy, memo=memo, transform=transform) # type: ignore return pat def polygonize(self, - poly_num_points: int = None, - poly_max_arclen: float = None, + poly_num_points: Optional[int] = None, + poly_max_arclen: Optional[float] = None, ) -> 'Pattern': """ Calls `.to_polygons(...)` on all the shapes in this Pattern and any referenced patterns, @@ -349,7 +350,7 @@ class Pattern: def subpatternize(self, recursive: bool = True, norm_value: int = int(1e6), - exclude_types: Tuple[Shape] = (Polygon,) + exclude_types: Tuple[Type] = (Polygon,) ) -> 'Pattern': """ Iterates through this `Pattern` and all referenced `Pattern`s. Within each `Pattern`, it iterates @@ -387,7 +388,7 @@ class Pattern: # 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()]) + shape_table: MutableMapping[Tuple, List] = defaultdict(lambda: [None, list()]) for i, shape in enumerate(self.shapes): if not any((isinstance(shape, t) for t in exclude_types)): label, values, func = shape.normalized_form(norm_value) @@ -429,9 +430,9 @@ class Pattern: is of the form `[[x0, y0], [x1, y1],...]`. """ pat = self.deepcopy().deepunlock().polygonize().flatten() - return [shape.vertices + shape.offset for shape in pat.shapes] + return [shape.vertices + shape.offset for shape in pat.shapes] # type: ignore # mypy can't figure out that shapes are all Polygons now - def referenced_patterns_by_id(self) -> Dict[int, 'Pattern']: + def referenced_patterns_by_id(self) -> Dict[int, Optional['Pattern']]: """ Create a dictionary with `{id(pat): pat}` for all Pattern objects referenced by this Pattern (operates recursively on all referenced Patterns as well) @@ -447,7 +448,7 @@ class Pattern: ids.update(subpat.pattern.referenced_patterns_by_id()) return ids - def referenced_patterns_by_name(self) -> List[Tuple[str, 'Pattern']]: + def referenced_patterns_by_name(self) -> List[Tuple[Optional[str], Optional['Pattern']]]: """ Create a list of `(pat.name, pat)` tuples for all Pattern objects referenced by this Pattern (operates recursively on all referenced Patterns as well). @@ -507,7 +508,7 @@ class Pattern: """ subpatterns = copy.deepcopy(self.subpatterns) self.subpatterns = [] - shape_counts = {} + shape_counts: Dict[Tuple, int] = {} for subpat in subpatterns: if subpat.pattern is None: continue @@ -839,7 +840,7 @@ class Pattern: pyplot.show() @staticmethod - def find_toplevel(patterns: List['Pattern']) -> List['Pattern']: + def find_toplevel(patterns: Iterable['Pattern']) -> List['Pattern']: """ Given a list of Pattern objects, return those that are not referenced by any other pattern. @@ -863,7 +864,7 @@ class Pattern: return memo patterns = set(patterns) - not_toplevel = set() + not_toplevel: Set['Pattern'] = set() for pattern in patterns: not_toplevel |= get_children(pattern, not_toplevel) diff --git a/masque/repetition.py b/masque/repetition.py index 79cc38d..37bd350 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -12,6 +12,8 @@ from numpy import pi from .error import PatternError, PatternLockedError from .utils import is_scalar, rotation_matrix_2d, vector2 +if TYPE_CHECKING: + from . import Pattern # TODO need top-level comment about what order rotation/scale/offset/mirror/array are applied @@ -51,7 +53,7 @@ class GridRepetition: _scale: float """ Scaling factor applied to individual instances in the grid (not the grid vectors) """ - _mirrored: List[bool] + _mirrored: numpy.ndarray # ndarray[bool] """ Whether to mirror individual instances across the x and y axes (Applies to individual instances in the grid, not the grid vectors) """ @@ -64,7 +66,7 @@ class GridRepetition: _a_count: int """ Number of instances along the direction specified by the `a_vector` """ - _b_vector: numpy.ndarray or None + _b_vector: Optional[numpy.ndarray] """ Vector `[x, y]` specifying a second lattice vector for the grid. Specifies center-to-center spacing between adjacent elements. Can be `None` for a 1D array. @@ -80,14 +82,14 @@ class GridRepetition: """ If `True`, disallows changes to the GridRepetition """ def __init__(self, - pattern: 'Pattern', + pattern: Optional['Pattern'], a_vector: numpy.ndarray, a_count: int, - b_vector: numpy.ndarray = None, + b_vector: Optional[numpy.ndarray] = None, b_count: int = 1, offset: vector2 = (0.0, 0.0), rotation: float = 0.0, - mirrored: List[bool] = None, + mirrored: Optional[Sequence[bool]] = None, dose: float = 1.0, scale: float = 1.0, locked: bool = False): @@ -155,7 +157,7 @@ class GridRepetition: locked=self.locked) return new - def __deepcopy__(self, memo: Dict = None) -> 'GridReptition': + def __deepcopy__(self, memo: Dict = None) -> 'GridRepetition': memo = {} if memo is None else memo new = copy.copy(self).unlock() new.pattern = copy.deepcopy(self.pattern, memo) @@ -230,11 +232,11 @@ class GridRepetition: # Mirrored property @property - def mirrored(self) -> List[bool]: + def mirrored(self) -> numpy.ndarray: # ndarray[bool] return self._mirrored @mirrored.setter - def mirrored(self, val: List[bool]): + def mirrored(self, val: Sequence[bool]): if is_scalar(val): raise PatternError('Mirrored must be a 2-element list of booleans') self._mirrored = numpy.array(val, dtype=bool, copy=True) diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index c6cc24a..f082c3b 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -1,4 +1,4 @@ -from typing import List, Tuple, Dict +from typing import List, Tuple, Dict, Optional, Sequence import copy import math import numpy @@ -32,10 +32,10 @@ class Arc(Shape): _width: float """ Width of the arc """ - poly_num_points: int + poly_num_points: Optional[int] """ Sets the default number of points for `.polygonize()` """ - poly_max_arclen: float + poly_max_arclen: Optional[float] """ Sets the default max segement length for `.polygonize()` """ # radius properties @@ -77,7 +77,7 @@ class Arc(Shape): # arc start/stop angle properties @property - def angles(self) -> vector2: + def angles(self) -> numpy.ndarray: #ndarray[float] """ Return the start and stop angles `[a_start, a_stop]`. Angles are measured from x-axis after rotation @@ -150,11 +150,11 @@ class Arc(Shape): radii: vector2, angles: vector2, width: float, - poly_num_points: int = DEFAULT_POLY_NUM_POINTS, - poly_max_arclen: float = None, + poly_num_points: Optional[int] = DEFAULT_POLY_NUM_POINTS, + poly_max_arclen: Optional[float] = None, offset: vector2 = (0.0, 0.0), rotation: float = 0, - mirrored: Tuple[bool] = (False, False), + mirrored: Sequence[bool] = (False, False), layer: layer_t = 0, dose: float = 1.0, locked: bool = False): @@ -182,8 +182,8 @@ class Arc(Shape): return new def to_polygons(self, - poly_num_points: int = None, - poly_max_arclen: float = None, + poly_num_points: Optional[int] = None, + poly_max_arclen: Optional[float] = None, ) -> List[Polygon]: if poly_num_points is None: poly_num_points = self.poly_num_points diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index 96c46e4..1f6b5c9 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -1,4 +1,4 @@ -from typing import List, Dict +from typing import List, Dict, Optional import copy import numpy from numpy import pi @@ -16,10 +16,10 @@ class Circle(Shape): _radius: float """ Circle radius """ - poly_num_points: int + poly_num_points: Optional[int] """ Sets the default number of points for `.polygonize()` """ - poly_max_arclen: float + poly_max_arclen: Optional[float] """ Sets the default max segement length for `.polygonize()` """ # radius property @@ -40,8 +40,8 @@ class Circle(Shape): def __init__(self, radius: float, - poly_num_points: int = DEFAULT_POLY_NUM_POINTS, - poly_max_arclen: float = None, + poly_num_points: Optional[int] = DEFAULT_POLY_NUM_POINTS, + poly_max_arclen: Optional[float] = None, offset: vector2 = (0.0, 0.0), layer: layer_t = 0, dose: float = 1.0, @@ -64,8 +64,8 @@ class Circle(Shape): return new def to_polygons(self, - poly_num_points: int = None, - poly_max_arclen: float = None, + poly_num_points: Optional[int] = None, + poly_max_arclen: Optional[float] = None, ) -> List[Polygon]: if poly_num_points is None: poly_num_points = self.poly_num_points diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index 48cd9af..fc8a2d4 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -1,4 +1,4 @@ -from typing import List, Tuple, Dict +from typing import List, Tuple, Dict, Sequence, Optional import copy import math import numpy @@ -22,10 +22,10 @@ class Ellipse(Shape): _rotation: float """ Angle from x-axis to first radius (ccw, radians) """ - poly_num_points: int + poly_num_points: Optional[int] """ Sets the default number of points for `.polygonize()` """ - poly_max_arclen: float + poly_max_arclen: Optional[float] """ Sets the default max segement length for `.polygonize()` """ # radius properties @@ -85,11 +85,11 @@ class Ellipse(Shape): def __init__(self, radii: vector2, - poly_num_points: int = DEFAULT_POLY_NUM_POINTS, - poly_max_arclen: float = None, + poly_num_points: Optional[int] = DEFAULT_POLY_NUM_POINTS, + poly_max_arclen: Optional[float] = None, offset: vector2 = (0.0, 0.0), rotation: float = 0, - mirrored: Tuple[bool] = (False, False), + mirrored: Sequence[bool] = (False, False), layer: layer_t = 0, dose: float = 1.0, locked: bool = False): @@ -114,8 +114,8 @@ class Ellipse(Shape): return new def to_polygons(self, - poly_num_points: int = None, - poly_max_arclen: float = None, + poly_num_points: Optional[int] = None, + poly_max_arclen: Optional[float] = None, ) -> List[Polygon]: if poly_num_points is None: poly_num_points = self.poly_num_points diff --git a/masque/shapes/path.py b/masque/shapes/path.py index e320f4b..899e462 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -1,4 +1,4 @@ -from typing import List, Tuple, Dict +from typing import List, Tuple, Dict, Optional, Sequence import copy from enum import Enum import numpy @@ -28,8 +28,8 @@ class Path(Shape): __slots__ = ('_vertices', '_width', '_cap', '_cap_extensions') _vertices: numpy.ndarray _width: float - _cap_extensions: numpy.ndarray or None _cap: PathCap + _cap_extensions: Optional[numpy.ndarray] Cap = PathCap @@ -69,7 +69,7 @@ class Path(Shape): # cap_extensions property @property - def cap_extensions(self) -> numpy.ndarray or None: + def cap_extensions(self) -> Optional[numpy.ndarray]: """ Path end-cap extension @@ -144,11 +144,11 @@ class Path(Shape): cap_extensions: numpy.ndarray = None, offset: vector2 = (0.0, 0.0), rotation: float = 0, - mirrored: Tuple[bool] = (False, False), + mirrored: Sequence[bool] = (False, False), layer: layer_t = 0, dose: float = 1.0, locked: bool = False, - ) -> 'Path': + ): self.unlock() self._cap_extensions = None # Since .cap setter might access it @@ -182,7 +182,7 @@ class Path(Shape): cap_extensions = None, offset: vector2 = (0.0, 0.0), rotation: float = 0, - mirrored: Tuple[bool] = (False, False), + mirrored: Sequence[bool] = (False, False), layer: layer_t = 0, dose: float = 1.0, ) -> 'Path': diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 4bd8384..da98d4d 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -1,4 +1,4 @@ -from typing import List, Tuple, Dict +from typing import List, Tuple, Dict, Optional, Sequence import copy import numpy from numpy import pi @@ -71,7 +71,7 @@ class Polygon(Shape): vertices: numpy.ndarray, offset: vector2 = (0.0, 0.0), rotation: float = 0.0, - mirrored: Tuple[bool] = (False, False), + mirrored: Sequence[bool] = (False, False), layer: layer_t = 0, dose: float = 1.0, locked: bool = False, @@ -86,7 +86,7 @@ class Polygon(Shape): [self.mirror(a) for a, do in enumerate(mirrored) if do] self.locked = locked - def __deepcopy__(self, memo: Dict = None) -> 'Polygon': + def __deepcopy__(self, memo: Optional[Dict] = None) -> 'Polygon': memo = {} if memo is None else memo new = copy.copy(self).unlock() new._offset = self._offset.copy() @@ -154,14 +154,14 @@ class Polygon(Shape): 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, + def rect(xmin: Optional[float] = None, + xctr: Optional[float] = None, + xmax: Optional[float] = None, + lx: Optional[float] = None, + ymin: Optional[float] = None, + yctr: Optional[float] = None, + ymax: Optional[float] = None, + ly: Optional[float] = None, layer: layer_t = 0, dose: float = 1.0, ) -> 'Polygon': @@ -188,11 +188,17 @@ class Polygon(Shape): """ if lx is None: if xctr is None: + assert(xmin is not None) + assert(xmax is not None) xctr = 0.5 * (xmax + xmin) lx = xmax - xmin elif xmax is None: + assert(xmin is not None) + assert(xctr is not None) lx = 2 * (xctr - xmin) elif xmin is None: + assert(xctr is not None) + assert(xmax is not None) lx = 2 * (xmax - xctr) else: raise PatternError('Two of xmin, xctr, xmax, lx must be None!') @@ -200,19 +206,29 @@ class Polygon(Shape): if xctr is not None: pass elif xmax is None: + assert(xmin is not None) + assert(lx is not None) xctr = xmin + 0.5 * lx elif xmin is None: + assert(xmax is not None) + assert(lx is not 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: + assert(ymin is not None) + assert(ymax is not None) yctr = 0.5 * (ymax + ymin) ly = ymax - ymin elif ymax is None: + assert(ymin is not None) + assert(yctr is not None) ly = 2 * (yctr - ymin) elif ymin is None: + assert(yctr is not None) + assert(ymax is not None) ly = 2 * (ymax - yctr) else: raise PatternError('Two of ymin, yctr, ymax, ly must be None!') @@ -220,8 +236,12 @@ class Polygon(Shape): if yctr is not None: pass elif ymax is None: + assert(ymin is not None) + assert(ly is not None) yctr = ymin + 0.5 * ly elif ymin is None: + assert(ly is not None) + assert(ymax is not None) yctr = ymax - 0.5 * ly else: raise PatternError('Two of ymin, yctr, ymax, ly must be None!') diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index 3aae0bb..7db14c3 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -1,4 +1,4 @@ -from typing import List, Tuple, Callable +from typing import List, Tuple, Callable, TypeVar, Optional, TYPE_CHECKING from abc import ABCMeta, abstractmethod import copy import numpy @@ -6,6 +6,8 @@ import numpy from ..error import PatternError, PatternLockedError from ..utils import is_scalar, rotation_matrix_2d, vector2, layer_t +if TYPE_CHECKING: + from . import Polygon # Type definitions @@ -18,6 +20,9 @@ normalized_shape_tuple = Tuple[Tuple, DEFAULT_POLY_NUM_POINTS = 24 +T = TypeVar('T', bound='Shape') + + class Shape(metaclass=ABCMeta): """ Abstract class specifying functions common to all shapes. @@ -53,7 +58,10 @@ class Shape(metaclass=ABCMeta): # --- Abstract methods @abstractmethod - def to_polygons(self, num_vertices: int, max_arclen: float) -> List['Polygon']: + def to_polygons(self, + num_vertices: Optional[int] = None, + max_arclen: Optional[float] = None, + ) -> List['Polygon']: """ Returns a list of polygons which approximate the shape. @@ -77,7 +85,7 @@ class Shape(metaclass=ABCMeta): pass @abstractmethod - def rotate(self, theta: float) -> 'Shape': + def rotate(self: T, theta: float) -> T: """ Rotate the shape around its origin (0, 0), ignoring its offset. @@ -90,7 +98,7 @@ class Shape(metaclass=ABCMeta): pass @abstractmethod - def mirror(self, axis: int) -> 'Shape': + def mirror(self: T, axis: int) -> T: """ Mirror the shape across an axis. @@ -104,7 +112,7 @@ class Shape(metaclass=ABCMeta): pass @abstractmethod - def scale_by(self, c: float) -> 'Shape': + def scale_by(self: T, c: float) -> T: """ Scale the shape's size (eg. radius, for a circle) by a constant factor. @@ -117,7 +125,7 @@ class Shape(metaclass=ABCMeta): pass @abstractmethod - def normalized_form(self, norm_value: int) -> normalized_shape_tuple: + def normalized_form(self: T, 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. @@ -187,7 +195,7 @@ class Shape(metaclass=ABCMeta): self._dose = val # ---- Non-abstract methods - def copy(self) -> 'Shape': + def copy(self: T) -> T: """ Returns a deep copy of the shape. @@ -196,7 +204,7 @@ class Shape(metaclass=ABCMeta): """ return copy.deepcopy(self) - def translate(self, offset: vector2) -> 'Shape': + def translate(self: T, offset: vector2) -> T: """ Translate the shape by the given offset @@ -209,7 +217,7 @@ class Shape(metaclass=ABCMeta): self.offset += offset return self - def rotate_around(self, pivot: vector2, rotation: float) -> 'Shape': + def rotate_around(self: T, pivot: vector2, rotation: float) -> T: """ Rotate the shape around a point. @@ -428,7 +436,7 @@ class Shape(metaclass=ABCMeta): return manhattan_polygons - def lock(self) -> 'Shape': + def lock(self: T) -> T: """ Lock the Shape, disallowing further changes @@ -438,7 +446,7 @@ class Shape(metaclass=ABCMeta): object.__setattr__(self, 'locked', True) return self - def unlock(self) -> 'Shape': + def unlock(self: T) -> T: """ Unlock the Shape diff --git a/masque/shapes/text.py b/masque/shapes/text.py index 684c637..af7beae 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -1,4 +1,4 @@ -from typing import List, Tuple, Dict +from typing import List, Tuple, Dict, Sequence, Optional, MutableSequence import copy import numpy from numpy import pi, inf @@ -21,7 +21,7 @@ class Text(Shape): _string: str _height: float _rotation: float - _mirrored: List[str] + _mirrored: numpy.ndarray #ndarray[bool] font_path: str # vertices property @@ -57,11 +57,11 @@ class Text(Shape): # Mirrored property @property - def mirrored(self) -> List[bool]: + def mirrored(self) -> numpy.ndarray: #ndarray[bool] return self._mirrored @mirrored.setter - def mirrored(self, val: List[bool]): + def mirrored(self, val: Sequence[bool]): if is_scalar(val): raise PatternError('Mirrored must be a 2-element list of booleans') self._mirrored = numpy.ndarray(val, dtype=bool, copy=True) @@ -72,7 +72,7 @@ class Text(Shape): font_path: str, offset: vector2 = (0.0, 0.0), rotation: float = 0.0, - mirrored: Tuple[bool] = (False, False), + mirrored: Tuple[bool, bool] = (False, False), layer: layer_t = 0, dose: float = 1.0, locked: bool = False, @@ -98,11 +98,11 @@ class Text(Shape): return new def to_polygons(self, - poly_num_points: int = None, # unused - poly_max_arclen: float = None, # unused + poly_num_points: Optional[int] = None, # unused + poly_max_arclen: Optional[float] = None, # unused ) -> List[Polygon]: all_polygons = [] - total_advance = 0 + total_advance = 0.0 for char in self.string: raw_polys, advance = get_char_as_polygons(self.font_path, char) @@ -198,7 +198,7 @@ def get_char_as_polygons(font_path: str, tags = outline.tags[start:end + 1] tags.append(tags[0]) - segments = [] + segments: List[List[List[float]]] = [] for j, point in enumerate(points): # If we already have a segment, add this point to it if j > 0: diff --git a/masque/subpattern.py b/masque/subpattern.py index 16c3c7c..7115ed6 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -12,6 +12,8 @@ from numpy import pi from .error import PatternError, PatternLockedError from .utils import is_scalar, rotation_matrix_2d, vector2 +if TYPE_CHECKING: + from . import Pattern class SubPattern: @@ -43,7 +45,7 @@ class SubPattern: _scale: float """ scale factor for the instance """ - _mirrored: List[bool] + _mirrored: numpy.ndarray # ndarray[bool] """ Whether to mirror the instanc across the x and/or y axes. """ identifier: Tuple @@ -58,11 +60,11 @@ class SubPattern: pattern: Optional['Pattern'], offset: vector2 = (0.0, 0.0), rotation: float = 0.0, - mirrored: List[bool] = None, + mirrored: Optional[Sequence[bool]] = None, dose: float = 1.0, scale: float = 1.0, locked: bool = False): - self.unlock() + object.__setattr__(self, 'locked', False) self.identifier = () self.pattern = pattern self.offset = offset @@ -161,11 +163,11 @@ class SubPattern: # Mirrored property @property - def mirrored(self) -> List[bool]: + def mirrored(self) -> numpy.ndarray: # ndarray[bool] return self._mirrored @mirrored.setter - def mirrored(self, val: List[bool]): + def mirrored(self, val: Sequence[bool]): if is_scalar(val): raise PatternError('Mirrored must be a 2-element list of booleans') self._mirrored = numpy.array(val, dtype=bool, copy=True) diff --git a/masque/utils.py b/masque/utils.py index bdb06a2..22495d9 100644 --- a/masque/utils.py +++ b/masque/utils.py @@ -2,12 +2,12 @@ Various helper functions """ -from typing import Any, Union, Tuple +from typing import Any, Union, Tuple, Sequence import numpy # Type definitions -vector2 = Union[numpy.ndarray, Tuple[float, float]] +vector2 = Union[numpy.ndarray, Tuple[float, float], Sequence[float]] layer_t = Union[int, Tuple[int, int]] @@ -68,7 +68,7 @@ def rotation_matrix_2d(theta: float) -> numpy.ndarray: [numpy.sin(theta), +numpy.cos(theta)]]) -def normalize_mirror(mirrored: Tuple[bool, bool]) -> Tuple[bool, float]: +def normalize_mirror(mirrored: Sequence[bool]) -> Tuple[bool, float]: """ Converts 0-2 mirror operations `(mirror_across_x_axis, mirror_across_y_axis)` into 0-1 mirror operations and a rotation From 79990e38060f127a542468687a1bae56ddba95f9 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 11 May 2020 19:10:00 -0700 Subject: [PATCH 092/109] cosmetic changes --- masque/label.py | 2 +- masque/pattern.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/masque/label.py b/masque/label.py index 390ca84..36a8a0b 100644 --- a/masque/label.py +++ b/masque/label.py @@ -77,7 +77,7 @@ class Label: def __init__(self, string: str, - offset: vector2=(0.0, 0.0), + offset: vector2 = (0.0, 0.0), layer: layer_t = 0, locked: bool = False): self.unlock() diff --git a/masque/pattern.py b/masque/pattern.py index cefcdbd..d4cbb19 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -1,5 +1,5 @@ """ - Base object for containing a lithography mask. + Base object representing a lithography mask. """ from typing import List, Callable, Tuple, Dict, Union, Set, Sequence, Optional, Type From c79eee2a8c65f8ee8844d84df27292c5c513eb29 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 11 May 2020 19:29:00 -0700 Subject: [PATCH 093/109] Stricter locking (lock contained ndarrays and turn lists into tuples) --- masque/label.py | 4 +++- masque/pattern.py | 8 +++++++- masque/repetition.py | 13 ++++++++++++- masque/shapes/arc.py | 14 +++++++++++++- masque/shapes/circle.py | 2 +- masque/shapes/ellipse.py | 11 ++++++++++- masque/shapes/path.py | 15 ++++++++++++++- masque/shapes/polygon.py | 12 +++++++++++- masque/shapes/shape.py | 2 ++ masque/shapes/text.py | 12 +++++++++++- masque/subpattern.py | 4 ++++ 11 files changed, 88 insertions(+), 9 deletions(-) diff --git a/masque/label.py b/masque/label.py index 36a8a0b..972370b 100644 --- a/masque/label.py +++ b/masque/label.py @@ -80,7 +80,7 @@ class Label: offset: vector2 = (0.0, 0.0), layer: layer_t = 0, locked: bool = False): - self.unlock() + object.__setattr__(self, 'locked', False) self.identifier = () self.string = string self.offset = numpy.array(offset, dtype=float, copy=True) @@ -156,6 +156,7 @@ class Label: Return: self """ + self.offset.flags.writeable = False object.__setattr__(self, 'locked', True) return self @@ -167,4 +168,5 @@ class Label: self """ object.__setattr__(self, 'locked', False) + self.offset.flags.writeable = True return self diff --git a/masque/pattern.py b/masque/pattern.py index d4cbb19..a0313a6 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -71,7 +71,7 @@ class Pattern: name: An identifier for the Pattern locked: Whether to lock the pattern after construction """ - self.unlock() + object.__setattr__(self, 'locked', False) if isinstance(shapes, list): self.shapes = shapes else: @@ -715,6 +715,9 @@ class Pattern: Returns: self """ + self.shapes = tuple(self.shapes) + self.labels = tuple(self.labels) + self.subpatterns = tuple(self.subpatterns) object.__setattr__(self, 'locked', True) return self @@ -726,6 +729,9 @@ class Pattern: self """ object.__setattr__(self, 'locked', False) + self.shapes = list(self.shapes) + self.labels = list(self.labels) + self.subpatterns = list(self.subpatterns) return self def deeplock(self) -> 'Pattern': diff --git a/masque/repetition.py b/masque/repetition.py index 37bd350..6c7894b 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -121,7 +121,8 @@ class GridRepetition: if b_count < 1: raise PatternError('Repetition has too-small b_count: ' '{}'.format(b_count)) - self.unlock() + + object.__setattr__(self, 'locked', False) self.a_vector = a_vector self.b_vector = b_vector self.a_count = a_count @@ -481,6 +482,11 @@ class GridRepetition: Returns: self """ + self.offset.flags.writeable = False + self.a_vector.flags.writeable = False + self.mirrored.flags.writeable = False + if self.b_vector is not None: + self.b_vector.flags.writeable = False object.__setattr__(self, 'locked', True) return self @@ -491,6 +497,11 @@ class GridRepetition: Returns: self """ + self.offset.flags.writeable = True + self.a_vector.flags.writeable = True + self.mirrored.flags.writeable = True + if self.b_vector is not None: + self.b_vector.flags.writeable = True object.__setattr__(self, 'locked', False) return self diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index f082c3b..6b1a5bf 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -158,7 +158,7 @@ class Arc(Shape): layer: layer_t = 0, dose: float = 1.0, locked: bool = False): - self.unlock() + object.__setattr__(self, 'locked', False) self.identifier = () self.radii = radii self.angles = angles @@ -386,3 +386,15 @@ class Arc(Shape): a.append((a0, a1)) return numpy.array(a) + + def lock(self) -> 'Arc': + self.radii.flags.writeable = False + self.angles.flags.writeable = False + Shape.lock(self) + return self + + def unlock(self) -> 'Arc': + Shape.unlock(self) + self.radii.flags.writeable = True + self.angles.flags.writeable = True + return self diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index 1f6b5c9..8f008dd 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -46,7 +46,7 @@ class Circle(Shape): layer: layer_t = 0, dose: float = 1.0, locked: bool = False): - self.unlock() + object.__setattr__(self, 'locked', False) self.identifier = () self.offset = numpy.array(offset, dtype=float) self.layer = layer diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index fc8a2d4..12a2baf 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -93,7 +93,7 @@ class Ellipse(Shape): layer: layer_t = 0, dose: float = 1.0, locked: bool = False): - self.unlock() + object.__setattr__(self, 'locked', False) self.identifier = () self.radii = radii self.offset = offset @@ -180,3 +180,12 @@ class Ellipse(Shape): (self.offset, scale/norm_value, angle, False, self.dose), \ lambda: Ellipse(radii=radii*norm_value, layer=self.layer) + def lock(self) -> 'Ellipse': + self.radii.flags.writeable = False + Shape.lock(self) + return self + + def unlock(self) -> 'Ellipse': + Shape.unlock(self) + self.radii.flags.writeable = True + return self diff --git a/masque/shapes/path.py b/masque/shapes/path.py index 899e462..57ae873 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -149,7 +149,7 @@ class Path(Shape): dose: float = 1.0, locked: bool = False, ): - self.unlock() + object.__setattr__(self, 'locked', False) self._cap_extensions = None # Since .cap setter might access it self.identifier = () @@ -394,3 +394,16 @@ class Path(Shape): extensions = numpy.zeros(2) return extensions + def lock(self) -> 'Path': + self.vertices.flags.writeable = False + if self.cap_extensions is not None: + self.cap_extensions.flags.writeable = False + Shape.lock(self) + return self + + def unlock(self) -> 'Path': + Shape.unlock(self) + self.vertices.flags.writeable = True + if self.cap_extensions is not None: + self.cap_extensions.flags.writeable = True + return self diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index da98d4d..6d19714 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -76,7 +76,7 @@ class Polygon(Shape): dose: float = 1.0, locked: bool = False, ): - self.unlock() + object.__setattr__(self, 'locked', False) self.identifier = () self.layer = layer self.dose = dose @@ -329,3 +329,13 @@ class Polygon(Shape): ''' self.vertices = remove_colinear_vertices(self.vertices, closed_path=True) return self + + def lock(self) -> 'Polygon': + self.vertices.flags.writeable = False + Shape.lock(self) + return self + + def unlock(self) -> 'Polygon': + Shape.unlock(self) + self.vertices.flags.writeable = True + return self diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index 7db14c3..6a664a5 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -443,6 +443,7 @@ class Shape(metaclass=ABCMeta): Returns: self """ + self.offset.flags.writeable = False object.__setattr__(self, 'locked', True) return self @@ -454,4 +455,5 @@ class Shape(metaclass=ABCMeta): self """ object.__setattr__(self, 'locked', False) + self.offset.flags.writeable = True return self diff --git a/masque/shapes/text.py b/masque/shapes/text.py index af7beae..355600e 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -77,7 +77,7 @@ class Text(Shape): dose: float = 1.0, locked: bool = False, ): - self.unlock() + object.__setattr__(self, 'locked', False) self.identifier = () self.offset = offset self.layer = layer @@ -243,3 +243,13 @@ def get_char_as_polygons(font_path: str, polygons = path.to_polygons() return polygons, advance + + def lock(self) -> 'Text': + self.mirrored.flags.writeable = False + Shape.lock(self) + return self + + def unlock(self) -> 'Text': + Shape.unlock(self) + self.mirrored.flags.writeable = True + return self diff --git a/masque/subpattern.py b/masque/subpattern.py index 7115ed6..a6cc123 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -296,6 +296,8 @@ class SubPattern: Returns: self """ + self.offset.flags.writeable = False + self.mirrored.flags.writeable = False object.__setattr__(self, 'locked', True) return self @@ -306,6 +308,8 @@ class SubPattern: Returns: self """ + self.offset.flags.writeable = True + self.mirrored.flags.writeable = True object.__setattr__(self, 'locked', False) return self From c236fdb81b1249e1bcab0a027af81c987f96a60b Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 11 May 2020 20:31:07 -0700 Subject: [PATCH 094/109] Add __repr__ methods to most objects` These are closer to what __str__ should be and will likely change in the future. --- masque/label.py | 4 ++++ masque/pattern.py | 4 ++++ masque/repetition.py | 11 +++++++++++ masque/shapes/arc.py | 7 +++++++ masque/shapes/circle.py | 4 ++++ masque/shapes/ellipse.py | 6 ++++++ masque/shapes/path.py | 6 ++++++ masque/shapes/polygon.py | 6 ++++++ masque/shapes/text.py | 7 +++++++ masque/subpattern.py | 9 +++++++++ 10 files changed, 64 insertions(+) diff --git a/masque/label.py b/masque/label.py index 972370b..e09c60e 100644 --- a/masque/label.py +++ b/masque/label.py @@ -170,3 +170,7 @@ class Label: object.__setattr__(self, 'locked', False) self.offset.flags.writeable = True return self + + def __repr__(self) -> str: + locked = ' L' if self.locked else '' + return f'