You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
masque/masque/shapes/shape.py

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