Docstring format change
(new param and return format) Also some minor code formatting fixes in utils
This commit is contained in:
parent
20981f10b9
commit
5adabfd25a
@ -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
|
||||
|
@ -49,39 +49,40 @@ 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.
|
||||
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" unit which is different from the "database" unit, for display purposes.
|
||||
Default 1.
|
||||
:param 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
|
||||
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.
|
||||
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.
|
||||
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`.
|
||||
library_name: Library name written into the GDSII file.
|
||||
Default 'masque-gdsii-write'.
|
||||
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.
|
||||
"""
|
||||
if isinstance(patterns, Pattern):
|
||||
patterns = [patterns]
|
||||
@ -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
|
||||
|
@ -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:]:
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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,16 +139,19 @@ 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
|
||||
of the subset. Default always returns False.
|
||||
:param 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
|
||||
of the subset. Default always returns False.
|
||||
:param 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
|
||||
functions return True
|
||||
Args:
|
||||
shapes_func: Given a shape, returns a boolean denoting whether the shape is a member
|
||||
of the subset. Default always returns False.
|
||||
labels_func: Given a label, returns a boolean denoting whether the label is a member
|
||||
of the subset. Default always returns False.
|
||||
subpatterns_func: Given a subpattern, returns a boolean denoting if it is a member
|
||||
of the subset. Default always returns False.
|
||||
recursive: If True, also calls .subset() recursively on patterns referenced by this
|
||||
pattern.
|
||||
|
||||
Returns:
|
||||
A Pattern containing all the shapes and subpatterns for which the parameter
|
||||
functions return True
|
||||
"""
|
||||
def do_subset(src):
|
||||
pat = Pattern(name=src.name)
|
||||
@ -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'
|
||||
internal defaults.
|
||||
:param poly_max_arclen: Maximum arclength which can be approximated by a single line
|
||||
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.
|
||||
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
|
||||
to GDSII, which uses integer values for pixel coordinates.
|
||||
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
|
||||
|
@ -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].
|
||||
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].
|
||||
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.
|
||||
Args:
|
||||
a_vector: First lattice vector, of the form `[x, y]`.
|
||||
Specifies center-to-center spacing between adjacent elements.
|
||||
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.
|
||||
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()
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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()
|
||||
|
104
masque/utils.py
104
masque/utils.py
@ -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,34 +87,48 @@ def normalize_mirror(mirrored: Tuple[bool, bool]) -> Tuple[bool, float]:
|
||||
|
||||
|
||||
def remove_duplicate_vertices(vertices: numpy.ndarray, closed_path: bool = True) -> numpy.ndarray:
|
||||
duplicates = (vertices == numpy.roll(vertices, 1, axis=0)).all(axis=1)
|
||||
if not closed_path:
|
||||
duplicates[0] = False
|
||||
return vertices[~duplicates]
|
||||
"""
|
||||
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
|
||||
return vertices[~duplicates]
|
||||
|
||||
|
||||
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)
|
||||
"""
|
||||
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:
|
||||
'''
|
||||
vertices = numpy.array(vertices)
|
||||
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`.
|
||||
|
||||
# Check for dx0/dy0 == dx1/dy1
|
||||
Returns:
|
||||
`vertices` with colinear (superflous) vertices removed.
|
||||
"""
|
||||
vertices = numpy.array(vertices)
|
||||
|
||||
dv = numpy.roll(vertices, -1, axis=0) - vertices # [y1-y0, y2-y1, ...]
|
||||
dxdy = dv * numpy.roll(dv, 1, axis=0)[:, ::-1] #[[dx0*(dy_-1), (dx_-1)*dy0], dx1*dy0, dy1*dy0]]
|
||||
# Check for dx0/dy0 == dx1/dy1
|
||||
|
||||
dxdy_diff = numpy.abs(numpy.diff(dxdy, axis=1))[:, 0]
|
||||
err_mult = 2 * numpy.abs(dxdy).sum(axis=1) + 1e-40
|
||||
dv = numpy.roll(vertices, -1, axis=0) - vertices # [y1-y0, y2-y1, ...]
|
||||
dxdy = dv * numpy.roll(dv, 1, axis=0)[:, ::-1] #[[dx0*(dy_-1), (dx_-1)*dy0], dx1*dy0, dy1*dy0]]
|
||||
|
||||
slopes_equal = (dxdy_diff / err_mult) < 1e-15
|
||||
if not closed_path:
|
||||
slopes_equal[[0, -1]] = False
|
||||
dxdy_diff = numpy.abs(numpy.diff(dxdy, axis=1))[:, 0]
|
||||
err_mult = 2 * numpy.abs(dxdy).sum(axis=1) + 1e-40
|
||||
|
||||
return vertices[~slopes_equal]
|
||||
slopes_equal = (dxdy_diff / err_mult) < 1e-15
|
||||
if not closed_path:
|
||||
slopes_equal[[0, -1]] = False
|
||||
|
||||
return vertices[~slopes_equal]
|
||||
|
Loading…
Reference in New Issue
Block a user