Docstring format change

(new param and return format)
Also some minor code formatting fixes in utils
lethe/HEAD
jan 4 years ago
parent 20981f10b9
commit 5adabfd25a

@ -6,21 +6,24 @@
with some vectorized element types (eg. circles, not just polygons), better support for with some vectorized element types (eg. circles, not just polygons), better support for
E-beam doses, and the ability to output to multiple formats. 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 `Pattern` is a basic object containing a 2D lithography mask, composed of a list of `Shape`
objects and a list of SubPattern objects. 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. 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 Note that the methods for these classes try to avoid copying wherever possible, so unless
otherwise noted, assume that arguments are stored by-reference. otherwise noted, assume that arguments are stored by-reference.
Dependencies: Dependencies:
- numpy - `numpy`
- matplotlib [Pattern.visualize(...)] - `matplotlib` [Pattern.visualize(...)]
- python-gdsii [masque.file.gdsii] - `python-gdsii` [masque.file.gdsii]
- svgwrite [masque.file.svg] - `svgwrite` [masque.file.svg]
""" """
import pathlib import pathlib

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

@ -23,20 +23,21 @@ def writefile(pattern: Pattern,
Note that this function modifies the 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. are written to the relevant elements.
It is often a good idea to run pattern.subpatternize() on pattern prior to 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 calling this function, especially if calling `.polygonize()` will result in very
many vertices. 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. prior to calling this function.
:param pattern: Pattern to write to file. Modified by this function. Args:
:param filename: Filename to write to. pattern: Pattern to write to file. Modified by this function.
:param custom_attributes: Whether to write non-standard pattern_layer and filename: Filename to write to.
pattern_dose attributes to the SVG elements. custom_attributes: Whether to write non-standard `pattern_layer` and
`pattern_dose` attributes to the SVG elements.
""" """
# Polygonize pattern # Polygonize pattern
@ -85,18 +86,19 @@ def writefile(pattern: Pattern,
def writefile_inverted(pattern: Pattern, filename: str): def writefile_inverted(pattern: Pattern, filename: str):
""" """
Write an inverted Pattern to an SVG file, by first calling .polygonize() and 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 `.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 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. 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. prior to calling this function.
:param pattern: Pattern to write to file. Modified by this function. Args:
:param filename: Filename to write to. pattern: Pattern to write to file. Modified by this function.
filename: Filename to write to.
""" """
# Polygonize and flatten pattern # Polygonize and flatten pattern
pattern.polygonize().flatten() pattern.polygonize().flatten()
@ -129,8 +131,11 @@ def poly2path(vertices: numpy.ndarray) -> str:
""" """
Create an SVG path string from an Nx2 list of vertices. Create an SVG path string from an Nx2 list of vertices.
:param vertices: Nx2 array of vertices. Args:
:return: SVG path-string. vertices: Nx2 array of vertices.
Returns:
SVG path-string.
""" """
commands = 'M{:g},{:g} '.format(vertices[0][0], vertices[0][1]) commands = 'M{:g},{:g} '.format(vertices[0][0], vertices[0][1])
for vertex in vertices[1:]: for vertex in vertices[1:]:

@ -12,11 +12,14 @@ __author__ = 'Jan Petykiewicz'
def mangle_name(pattern: Pattern, dose_multiplier: float=1.0) -> str: 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. Args:
:param dose_multiplier: Dose multiplier to mangle with. pattern: Pattern whose name we want to mangle.
:return: Mangled name. dose_multiplier: Dose multiplier to mangle with.
Returns:
Mangled name.
""" """
expression = re.compile('[^A-Za-z0-9_\?\$]') expression = re.compile('[^A-Za-z0-9_\?\$]')
full_name = '{}_{}_{}'.format(pattern.name, dose_multiplier, id(pattern)) 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]]: 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. Args:
:param dose_multiplier: Multiplier for all written_dose entries. pattern: Source Patterns.
:return: {(id(subpat.pattern), written_dose), ...} dose_multiplier: Multiplier for all written_dose entries.
Returns:
`{(id(subpat.pattern), written_dose), ...}`
""" """
dose_table = {(id(pattern), dose_multiplier) for pattern in patterns} dose_table = {(id(pattern), dose_multiplier) for pattern in patterns}
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) A text annotation with a position and layer (but no size; it is not drawn)
""" """
__slots__ = ('_offset', '_layer', '_string', 'identifier', 'locked') __slots__ = ('_offset', '_layer', '_string', 'identifier', 'locked')
# [x_offset, y_offset]
_offset: numpy.ndarray _offset: numpy.ndarray
""" [x_offset, y_offset] """
# Layer (integer >= 0) or 2-Tuple of integers
_layer: int or Tuple _layer: int or Tuple
""" Layer (integer >= 0, or 2-Tuple of integers) """
# Label string
_string: str _string: str
""" Label string """
# Arbitrary identifier tuple
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): def __setattr__(self, name, value):
if self.locked and name != 'locked': if self.locked and name != 'locked':
@ -40,8 +42,6 @@ class Label:
def offset(self) -> numpy.ndarray: def offset(self) -> numpy.ndarray:
""" """
[x, y] offset [x, y] offset
:return: [x_offset, y_offset]
""" """
return self._offset return self._offset
@ -59,8 +59,6 @@ class Label:
def layer(self) -> int or Tuple[int]: def layer(self) -> int or Tuple[int]:
""" """
Layer number (int or tuple of ints) Layer number (int or tuple of ints)
:return: Layer
""" """
return self._layer return self._layer
@ -73,8 +71,6 @@ class Label:
def string(self) -> str: def string(self) -> str:
""" """
Label string (str) Label string (str)
:return: string
""" """
return self._string return self._string
@ -109,29 +105,33 @@ class Label:
def copy(self) -> 'Label': def copy(self) -> 'Label':
""" """
Returns a deep copy of the shape. Returns a deep copy of the label.
:return: Deep copy of self
""" """
return copy.deepcopy(self) return copy.deepcopy(self)
def translate(self, offset: vector2) -> 'Label': 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] Args:
:return: self offset: [x_offset, y,offset]
Returns:
self
""" """
self.offset += offset self.offset += offset
return self return self
def rotate_around(self, pivot: vector2, rotation: float) -> 'Label': 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 Args:
:param rotation: Angle to rotate by (counterclockwise, radians) pivot: Point (x, y) to rotate around
:return: self rotation: Angle to rotate by (counterclockwise, radians)
Returns:
self
""" """
pivot = numpy.array(pivot, dtype=float) pivot = numpy.array(pivot, dtype=float)
self.translate(-pivot) self.translate(-pivot)
@ -147,24 +147,27 @@ class Label:
bounds = [self.offset, bounds = [self.offset,
self.offset] self.offset]
:return: Bounds [[xmin, xmax], [ymin, ymax]] Returns:
Bounds [[xmin, xmax], [ymin, ymax]]
""" """
return numpy.array([self.offset, self.offset]) return numpy.array([self.offset, self.offset])
def lock(self) -> 'Label': 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) object.__setattr__(self, 'locked', True)
return self return self
def unlock(self) -> 'Label': def unlock(self) -> 'Label':
""" """
Unlock the Label Unlock the Label, re-allowing changes.
:return: self Return:
self
""" """
object.__setattr__(self, 'locked', False) object.__setattr__(self, 'locked', False)
return self return self

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

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

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

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

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

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

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

@ -26,12 +26,20 @@ class Shape(metaclass=ABCMeta):
""" """
__slots__ = ('_offset', '_layer', '_dose', 'identifier', 'locked') __slots__ = ('_offset', '_layer', '_dose', 'identifier', 'locked')
_offset: numpy.ndarray # [x_offset, y_offset] _offset: numpy.ndarray
_layer: int or Tuple # Layer (integer >= 0 or tuple) """ `[x_offset, y_offset]` """
_dose: float # Dose
identifier: Tuple # An arbitrary identifier for the shape, _layer: int or Tuple
# usually empty but used by Pattern.flatten() """ Layer (integer >= 0 or tuple) """
locked: bool # If True, any changes to the shape will raise a PatternLockedError
_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): def __setattr__(self, name, value):
if self.locked and name != 'locked': if self.locked and name != 'locked':
@ -51,31 +59,35 @@ class Shape(metaclass=ABCMeta):
""" """
Returns a list of polygons which approximate the shape. Returns a list of polygons which approximate the shape.
:param num_vertices: Number of points to use for each polygon. Can be overridden by Args:
max_arclen if that results in more points. Optional, defaults to shapes' num_vertices: Number of points to use for each polygon. Can be overridden by
internal defaults. max_arclen if that results in more points. Optional, defaults to shapes'
:param max_arclen: Maximum arclength which can be approximated by a single line internal defaults.
segment. Optional, defaults to shapes' internal defaults. max_arclen: Maximum arclength which can be approximated by a single line
:return: List of polygons equivalent to the shape segment. Optional, defaults to shapes' internal defaults.
Returns:
List of polygons equivalent to the shape
""" """
pass pass
@abstractmethod @abstractmethod
def get_bounds(self) -> numpy.ndarray: def get_bounds(self) -> numpy.ndarray:
""" """
Returns [[x_min, y_min], [x_max, y_max]] which specify a minimal bounding box for the shape. Returns `[[x_min, y_min], [x_max, y_max]]` which specify a minimal bounding box for the shape.
:return: [[x_min, y_min], [x_max, y_max]]
""" """
pass pass
@abstractmethod @abstractmethod
def rotate(self, theta: float) -> 'Shape': 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) Args:
:return: self theta: Angle to rotate by (counterclockwise, radians)
Returns:
self
""" """
pass pass
@ -84,8 +96,12 @@ class Shape(metaclass=ABCMeta):
""" """
Mirror the shape across an axis. Mirror the shape across an axis.
:param axis: Axis to mirror across. Args:
:return: self axis: Axis to mirror across.
(0: mirror across x axis, 1: mirror across y axis)
Returns:
self
""" """
pass pass
@ -94,8 +110,11 @@ class Shape(metaclass=ABCMeta):
""" """
Scale the shape's size (eg. radius, for a circle) by a constant factor. Scale the shape's size (eg. radius, for a circle) by a constant factor.
:param c: Factor to scale by Args:
:return: self c: Factor to scale by
Returns:
self
""" """
pass pass
@ -105,18 +124,21 @@ class Shape(metaclass=ABCMeta):
Writes the shape in a standardized notation, with offset, scale, rotation, and dose Writes the shape in a standardized notation, with offset, scale, rotation, and dose
information separated out from the remaining values. 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 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 when you find it important for quantities to remain in a certain range, eg. for
GDSII where vertex locations are stored as integers. 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: Returns:
intrinsic: A tuple of basic types containing all information about the instance that The returned information takes the form of a 3-element tuple,
is not contained in 'extrinsic'. Usually, intrinsic[0] == type(self). `(intrinsic, extrinsic, constructor)`. These are further broken down as:
extrinsic: ([x_offset, y_offset], scale, rotation, mirror_across_x_axis, dose) `intrinsic`: A tuple of basic types containing all information about the instance that
constructor: A callable (no arguments) which returns an instance of type(self) with is not contained in 'extrinsic'. Usually, `intrinsic[0] == type(self)`.
internal state equivalent to 'intrinsic'. `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 pass
@ -126,8 +148,6 @@ class Shape(metaclass=ABCMeta):
def offset(self) -> numpy.ndarray: def offset(self) -> numpy.ndarray:
""" """
[x, y] offset [x, y] offset
:return: [x_offset, y_offset]
""" """
return self._offset return self._offset
@ -145,8 +165,6 @@ class Shape(metaclass=ABCMeta):
def layer(self) -> int or Tuple[int]: def layer(self) -> int or Tuple[int]:
""" """
Layer number (int or tuple of ints) Layer number (int or tuple of ints)
:return: Layer
""" """
return self._layer return self._layer
@ -159,8 +177,6 @@ class Shape(metaclass=ABCMeta):
def dose(self) -> float: def dose(self) -> float:
""" """
Dose (float >= 0) Dose (float >= 0)
:return: Dose value
""" """
return self._dose return self._dose
@ -177,7 +193,8 @@ class Shape(metaclass=ABCMeta):
""" """
Returns a deep copy of the shape. Returns a deep copy of the shape.
:return: Deep copy of self Returns:
copy.deepcopy(self)
""" """
return copy.deepcopy(self) return copy.deepcopy(self)
@ -185,8 +202,11 @@ class Shape(metaclass=ABCMeta):
""" """
Translate the shape by the given offset Translate the shape by the given offset
:param offset: [x_offset, y,offset] Args:
:return: self offset: [x_offset, y,offset]
Returns:
self
""" """
self.offset += offset self.offset += offset
return self return self
@ -195,9 +215,12 @@ class Shape(metaclass=ABCMeta):
""" """
Rotate the shape around a point. Rotate the shape around a point.
:param pivot: Point (x, y) to rotate around Args:
:param rotation: Angle to rotate by (counterclockwise, radians) pivot: Point (x, y) to rotate around
:return: self rotation: Angle to rotate by (counterclockwise, radians)
Returns:
self
""" """
pivot = numpy.array(pivot, dtype=float) pivot = numpy.array(pivot, dtype=float)
self.translate(-pivot) self.translate(-pivot)
@ -214,14 +237,17 @@ class Shape(metaclass=ABCMeta):
Returns a list of polygons with grid-aligned ("Manhattan") edges approximating the shape. Returns a list of polygons with grid-aligned ("Manhattan") edges approximating the shape.
This function works by 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 2) Approximating each edge with an equivalent Manhattan edge
This process results in a reasonable Manhattan representation of the shape, but is This process results in a reasonable Manhattan representation of the shape, but is
imprecise near non-Manhattan or off-grid corners. imprecise near non-Manhattan or off-grid corners.
:param grid_x: List of allowed x-coordinates for the Manhattanized polygon edges. Args:
:param grid_y: List of allowed y-coordinates for the Manhattanized polygon edges. grid_x: List of allowed x-coordinates for the Manhattanized polygon edges.
:return: List of Polygon objects with grid-aligned 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 from . import Polygon
@ -319,7 +345,7 @@ class Shape(metaclass=ABCMeta):
Returns a list of polygons with grid-aligned ("Manhattan") edges approximating the shape. Returns a list of polygons with grid-aligned ("Manhattan") edges approximating the shape.
This function works by 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, 2) Accurately rasterizing each polygon on a grid,
where the edges of each grid cell correspond to the allowed coordinates where the edges of each grid cell correspond to the allowed coordinates
3) Thresholding the (anti-aliased) rasterized image 3) Thresholding the (anti-aliased) rasterized image
@ -328,7 +354,7 @@ class Shape(metaclass=ABCMeta):
caveats include: caveats include:
a) If high accuracy is important, perform any polygonization and clipping operations 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 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). 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 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. by breaking the shape apart into multiple, smaller shapes.
@ -336,19 +362,22 @@ class Shape(metaclass=ABCMeta):
equidistant from allowed edge location. equidistant from allowed edge location.
Implementation notes: 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. rasterized image.
ii) To find the exact polygon edges, the thresholded rasterized image is supersampled ii) To find the exact polygon edges, the thresholded rasterized image is supersampled
prior to calling skimage.measure.find_contours(), which uses marching squares prior to calling `skimage.measure.find_contours()`, which uses marching squares
to find the contours. This is done because find_contours() performs interpolation, 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 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 boundary tracing method could remove this requirement, but for now this seems to
be the most performant approach. be the most performant approach.
:param grid_x: List of allowed x-coordinates for the Manhattanized polygon edges. Args:
:param grid_y: List of allowed y-coordinates for the Manhattanized polygon edges. grid_x: List of allowed x-coordinates for the Manhattanized polygon edges.
:return: List of Polygon objects with grid-aligned 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 from . import Polygon
import skimage.measure import skimage.measure
@ -403,9 +432,10 @@ class Shape(metaclass=ABCMeta):
def lock(self) -> 'Shape': def lock(self) -> 'Shape':
""" """
Lock the Shape Lock the Shape, disallowing further changes
:return: self Returns:
self
""" """
object.__setattr__(self, 'locked', True) object.__setattr__(self, 'locked', True)
return self return self
@ -414,7 +444,8 @@ class Shape(metaclass=ABCMeta):
""" """
Unlock the Shape Unlock the Shape
:return: self Returns:
self
""" """
object.__setattr__(self, 'locked', False) object.__setattr__(self, 'locked', False)
return self 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. The output is normalized so that the font size is 1 unit.
:param font_path: File path specifying a font loadable by freetype Args:
:param char: Character to convert to polygons font_path: File path specifying a font loadable by freetype
:param resolution: Internal resolution setting (used for freetype char: Character to convert to polygons
Face.set_font_size(resolution)). Modify at your own peril! resolution: Internal resolution setting (used for freetype
:return: List of polygons [[[x0, y0], [x1, y1], ...], ...] and 'advance' distance (distance `Face.set_font_size(resolution))`. Modify at your own peril!
from the start of this glyph to the start of the next one)
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: if len(char) != 1:
raise Exception('get_char_as_polygons called with non-char') raise Exception('get_char_as_polygons called with non-char')

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

@ -14,30 +14,37 @@ def is_scalar(var: Any) -> bool:
""" """
Alias for 'not hasattr(var, "__len__")' 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__") return not hasattr(var, "__len__")
def get_bit(bit_string: Any, bit_id: int) -> bool: 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 Args:
:param bit_id: Bit number, 0-indexed from the right (lsb) bit_string: Bit string to test
:return: value of the requested bit (bool) bit_id: Bit number, 0-indexed from the right (lsb)
Returns:
Boolean value of the requested bit
""" """
return bit_string & (1 << bit_id) != 0 return bit_string & (1 << bit_id) != 0
def set_bit(bit_string: Any, bit_id: int, value: bool) -> Any: 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 Args:
:param bit_id: Bit number, 0-indexed from right (lsb) bit_string: Bit string to alter
:param value: Boolean value to set bit to bit_id: Bit number, 0-indexed from right (lsb)
:return: Altered 'bit_string' value: Boolean value to set bit to
Returns:
Altered `bit_string`
""" """
mask = (1 << bit_id) mask = (1 << bit_id)
bit_string &= ~mask bit_string &= ~mask
@ -50,14 +57,29 @@ def rotation_matrix_2d(theta: float) -> numpy.ndarray:
""" """
2D rotation matrix for rotating counterclockwise around the origin. 2D rotation matrix for rotating counterclockwise around the origin.
:param theta: Angle to rotate, in radians Args:
:return: rotation matrix theta: Angle to rotate, in radians
Returns:
rotation matrix
""" """
return numpy.array([[numpy.cos(theta), -numpy.sin(theta)], return numpy.array([[numpy.cos(theta), -numpy.sin(theta)],
[numpy.sin(theta), +numpy.cos(theta)]]) [numpy.sin(theta), +numpy.cos(theta)]])
def normalize_mirror(mirrored: Tuple[bool, bool]) -> Tuple[bool, float]: 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 mirrored_x, mirrored_y = mirrored
mirror_x = (mirrored_x != mirrored_y) #XOR mirror_x = (mirrored_x != mirrored_y) #XOR
angle = numpy.pi if mirrored_y else 0 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: 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: Given a list of vertices, remove any consecutive duplicates.
duplicates[0] = False
return vertices[~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: def remove_colinear_vertices(vertices: numpy.ndarray, closed_path: bool = True) -> numpy.ndarray:
''' """
Given a list of vertices, remove any superflous vertices (i.e. Given a list of vertices, remove any superflous vertices (i.e.
those which lie along the line formed by their neighbors) those which lie along the line formed by their neighbors)
:param vertices: Nx2 ndarray of vertices Args:
:param closed_path: If True, the vertices are assumed to represent an implicitly vertices: Nx2 ndarray of vertices
closed path. If False, the path is assumed to be open. Default True. closed_path: If `True`, the vertices are assumed to represent an implicitly
:return: closed path. If `False`, the path is assumed to be open. Default `True`.
'''
vertices = numpy.array(vertices)
# 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, ...] # Check for dx0/dy0 == dx1/dy1
dxdy = dv * numpy.roll(dv, 1, axis=0)[:, ::-1] #[[dx0*(dy_-1), (dx_-1)*dy0], dx1*dy0, dy1*dy0]]
dxdy_diff = numpy.abs(numpy.diff(dxdy, axis=1))[:, 0] dv = numpy.roll(vertices, -1, axis=0) - vertices # [y1-y0, y2-y1, ...]
err_mult = 2 * numpy.abs(dxdy).sum(axis=1) + 1e-40 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 dxdy_diff = numpy.abs(numpy.diff(dxdy, axis=1))[:, 0]
if not closed_path: err_mult = 2 * numpy.abs(dxdy).sum(axis=1) + 1e-40
slopes_equal[[0, -1]] = False
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…
Cancel
Save