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
parent 20981f10b9
commit 5adabfd25a
16 changed files with 845 additions and 497 deletions

View File

@ -6,21 +6,24 @@
with some vectorized element types (eg. circles, not just polygons), better support for
E-beam doses, and the ability to output to multiple formats.
Pattern is a basic object containing a 2D lithography mask, composed of a list of Shape
objects and a list of SubPattern objects.
`Pattern` is a basic object containing a 2D lithography mask, composed of a list of `Shape`
objects, a list of `Label` objects, and a list of references to other `Patterns` (using
`SubPattern` and `GridRepetition`).
SubPattern provides basic support for nesting Pattern objects within each other, by adding
`SubPattern` provides basic support for nesting `Pattern` objects within each other, by adding
offset, rotation, scaling, and other such properties to a Pattern reference.
`GridRepetition` provides support for nesting regular arrays of `Pattern` objects.
Note that the methods for these classes try to avoid copying wherever possible, so unless
otherwise noted, assume that arguments are stored by-reference.
Dependencies:
- numpy
- matplotlib [Pattern.visualize(...)]
- python-gdsii [masque.file.gdsii]
- svgwrite [masque.file.svg]
- `numpy`
- `matplotlib` [Pattern.visualize(...)]
- `python-gdsii` [masque.file.gdsii]
- `svgwrite` [masque.file.svg]
"""
import pathlib

View File

@ -49,36 +49,37 @@ def write(patterns: Pattern or List[Pattern],
modify_originals: bool = False,
disambiguate_func: Callable[[List[Pattern]], None] = None):
"""
Write a Pattern or list of patterns to a GDSII file, by first calling
.polygonize() to change the shapes into polygons, and then writing patterns
Write a `Pattern` or list of patterns to a GDSII file, by first calling
`.polygonize()` to change the shapes into polygons, and then writing patterns
as GDSII structures, polygons as boundary elements, and subpatterns as structure
references (sref).
For each shape,
layer is chosen to be equal to shape.layer if it is an int,
or shape.layer[0] if it is a tuple
datatype is chosen to be shape.layer[1] if available,
otherwise 0
layer is chosen to be equal to `shape.layer` if it is an int,
or `shape.layer[0]` if it is a tuple
datatype is chosen to be `shape.layer[1]` if available,
otherwise `0`
It is often a good idea to run pattern.subpatternize() prior to calling this function,
especially if calling .polygonize() will result in very many vertices.
It is often a good idea to run `pattern.subpatternize()` prior to calling this function,
especially if calling `.polygonize()` will result in very many vertices.
If you want pattern polygonized with non-default arguments, just call pattern.polygonize()
If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()`
prior to calling this function.
:param patterns: A Pattern or list of patterns to write to file.
:param file: Filename or stream object to write to.
:param meters_per_unit: Written into the GDSII file, meters per (database) length unit.
Args:
patterns: A Pattern or list of patterns to write to file.
file: Filename or stream object to write to.
meters_per_unit: Written into the GDSII file, meters per (database) length unit.
All distances are assumed to be an integer multiple of this unit, and are stored as such.
:param logical_units_per_unit: Written into the GDSII file. Allows the GDSII to specify a
logical_units_per_unit: Written into the GDSII file. Allows the GDSII to specify a
"logical" unit which is different from the "database" unit, for display purposes.
Default 1.
:param library_name: Library name written into the GDSII file.
Default `1`.
library_name: Library name written into the GDSII file.
Default 'masque-gdsii-write'.
:param modify_originals: If True, the original pattern is modified as part of the writing
process. Otherwise, a copy is made and deepunlock()-ed.
Default False.
:param disambiguate_func: Function which takes a list of patterns and alters them
modify_originals: If `True`, the original pattern is modified as part of the writing
process. Otherwise, a copy is made and `deepunlock()`-ed.
Default `False`.
disambiguate_func: Function which takes a list of patterns and alters them
to make their names valid and unique. Default is `disambiguate_pattern_names`, which
attempts to adhere to the GDSII standard as well as possible.
WARNING: No additional error checking is performed on the results.
@ -124,9 +125,15 @@ def writefile(patterns: List[Pattern] or Pattern,
**kwargs,
):
"""
Wrapper for gdsii.write() that takes a filename or path instead of a stream.
Wrapper for `gdsii.write()` that takes a filename or path instead of a stream.
Will automatically compress the file if it has a .gz suffix.
Args:
patterns: `Pattern` or list of patterns to save
filename: Filename to save to.
*args: passed to `gdsii.write`
**kwargs: passed to `gdsii.write`
"""
path = pathlib.Path(filename)
if path.suffix == '.gz':
@ -153,8 +160,11 @@ def dose2dtype(patterns: List[Pattern],
Note that this function modifies the input Pattern(s).
:param patterns: A Pattern or list of patterns to write to file. Modified by this function.
:returns: (patterns, dose_list)
Args:
patterns: A `Pattern` or list of patterns to write to file. Modified by this function.
Returns:
(patterns, dose_list)
patterns: modified input patterns
dose_list: A list of doses, providing a mapping between datatype (int, list index)
and dose (float, list entry).
@ -221,9 +231,14 @@ def readfile(filename: str or pathlib.Path,
**kwargs,
) -> (Dict[str, Pattern], Dict[str, Any]):
"""
Wrapper for gdsii.read() that takes a filename or path instead of a stream.
Wrapper for `gdsii.read()` that takes a filename or path instead of a stream.
Tries to autodetermine file type based on suffixes
Will automatically decompress files with a .gz suffix.
Args:
filename: Filename to save to.
*args: passed to `gdsii.read`
**kwargs: passed to `gdsii.read`
"""
path = pathlib.Path(filename)
if path.suffix == '.gz':
@ -251,14 +266,18 @@ def read(stream: io.BufferedIOBase,
'logical_units_per_unit': number of "logical" units displayed by layout tools (typically microns)
per database unit
:param filename: Filename specifying a GDSII file to read from.
:param use_dtype_as_dose: If false, set each polygon's layer to (gds_layer, gds_datatype).
If true, set the layer to gds_layer and the dose to gds_datatype.
Default False.
:param clean_vertices: If true, remove any redundant vertices when loading polygons.
Args:
filename: Filename specifying a GDSII file to read from.
use_dtype_as_dose: If `False`, set each polygon's layer to `(gds_layer, gds_datatype)`.
If `True`, set the layer to `gds_layer` and the dose to `gds_datatype`.
Default `False`.
clean_vertices: If `True`, remove any redundant vertices when loading polygons.
The cleaning process removes any polygons with zero area or <3 vertices.
Default True.
:return: Tuple: (Dict of pattern_name:Patterns generated from GDSII structures, Dict of GDSII library info)
Default `True`.
Returns:
- Dict of pattern_name:Patterns generated from GDSII structures
- Dict of GDSII library info
"""
lib = gdsii.library.Library.load(stream)
@ -353,6 +372,7 @@ def read(stream: io.BufferedIOBase,
def _mlayer2gds(mlayer):
""" Helper to turn a layer tuple-or-int into a layer and datatype"""
if is_scalar(mlayer):
layer = mlayer
data_type = 0
@ -366,12 +386,15 @@ def _mlayer2gds(mlayer):
def _sref_to_subpat(element: gdsii.elements.SRef) -> SubPattern:
# Helper function to create a SubPattern from an SREF. Sets subpat.pattern to None
# and sets the instance .identifier to the struct_name.
#
# BUG: "Absolute" means not affected by parent elements.
# That's not currently supported by masque at all, so need to either tag it and
# undo the parent transformations, or implement it in masque.
"""
Helper function to create a SubPattern from an SREF. Sets subpat.pattern to None
and sets the instance .identifier to the struct_name.
BUG:
"Absolute" means not affected by parent elements.
That's not currently supported by masque at all, so need to either tag it and
undo the parent transformations, or implement it in masque.
"""
subpat = SubPattern(pattern=None, offset=element.xy)
subpat.identifier = element.struct_name
if element.strans is not None:
@ -394,13 +417,15 @@ def _sref_to_subpat(element: gdsii.elements.SRef) -> SubPattern:
def _aref_to_gridrep(element: gdsii.elements.ARef) -> GridRepetition:
# Helper function to create a GridRepetition from an AREF. Sets gridrep.pattern to None
# and sets the instance .identifier to the struct_name.
#
# BUG: "Absolute" means not affected by parent elements.
# That's not currently supported by masque at all, so need to either tag it and
# undo the parent transformations, or implement it in masque.i
"""
Helper function to create a GridRepetition from an AREF. Sets gridrep.pattern to None
and sets the instance .identifier to the struct_name.
BUG:
"Absolute" means not affected by parent elements.
That's not currently supported by masque at all, so need to either tag it and
undo the parent transformations, or implement it in masque.
"""
rotation = 0
offset = numpy.array(element.xy[0])
scale = 1

View File

@ -23,20 +23,21 @@ def writefile(pattern: Pattern,
Note that this function modifies the Pattern.
If custom_attributes is True, non-standard pattern_layer and pattern_dose attributes
If `custom_attributes` is `True`, non-standard `pattern_layer` and `pattern_dose` attributes
are written to the relevant elements.
It is often a good idea to run pattern.subpatternize() on pattern prior to
calling this function, especially if calling .polygonize() will result in very
It is often a good idea to run `pattern.subpatternize()` on pattern prior to
calling this function, especially if calling `.polygonize()` will result in very
many vertices.
If you want pattern polygonized with non-default arguments, just call pattern.polygonize()
If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()`
prior to calling this function.
:param pattern: Pattern to write to file. Modified by this function.
:param filename: Filename to write to.
:param custom_attributes: Whether to write non-standard pattern_layer and
pattern_dose attributes to the SVG elements.
Args:
pattern: Pattern to write to file. Modified by this function.
filename: Filename to write to.
custom_attributes: Whether to write non-standard `pattern_layer` and
`pattern_dose` attributes to the SVG elements.
"""
# Polygonize pattern
@ -85,18 +86,19 @@ def writefile(pattern: Pattern,
def writefile_inverted(pattern: Pattern, filename: str):
"""
Write an inverted Pattern to an SVG file, by first calling .polygonize() and
.flatten() on it to change the shapes into polygons, then drawing a bounding
Write an inverted Pattern to an SVG file, by first calling `.polygonize()` and
`.flatten()` on it to change the shapes into polygons, then drawing a bounding
box and drawing the polygons with reverse vertex order inside it, all within
one <path> element.
one `<path>` element.
Note that this function modifies the Pattern.
If you want pattern polygonized with non-default arguments, just call pattern.polygonize()
If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()`
prior to calling this function.
:param pattern: Pattern to write to file. Modified by this function.
:param filename: Filename to write to.
Args:
pattern: Pattern to write to file. Modified by this function.
filename: Filename to write to.
"""
# Polygonize and flatten pattern
pattern.polygonize().flatten()
@ -129,8 +131,11 @@ def poly2path(vertices: numpy.ndarray) -> str:
"""
Create an SVG path string from an Nx2 list of vertices.
:param vertices: Nx2 array of vertices.
:return: SVG path-string.
Args:
vertices: Nx2 array of vertices.
Returns:
SVG path-string.
"""
commands = 'M{:g},{:g} '.format(vertices[0][0], vertices[0][1])
for vertex in vertices[1:]:

View File

@ -12,11 +12,14 @@ __author__ = 'Jan Petykiewicz'
def mangle_name(pattern: Pattern, dose_multiplier: float=1.0) -> str:
"""
Create a name using pattern.name, id(pattern), and the dose multiplier.
Create a name using `pattern.name`, `id(pattern)`, and the dose multiplier.
:param pattern: Pattern whose name we want to mangle.
:param dose_multiplier: Dose multiplier to mangle with.
:return: Mangled name.
Args:
pattern: Pattern whose name we want to mangle.
dose_multiplier: Dose multiplier to mangle with.
Returns:
Mangled name.
"""
expression = re.compile('[^A-Za-z0-9_\?\$]')
full_name = '{}_{}_{}'.format(pattern.name, dose_multiplier, id(pattern))
@ -26,11 +29,14 @@ def mangle_name(pattern: Pattern, dose_multiplier: float=1.0) -> str:
def make_dose_table(patterns: List[Pattern], dose_multiplier: float=1.0) -> Set[Tuple[int, float]]:
"""
Create a set containing (id(pat), written_dose) for each pattern (including subpatterns)
Create a set containing `(id(pat), written_dose)` for each pattern (including subpatterns)
:param pattern: Source Patterns.
:param dose_multiplier: Multiplier for all written_dose entries.
:return: {(id(subpat.pattern), written_dose), ...}
Args:
pattern: Source Patterns.
dose_multiplier: Multiplier for all written_dose entries.
Returns:
`{(id(subpat.pattern), written_dose), ...}`
"""
dose_table = {(id(pattern), dose_multiplier) for pattern in patterns}
for pattern in patterns:

View File

@ -15,19 +15,21 @@ class Label:
A text annotation with a position and layer (but no size; it is not drawn)
"""
__slots__ = ('_offset', '_layer', '_string', 'identifier', 'locked')
# [x_offset, y_offset]
_offset: numpy.ndarray
""" [x_offset, y_offset] """
# Layer (integer >= 0) or 2-Tuple of integers
_layer: int or Tuple
""" Layer (integer >= 0, or 2-Tuple of integers) """
# Label string
_string: str
""" Label string """
# Arbitrary identifier tuple
identifier: Tuple
""" Arbitrary identifier tuple, useful for keeping track of history when flattening """
locked: bool # If True, any changes to the label will raise a PatternLockedError
locked: bool
""" If `True`, any changes to the label will raise a `PatternLockedError` """
def __setattr__(self, name, value):
if self.locked and name != 'locked':
@ -40,8 +42,6 @@ class Label:
def offset(self) -> numpy.ndarray:
"""
[x, y] offset
:return: [x_offset, y_offset]
"""
return self._offset
@ -59,8 +59,6 @@ class Label:
def layer(self) -> int or Tuple[int]:
"""
Layer number (int or tuple of ints)
:return: Layer
"""
return self._layer
@ -73,8 +71,6 @@ class Label:
def string(self) -> str:
"""
Label string (str)
:return: string
"""
return self._string
@ -109,29 +105,33 @@ class Label:
def copy(self) -> 'Label':
"""
Returns a deep copy of the shape.
:return: Deep copy of self
Returns a deep copy of the label.
"""
return copy.deepcopy(self)
def translate(self, offset: vector2) -> 'Label':
"""
Translate the shape by the given offset
Translate the label 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
def rotate_around(self, pivot: vector2, rotation: float) -> 'Label':
"""
Rotate the shape around a point.
Rotate the label 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)
@ -147,24 +147,27 @@ class Label:
bounds = [self.offset,
self.offset]
:return: Bounds [[xmin, xmax], [ymin, ymax]]
Returns:
Bounds [[xmin, xmax], [ymin, ymax]]
"""
return numpy.array([self.offset, self.offset])
def lock(self) -> 'Label':
"""
Lock the Label
Lock the Label, causing any modifications to raise an exception.
:return: self
Return:
self
"""
object.__setattr__(self, 'locked', True)
return self
def unlock(self) -> 'Label':
"""
Unlock the Label
Unlock the Label, re-allowing changes.
:return: self
Return:
self
"""
object.__setattr__(self, 'locked', False)
return self

View File

@ -27,22 +27,32 @@ visitor_function_t = Callable[['Pattern', Tuple['Pattern'], Dict, numpy.ndarray]
class Pattern:
"""
2D layout consisting of some set of shapes and references to other Pattern objects
(via SubPattern). Shapes are assumed to inherit from .shapes.Shape or provide equivalent
functions.
:var shapes: List of all shapes in this Pattern. Elements in this list are assumed to inherit
from Shape or provide equivalent functions.
:var subpatterns: List of all SubPattern objects in this Pattern. Multiple SubPattern objects
may reference the same Pattern object.
:var name: An identifier for this object. Not necessarily unique.
2D layout consisting of some set of shapes, labels, and references to other Pattern objects
(via SubPattern and GridRepetition). Shapes are assumed to inherit from
masque.shapes.Shape or provide equivalent functions.
"""
__slots__ = ('shapes', 'labels', 'subpatterns', 'name', 'locked')
shapes: List[Shape]
""" List of all shapes in this Pattern.
Elements in this list are assumed to inherit from Shape or provide equivalent functions.
"""
labels: List[Label]
""" List of all labels in this Pattern. """
subpatterns: List[SubPattern or GridRepetition]
""" List of all objects referencing other patterns in this Pattern.
Examples are SubPattern (gdsii "instances") or GridRepetition (gdsii "arrays")
Multiple objects in this list may reference the same Pattern object
(multiple instances of the same object).
"""
name: str
""" A name for this pattern """
locked: bool
""" When the pattern is locked, no changes may be made. """
def __init__(self,
name: str = '',
@ -55,11 +65,12 @@ class Pattern:
Basic init; arguments get assigned to member variables.
Non-list inputs for shapes and subpatterns get converted to lists.
:param shapes: Initial shapes in the Pattern
:param labels: Initial labels in the Pattern
:param subpatterns: Initial subpatterns in the Pattern
:param name: An identifier for the Pattern
:param locked: Whether to lock the pattern after construction
Args:
shapes: Initial shapes in the Pattern
labels: Initial labels in the Pattern
subpatterns: Initial subpatterns in the Pattern
name: An identifier for the Pattern
locked: Whether to lock the pattern after construction
"""
self.unlock()
if isinstance(shapes, list):
@ -106,8 +117,11 @@ class Pattern:
Appends all shapes, labels and subpatterns from other_pattern to self's shapes,
labels, and supbatterns.
:param other_pattern: The Pattern to append
:return: self
Args:
other_pattern: The Pattern to append
Returns:
self
"""
self.subpatterns += other_pattern.subpatterns
self.shapes += other_pattern.shapes
@ -125,15 +139,18 @@ class Pattern:
given entity_func returns True.
Self is _not_ altered, but shapes, labels, and subpatterns are _not_ copied.
:param shapes_func: Given a shape, returns a boolean denoting whether the shape is a member
Args:
shapes_func: Given a shape, returns a boolean denoting whether the shape is a member
of the subset. Default always returns False.
:param labels_func: Given a label, returns a boolean denoting whether the label is a member
labels_func: Given a label, returns a boolean denoting whether the label is a member
of the subset. Default always returns False.
:param subpatterns_func: Given a subpattern, returns a boolean denoting if it is a member
subpatterns_func: Given a subpattern, returns a boolean denoting if it is a member
of the subset. Default always returns False.
:param recursive: If True, also calls .subset() recursively on patterns referenced by this
recursive: If True, also calls .subset() recursively on patterns referenced by this
pattern.
:return: A Pattern containing all the shapes and subpatterns for which the parameter
Returns:
A Pattern containing all the shapes and subpatterns for which the parameter
functions return True
"""
def do_subset(src):
@ -163,12 +180,17 @@ class Pattern:
It is only applied to any given pattern once, regardless of how many times it is
referenced.
:param func: Function which accepts a Pattern, and returns a pattern.
:param memo: Dictionary used to avoid re-running on multiply-referenced patterns.
Stores {id(pattern): func(pattern)} for patterns which have already been processed.
Default None (no already-processed patterns).
:return: The result of applying func() to this pattern and all subpatterns.
:raises: PatternError if called on a pattern containing a circular reference.
Args:
func: Function which accepts a Pattern, and returns a pattern.
memo: Dictionary used to avoid re-running on multiply-referenced patterns.
Stores `{id(pattern): func(pattern)}` for patterns which have already been processed.
Default `None` (no already-processed patterns).
Returns:
The result of applying func() to this pattern and all subpatterns.
Raises:
PatternError if called on a pattern containing a circular reference.
"""
if memo is None:
memo = {}
@ -212,19 +234,24 @@ class Pattern:
for the instance being visited
`memo`: Arbitrary dict (not altered except by visit_*())
:param visit_before: Function to call before traversing subpatterns.
Args:
visit_before: Function to call before traversing subpatterns.
Should accept a `Pattern` and `**visit_args`, and return the (possibly modified)
pattern. Default `None` (not called).
visit_after: Function to call after traversing subpatterns.
Should accept a Pattern and **visit_args, and return the (possibly modified)
pattern. Default None (not called).
:param visit_after: Function to call after traversing subpatterns.
Should accept a Pattern and **visit_args, and return the (possibly modified)
pattern. Default None (not called).
:param transform: Initial value for `visit_args['transform']`.
pattern. Default `None` (not called).
transform: Initial value for `visit_args['transform']`.
Can be `False`, in which case the transform is not calculated.
`True` or `None` is interpreted as [0, 0, 0, 0].
:param memo: Arbitrary dict for use by visit_*() functions. Default None (empty dict).
:param hierarchy: Tuple of patterns specifying the hierarchy above the current pattern.
`True` or `None` is interpreted as `[0, 0, 0, 0]`.
memo: Arbitrary dict for use by `visit_*()` functions. Default `None` (empty dict).
hierarchy: Tuple of patterns specifying the hierarchy above the current pattern.
Appended to the start of the generated `visit_args['hierarchy']`.
Default is an empty tuple.
Returns:
The result, including `visit_before(self, ...)` and `visit_after(self, ...)`.
Note that `self` may also be altered!
"""
if memo is None:
memo = {}
@ -267,16 +294,19 @@ class Pattern:
poly_max_arclen: float = None,
) -> 'Pattern':
"""
Calls .to_polygons(...) on all the shapes in this Pattern and any referenced patterns,
Calls `.to_polygons(...)` on all the shapes in this Pattern and any referenced patterns,
replacing them with the returned polygons.
Arguments are passed directly to shape.to_polygons(...).
Arguments are passed directly to `shape.to_polygons(...)`.
:param poly_num_points: Number of points to use for each polygon. Can be overridden by
poly_max_arclen if that results in more points. Optional, defaults to shapes'
Args:
poly_num_points: Number of points to use for each polygon. Can be overridden by
`poly_max_arclen` if that results in more points. Optional, defaults to shapes'
internal defaults.
:param poly_max_arclen: Maximum arclength which can be approximated by a single line
poly_max_arclen: Maximum arclength which can be approximated by a single line
segment. Optional, defaults to shapes' internal defaults.
:return: self
Returns:
self
"""
old_shapes = self.shapes
self.shapes = list(itertools.chain.from_iterable(
@ -291,12 +321,15 @@ class Pattern:
grid_y: numpy.ndarray,
) -> 'Pattern':
"""
Calls .polygonize() and .flatten on the pattern, then calls .manhattanize() on all the
Calls `.polygonize()` and `.flatten()` on the pattern, then calls `.manhattanize()` on all the
resulting shapes, replacing them with the returned Manhattan polygons.
: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: self
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:
self
"""
self.polygonize().flatten()
@ -311,21 +344,25 @@ class Pattern:
exclude_types: Tuple[Shape] = (Polygon,)
) -> 'Pattern':
"""
Iterates through this Pattern and all referenced Patterns. Within each Pattern, it iterates
over all shapes, calling .normalized_form(norm_value) on them to retrieve a scale-,
Iterates through this `Pattern` and all referenced `Pattern`s. Within each `Pattern`, it iterates
over all shapes, calling `.normalized_form(norm_value)` on them to retrieve a scale-,
offset-, dose-, and rotation-independent form. Each shape whose normalized form appears
more than once is removed and re-added using subpattern objects referencing a newly-created
Pattern containing only the normalized form of the shape.
`Pattern` containing only the normalized form of the shape.
Note that the default norm_value was chosen to give a reasonable precision when converting
Note:
The default norm_value was chosen to give a reasonable precision when converting
to GDSII, which uses integer values for pixel coordinates.
:param recursive: Whether to call recursively on self's subpatterns. Default True.
:param norm_value: Passed to shape.normalized_form(norm_value). Default 1e6 (see function
Args:
recursive: Whether to call recursively on self's subpatterns. Default `True`.
norm_value: Passed to `shape.normalized_form(norm_value)`. Default `1e6` (see function
note about GDSII)
:param exclude_types: Shape types passed in this argument are always left untouched, for
speed or convenience. Default: (Shapes.Polygon,)
:return: self
exclude_types: Shape types passed in this argument are always left untouched, for
speed or convenience. Default: `(shapes.Polygon,)`
Returns:
self
"""
if exclude_types is None:
@ -337,9 +374,9 @@ class Pattern:
norm_value=norm_value,
exclude_types=exclude_types)
# Create a dict which uses the label tuple from .normalized_form() as a key, and which
# stores (function_to_create_normalized_shape, [(index_in_shapes, values), ...]), where
# values are the (offset, scale, rotation, dose) values as calculated by .normalized_form()
# Create a dict which uses the label tuple from `.normalized_form()` as a key, and which
# stores `(function_to_create_normalized_shape, [(index_in_shapes, values), ...])`, where
# values are the `(offset, scale, rotation, dose)` values as calculated by `.normalized_form()`
shape_table = defaultdict(lambda: [None, list()])
for i, shape in enumerate(self.shapes):
if not any((isinstance(shape, t) for t in exclude_types)):
@ -348,9 +385,9 @@ class Pattern:
shape_table[label][1].append((i, values))
# Iterate over the normalized shapes in the table. If any normalized shape occurs more than
# once, create a Pattern holding a normalized shape object, and add self.subpatterns
# once, create a `Pattern` holding a normalized shape object, and add `self.subpatterns`
# entries for each occurrence in self. Also, note down that we should delete the
# self.shapes entries for which we made SubPatterns.
# `self.shapes` entries for which we made SubPatterns.
shapes_to_remove = []
for label in shape_table:
if len(shape_table[label][1]) > 1:
@ -374,21 +411,23 @@ class Pattern:
"""
Represents the pattern as a list of polygons.
Deep-copies the pattern, then calls .polygonize() and .flatten() on the copy in order to
Deep-copies the pattern, then calls `.polygonize()` and `.flatten()` on the copy in order to
generate the list of polygons.
:return: A list of (Ni, 2) numpy.ndarrays specifying vertices of the polygons. Each ndarray
is of the form [[x0, y0], [x1, y1],...].
Returns:
A list of `(Ni, 2)` `numpy.ndarray`s specifying vertices of the polygons. Each ndarray
is of the form `[[x0, y0], [x1, y1],...]`.
"""
pat = self.deepcopy().deepunlock().polygonize().flatten()
return [shape.vertices + shape.offset for shape in pat.shapes]
def referenced_patterns_by_id(self) -> Dict[int, 'Pattern']:
"""
Create a dictionary of {id(pat): pat} for all Pattern objects referenced by this
Create a dictionary of `{id(pat): pat}` for all Pattern objects referenced by this
Pattern (operates recursively on all referenced Patterns as well)
:return: Dictionary of {id(pat): pat} for all referenced Pattern objects
Returns:
Dictionary of `{id(pat): pat}` for all referenced Pattern objects
"""
ids = {}
for subpat in self.subpatterns:
@ -399,11 +438,12 @@ class Pattern:
def get_bounds(self) -> Union[numpy.ndarray, None]:
"""
Return a numpy.ndarray containing [[x_min, y_min], [x_max, y_max]], corresponding to the
Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the
extent of the Pattern's contents in each dimension.
Returns None if the Pattern is empty.
Returns `None` if the Pattern is empty.
:return: [[x_min, y_min], [x_max, y_max]] or None
Returns:
`[[x_min, y_min], [x_max, y_max]]` or `None`
"""
entries = self.shapes + self.subpatterns + self.labels
if not entries:
@ -428,13 +468,16 @@ class Pattern:
Shape identifiers are changed to represent their original position in the
pattern hierarchy:
(L1_name (str), L1_index (int), L2_name, L2_index, ..., *original_shape_identifier)
where L1_name is the first-level subpattern's name (e.g. self.subpatterns[0].pattern.name),
L2_name is the next-level subpattern's name (e.g.
self.subpatterns[0].pattern.subpatterns[0].pattern.name) and L1_index is an integer
used to differentiate between multiple instance of the same (or same-named) subpatterns.
`(L1_name (str), L1_index (int), L2_name, L2_index, ..., *original_shape_identifier)`
where
`L1_name` is the first-level subpattern's name (e.g. `self.subpatterns[0].pattern.name`),
`L2_name` is the next-level subpattern's name (e.g.
`self.subpatterns[0].pattern.subpatterns[0].pattern.name`) and
`L1_index` is an integer used to differentiate between multiple instance ofi the same
(or same-named) subpatterns.
:return: self
Returns:
self
"""
subpatterns = copy.deepcopy(self.subpatterns)
self.subpatterns = []
@ -457,22 +500,28 @@ class Pattern:
"""
Translates all shapes, label, and subpatterns by the given offset.
:param offset: Offset to translate by
:return: self
Args:
offset: (x, y) to translate by
Returns:
self
"""
for entry in self.shapes + self.subpatterns + self.labels:
entry.translate(offset)
return self
def scale_elements(self, scale: float) -> 'Pattern':
def scale_elements(self, c: float) -> 'Pattern':
""""
Scales all shapes and subpatterns by the given value.
:param scale: value to scale by
:return: self
Args:
c: factor to scale by
Returns:
self
"""
for entry in self.shapes + self.subpatterns:
entry.scale(scale)
entry.scale(c)
return self
def scale_by(self, c: float) -> 'Pattern':
@ -480,8 +529,11 @@ class Pattern:
Scale this Pattern by the given value
(all shapes and subpatterns and their offsets are scaled)
:param c: value to scale by
:return: self
Args:
c: factor to scale by
Returns:
self
"""
for entry in self.shapes + self.subpatterns:
entry.offset *= c
@ -494,9 +546,12 @@ class Pattern:
"""
Rotate the Pattern around the a location.
:param pivot: Location to rotate around
:param rotation: Angle to rotate by (counter-clockwise, radians)
:return: self
Args:
pivot: (x, y) location to rotate around
rotation: Angle to rotate by (counter-clockwise, radians)
Returns:
self
"""
pivot = numpy.array(pivot)
self.translate_elements(-pivot)
@ -509,8 +564,11 @@ class Pattern:
"""
Rotate the offsets of all shapes, labels, and subpatterns around (0, 0)
:param rotation: Angle to rotate by (counter-clockwise, radians)
:return: self
Args:
rotation: Angle to rotate by (counter-clockwise, radians)
Returns:
self
"""
for entry in self.shapes + self.subpatterns + self.labels:
entry.offset = numpy.dot(rotation_matrix_2d(rotation), entry.offset)
@ -520,8 +578,11 @@ class Pattern:
"""
Rotate each shape and subpattern around its center (offset)
:param rotation: Angle to rotate by (counter-clockwise, radians)
:return: self
Args:
rotation: Angle to rotate by (counter-clockwise, radians)
Returns:
self
"""
for entry in self.shapes + self.subpatterns:
entry.rotate(rotation)
@ -531,8 +592,12 @@ class Pattern:
"""
Mirror the offsets of all shapes, labels, and subpatterns 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
"""
for entry in self.shapes + self.subpatterns + self.labels:
entry.offset[axis - 1] *= -1
@ -541,10 +606,14 @@ class Pattern:
def mirror_elements(self, axis: int) -> 'Pattern':
"""
Mirror each shape and subpattern across an axis, relative to its
center (offset)
offset
: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
"""
for entry in self.shapes + self.subpatterns:
entry.mirror(axis)
@ -554,22 +623,29 @@ class Pattern:
"""
Mirror the Pattern 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
"""
self.mirror_elements(axis)
self.mirror_element_centers(axis)
return self
def scale_element_doses(self, factor: float) -> 'Pattern':
def scale_element_doses(self, c: float) -> 'Pattern':
"""
Multiply all shape and subpattern doses by a factor
:param factor: Factor to multiply doses by
:return: self
Args:
c: Factor to multiply doses by
Return:
self
"""
for entry in self.shapes + self.subpatterns:
entry.dose *= factor
entry.dose *= c
return self
def copy(self) -> 'Pattern':
@ -577,25 +653,26 @@ class Pattern:
Return a copy of the Pattern, deep-copying shapes and copying subpattern
entries, but not deep-copying any referenced patterns.
See also: Pattern.deepcopy()
See also: `Pattern.deepcopy()`
:return: A copy of the current Pattern.
Returns:
A copy of the current Pattern.
"""
return copy.copy(self)
def deepcopy(self) -> 'Pattern':
"""
Convenience method for copy.deepcopy(pattern)
Convenience method for `copy.deepcopy(pattern)`
:return: A deep copy of the current Pattern.
Returns:
A deep copy of the current Pattern.
"""
return copy.deepcopy(self)
def is_empty(self) -> bool:
"""
Returns true if the Pattern contains no shapes, labels, or subpatterns.
:return: True if the pattern is empty.
Returns:
True if the pattern is contains no shapes, labels, or subpatterns.
"""
return (len(self.subpatterns) == 0 and
len(self.shapes) == 0 and
@ -603,9 +680,11 @@ class Pattern:
def lock(self) -> 'Pattern':
"""
Lock the pattern
Lock the pattern, raising an exception if it is modified.
Also see `deeplock()`.
:return: self
Returns:
self
"""
object.__setattr__(self, 'locked', True)
return self
@ -614,16 +693,18 @@ class Pattern:
"""
Unlock the pattern
:return: self
Returns:
self
"""
object.__setattr__(self, 'locked', False)
return self
def deeplock(self) -> 'Pattern':
"""
Recursively lock the pattern, all referenced shapes, subpatterns, and labels
Recursively lock the pattern, all referenced shapes, subpatterns, and labels.
:return: self
Returns:
self
"""
self.lock()
for ss in self.shapes + self.labels:
@ -634,11 +715,13 @@ class Pattern:
def deepunlock(self) -> 'Pattern':
"""
Recursively unlock the pattern, all referenced shapes, subpatterns, and labels
Recursively unlock the pattern, all referenced shapes, subpatterns, and labels.
This is dangerous unless you have just performed a deepcopy!
This is dangerous unless you have just performed a deepcopy, since anything
you change will be changed everywhere it is referenced!
:return: self
Return:
self
"""
self.unlock()
for ss in self.shapes + self.labels:
@ -650,10 +733,13 @@ class Pattern:
@staticmethod
def load(filename: str) -> 'Pattern':
"""
Load a Pattern from a file
Load a Pattern from a file using pickle
:param filename: Filename to load from
:return: Loaded Pattern
Args:
filename: Filename to load from
Returns:
Loaded Pattern
"""
with open(filename, 'rb') as f:
pattern = pickle.load(f)
@ -662,10 +748,13 @@ class Pattern:
def save(self, filename: str) -> 'Pattern':
"""
Save the Pattern to a file
Save the Pattern to a file using pickle
:param filename: Filename to save to
:return: self
Args:
filename: Filename to save to
Returns:
self
"""
with open(filename, 'wb') as f:
pickle.dump(self, f, protocol=pickle.HIGHEST_PROTOCOL)
@ -679,12 +768,16 @@ class Pattern:
"""
Draw a picture of the Pattern and wait for the user to inspect it
Imports matplotlib.
Imports `matplotlib`.
:param offset: Coordinates to offset by before drawing
:param line_color: Outlines are drawn with this color (passed to matplotlib PolyCollection)
:param fill_color: Interiors are drawn with this color (passed to matplotlib PolyCollection)
:param overdraw: Whether to create a new figure or draw on a pre-existing one
Note that this can be slow; it is often faster to export to GDSII and use
klayout or a different GDS viewer!
Args:
offset: Coordinates to offset by before drawing
line_color: Outlines are drawn with this color (passed to `matplotlib.collections.PolyCollection`)
fill_color: Interiors are drawn with this color (passed to `matplotlib.collections.PolyCollection`)
overdraw: Whether to create a new figure or draw on a pre-existing one
"""
# TODO: add text labels to visualize()
from matplotlib import pyplot

View File

@ -20,8 +20,8 @@ __author__ = 'Jan Petykiewicz'
class GridRepetition:
"""
GridRepetition provides support for efficiently embedding multiple copies of a Pattern
into another Pattern at regularly-spaced offsets.
GridRepetition provides support for efficiently embedding multiple copies of a `Pattern`
into another `Pattern` at regularly-spaced offsets.
"""
__slots__ = ('pattern',
'_offset',
@ -37,24 +37,49 @@ class GridRepetition:
'locked')
pattern: 'Pattern'
""" The `Pattern` being instanced """
_offset: numpy.ndarray
""" (x, y) offset for the base instance """
_dose: float
""" Dose factor """
_rotation: float
''' Applies to individual instances in the grid, not the grid vectors '''
""" Rotation of the individual instances in the grid (not the grid vectors).
Radians, counterclockwise.
"""
_scale: float
''' Applies to individual instances in the grid, not the grid vectors '''
""" Scaling factor applied to individual instances in the grid (not the grid vectors) """
_mirrored: List[bool]
''' Applies to individual instances in the grid, not the grid vectors '''
""" Whether to mirror individual instances across the x and y axes
(Applies to individual instances in the grid, not the grid vectors)
"""
_a_vector: numpy.ndarray
_b_vector: numpy.ndarray or None
""" Vector `[x, y]` specifying the first lattice vector of the grid.
Specifies center-to-center spacing between adjacent elements.
"""
_a_count: int
""" Number of instances along the direction specified by the `a_vector` """
_b_vector: numpy.ndarray or None
""" Vector `[x, y]` specifying a second lattice vector for the grid.
Specifies center-to-center spacing between adjacent elements.
Can be `None` for a 1D array.
"""
_b_count: int
""" Number of instances along the direction specified by the `b_vector` """
identifier: Tuple
""" Arbitrary identifier """
locked: bool
""" If `True`, disallows changes to the GridRepetition """
def __init__(self,
pattern: 'Pattern',
@ -69,17 +94,20 @@ class GridRepetition:
scale: float = 1.0,
locked: bool = False):
"""
:param a_vector: First lattice vector, of the form [x, y].
Args:
a_vector: First lattice vector, of the form `[x, y]`.
Specifies center-to-center spacing between adjacent elements.
:param a_count: Number of elements in the a_vector direction.
:param b_vector: Second lattice vector, of the form [x, y].
a_count: Number of elements in the a_vector direction.
b_vector: Second lattice vector, of the form `[x, y]`.
Specifies center-to-center spacing between adjacent elements.
Can be omitted when specifying a 1D array.
:param b_count: Number of elements in the b_vector direction.
Should be omitted if b_vector was omitted.
:param locked: Whether the subpattern is locked after initialization.
:raises: PatternError if b_* inputs conflict with each other
or a_count < 1.
b_count: Number of elements in the `b_vector` direction.
Should be omitted if `b_vector` was omitted.
locked: Whether the `GridRepetition` is locked after initialization.
Raises:
PatternError if `b_*` inputs conflict with each other
or `a_count < 1`.
"""
if b_vector is None:
if b_count > 1:
@ -254,9 +282,11 @@ class GridRepetition:
def as_pattern(self) -> 'Pattern':
"""
Returns a copy of self.pattern which has been scaled, rotated, repeated, etc.
etc. according to this GridRepetitions's properties.
:return: Copy of self.pattern that has been repeated / altered as implied by
this object's other properties.
etc. according to this `GridRepetition`'s properties.
Returns:
A copy of self.pattern which has been scaled, rotated, repeated, etc.
etc. according to this `GridRepetition`'s properties.
"""
patterns = []
@ -283,8 +313,11 @@ class GridRepetition:
"""
Translate by the given offset
:param offset: Translate by this offset
:return: self
Args:
offset: `[x, y]` to translate by
Returns:
self
"""
self.offset += offset
return self
@ -293,9 +326,12 @@ class GridRepetition:
"""
Rotate the array around a point
:param pivot: Point 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)
@ -308,8 +344,11 @@ class GridRepetition:
"""
Rotate around (0, 0)
:param rotation: Angle to rotate by (counterclockwise, radians)
:return: self
Args:
rotation: Angle to rotate by (counterclockwise, radians)
Returns:
self
"""
self.rotate_elements(rotation)
self.a_vector = numpy.dot(rotation_matrix_2d(rotation), self.a_vector)
@ -321,8 +360,11 @@ class GridRepetition:
"""
Rotate each element around its origin
:param rotation: Angle to rotate by (counterclockwise, radians)
:return: self
Args:
rotation: Angle to rotate by (counterclockwise, radians)
Returns:
self
"""
self.rotation += rotation
return self
@ -331,8 +373,12 @@ class GridRepetition:
"""
Mirror the GridRepetition 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
"""
self.mirror_elements(axis)
self.a_vector[1-axis] *= -1
@ -344,8 +390,12 @@ class GridRepetition:
"""
Mirror each element across an axis relative to its origin.
: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
"""
self.mirrored[axis] = not self.mirrored[axis]
self.rotation *= -1
@ -353,11 +403,12 @@ class GridRepetition:
def get_bounds(self) -> numpy.ndarray or None:
"""
Return a numpy.ndarray containing [[x_min, y_min], [x_max, y_max]], corresponding to the
extent of the GridRepetition in each dimension.
Returns None if the contained Pattern is empty.
Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the
extent of the `GridRepetition` in each dimension.
Returns `None` if the contained `Pattern` is empty.
:return: [[x_min, y_min], [x_max, y_max]] or None
Returns:
`[[x_min, y_min], [x_max, y_max]]` or `None`
"""
return self.as_pattern().get_bounds()
@ -365,7 +416,11 @@ class GridRepetition:
"""
Scale the GridRepetition by a factor
:param c: scaling factor
Args:
c: scaling factor
Returns:
self
"""
self.scale_elements_by(c)
self.a_vector *= c
@ -377,7 +432,11 @@ class GridRepetition:
"""
Scale each element by a factor
:param c: scaling factor
Args:
c: scaling factor
Returns:
self
"""
self.scale *= c
return self
@ -386,7 +445,8 @@ class GridRepetition:
"""
Return a shallow copy of the repetition.
:return: copy.copy(self)
Returns:
`copy.copy(self)`
"""
return copy.copy(self)
@ -394,33 +454,37 @@ class GridRepetition:
"""
Return a deep copy of the repetition.
:return: copy.copy(self)
Returns:
`copy.deepcopy(self)`
"""
return copy.deepcopy(self)
def lock(self) -> 'GridRepetition':
"""
Lock the GridRepetition
Lock the `GridRepetition`, disallowing changes.
:return: self
Returns:
self
"""
object.__setattr__(self, 'locked', True)
return self
def unlock(self) -> 'GridRepetition':
"""
Unlock the GridRepetition
Unlock the `GridRepetition`
:return: self
Returns:
self
"""
object.__setattr__(self, 'locked', False)
return self
def deeplock(self) -> 'GridRepetition':
"""
Recursively lock the GridRepetition and its contained pattern
Recursively lock the `GridRepetition` and its contained pattern
:return: self
Returns:
self
"""
self.lock()
self.pattern.deeplock()
@ -428,11 +492,13 @@ class GridRepetition:
def deepunlock(self) -> 'GridRepetition':
"""
Recursively unlock the GridRepetition and its contained pattern
Recursively unlock the `GridRepetition` and its contained pattern
This is dangerous unless you have just performed a deepcopy!
This is dangerous unless you have just performed a deepcopy, since
the component parts may be reused elsewhere.
:return: self
Returns:
self
"""
self.unlock()
self.pattern.deepunlock()

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
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
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).
: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
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
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.
:param max_arclen: Maximum arclength which can be approximated by a single line
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
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')

View File

@ -24,13 +24,29 @@ class SubPattern:
__slots__ = ('pattern', '_offset', '_rotation', '_dose', '_scale', '_mirrored',
'identifier', 'locked')
pattern: 'Pattern'
""" The `Pattern` being instanced """
_offset: numpy.ndarray
""" (x, y) offset for the instance """
_rotation: float
""" rotation for the instance, radians counterclockwise """
_dose: float
""" dose factor for the instance """
_scale: float
""" scale factor for the instance """
_mirrored: List[bool]
""" Whether to mirror the instanc across the x and/or y axes. """
identifier: Tuple
""" An arbitrary identifier """
locked: bool
""" If `True`, disallows changes to the GridRepetition """
#TODO more documentation?
def __init__(self,
@ -139,9 +155,9 @@ class SubPattern:
def as_pattern(self) -> 'Pattern':
"""
Returns a copy of self.pattern which has been scaled, rotated, etc. according to this
SubPattern's properties.
:return: Copy of self.pattern that has been altered to reflect the SubPattern's properties.
Returns:
A copy of self.pattern which has been scaled, rotated, etc. according to this
`SubPattern`'s properties.
"""
pattern = self.pattern.deepcopy().deepunlock()
pattern.scale_by(self.scale)
@ -155,8 +171,11 @@ class SubPattern:
"""
Translate by the given offset
:param offset: Translate by this offset
:return: self
Args:
offset: Offset `[x, y]` to translate by
Returns:
self
"""
self.offset += offset
return self
@ -165,9 +184,12 @@ class SubPattern:
"""
Rotate around a point
:param pivot: Point 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)
@ -178,10 +200,13 @@ class SubPattern:
def rotate(self, rotation: float) -> 'SubPattern':
"""
Rotate around (0, 0)
Rotate the instance around it's origin
:param rotation: Angle to rotate by (counterclockwise, radians)
:return: self
Args:
rotation: Angle to rotate by (counterclockwise, radians)
Returns:
self
"""
self.rotation += rotation
return self
@ -190,8 +215,11 @@ class SubPattern:
"""
Mirror the subpattern across an axis.
:param axis: Axis to mirror across.
:return: self
Args:
axis: Axis to mirror across.
Returns:
self
"""
self.mirrored[axis] = not self.mirrored[axis]
self.rotation *= -1
@ -199,11 +227,12 @@ class SubPattern:
def get_bounds(self) -> numpy.ndarray or None:
"""
Return a numpy.ndarray containing [[x_min, y_min], [x_max, y_max]], corresponding to the
extent of the SubPattern in each dimension.
Returns None if the contained Pattern is empty.
Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the
extent of the `SubPattern` in each dimension.
Returns `None` if the contained `Pattern` is empty.
:return: [[x_min, y_min], [x_max, y_max]] or None
Returns:
`[[x_min, y_min], [x_max, y_max]]` or `None`
"""
return self.as_pattern().get_bounds()
@ -211,7 +240,11 @@ class SubPattern:
"""
Scale the subpattern by a factor
:param c: scaling factor
Args:
c: scaling factor
Returns:
self
"""
self.scale *= c
return self
@ -220,7 +253,8 @@ class SubPattern:
"""
Return a shallow copy of the subpattern.
:return: copy.copy(self)
Returns:
`copy.copy(self)`
"""
return copy.copy(self)
@ -228,15 +262,17 @@ class SubPattern:
"""
Return a deep copy of the subpattern.
:return: copy.copy(self)
Returns:
`copy.deepcopy(self)`
"""
return copy.deepcopy(self)
def lock(self) -> 'SubPattern':
"""
Lock the SubPattern
Lock the SubPattern, disallowing changes
:return: self
Returns:
self
"""
object.__setattr__(self, 'locked', True)
return self
@ -245,7 +281,8 @@ class SubPattern:
"""
Unlock the SubPattern
:return: self
Returns:
self
"""
object.__setattr__(self, 'locked', False)
return self
@ -254,7 +291,8 @@ class SubPattern:
"""
Recursively lock the SubPattern and its contained pattern
:return: self
Returns:
self
"""
self.lock()
self.pattern.deeplock()
@ -264,9 +302,11 @@ class SubPattern:
"""
Recursively unlock the SubPattern and its contained pattern
This is dangerous unless you have just performed a deepcopy!
This is dangerous unless you have just performed a deepcopy, since
the subpattern and its components may be used in more than one once!
:return: self
Returns:
self
"""
self.unlock()
self.pattern.deepunlock()

View File

@ -14,30 +14,37 @@ def is_scalar(var: Any) -> bool:
"""
Alias for 'not hasattr(var, "__len__")'
:param var: Checks if var has a length.
Args:
var: Checks if `var` has a length.
"""
return not hasattr(var, "__len__")
def get_bit(bit_string: Any, bit_id: int) -> bool:
"""
Returns true iff bit number 'bit_id' from the right of 'bit_string' is 1
Interprets bit number `bit_id` from the right (lsb) of `bit_string` as a boolean
:param bit_string: Bit string to test
:param bit_id: Bit number, 0-indexed from the right (lsb)
:return: value of the requested bit (bool)
Args:
bit_string: Bit string to test
bit_id: Bit number, 0-indexed from the right (lsb)
Returns:
Boolean value of the requested bit
"""
return bit_string & (1 << bit_id) != 0
def set_bit(bit_string: Any, bit_id: int, value: bool) -> Any:
"""
Returns 'bit_string' with bit number 'bit_id' set to 'value'.
Returns `bit_string`, with bit number `bit_id` set to boolean `value`.
:param bit_string: Bit string to alter
:param bit_id: Bit number, 0-indexed from right (lsb)
:param value: Boolean value to set bit to
:return: Altered 'bit_string'
Args:
bit_string: Bit string to alter
bit_id: Bit number, 0-indexed from right (lsb)
value: Boolean value to set bit to
Returns:
Altered `bit_string`
"""
mask = (1 << bit_id)
bit_string &= ~mask
@ -50,14 +57,29 @@ def rotation_matrix_2d(theta: float) -> numpy.ndarray:
"""
2D rotation matrix for rotating counterclockwise around the origin.
:param theta: Angle to rotate, in radians
:return: rotation matrix
Args:
theta: Angle to rotate, in radians
Returns:
rotation matrix
"""
return numpy.array([[numpy.cos(theta), -numpy.sin(theta)],
[numpy.sin(theta), +numpy.cos(theta)]])
def normalize_mirror(mirrored: Tuple[bool, bool]) -> Tuple[bool, float]:
"""
Converts 0-2 mirror operations `(mirror_across_x_axis, mirror_across_y_axis)`
into 0-1 mirror operations and a rotation
Args:
mirrored: `(mirror_across_x_axis, mirror_across_y_axis)`
Returns:
`mirror_across_x_axis` (bool) and
`angle_to_rotate` in radians
"""
mirrored_x, mirrored_y = mirrored
mirror_x = (mirrored_x != mirrored_y) #XOR
angle = numpy.pi if mirrored_y else 0
@ -65,6 +87,17 @@ def normalize_mirror(mirrored: Tuple[bool, bool]) -> Tuple[bool, float]:
def remove_duplicate_vertices(vertices: numpy.ndarray, closed_path: bool = True) -> numpy.ndarray:
"""
Given a list of vertices, remove any consecutive duplicates.
Args:
vertices: `[[x0, y0], [x1, y1], ...]`
closed_path: If True, `vertices` is interpreted as an implicity-closed path
(i.e. the last vertex will be removed if it is the same as the first)
Returns:
`vertices` with no consecutive duplicates.
"""
duplicates = (vertices == numpy.roll(vertices, 1, axis=0)).all(axis=1)
if not closed_path:
duplicates[0] = False
@ -72,15 +105,18 @@ def remove_duplicate_vertices(vertices: numpy.ndarray, closed_path: bool = True)
def remove_colinear_vertices(vertices: numpy.ndarray, closed_path: bool = True) -> numpy.ndarray:
'''
"""
Given a list of vertices, remove any superflous vertices (i.e.
those which lie along the line formed by their neighbors)
:param vertices: Nx2 ndarray of vertices
:param closed_path: If True, the vertices are assumed to represent an implicitly
closed path. If False, the path is assumed to be open. Default True.
:return:
'''
Args:
vertices: Nx2 ndarray of vertices
closed_path: If `True`, the vertices are assumed to represent an implicitly
closed path. If `False`, the path is assumed to be open. Default `True`.
Returns:
`vertices` with colinear (superflous) vertices removed.
"""
vertices = numpy.array(vertices)
# Check for dx0/dy0 == dx1/dy1