279 lines
10 KiB
Python
279 lines
10 KiB
Python
from typing import List, Tuple, Callable
|
|
from abc import ABCMeta, abstractmethod
|
|
import copy
|
|
import numpy
|
|
|
|
from .. import PatternError
|
|
from ..utils import is_scalar, rotation_matrix_2d, vector2
|
|
|
|
|
|
__author__ = 'Jan Petykiewicz'
|
|
|
|
|
|
# Type definitions
|
|
normalized_shape_tuple = Tuple[Tuple,
|
|
Tuple[numpy.ndarray, float, float, float],
|
|
Callable[[], 'Shape']]
|
|
|
|
# ## Module-wide defaults
|
|
# Default number of points per polygon for shapes
|
|
DEFAULT_POLY_NUM_POINTS = 24
|
|
|
|
|
|
class Shape(metaclass=ABCMeta):
|
|
"""
|
|
Abstract class specifying functions common to all shapes.
|
|
"""
|
|
|
|
# [x_offset, y_offset]
|
|
_offset = numpy.array([0.0, 0.0]) # type: numpy.ndarray
|
|
|
|
# Layer (integer >= 0)
|
|
_layer = 0 # type: int
|
|
|
|
# Dose
|
|
_dose = 1.0 # type: float
|
|
|
|
# --- Abstract methods
|
|
@abstractmethod
|
|
def to_polygons(self, num_vertices: int, max_arclen: float) -> List['Polygon']:
|
|
"""
|
|
Returns a list of polygons which approximate the shape.
|
|
|
|
:param num_vertices: Number of points to use for each polygon. Can be overridden by
|
|
max_arclen if that results in more points. Optional, defaults to shapes'
|
|
internal defaults.
|
|
:param max_arclen: Maximum arclength which can be approximated by a single line
|
|
segment. Optional, defaults to shapes' internal defaults.
|
|
:return: List of polygons equivalent to the shape
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_bounds(self) -> numpy.ndarray:
|
|
"""
|
|
Returns [[x_min, y_min], [x_max, y_max]] which specify a minimal bounding box for the shape.
|
|
|
|
:return: [[x_min, y_min], [x_max, y_max]]
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def rotate(self, theta: float) -> 'Shape':
|
|
"""
|
|
Rotate the shape around its center (0, 0), ignoring its offset.
|
|
|
|
:param theta: Angle to rotate by (counterclockwise, radians)
|
|
:return: self
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def scale_by(self, c: float) -> 'Shape':
|
|
"""
|
|
Scale the shape's size (eg. radius, for a circle) by a constant factor.
|
|
|
|
:param c: Factor to scale by
|
|
:return: self
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def normalized_form(self, norm_value: int) -> normalized_shape_tuple:
|
|
"""
|
|
Writes the shape in a standardized notation, with offset, scale, rotation, and dose
|
|
information separated out from the remaining values.
|
|
|
|
:param norm_value: This value is used to normalize lengths intrinsic to teh shape;
|
|
eg. for a circle, the returned magnitude value will be (radius / norm_value), and
|
|
the returned callable will create a Circle(radius=norm_value, ...). This is useful
|
|
when you find it important for quantities to remain in a certain range, eg. for
|
|
GDSII where vertex locations are stored as integers.
|
|
:return: The returned information takes the form of a 3-element tuple,
|
|
(intrinsic, extrinsic, constructor). These are further broken down as:
|
|
extrinsic: ([x_offset, y_offset], scale, rotation, dose)
|
|
intrinsic: A tuple of basic types containing all information about the instance that
|
|
is not contained in 'extrinsic'. Usually, intrinsic[0] == type(self).
|
|
constructor: A callable (no arguments) which returns an instance of type(self) with
|
|
internal state equivalent to 'intrinsic'.
|
|
"""
|
|
pass
|
|
|
|
# ---- Non-abstract properties
|
|
# offset property
|
|
@property
|
|
def offset(self) -> numpy.ndarray:
|
|
"""
|
|
[x, y] offset
|
|
|
|
:return: [x_offset, y_offset]
|
|
"""
|
|
return self._offset
|
|
|
|
@offset.setter
|
|
def offset(self, val: vector2):
|
|
if not isinstance(val, numpy.ndarray):
|
|
val = numpy.array(val, dtype=float)
|
|
|
|
if val.size != 2:
|
|
raise PatternError('Offset must be convertible to size-2 ndarray')
|
|
self._offset = val.flatten()
|
|
|
|
# layer property
|
|
@property
|
|
def layer(self) -> int or Tuple[int]:
|
|
"""
|
|
Layer number (int or tuple of ints)
|
|
|
|
:return: Layer
|
|
"""
|
|
return self._layer
|
|
|
|
@layer.setter
|
|
def layer(self, val: int or List[int]):
|
|
self._layer = val
|
|
|
|
# dose property
|
|
@property
|
|
def dose(self) -> float:
|
|
"""
|
|
Dose (float >= 0)
|
|
|
|
:return: Dose value
|
|
"""
|
|
return self._dose
|
|
|
|
@dose.setter
|
|
def dose(self, val: float):
|
|
if not is_scalar(val):
|
|
raise PatternError('Dose must be a scalar')
|
|
if not val >= 0:
|
|
raise PatternError('Dose must be non-negative')
|
|
self._dose = val
|
|
|
|
# ---- Non-abstract methods
|
|
def copy(self) -> 'Shape':
|
|
"""
|
|
Returns a deep copy of the shape.
|
|
|
|
:return: Deep copy of self
|
|
"""
|
|
return copy.deepcopy(self)
|
|
|
|
def translate(self, offset: vector2) -> 'Shape':
|
|
"""
|
|
Translate the shape by the given offset
|
|
|
|
:param offset: [x_offset, y,offset]
|
|
:return: self
|
|
"""
|
|
self.offset += offset
|
|
return self
|
|
|
|
def rotate_around(self, pivot: vector2, rotation: float) -> 'Shape':
|
|
"""
|
|
Rotate the shape around a point.
|
|
|
|
:param pivot: Point (x, y) to rotate around
|
|
:param rotation: Angle to rotate by (counterclockwise, radians)
|
|
:return: self
|
|
"""
|
|
pivot = numpy.array(pivot, dtype=float)
|
|
self.translate(-pivot)
|
|
self.rotate(rotation)
|
|
self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset)
|
|
self.translate(+pivot)
|
|
return self
|
|
|
|
def manhattanize(self, grid_x: numpy.ndarray, grid_y: numpy.ndarray) -> List['Polygon']:
|
|
"""
|
|
Returns a list of polygons with grid-aligned ("Manhattan") edges approximating the shape.
|
|
|
|
This function works by
|
|
1) Converting the shape to polygons using .to_polygons()
|
|
2) Accurately rasterizing each polygon on a grid,
|
|
where the edges of each grid cell correspond to the allowed coordinates
|
|
3) Thresholding the (anti-aliased) rasterized image
|
|
4) Finding the contours which outline the filled areas in the thresholded image
|
|
This process results in a fairly accurate Manhattan representation of the shape. Possible
|
|
caveats include:
|
|
a) If high accuracy is important, perform any polygonization and clipping operations
|
|
prior to calling this function. This allows you to specify any arguments you may
|
|
need for .to_polygons(), and also avoids calling .manhattanize() multiple times for
|
|
the same grid location (which causes inaccuracies in the final representation).
|
|
b) If the shape is very large or the grid very fine, memory requirements can be reduced
|
|
by breaking the shape apart into multiple, smaller shapes.
|
|
c) Inaccuracies in edge shape can result from Manhattanization of edges which are
|
|
equidistant from allowed edge location.
|
|
|
|
Implementation notes:
|
|
i) Rasterization is performed using float_raster, giving a high-precision anti-aliased
|
|
rasterized image.
|
|
ii) To find the exact polygon edges, the thresholded rasterized image is supersampled
|
|
prior to calling skimage.measure.find_contours(), which uses marching squares
|
|
to find the contours. This is done because find_contours() performs interpolation,
|
|
which has to be undone in order to regain the axis-aligned contours. A targetted
|
|
rewrite of find_contours() for this specific application, or use of a different
|
|
boundary tracing method could remove this requirement, but for now this seems to
|
|
be the most performant approach.
|
|
|
|
:param grid_x: List of allowed x-coordinates for the Manhattanized polygon edges.
|
|
:param grid_y: List of allowed y-coordinates for the Manhattanized polygon edges.
|
|
:return: List of Polygon objects with grid-aligned edges.
|
|
"""
|
|
from . import Polygon
|
|
import skimage.measure
|
|
import float_raster
|
|
|
|
grid_x = numpy.unique(grid_x)
|
|
grid_y = numpy.unique(grid_y)
|
|
|
|
polygon_contours = []
|
|
for polygon in self.to_polygons():
|
|
mins, maxs = polygon.get_bounds()
|
|
keep_x = numpy.logical_and(grid_x > mins[0], grid_x < maxs[0])
|
|
keep_y = numpy.logical_and(grid_y > mins[1], grid_y < maxs[1])
|
|
for k in (keep_x, keep_y):
|
|
for s in (1, 2):
|
|
k[s:] += k[:-s]
|
|
k[:-s] += k[s:]
|
|
k = k > 0
|
|
|
|
gx = grid_x[keep_x]
|
|
gy = grid_y[keep_y]
|
|
|
|
if len(gx) == 0 or len(gy) == 0:
|
|
continue
|
|
|
|
offset = (numpy.where(keep_x)[0][0],
|
|
numpy.where(keep_y)[0][0])
|
|
|
|
rastered = float_raster.raster((polygon.vertices + polygon.offset).T, gx, gy)
|
|
binary_rastered = (rastered >= 0.5)
|
|
supersampled = binary_rastered.repeat(2, axis=0).repeat(2, axis=1)
|
|
|
|
from matplotlib import pyplot
|
|
pyplot.pcolormesh(binary_rastered)
|
|
pyplot.colorbar()
|
|
pyplot.show()
|
|
|
|
contours = skimage.measure.find_contours(supersampled, 0.5)
|
|
polygon_contours.append((offset, contours))
|
|
|
|
manhattan_polygons = []
|
|
for offset_i, contours in polygon_contours:
|
|
for contour in contours:
|
|
# /2 deals with supersampling
|
|
# +.5 deals with the fact that our 0-edge becomes -.5 in the super-sampled contour output
|
|
snapped_contour = numpy.round((contour + .5) / 2).astype(int)
|
|
vertices = numpy.hstack((grid_x[snapped_contour[:, None, 0] + offset_i[0]],
|
|
grid_y[snapped_contour[:, None, 1] + offset_i[1]]))
|
|
|
|
manhattan_polygons.append(Polygon(
|
|
vertices=vertices,
|
|
layer=self.layer,
|
|
dose=self.dose))
|
|
|
|
return manhattan_polygons
|
|
|