from typing import List import copy import numpy from numpy import pi from . import Shape, normalized_shape_tuple from .. import PatternError from ..utils import is_scalar, rotation_matrix_2d, vector2 __author__ = 'Jan Petykiewicz' class Polygon(Shape): """ A polygon, consisting of a bunch of vertices (Nx2 ndarray) along with an offset. A normalized_form(...) is available, but can be quite slow with lots of vertices. """ _vertices = None # type: numpy.ndarray # vertices property @property def vertices(self) -> numpy.ndarray: """ Vertices of the polygon (Nx2 ndarray: [[x0, y0], [x1, y1], ...] :return: vertices """ return self._vertices @vertices.setter def vertices(self, val: numpy.ndarray): val = numpy.array(val, dtype=float) if val.shape[1] != 2: raise PatternError('Vertices must be an Nx2 array') if val.shape[0] < 3: raise PatternError('Must have at least 3 vertices (Nx2, N>3)') self._vertices = val # xs property @property def xs(self) -> numpy.ndarray: """ All x vertices in a 1D ndarray """ return self.vertices[:, 0] @xs.setter def xs(self, val: numpy.ndarray): val = numpy.array(val, dtype=float).flatten() if val.size != self.vertices.shape[0]: raise PatternError('Wrong number of vertices') self.vertices[:, 0] = val # ys property @property def ys(self) -> numpy.ndarray: """ All y vertices in a 1D ndarray """ return self.vertices[:, 1] @ys.setter def ys(self, val: numpy.ndarray): val = numpy.array(val, dtype=float).flatten() if val.size != self.vertices.shape[0]: raise PatternError('Wrong number of vertices') self.vertices[:, 1] = val def __init__(self, vertices: numpy.ndarray, offset: vector2=(0.0, 0.0), layer: int=0, dose: float=1.0): self.offset = offset self.layer = layer self.dose = dose self.vertices = vertices @staticmethod def square(side_length: float, rotation: float=0.0, offset: vector2=(0.0, 0.0), layer: int=0, dose: float=1.0 ) -> 'Polygon': """ Draw a square given side_length :param side_length: Length of one side :param rotation: Rotation counterclockwise, in radians :param offset: Offset, default (0, 0) :param layer: Layer, default 0 :param dose: Dose, default 1.0 :return: A Polygon object containing the requested square """ norm_square = numpy.array([[-1, -1], [-1, +1], [+1, +1], [+1, -1]], dtype=float) vertices = 0.5 * side_length * norm_square poly = Polygon(vertices, offset, layer, dose) poly.rotate(rotation) return poly @staticmethod def rectangle(lx: float, ly: float, rotation: float=0, offset: vector2=(0.0, 0.0), layer: int=0, dose: float=1.0 ) -> 'Polygon': """ Draw a rectangle with side lengths lx and ly :param lx: Length along x (before rotation) :param ly: Length along y (before rotation) :param rotation: Rotation counterclockwise, in radians :param offset: Offset, default (0, 0) :param layer: Layer, default 0 :param dose: Dose, default 1.0 :return: A Polygon object containing the requested rectangle """ vertices = 0.5 * numpy.array([[-lx, -ly], [-lx, +ly], [+lx, +ly], [+lx, -ly]], dtype=float) poly = Polygon(vertices, offset, layer, dose) poly.rotate(rotation) return poly def to_polygons(self, _poly_num_points: int=None, _poly_max_arclen: float=None, ) -> List['Polygon']: return [copy.deepcopy(self)] def get_bounds(self) -> numpy.ndarray: return numpy.vstack((self.offset + numpy.min(self.vertices, axis=0), self.offset + numpy.max(self.vertices, axis=0))) def rotate(self, theta: float) -> 'Polygon': self.vertices = numpy.dot(rotation_matrix_2d(theta), self.vertices.T).T return self def scale_by(self, c: float) -> 'Polygon': self.vertices *= c return self def normalized_form(self, norm_value: float) -> normalized_shape_tuple: # Note: this function is going to be pretty slow for many-vertexed polygons, relative to # other shapes offset = self.vertices.mean(axis=0) + self.offset zeroed_vertices = self.vertices - offset scale = zeroed_vertices.std() normed_vertices = zeroed_vertices / scale _, _, vertex_axis = numpy.linalg.svd(zeroed_vertices) rotation = numpy.arctan2(vertex_axis[0][1], vertex_axis[0][0]) % (2 * pi) rotated_vertices = numpy.vstack([numpy.dot(rotation_matrix_2d(-rotation), v) for v in normed_vertices]) # Reorder the vertices so that the one with lowest x, then y, comes first. x_min = rotated_vertices[:, 0].argmin() if not is_scalar(x_min): y_min = rotated_vertices[x_min, 1].argmin() x_min = x_min[y_min] reordered_vertices = numpy.roll(rotated_vertices, -x_min, axis=0) return (type(self), reordered_vertices.data.tobytes(), self.layer), \ (offset, scale/norm_value, rotation, self.dose), \ lambda: Polygon(reordered_vertices*norm_value, layer=self.layer)