183 lines
5.7 KiB
Python
183 lines
5.7 KiB
Python
from typing import List, Tuple, Callable
|
|
from abc import ABCMeta, abstractmethod
|
|
import numpy
|
|
|
|
from .. import PatternError
|
|
from ..utils import is_scalar, rotation_matrix_2d, vector2
|
|
|
|
|
|
__author__ = 'Jan Petykiewicz'
|
|
|
|
|
|
# Type definitions
|
|
normalized_shape_tuple = Tuple[Tuple,
|
|
Tuple[numpy.ndarray, float, float, float],
|
|
Callable[[], 'Shape']]
|
|
|
|
# ## Module-wide defaults
|
|
# Default number of points per polygon for shapes
|
|
DEFAULT_POLY_NUM_POINTS = 24
|
|
|
|
|
|
class Shape(object, metaclass=ABCMeta):
|
|
"""
|
|
Abstract class specifying functions common to all shapes.
|
|
"""
|
|
|
|
# [x_offset, y_offset]
|
|
_offset = numpy.array([0.0, 0.0]) # type: numpy.ndarray
|
|
|
|
# Layer (integer >= 0)
|
|
_layer = 0 # type: int
|
|
|
|
# Dose
|
|
_dose = 1.0 # type: float
|
|
|
|
# --- Abstract methods
|
|
@abstractmethod
|
|
def to_polygons(self, num_vertices: int, max_arclen: float) -> List['Polygon']:
|
|
"""
|
|
Returns a list of polygons which approximate the shape.
|
|
|
|
:param num_vertices: Number of points to use for each polygon. Can be overridden by
|
|
max_arclen if that results in more points. Optional, defaults to shapes'
|
|
internal defaults.
|
|
:param max_arclen: Maximum arclength which can be approximated by a single line
|
|
segment. Optional, defaults to shapes' internal defaults.
|
|
:return: List of polygons equivalent to the shape
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_bounds(self) -> numpy.ndarray:
|
|
"""
|
|
Returns [[x_min, y_min], [x_max, y_max]] which specify a minimal bounding box for the shape.
|
|
|
|
:return: [[x_min, y_min], [x_max, y_max]]
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def rotate(self, theta: float) -> 'Shape':
|
|
"""
|
|
Rotate the shape around its center (0, 0), ignoring its offset.
|
|
|
|
:param theta: Angle to rotate by (counterclockwise, radians)
|
|
:return: self
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def scale_by(self, c: float) -> 'Shape':
|
|
"""
|
|
Scale the shape's size (eg. radius, for a circle) by a constant factor.
|
|
|
|
:param c: Factor to scale by
|
|
:return: self
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def normalized_form(self, norm_value: int) -> normalized_shape_tuple:
|
|
"""
|
|
Writes the shape in a standardized notation, with offset, scale, rotation, and dose
|
|
information separated out from the remaining values.
|
|
|
|
:param norm_value: This value is used to normalize lengths intrinsic to teh shape;
|
|
eg. for a circle, the returned magnitude value will be (radius / norm_value), and
|
|
the returned callable will create a Circle(radius=norm_value, ...). This is useful
|
|
when you find it important for quantities to remain in a certain range, eg. for
|
|
GDSII where vertex locations are stored as integers.
|
|
:return: The returned information takes the form of a 3-element tuple,
|
|
(intrinsic, extrinsic, constructor). These are further broken down as:
|
|
extrinsic: ([x_offset, y_offset], scale, rotation, dose)
|
|
intrinsic: A tuple of basic types containing all information about the instance that
|
|
is not contained in 'extrinsic'. Usually, intrinsic[0] == type(self).
|
|
constructor: A callable (no arguments) which returns an instance of type(self) with
|
|
internal state equivalent to 'intrinsic'.
|
|
"""
|
|
pass
|
|
|
|
# ---- Non-abstract properties
|
|
# offset property
|
|
@property
|
|
def offset(self) -> numpy.ndarray:
|
|
"""
|
|
[x, y] offset
|
|
|
|
:return: [x_offset, y_offset]
|
|
"""
|
|
return self._offset
|
|
|
|
@offset.setter
|
|
def offset(self, val: vector2):
|
|
if not isinstance(val, numpy.ndarray):
|
|
val = numpy.array(val, dtype=float)
|
|
|
|
if val.size != 2:
|
|
raise PatternError('Offset must be convertible to size-2 ndarray')
|
|
self._offset = val.flatten()
|
|
|
|
# layer property
|
|
@property
|
|
def layer(self) -> int:
|
|
"""
|
|
Layer number (int, >=0)
|
|
|
|
:return: Layer
|
|
"""
|
|
return self._layer
|
|
|
|
@layer.setter
|
|
def layer(self, val: int):
|
|
if not isinstance(val, int):
|
|
raise PatternError('Layer must be an integer')
|
|
if not val >= 0:
|
|
raise PatternError('Layer must be non-negative')
|
|
self._layer = val
|
|
|
|
# dose property
|
|
@property
|
|
def dose(self) -> float:
|
|
"""
|
|
Dose (float >= 0)
|
|
|
|
:return: Dose value
|
|
"""
|
|
return self._dose
|
|
|
|
@dose.setter
|
|
def dose(self, val: float):
|
|
if not is_scalar(val):
|
|
raise PatternError('Dose must be a scalar')
|
|
if not val >= 0:
|
|
raise PatternError('Dose must be non-negative')
|
|
self._dose = val
|
|
|
|
# ---- Non-abstract methods
|
|
def translate(self, offset: vector2) -> 'Shape':
|
|
"""
|
|
Translate the shape by the given offset
|
|
|
|
:param offset: [x_offset, y,offset]
|
|
:return: self
|
|
"""
|
|
self.offset += offset
|
|
return self
|
|
|
|
def rotate_around(self, pivot: vector2, rotation: float) -> 'Shape':
|
|
"""
|
|
Rotate the shape around a point.
|
|
|
|
:param pivot: Point (x, y) to rotate around
|
|
:param rotation: Angle to rotate by (counterclockwise, radians)
|
|
:return: self
|
|
"""
|
|
pivot = numpy.array(pivot, dtype=float)
|
|
self.translate(-pivot)
|
|
self.rotate(rotation)
|
|
self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset)
|
|
self.translate(+pivot)
|
|
return self
|
|
|