Docstring format change

(new param and return format)
Also some minor code formatting fixes in utils
This commit is contained in:
jan 2020-02-17 21:02:53 -08:00
commit 5adabfd25a
16 changed files with 844 additions and 496 deletions

View file

@ -24,19 +24,28 @@ class Arc(Shape):
__slots__ = ('_radii', '_angles', '_width', '_rotation',
'poly_num_points', 'poly_max_arclen')
_radii: numpy.ndarray
_angles: numpy.ndarray
_width: float
""" Two radii for defining an ellipse """
_rotation: float
""" Rotation (ccw, radians) from the x axis to the first radius """
_angles: numpy.ndarray
""" Start and stop angles (ccw, radians) for choosing an arc from the ellipse, measured from the first radius """
_width: float
""" Width of the arc """
poly_num_points: int
""" Sets the default number of points for `.polygonize()` """
poly_max_arclen: float
""" Sets the default max segement length for `.polygonize()` """
# radius properties
@property
def radii(self) -> numpy.ndarray:
"""
Return the radii [rx, ry]
:return: [rx, ry]
Return the radii `[rx, ry]`
"""
return self._radii
@ -73,10 +82,11 @@ class Arc(Shape):
@property
def angles(self) -> vector2:
"""
Return the start and stop angles [a_start, a_stop].
Return the start and stop angles `[a_start, a_stop]`.
Angles are measured from x-axis after rotation
:return: [a_start, a_stop]
Returns:
`[a_start, a_stop]`
"""
return self._angles
@ -109,7 +119,8 @@ class Arc(Shape):
"""
Rotation of radius_x from x_axis, counterclockwise, in radians. Stored mod 2*pi
:return: rotation counterclockwise in radians
Returns:
rotation counterclockwise in radians
"""
return self._rotation
@ -125,7 +136,8 @@ class Arc(Shape):
"""
Width of the arc (difference between inner and outer radii)
:return: width
Returns:
width
"""
return self._width
@ -225,12 +237,12 @@ class Arc(Shape):
def get_bounds(self) -> numpy.ndarray:
'''
Equation for rotated ellipse is
x = x0 + a * cos(t) * cos(rot) - b * sin(t) * sin(phi)
y = y0 + a * cos(t) * sin(rot) + b * sin(t) * cos(rot)
where t is our parameter.
`x = x0 + a * cos(t) * cos(rot) - b * sin(t) * sin(phi)`
`y = y0 + a * cos(t) * sin(rot) + b * sin(t) * cos(rot)`
where `t` is our parameter.
Differentiating and solving for 0 slope wrt. t, we find
tan(t) = -+ b/a cot(phi)
Differentiating and solving for 0 slope wrt. `t`, we find
`tan(t) = -+ b/a cot(phi)`
where -+ is for x, y cases, so that's where the extrema are.
If the extrema are innaccessible due to arc constraints, check the arc endpoints instead.
@ -329,8 +341,11 @@ class Arc(Shape):
def get_cap_edges(self) -> numpy.ndarray:
'''
:returns: [[[x0, y0], [x1, y1]], array of 4 points, specifying the two cuts which
[[x2, y2], [x3, y3]]], would create this arc from its corresponding ellipse.
Returns:
```
[[[x0, y0], [x1, y1]], array of 4 points, specifying the two cuts which
[[x2, y2], [x3, y3]]], would create this arc from its corresponding ellipse.
```
'''
a_ranges = self._angles_to_parameters()
@ -356,8 +371,9 @@ class Arc(Shape):
def _angles_to_parameters(self) -> numpy.ndarray:
'''
:return: "Eccentric anomaly" parameter ranges for the inner and outer edges, in the form
[[a_min_inner, a_max_inner], [a_min_outer, a_max_outer]]
Returns:
"Eccentric anomaly" parameter ranges for the inner and outer edges, in the form
`[[a_min_inner, a_max_inner], [a_min_outer, a_max_outer]]`
'''
a = []
for sgn in (-1, +1):

View file

@ -17,16 +17,19 @@ class Circle(Shape):
"""
__slots__ = ('_radius', 'poly_num_points', 'poly_max_arclen')
_radius: float
""" Circle radius """
poly_num_points: int
""" Sets the default number of points for `.polygonize()` """
poly_max_arclen: float
""" Sets the default max segement length for `.polygonize()` """
# radius property
@property
def radius(self) -> float:
"""
Circle's radius (float, >= 0)
:return: radius
"""
return self._radius

View file

@ -20,17 +20,22 @@ class Ellipse(Shape):
__slots__ = ('_radii', '_rotation',
'poly_num_points', 'poly_max_arclen')
_radii: numpy.ndarray
""" Ellipse radii """
_rotation: float
""" Angle from x-axis to first radius (ccw, radians) """
poly_num_points: int
""" Sets the default number of points for `.polygonize()` """
poly_max_arclen: float
""" Sets the default max segement length for `.polygonize()` """
# radius properties
@property
def radii(self) -> numpy.ndarray:
"""
Return the radii [rx, ry]
:return: [rx, ry]
Return the radii `[rx, ry]`
"""
return self._radii
@ -70,7 +75,8 @@ class Ellipse(Shape):
Rotation of rx from the x axis. Uses the interval [0, pi) in radians (counterclockwise
is positive)
:return: counterclockwise rotation in radians
Returns:
counterclockwise rotation in radians
"""
return self._rotation

View file

@ -37,8 +37,6 @@ class Path(Shape):
def width(self) -> float:
"""
Path width (float, >= 0)
:return: width
"""
return self._width
@ -55,8 +53,6 @@ class Path(Shape):
def cap(self) -> 'Path.Cap':
"""
Path end-cap
:return: Path.Cap enum
"""
return self._cap
@ -74,9 +70,10 @@ class Path(Shape):
@property
def cap_extensions(self) -> numpy.ndarray or None:
"""
Path end-cap extensionf
Path end-cap extension
:return: 2-element ndarray or None
Returns:
2-element ndarray or `None`
"""
return self._cap_extensions
@ -96,9 +93,7 @@ class Path(Shape):
@property
def vertices(self) -> numpy.ndarray:
"""
Vertices of the path (Nx2 ndarray: [[x0, y0], [x1, y1], ...]
:return: vertices
Vertices of the path (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`)
"""
return self._vertices
@ -194,22 +189,25 @@ class Path(Shape):
Build a path by specifying the turn angles and travel distances
rather than setting the distances directly.
:param travel_pairs: A list of (angle, distance) pairs that define
the path. Angles are counterclockwise, in radians, and are relative
to the previous segment's direction (the initial angle is relative
to the +x axis).
:param width: Path width, default 0
:param cap: End-cap type, default Path.Cap.Flush (no end-cap)
:param cap_extensions: End-cap extension distances, when using Path.Cap.CustomSquare.
Default (0, 0) or None, depending on cap type
:param offset: Offset, default (0, 0)
:param rotation: Rotation counterclockwise, in radians. Default 0
:param mirrored: Whether to mirror across the x or y axes. For example,
mirrored=(True, False) results in a reflection across the x-axis,
multiplying the path's y-coordinates by -1. Default (False, False)
:param layer: Layer, default 0
:param dose: Dose, default 1.0
:return: The resulting Path object
Args:
travel_pairs: A list of (angle, distance) pairs that define
the path. Angles are counterclockwise, in radians, and are relative
to the previous segment's direction (the initial angle is relative
to the +x axis).
width: Path width, default `0`
cap: End-cap type, default `Path.Cap.Flush` (no end-cap)
cap_extensions: End-cap extension distances, when using `Path.Cap.CustomSquare`.
Default `(0, 0)` or `None`, depending on cap type
offset: Offset, default `(0, 0)`
rotation: Rotation counterclockwise, in radians. Default `0`
mirrored: Whether to mirror across the x or y axes. For example,
`mirrored=(True, False)` results in a reflection across the x-axis,
multiplying the path's y-coordinates by -1. Default `(False, False)`
layer: Layer, default `0`
dose: Dose, default `1.0`
Returns:
The resulting Path object
"""
#TODO: needs testing
direction = numpy.array([1, 0])
@ -359,7 +357,8 @@ class Path(Shape):
"""
Removes duplicate, co-linear and otherwise redundant vertices.
:returns: self
Returns:
self
"""
self.remove_colinear_vertices()
return self
@ -368,7 +367,8 @@ class Path(Shape):
'''
Removes all consecutive duplicate (repeated) vertices.
:returns: self
Returns:
self
'''
self.vertices = remove_duplicate_vertices(self.vertices, closed_path=False)
return self
@ -377,7 +377,8 @@ class Path(Shape):
'''
Removes consecutive co-linear vertices.
:returns: self
Returns:
self
'''
self.vertices = remove_colinear_vertices(self.vertices, closed_path=False)
return self

View file

@ -16,18 +16,17 @@ class Polygon(Shape):
A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an
implicitly-closed boundary, and an offset.
A normalized_form(...) is available, but can be quite slow with lots of vertices.
A `normalized_form(...)` is available, but can be quite slow with lots of vertices.
"""
__slots__ = ('_vertices',)
_vertices: numpy.ndarray
""" Nx2 ndarray of vertices `[[x0, y0], [x1, y1], ...]` """
# vertices property
@property
def vertices(self) -> numpy.ndarray:
"""
Vertices of the polygon (Nx2 ndarray: [[x0, y0], [x1, y1], ...]
:return: vertices
Vertices of the polygon (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`)
"""
return self._vertices
@ -107,12 +106,15 @@ class Polygon(Shape):
"""
Draw a square given side_length, centered on the origin.
: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
Args:
side_length: Length of one side
rotation: Rotation counterclockwise, in radians
offset: Offset, default `(0, 0)`
layer: Layer, default `0`
dose: Dose, default `1.0`
Returns:
A Polygon object containing the requested square
"""
norm_square = numpy.array([[-1, -1],
[-1, +1],
@ -134,13 +136,16 @@ class Polygon(Shape):
"""
Draw a rectangle with side lengths lx and ly, centered on the origin.
: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
Args:
lx: Length along x (before rotation)
ly: Length along y (before rotation)
rotation: Rotation counterclockwise, in radians
offset: Offset, default `(0, 0)`
layer: Layer, default `0`
dose: Dose, default `1.0`
Returns:
A Polygon object containing the requested rectangle
"""
vertices = 0.5 * numpy.array([[-lx, -ly],
[-lx, +ly],
@ -168,17 +173,20 @@ class Polygon(Shape):
Must provide 2 of (xmin, xctr, xmax, lx),
and 2 of (ymin, yctr, ymax, ly).
:param xmin: Minimum x coordinate
:param xctr: Center x coordinate
:param xmax: Maximum x coordinate
:param lx: Length along x direction
:param ymin: Minimum y coordinate
:param yctr: Center y coordinate
:param ymax: Maximum y coordinate
:param ly: Length along y direction
:param layer: Layer, default 0
:param dose: Dose, default 1.0
:return: A Polygon object containing the requested rectangle
Args:
xmin: Minimum x coordinate
xctr: Center x coordinate
xmax: Maximum x coordinate
lx: Length along x direction
ymin: Minimum y coordinate
yctr: Center y coordinate
ymax: Maximum y coordinate
ly: Length along y direction
layer: Layer, default `0`
dose: Dose, default `1.0`
Returns:
A Polygon object containing the requested rectangle
"""
if lx is None:
if xctr is None:
@ -278,7 +286,8 @@ class Polygon(Shape):
"""
Removes duplicate, co-linear and otherwise redundant vertices.
:returns: self
Returns:
self
"""
self.remove_colinear_vertices()
return self
@ -287,7 +296,8 @@ class Polygon(Shape):
'''
Removes all consecutive duplicate (repeated) vertices.
:returns: self
Returns:
self
'''
self.vertices = remove_duplicate_vertices(self.vertices, closed_path=True)
return self
@ -296,7 +306,8 @@ class Polygon(Shape):
'''
Removes consecutive co-linear vertices.
:returns: self
Returns:
self
'''
self.vertices = remove_colinear_vertices(self.vertices, closed_path=True)
return self

View file

@ -26,12 +26,20 @@ class Shape(metaclass=ABCMeta):
"""
__slots__ = ('_offset', '_layer', '_dose', 'identifier', 'locked')
_offset: numpy.ndarray # [x_offset, y_offset]
_layer: int or Tuple # Layer (integer >= 0 or tuple)
_dose: float # Dose
identifier: Tuple # An arbitrary identifier for the shape,
# usually empty but used by Pattern.flatten()
locked: bool # If True, any changes to the shape will raise a PatternLockedError
_offset: numpy.ndarray
""" `[x_offset, y_offset]` """
_layer: int or Tuple
""" Layer (integer >= 0 or tuple) """
_dose: float
""" Dose """
identifier: Tuple
""" An arbitrary identifier for the shape, usually empty but used by `Pattern.flatten()` """
locked: bool
""" If `True`, any changes to the shape will raise a `PatternLockedError` """
def __setattr__(self, name, value):
if self.locked and name != 'locked':
@ -51,31 +59,35 @@ class Shape(metaclass=ABCMeta):
"""
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
Args:
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.
max_arclen: Maximum arclength which can be approximated by a single line
segment. Optional, defaults to shapes' internal defaults.
Returns:
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]]
Returns `[[x_min, y_min], [x_max, y_max]]` which specify a minimal bounding box for the shape.
"""
pass
@abstractmethod
def rotate(self, theta: float) -> 'Shape':
"""
Rotate the shape around its center (0, 0), ignoring its offset.
Rotate the shape around its origin (0, 0), ignoring its offset.
:param theta: Angle to rotate by (counterclockwise, radians)
:return: self
Args:
theta: Angle to rotate by (counterclockwise, radians)
Returns:
self
"""
pass
@ -84,8 +96,12 @@ class Shape(metaclass=ABCMeta):
"""
Mirror the shape across an axis.
:param axis: Axis to mirror across.
:return: self
Args:
axis: Axis to mirror across.
(0: mirror across x axis, 1: mirror across y axis)
Returns:
self
"""
pass
@ -94,8 +110,11 @@ class Shape(metaclass=ABCMeta):
"""
Scale the shape's size (eg. radius, for a circle) by a constant factor.
:param c: Factor to scale by
:return: self
Args:
c: Factor to scale by
Returns:
self
"""
pass
@ -105,18 +124,21 @@ class Shape(metaclass=ABCMeta):
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 the shape;
Args:
norm_value: This value is used to normalize lengths intrinsic to the shape;
eg. for a circle, the returned intrinsic radius value will be (radius / norm_value), and
the returned callable will create a Circle(radius=norm_value, ...). This is useful
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:
intrinsic: A tuple of basic types containing all information about the instance that
is not contained in 'extrinsic'. Usually, intrinsic[0] == type(self).
extrinsic: ([x_offset, y_offset], scale, rotation, mirror_across_x_axis, dose)
constructor: A callable (no arguments) which returns an instance of type(self) with
internal state equivalent to 'intrinsic'.
Returns:
The returned information takes the form of a 3-element tuple,
`(intrinsic, extrinsic, constructor)`. These are further broken down as:
`intrinsic`: A tuple of basic types containing all information about the instance that
is not contained in 'extrinsic'. Usually, `intrinsic[0] == type(self)`.
`extrinsic`: `([x_offset, y_offset], scale, rotation, mirror_across_x_axis, dose)`
`constructor`: A callable (no arguments) which returns an instance of `type(self)` with
internal state equivalent to `intrinsic`.
"""
pass
@ -126,8 +148,6 @@ class Shape(metaclass=ABCMeta):
def offset(self) -> numpy.ndarray:
"""
[x, y] offset
:return: [x_offset, y_offset]
"""
return self._offset
@ -145,8 +165,6 @@ class Shape(metaclass=ABCMeta):
def layer(self) -> int or Tuple[int]:
"""
Layer number (int or tuple of ints)
:return: Layer
"""
return self._layer
@ -159,8 +177,6 @@ class Shape(metaclass=ABCMeta):
def dose(self) -> float:
"""
Dose (float >= 0)
:return: Dose value
"""
return self._dose
@ -177,7 +193,8 @@ class Shape(metaclass=ABCMeta):
"""
Returns a deep copy of the shape.
:return: Deep copy of self
Returns:
copy.deepcopy(self)
"""
return copy.deepcopy(self)
@ -185,8 +202,11 @@ class Shape(metaclass=ABCMeta):
"""
Translate the shape by the given offset
:param offset: [x_offset, y,offset]
:return: self
Args:
offset: [x_offset, y,offset]
Returns:
self
"""
self.offset += offset
return self
@ -195,9 +215,12 @@ class Shape(metaclass=ABCMeta):
"""
Rotate the shape around a point.
:param pivot: Point (x, y) to rotate around
:param rotation: Angle to rotate by (counterclockwise, radians)
:return: self
Args:
pivot: Point (x, y) to rotate around
rotation: Angle to rotate by (counterclockwise, radians)
Returns:
self
"""
pivot = numpy.array(pivot, dtype=float)
self.translate(-pivot)
@ -214,14 +237,17 @@ class Shape(metaclass=ABCMeta):
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()
1) Converting the shape to polygons using `.to_polygons()`
2) Approximating each edge with an equivalent Manhattan edge
This process results in a reasonable Manhattan representation of the shape, but is
imprecise near non-Manhattan or off-grid corners.
: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.
Args:
grid_x: List of allowed x-coordinates for the Manhattanized polygon edges.
grid_y: List of allowed y-coordinates for the Manhattanized polygon edges.
Returns:
List of `Polygon` objects with grid-aligned edges.
"""
from . import Polygon
@ -319,7 +345,7 @@ class Shape(metaclass=ABCMeta):
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()
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
@ -328,7 +354,7 @@ class Shape(metaclass=ABCMeta):
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
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.
@ -336,19 +362,22 @@ class Shape(metaclass=ABCMeta):
equidistant from allowed edge location.
Implementation notes:
i) Rasterization is performed using float_raster, giving a high-precision anti-aliased
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,
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
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.
Args:
grid_x: List of allowed x-coordinates for the Manhattanized polygon edges.
grid_y: List of allowed y-coordinates for the Manhattanized polygon edges.
Returns:
List of `Polygon` objects with grid-aligned edges.
"""
from . import Polygon
import skimage.measure
@ -403,9 +432,10 @@ class Shape(metaclass=ABCMeta):
def lock(self) -> 'Shape':
"""
Lock the Shape
Lock the Shape, disallowing further changes
:return: self
Returns:
self
"""
object.__setattr__(self, 'locked', True)
return self
@ -414,7 +444,8 @@ class Shape(metaclass=ABCMeta):
"""
Unlock the Shape
:return: self
Returns:
self
"""
object.__setattr__(self, 'locked', False)
return self

View file

@ -173,12 +173,15 @@ def get_char_as_polygons(font_path: str,
The output is normalized so that the font size is 1 unit.
:param font_path: File path specifying a font loadable by freetype
:param char: Character to convert to polygons
:param resolution: Internal resolution setting (used for freetype
Face.set_font_size(resolution)). Modify at your own peril!
:return: List of polygons [[[x0, y0], [x1, y1], ...], ...] and 'advance' distance (distance
from the start of this glyph to the start of the next one)
Args:
font_path: File path specifying a font loadable by freetype
char: Character to convert to polygons
resolution: Internal resolution setting (used for freetype
`Face.set_font_size(resolution))`. Modify at your own peril!
Returns:
List of polygons `[[[x0, y0], [x1, y1], ...], ...]` and
'advance' distance (distance from the start of this glyph to the start of the next one)
"""
if len(char) != 1:
raise Exception('get_char_as_polygons called with non-char')