Add all files to repository
This commit is contained in:
commit
5bf486ac81
19 changed files with 1998 additions and 0 deletions
161
masque/shapes/ellipse.py
Normal file
161
masque/shapes/ellipse.py
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
from typing import List
|
||||
import math
|
||||
import numpy
|
||||
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'
|
||||
|
||||
|
||||
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
|
||||
|
||||
# radius properties
|
||||
@property
|
||||
def radii(self) -> numpy.ndarray:
|
||||
"""
|
||||
Return the radii [rx, ry]
|
||||
|
||||
:return: [rx, ry]
|
||||
"""
|
||||
return self.radii
|
||||
|
||||
@radii.setter
|
||||
def radii(self, val: vector2):
|
||||
val = numpy.array(val).flatten()
|
||||
if not val.size == 2:
|
||||
raise PatternError('Radii must have length 2')
|
||||
if not val.min() >= 0:
|
||||
raise PatternError('Radii must be non-negative')
|
||||
self.radii = val
|
||||
|
||||
@property
|
||||
def radius_x(self) -> float:
|
||||
return self.radii[0]
|
||||
|
||||
@radius_x.setter
|
||||
def radius_x(self, val: float):
|
||||
if not val >= 0:
|
||||
raise PatternError('Radius must be non-negative')
|
||||
self.radii[0] = val
|
||||
|
||||
@property
|
||||
def radius_y(self) -> float:
|
||||
return self.radii[1]
|
||||
|
||||
@radius_y.setter
|
||||
def radius_y(self, val: float):
|
||||
if not val >= 0:
|
||||
raise PatternError('Radius must be non-negative')
|
||||
self.radii[1] = val
|
||||
|
||||
# Rotation property
|
||||
@property
|
||||
def rotation(self) -> float:
|
||||
"""
|
||||
Rotation of rx from the x axis. Uses the interval [0, pi) in radians (counterclockwise
|
||||
is positive)
|
||||
|
||||
:return: counterclockwise rotation in radians
|
||||
"""
|
||||
return self._rotation
|
||||
|
||||
@rotation.setter
|
||||
def rotation(self, val: float):
|
||||
if not is_scalar(val):
|
||||
raise PatternError('Rotation must be a scalar')
|
||||
self._rotation = val % pi
|
||||
|
||||
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),
|
||||
layer: int=0,
|
||||
dose: float=1.0):
|
||||
self.offset = offset
|
||||
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
|
||||
|
||||
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:
|
||||
poly_max_arclen = self.poly_max_arclen
|
||||
|
||||
if (poly_num_points is None) and (poly_max_arclen is None):
|
||||
raise PatternError('Number of points and arclength left unspecified'
|
||||
' (default was also overridden)')
|
||||
|
||||
rxy = self.radii
|
||||
|
||||
# Approximate perimeter
|
||||
# Ramanujan, S., "Modular Equations and Approximations to ,"
|
||||
# Quart. J. Pure. Appl. Math., vol. 45 (1913-1914), pp. 350-372
|
||||
h = ((rxy[1] - rxy[0]) / rxy.sum()) ** 2
|
||||
perimeter = pi * rxy.sum() * (1 + 3 * h / (10 + math.sqrt(4 - 3 * h)))
|
||||
|
||||
n = []
|
||||
if poly_num_points is not None:
|
||||
n += [poly_num_points]
|
||||
if poly_max_arclen is not None:
|
||||
n += [perimeter / poly_max_arclen]
|
||||
thetas = numpy.linspace(2 * pi, 0, max(n), endpoint=False)
|
||||
|
||||
sin_th, cos_th = (numpy.sin(thetas), numpy.cos(thetas))
|
||||
xs = rxy[0] * cos_th - rxy[1] * sin_th
|
||||
ys = rxy[0] * sin_th - rxy[1] * cos_th
|
||||
xys = numpy.vstack((xs, ys)).T
|
||||
|
||||
poly = Polygon(xys, dose=self.dose, layer=self.layer, offset=self.offset)
|
||||
poly.rotate(self.rotation)
|
||||
return [poly]
|
||||
|
||||
def get_bounds(self) -> numpy.ndarray:
|
||||
rot_radii = numpy.dot(rotation_matrix_2d(self.rotation), self.radii)
|
||||
return numpy.vstack((self.offset - rot_radii[0],
|
||||
self.offset + rot_radii[1]))
|
||||
|
||||
def rotate(self, theta: float) -> 'Ellipse':
|
||||
self.rotation += theta
|
||||
return self
|
||||
|
||||
def scale_by(self, c: float) -> 'Ellipse':
|
||||
self.radii *= c
|
||||
return self
|
||||
|
||||
def normalized_form(self, norm_value: float) -> normalized_shape_tuple:
|
||||
if self.radius_x < self.radius_y:
|
||||
radii = self.radii / self.radius_x
|
||||
scale = self.radius_x
|
||||
angle = self.rotation
|
||||
else:
|
||||
radii = self.radii[::-1] / self.radius_y
|
||||
scale = self.radius_y
|
||||
angle = (self.rotation + pi / 2) % pi
|
||||
return (type(self), radii, self.layer), \
|
||||
(self.offset, scale/norm_value, angle, self.dose), \
|
||||
lambda: Ellipse(radii=radii*norm_value, layer=self.layer)
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue