From 5adabfd25ab1a20f6746695a4a95abb817dc53de Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 17 Feb 2020 21:02:53 -0800 Subject: [PATCH] Docstring format change (new param and return format) Also some minor code formatting fixes in utils --- masque/__init__.py | 17 +- masque/file/gdsii.py | 123 ++++++++------ masque/file/svg.py | 37 ++-- masque/file/utils.py | 22 ++- masque/label.py | 55 +++--- masque/pattern.py | 359 ++++++++++++++++++++++++--------------- masque/repetition.py | 166 ++++++++++++------ masque/shapes/arc.py | 52 ++++-- masque/shapes/circle.py | 7 +- masque/shapes/ellipse.py | 14 +- masque/shapes/path.py | 57 ++++--- masque/shapes/polygon.py | 73 ++++---- masque/shapes/shape.py | 149 +++++++++------- masque/shapes/text.py | 15 +- masque/subpattern.py | 92 +++++++--- masque/utils.py | 104 ++++++++---- 16 files changed, 845 insertions(+), 497 deletions(-) diff --git a/masque/__init__.py b/masque/__init__.py index bd6908a..dfb324b 100644 --- a/masque/__init__.py +++ b/masque/__init__.py @@ -6,21 +6,24 @@ with some vectorized element types (eg. circles, not just polygons), better support for E-beam doses, and the ability to output to multiple formats. - Pattern is a basic object containing a 2D lithography mask, composed of a list of Shape - objects and a list of SubPattern objects. + `Pattern` is a basic object containing a 2D lithography mask, composed of a list of `Shape` + objects, a list of `Label` objects, and a list of references to other `Patterns` (using + `SubPattern` and `GridRepetition`). - SubPattern provides basic support for nesting Pattern objects within each other, by adding + `SubPattern` provides basic support for nesting `Pattern` objects within each other, by adding offset, rotation, scaling, and other such properties to a Pattern reference. + `GridRepetition` provides support for nesting regular arrays of `Pattern` objects. + Note that the methods for these classes try to avoid copying wherever possible, so unless otherwise noted, assume that arguments are stored by-reference. Dependencies: - - numpy - - matplotlib [Pattern.visualize(...)] - - python-gdsii [masque.file.gdsii] - - svgwrite [masque.file.svg] + - `numpy` + - `matplotlib` [Pattern.visualize(...)] + - `python-gdsii` [masque.file.gdsii] + - `svgwrite` [masque.file.svg] """ import pathlib diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index 8b4373f..ca80cc6 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -49,39 +49,40 @@ def write(patterns: Pattern or List[Pattern], modify_originals: bool = False, disambiguate_func: Callable[[List[Pattern]], None] = None): """ - Write a Pattern or list of patterns to a GDSII file, by first calling - .polygonize() to change the shapes into polygons, and then writing patterns + Write a `Pattern` or list of patterns to a GDSII file, by first calling + `.polygonize()` to change the shapes into polygons, and then writing patterns as GDSII structures, polygons as boundary elements, and subpatterns as structure references (sref). For each shape, - layer is chosen to be equal to shape.layer if it is an int, - or shape.layer[0] if it is a tuple - datatype is chosen to be shape.layer[1] if available, - otherwise 0 + layer is chosen to be equal to `shape.layer` if it is an int, + or `shape.layer[0]` if it is a tuple + datatype is chosen to be `shape.layer[1]` if available, + otherwise `0` - It is often a good idea to run pattern.subpatternize() prior to calling this function, - especially if calling .polygonize() will result in very many vertices. + It is often a good idea to run `pattern.subpatternize()` prior to calling this function, + especially if calling `.polygonize()` will result in very many vertices. - If you want pattern polygonized with non-default arguments, just call pattern.polygonize() + If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()` prior to calling this function. - :param patterns: A Pattern or list of patterns to write to file. - :param file: Filename or stream object to write to. - :param meters_per_unit: Written into the GDSII file, meters per (database) length unit. - All distances are assumed to be an integer multiple of this unit, and are stored as such. - :param logical_units_per_unit: Written into the GDSII file. Allows the GDSII to specify a - "logical" unit which is different from the "database" unit, for display purposes. - Default 1. - :param library_name: Library name written into the GDSII file. - Default 'masque-gdsii-write'. - :param modify_originals: If True, the original pattern is modified as part of the writing - process. Otherwise, a copy is made and deepunlock()-ed. - Default False. - :param disambiguate_func: Function which takes a list of patterns and alters them - to make their names valid and unique. Default is `disambiguate_pattern_names`, which - attempts to adhere to the GDSII standard as well as possible. - WARNING: No additional error checking is performed on the results. + Args: + patterns: A Pattern or list of patterns to write to file. + file: Filename or stream object to write to. + meters_per_unit: Written into the GDSII file, meters per (database) length unit. + All distances are assumed to be an integer multiple of this unit, and are stored as such. + logical_units_per_unit: Written into the GDSII file. Allows the GDSII to specify a + "logical" unit which is different from the "database" unit, for display purposes. + Default `1`. + library_name: Library name written into the GDSII file. + Default 'masque-gdsii-write'. + modify_originals: If `True`, the original pattern is modified as part of the writing + process. Otherwise, a copy is made and `deepunlock()`-ed. + Default `False`. + disambiguate_func: Function which takes a list of patterns and alters them + to make their names valid and unique. Default is `disambiguate_pattern_names`, which + attempts to adhere to the GDSII standard as well as possible. + WARNING: No additional error checking is performed on the results. """ if isinstance(patterns, Pattern): patterns = [patterns] @@ -124,9 +125,15 @@ def writefile(patterns: List[Pattern] or Pattern, **kwargs, ): """ - Wrapper for gdsii.write() that takes a filename or path instead of a stream. + Wrapper for `gdsii.write()` that takes a filename or path instead of a stream. Will automatically compress the file if it has a .gz suffix. + + Args: + patterns: `Pattern` or list of patterns to save + filename: Filename to save to. + *args: passed to `gdsii.write` + **kwargs: passed to `gdsii.write` """ path = pathlib.Path(filename) if path.suffix == '.gz': @@ -153,8 +160,11 @@ def dose2dtype(patterns: List[Pattern], Note that this function modifies the input Pattern(s). - :param patterns: A Pattern or list of patterns to write to file. Modified by this function. - :returns: (patterns, dose_list) + Args: + patterns: A `Pattern` or list of patterns to write to file. Modified by this function. + + Returns: + (patterns, dose_list) patterns: modified input patterns dose_list: A list of doses, providing a mapping between datatype (int, list index) and dose (float, list entry). @@ -221,9 +231,14 @@ def readfile(filename: str or pathlib.Path, **kwargs, ) -> (Dict[str, Pattern], Dict[str, Any]): """ - Wrapper for gdsii.read() that takes a filename or path instead of a stream. + Wrapper for `gdsii.read()` that takes a filename or path instead of a stream. - Tries to autodetermine file type based on suffixes + Will automatically decompress files with a .gz suffix. + + Args: + filename: Filename to save to. + *args: passed to `gdsii.read` + **kwargs: passed to `gdsii.read` """ path = pathlib.Path(filename) if path.suffix == '.gz': @@ -251,14 +266,18 @@ def read(stream: io.BufferedIOBase, 'logical_units_per_unit': number of "logical" units displayed by layout tools (typically microns) per database unit - :param filename: Filename specifying a GDSII file to read from. - :param use_dtype_as_dose: If false, set each polygon's layer to (gds_layer, gds_datatype). - If true, set the layer to gds_layer and the dose to gds_datatype. - Default False. - :param clean_vertices: If true, remove any redundant vertices when loading polygons. + Args: + filename: Filename specifying a GDSII file to read from. + use_dtype_as_dose: If `False`, set each polygon's layer to `(gds_layer, gds_datatype)`. + If `True`, set the layer to `gds_layer` and the dose to `gds_datatype`. + Default `False`. + clean_vertices: If `True`, remove any redundant vertices when loading polygons. The cleaning process removes any polygons with zero area or <3 vertices. - Default True. - :return: Tuple: (Dict of pattern_name:Patterns generated from GDSII structures, Dict of GDSII library info) + Default `True`. + + Returns: + - Dict of pattern_name:Patterns generated from GDSII structures + - Dict of GDSII library info """ lib = gdsii.library.Library.load(stream) @@ -353,6 +372,7 @@ def read(stream: io.BufferedIOBase, def _mlayer2gds(mlayer): + """ Helper to turn a layer tuple-or-int into a layer and datatype""" if is_scalar(mlayer): layer = mlayer data_type = 0 @@ -366,12 +386,15 @@ def _mlayer2gds(mlayer): def _sref_to_subpat(element: gdsii.elements.SRef) -> SubPattern: - # Helper function to create a SubPattern from an SREF. Sets subpat.pattern to None - # and sets the instance .identifier to the struct_name. - # - # BUG: "Absolute" means not affected by parent elements. - # That's not currently supported by masque at all, so need to either tag it and - # undo the parent transformations, or implement it in masque. + """ + Helper function to create a SubPattern from an SREF. Sets subpat.pattern to None + and sets the instance .identifier to the struct_name. + + BUG: + "Absolute" means not affected by parent elements. + That's not currently supported by masque at all, so need to either tag it and + undo the parent transformations, or implement it in masque. + """ subpat = SubPattern(pattern=None, offset=element.xy) subpat.identifier = element.struct_name if element.strans is not None: @@ -394,13 +417,15 @@ def _sref_to_subpat(element: gdsii.elements.SRef) -> SubPattern: def _aref_to_gridrep(element: gdsii.elements.ARef) -> GridRepetition: - # Helper function to create a GridRepetition from an AREF. Sets gridrep.pattern to None - # and sets the instance .identifier to the struct_name. - # - # BUG: "Absolute" means not affected by parent elements. - # That's not currently supported by masque at all, so need to either tag it and - # undo the parent transformations, or implement it in masque.i + """ + Helper function to create a GridRepetition from an AREF. Sets gridrep.pattern to None + and sets the instance .identifier to the struct_name. + BUG: + "Absolute" means not affected by parent elements. + That's not currently supported by masque at all, so need to either tag it and + undo the parent transformations, or implement it in masque. + """ rotation = 0 offset = numpy.array(element.xy[0]) scale = 1 diff --git a/masque/file/svg.py b/masque/file/svg.py index 6e5ff75..3b8276a 100644 --- a/masque/file/svg.py +++ b/masque/file/svg.py @@ -23,20 +23,21 @@ def writefile(pattern: Pattern, Note that this function modifies the Pattern. - If custom_attributes is True, non-standard pattern_layer and pattern_dose attributes + If `custom_attributes` is `True`, non-standard `pattern_layer` and `pattern_dose` attributes are written to the relevant elements. - It is often a good idea to run pattern.subpatternize() on pattern prior to - calling this function, especially if calling .polygonize() will result in very + It is often a good idea to run `pattern.subpatternize()` on pattern prior to + calling this function, especially if calling `.polygonize()` will result in very many vertices. - If you want pattern polygonized with non-default arguments, just call pattern.polygonize() + If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()` prior to calling this function. - :param pattern: Pattern to write to file. Modified by this function. - :param filename: Filename to write to. - :param custom_attributes: Whether to write non-standard pattern_layer and - pattern_dose attributes to the SVG elements. + Args: + pattern: Pattern to write to file. Modified by this function. + filename: Filename to write to. + custom_attributes: Whether to write non-standard `pattern_layer` and + `pattern_dose` attributes to the SVG elements. """ # Polygonize pattern @@ -85,18 +86,19 @@ def writefile(pattern: Pattern, def writefile_inverted(pattern: Pattern, filename: str): """ - Write an inverted Pattern to an SVG file, by first calling .polygonize() and - .flatten() on it to change the shapes into polygons, then drawing a bounding + Write an inverted Pattern to an SVG file, by first calling `.polygonize()` and + `.flatten()` on it to change the shapes into polygons, then drawing a bounding box and drawing the polygons with reverse vertex order inside it, all within - one element. + one `` element. Note that this function modifies the Pattern. - If you want pattern polygonized with non-default arguments, just call pattern.polygonize() + If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()` prior to calling this function. - :param pattern: Pattern to write to file. Modified by this function. - :param filename: Filename to write to. + Args: + pattern: Pattern to write to file. Modified by this function. + filename: Filename to write to. """ # Polygonize and flatten pattern pattern.polygonize().flatten() @@ -129,8 +131,11 @@ def poly2path(vertices: numpy.ndarray) -> str: """ Create an SVG path string from an Nx2 list of vertices. - :param vertices: Nx2 array of vertices. - :return: SVG path-string. + Args: + vertices: Nx2 array of vertices. + + Returns: + SVG path-string. """ commands = 'M{:g},{:g} '.format(vertices[0][0], vertices[0][1]) for vertex in vertices[1:]: diff --git a/masque/file/utils.py b/masque/file/utils.py index 97e3d36..f8b2841 100644 --- a/masque/file/utils.py +++ b/masque/file/utils.py @@ -12,11 +12,14 @@ __author__ = 'Jan Petykiewicz' def mangle_name(pattern: Pattern, dose_multiplier: float=1.0) -> str: """ - Create a name using pattern.name, id(pattern), and the dose multiplier. + Create a name using `pattern.name`, `id(pattern)`, and the dose multiplier. - :param pattern: Pattern whose name we want to mangle. - :param dose_multiplier: Dose multiplier to mangle with. - :return: Mangled name. + Args: + pattern: Pattern whose name we want to mangle. + dose_multiplier: Dose multiplier to mangle with. + + Returns: + Mangled name. """ expression = re.compile('[^A-Za-z0-9_\?\$]') full_name = '{}_{}_{}'.format(pattern.name, dose_multiplier, id(pattern)) @@ -26,11 +29,14 @@ def mangle_name(pattern: Pattern, dose_multiplier: float=1.0) -> str: def make_dose_table(patterns: List[Pattern], dose_multiplier: float=1.0) -> Set[Tuple[int, float]]: """ - Create a set containing (id(pat), written_dose) for each pattern (including subpatterns) + Create a set containing `(id(pat), written_dose)` for each pattern (including subpatterns) - :param pattern: Source Patterns. - :param dose_multiplier: Multiplier for all written_dose entries. - :return: {(id(subpat.pattern), written_dose), ...} + Args: + pattern: Source Patterns. + dose_multiplier: Multiplier for all written_dose entries. + + Returns: + `{(id(subpat.pattern), written_dose), ...}` """ dose_table = {(id(pattern), dose_multiplier) for pattern in patterns} for pattern in patterns: diff --git a/masque/label.py b/masque/label.py index 5dd6e57..1e46e6a 100644 --- a/masque/label.py +++ b/masque/label.py @@ -15,19 +15,21 @@ class Label: A text annotation with a position and layer (but no size; it is not drawn) """ __slots__ = ('_offset', '_layer', '_string', 'identifier', 'locked') - # [x_offset, y_offset] + _offset: numpy.ndarray + """ [x_offset, y_offset] """ - # Layer (integer >= 0) or 2-Tuple of integers _layer: int or Tuple + """ Layer (integer >= 0, or 2-Tuple of integers) """ - # Label string _string: str + """ Label string """ - # Arbitrary identifier tuple identifier: Tuple + """ Arbitrary identifier tuple, useful for keeping track of history when flattening """ - locked: bool # If True, any changes to the label will raise a PatternLockedError + locked: bool + """ If `True`, any changes to the label will raise a `PatternLockedError` """ def __setattr__(self, name, value): if self.locked and name != 'locked': @@ -40,8 +42,6 @@ class Label: def offset(self) -> numpy.ndarray: """ [x, y] offset - - :return: [x_offset, y_offset] """ return self._offset @@ -59,8 +59,6 @@ class Label: def layer(self) -> int or Tuple[int]: """ Layer number (int or tuple of ints) - - :return: Layer """ return self._layer @@ -73,8 +71,6 @@ class Label: def string(self) -> str: """ Label string (str) - - :return: string """ return self._string @@ -109,29 +105,33 @@ class Label: def copy(self) -> 'Label': """ - Returns a deep copy of the shape. - - :return: Deep copy of self + Returns a deep copy of the label. """ return copy.deepcopy(self) def translate(self, offset: vector2) -> 'Label': """ - Translate the shape by the given offset + Translate the label by the given offset - :param offset: [x_offset, y,offset] - :return: self + Args: + offset: [x_offset, y,offset] + + Returns: + self """ self.offset += offset return self def rotate_around(self, pivot: vector2, rotation: float) -> 'Label': """ - Rotate the shape around a point. + Rotate the label around a point. - :param pivot: Point (x, y) to rotate around - :param rotation: Angle to rotate by (counterclockwise, radians) - :return: self + Args: + pivot: Point (x, y) to rotate around + rotation: Angle to rotate by (counterclockwise, radians) + + Returns: + self """ pivot = numpy.array(pivot, dtype=float) self.translate(-pivot) @@ -147,24 +147,27 @@ class Label: bounds = [self.offset, self.offset] - :return: Bounds [[xmin, xmax], [ymin, ymax]] + Returns: + Bounds [[xmin, xmax], [ymin, ymax]] """ return numpy.array([self.offset, self.offset]) def lock(self) -> 'Label': """ - Lock the Label + Lock the Label, causing any modifications to raise an exception. - :return: self + Return: + self """ object.__setattr__(self, 'locked', True) return self def unlock(self) -> 'Label': """ - Unlock the Label + Unlock the Label, re-allowing changes. - :return: self + Return: + self """ object.__setattr__(self, 'locked', False) return self diff --git a/masque/pattern.py b/masque/pattern.py index 50d4e34..ab0e99f 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -27,22 +27,32 @@ visitor_function_t = Callable[['Pattern', Tuple['Pattern'], Dict, numpy.ndarray] class Pattern: """ - 2D layout consisting of some set of shapes and references to other Pattern objects - (via SubPattern). Shapes are assumed to inherit from .shapes.Shape or provide equivalent - functions. - - :var shapes: List of all shapes in this Pattern. Elements in this list are assumed to inherit - from Shape or provide equivalent functions. - :var subpatterns: List of all SubPattern objects in this Pattern. Multiple SubPattern objects - may reference the same Pattern object. - :var name: An identifier for this object. Not necessarily unique. + 2D layout consisting of some set of shapes, labels, and references to other Pattern objects + (via SubPattern and GridRepetition). Shapes are assumed to inherit from + masque.shapes.Shape or provide equivalent functions. """ __slots__ = ('shapes', 'labels', 'subpatterns', 'name', 'locked') + shapes: List[Shape] + """ List of all shapes in this Pattern. + Elements in this list are assumed to inherit from Shape or provide equivalent functions. + """ + labels: List[Label] + """ List of all labels in this Pattern. """ + subpatterns: List[SubPattern or GridRepetition] + """ List of all objects referencing other patterns in this Pattern. + Examples are SubPattern (gdsii "instances") or GridRepetition (gdsii "arrays") + Multiple objects in this list may reference the same Pattern object + (multiple instances of the same object). + """ + name: str + """ A name for this pattern """ + locked: bool + """ When the pattern is locked, no changes may be made. """ def __init__(self, name: str = '', @@ -55,11 +65,12 @@ class Pattern: Basic init; arguments get assigned to member variables. Non-list inputs for shapes and subpatterns get converted to lists. - :param shapes: Initial shapes in the Pattern - :param labels: Initial labels in the Pattern - :param subpatterns: Initial subpatterns in the Pattern - :param name: An identifier for the Pattern - :param locked: Whether to lock the pattern after construction + Args: + shapes: Initial shapes in the Pattern + labels: Initial labels in the Pattern + subpatterns: Initial subpatterns in the Pattern + name: An identifier for the Pattern + locked: Whether to lock the pattern after construction """ self.unlock() if isinstance(shapes, list): @@ -106,8 +117,11 @@ class Pattern: Appends all shapes, labels and subpatterns from other_pattern to self's shapes, labels, and supbatterns. - :param other_pattern: The Pattern to append - :return: self + Args: + other_pattern: The Pattern to append + + Returns: + self """ self.subpatterns += other_pattern.subpatterns self.shapes += other_pattern.shapes @@ -125,16 +139,19 @@ class Pattern: given entity_func returns True. Self is _not_ altered, but shapes, labels, and subpatterns are _not_ copied. - :param shapes_func: Given a shape, returns a boolean denoting whether the shape is a member - of the subset. Default always returns False. - :param labels_func: Given a label, returns a boolean denoting whether the label is a member - of the subset. Default always returns False. - :param subpatterns_func: Given a subpattern, returns a boolean denoting if it is a member - of the subset. Default always returns False. - :param recursive: If True, also calls .subset() recursively on patterns referenced by this - pattern. - :return: A Pattern containing all the shapes and subpatterns for which the parameter - functions return True + Args: + shapes_func: Given a shape, returns a boolean denoting whether the shape is a member + of the subset. Default always returns False. + labels_func: Given a label, returns a boolean denoting whether the label is a member + of the subset. Default always returns False. + subpatterns_func: Given a subpattern, returns a boolean denoting if it is a member + of the subset. Default always returns False. + recursive: If True, also calls .subset() recursively on patterns referenced by this + pattern. + + Returns: + A Pattern containing all the shapes and subpatterns for which the parameter + functions return True """ def do_subset(src): pat = Pattern(name=src.name) @@ -163,12 +180,17 @@ class Pattern: It is only applied to any given pattern once, regardless of how many times it is referenced. - :param func: Function which accepts a Pattern, and returns a pattern. - :param memo: Dictionary used to avoid re-running on multiply-referenced patterns. - Stores {id(pattern): func(pattern)} for patterns which have already been processed. - Default None (no already-processed patterns). - :return: The result of applying func() to this pattern and all subpatterns. - :raises: PatternError if called on a pattern containing a circular reference. + Args: + func: Function which accepts a Pattern, and returns a pattern. + memo: Dictionary used to avoid re-running on multiply-referenced patterns. + Stores `{id(pattern): func(pattern)}` for patterns which have already been processed. + Default `None` (no already-processed patterns). + + Returns: + The result of applying func() to this pattern and all subpatterns. + + Raises: + PatternError if called on a pattern containing a circular reference. """ if memo is None: memo = {} @@ -212,19 +234,24 @@ class Pattern: for the instance being visited `memo`: Arbitrary dict (not altered except by visit_*()) - :param visit_before: Function to call before traversing subpatterns. + Args: + visit_before: Function to call before traversing subpatterns. + Should accept a `Pattern` and `**visit_args`, and return the (possibly modified) + pattern. Default `None` (not called). + visit_after: Function to call after traversing subpatterns. Should accept a Pattern and **visit_args, and return the (possibly modified) - pattern. Default None (not called). - :param visit_after: Function to call after traversing subpatterns. - Should accept a Pattern and **visit_args, and return the (possibly modified) - pattern. Default None (not called). - :param transform: Initial value for `visit_args['transform']`. + pattern. Default `None` (not called). + transform: Initial value for `visit_args['transform']`. Can be `False`, in which case the transform is not calculated. - `True` or `None` is interpreted as [0, 0, 0, 0]. - :param memo: Arbitrary dict for use by visit_*() functions. Default None (empty dict). - :param hierarchy: Tuple of patterns specifying the hierarchy above the current pattern. + `True` or `None` is interpreted as `[0, 0, 0, 0]`. + memo: Arbitrary dict for use by `visit_*()` functions. Default `None` (empty dict). + hierarchy: Tuple of patterns specifying the hierarchy above the current pattern. Appended to the start of the generated `visit_args['hierarchy']`. Default is an empty tuple. + + Returns: + The result, including `visit_before(self, ...)` and `visit_after(self, ...)`. + Note that `self` may also be altered! """ if memo is None: memo = {} @@ -267,16 +294,19 @@ class Pattern: poly_max_arclen: float = None, ) -> 'Pattern': """ - Calls .to_polygons(...) on all the shapes in this Pattern and any referenced patterns, + Calls `.to_polygons(...)` on all the shapes in this Pattern and any referenced patterns, replacing them with the returned polygons. - Arguments are passed directly to shape.to_polygons(...). + Arguments are passed directly to `shape.to_polygons(...)`. - :param poly_num_points: Number of points to use for each polygon. Can be overridden by - poly_max_arclen if that results in more points. Optional, defaults to shapes' - internal defaults. - :param poly_max_arclen: Maximum arclength which can be approximated by a single line + Args: + poly_num_points: Number of points to use for each polygon. Can be overridden by + `poly_max_arclen` if that results in more points. Optional, defaults to shapes' + internal defaults. + poly_max_arclen: Maximum arclength which can be approximated by a single line segment. Optional, defaults to shapes' internal defaults. - :return: self + + Returns: + self """ old_shapes = self.shapes self.shapes = list(itertools.chain.from_iterable( @@ -291,12 +321,15 @@ class Pattern: grid_y: numpy.ndarray, ) -> 'Pattern': """ - Calls .polygonize() and .flatten on the pattern, then calls .manhattanize() on all the + Calls `.polygonize()` and `.flatten()` on the pattern, then calls `.manhattanize()` on all the resulting shapes, replacing them with the returned Manhattan polygons. - :param grid_x: List of allowed x-coordinates for the Manhattanized polygon edges. - :param grid_y: List of allowed y-coordinates for the Manhattanized polygon edges. - :return: self + Args: + grid_x: List of allowed x-coordinates for the Manhattanized polygon edges. + grid_y: List of allowed y-coordinates for the Manhattanized polygon edges. + + Returns: + self """ self.polygonize().flatten() @@ -311,21 +344,25 @@ class Pattern: exclude_types: Tuple[Shape] = (Polygon,) ) -> 'Pattern': """ - Iterates through this Pattern and all referenced Patterns. Within each Pattern, it iterates - over all shapes, calling .normalized_form(norm_value) on them to retrieve a scale-, + Iterates through this `Pattern` and all referenced `Pattern`s. Within each `Pattern`, it iterates + over all shapes, calling `.normalized_form(norm_value)` on them to retrieve a scale-, offset-, dose-, and rotation-independent form. Each shape whose normalized form appears more than once is removed and re-added using subpattern objects referencing a newly-created - Pattern containing only the normalized form of the shape. + `Pattern` containing only the normalized form of the shape. - Note that the default norm_value was chosen to give a reasonable precision when converting - to GDSII, which uses integer values for pixel coordinates. + Note: + The default norm_value was chosen to give a reasonable precision when converting + to GDSII, which uses integer values for pixel coordinates. - :param recursive: Whether to call recursively on self's subpatterns. Default True. - :param norm_value: Passed to shape.normalized_form(norm_value). Default 1e6 (see function + Args: + recursive: Whether to call recursively on self's subpatterns. Default `True`. + norm_value: Passed to `shape.normalized_form(norm_value)`. Default `1e6` (see function note about GDSII) - :param exclude_types: Shape types passed in this argument are always left untouched, for - speed or convenience. Default: (Shapes.Polygon,) - :return: self + exclude_types: Shape types passed in this argument are always left untouched, for + speed or convenience. Default: `(shapes.Polygon,)` + + Returns: + self """ if exclude_types is None: @@ -337,9 +374,9 @@ class Pattern: norm_value=norm_value, exclude_types=exclude_types) - # Create a dict which uses the label tuple from .normalized_form() as a key, and which - # stores (function_to_create_normalized_shape, [(index_in_shapes, values), ...]), where - # values are the (offset, scale, rotation, dose) values as calculated by .normalized_form() + # Create a dict which uses the label tuple from `.normalized_form()` as a key, and which + # stores `(function_to_create_normalized_shape, [(index_in_shapes, values), ...])`, where + # values are the `(offset, scale, rotation, dose)` values as calculated by `.normalized_form()` shape_table = defaultdict(lambda: [None, list()]) for i, shape in enumerate(self.shapes): if not any((isinstance(shape, t) for t in exclude_types)): @@ -348,9 +385,9 @@ class Pattern: shape_table[label][1].append((i, values)) # Iterate over the normalized shapes in the table. If any normalized shape occurs more than - # once, create a Pattern holding a normalized shape object, and add self.subpatterns + # once, create a `Pattern` holding a normalized shape object, and add `self.subpatterns` # entries for each occurrence in self. Also, note down that we should delete the - # self.shapes entries for which we made SubPatterns. + # `self.shapes` entries for which we made SubPatterns. shapes_to_remove = [] for label in shape_table: if len(shape_table[label][1]) > 1: @@ -374,21 +411,23 @@ class Pattern: """ Represents the pattern as a list of polygons. - Deep-copies the pattern, then calls .polygonize() and .flatten() on the copy in order to + Deep-copies the pattern, then calls `.polygonize()` and `.flatten()` on the copy in order to generate the list of polygons. - :return: A list of (Ni, 2) numpy.ndarrays specifying vertices of the polygons. Each ndarray - is of the form [[x0, y0], [x1, y1],...]. + Returns: + A list of `(Ni, 2)` `numpy.ndarray`s specifying vertices of the polygons. Each ndarray + is of the form `[[x0, y0], [x1, y1],...]`. """ pat = self.deepcopy().deepunlock().polygonize().flatten() return [shape.vertices + shape.offset for shape in pat.shapes] def referenced_patterns_by_id(self) -> Dict[int, 'Pattern']: """ - Create a dictionary of {id(pat): pat} for all Pattern objects referenced by this + Create a dictionary of `{id(pat): pat}` for all Pattern objects referenced by this Pattern (operates recursively on all referenced Patterns as well) - :return: Dictionary of {id(pat): pat} for all referenced Pattern objects + Returns: + Dictionary of `{id(pat): pat}` for all referenced Pattern objects """ ids = {} for subpat in self.subpatterns: @@ -399,11 +438,12 @@ class Pattern: def get_bounds(self) -> Union[numpy.ndarray, None]: """ - Return a numpy.ndarray containing [[x_min, y_min], [x_max, y_max]], corresponding to the + Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the extent of the Pattern's contents in each dimension. - Returns None if the Pattern is empty. + Returns `None` if the Pattern is empty. - :return: [[x_min, y_min], [x_max, y_max]] or None + Returns: + `[[x_min, y_min], [x_max, y_max]]` or `None` """ entries = self.shapes + self.subpatterns + self.labels if not entries: @@ -428,13 +468,16 @@ class Pattern: Shape identifiers are changed to represent their original position in the pattern hierarchy: - (L1_name (str), L1_index (int), L2_name, L2_index, ..., *original_shape_identifier) - where L1_name is the first-level subpattern's name (e.g. self.subpatterns[0].pattern.name), - L2_name is the next-level subpattern's name (e.g. - self.subpatterns[0].pattern.subpatterns[0].pattern.name) and L1_index is an integer - used to differentiate between multiple instance of the same (or same-named) subpatterns. + `(L1_name (str), L1_index (int), L2_name, L2_index, ..., *original_shape_identifier)` + where + `L1_name` is the first-level subpattern's name (e.g. `self.subpatterns[0].pattern.name`), + `L2_name` is the next-level subpattern's name (e.g. + `self.subpatterns[0].pattern.subpatterns[0].pattern.name`) and + `L1_index` is an integer used to differentiate between multiple instance ofi the same + (or same-named) subpatterns. - :return: self + Returns: + self """ subpatterns = copy.deepcopy(self.subpatterns) self.subpatterns = [] @@ -457,22 +500,28 @@ class Pattern: """ Translates all shapes, label, and subpatterns by the given offset. - :param offset: Offset to translate by - :return: self + Args: + offset: (x, y) to translate by + + Returns: + self """ for entry in self.shapes + self.subpatterns + self.labels: entry.translate(offset) return self - def scale_elements(self, scale: float) -> 'Pattern': + def scale_elements(self, c: float) -> 'Pattern': """" Scales all shapes and subpatterns by the given value. - :param scale: value to scale by - :return: self + Args: + c: factor to scale by + + Returns: + self """ for entry in self.shapes + self.subpatterns: - entry.scale(scale) + entry.scale(c) return self def scale_by(self, c: float) -> 'Pattern': @@ -480,8 +529,11 @@ class Pattern: Scale this Pattern by the given value (all shapes and subpatterns and their offsets are scaled) - :param c: value to scale by - :return: self + Args: + c: factor to scale by + + Returns: + self """ for entry in self.shapes + self.subpatterns: entry.offset *= c @@ -494,9 +546,12 @@ class Pattern: """ Rotate the Pattern around the a location. - :param pivot: Location to rotate around - :param rotation: Angle to rotate by (counter-clockwise, radians) - :return: self + Args: + pivot: (x, y) location to rotate around + rotation: Angle to rotate by (counter-clockwise, radians) + + Returns: + self """ pivot = numpy.array(pivot) self.translate_elements(-pivot) @@ -509,8 +564,11 @@ class Pattern: """ Rotate the offsets of all shapes, labels, and subpatterns around (0, 0) - :param rotation: Angle to rotate by (counter-clockwise, radians) - :return: self + Args: + rotation: Angle to rotate by (counter-clockwise, radians) + + Returns: + self """ for entry in self.shapes + self.subpatterns + self.labels: entry.offset = numpy.dot(rotation_matrix_2d(rotation), entry.offset) @@ -520,8 +578,11 @@ class Pattern: """ Rotate each shape and subpattern around its center (offset) - :param rotation: Angle to rotate by (counter-clockwise, radians) - :return: self + Args: + rotation: Angle to rotate by (counter-clockwise, radians) + + Returns: + self """ for entry in self.shapes + self.subpatterns: entry.rotate(rotation) @@ -531,8 +592,12 @@ class Pattern: """ Mirror the offsets of all shapes, labels, and subpatterns across an axis - :param axis: Axis to mirror across - :return: self + Args: + axis: Axis to mirror across + (0: mirror across x axis, 1: mirror across y axis) + + Returns: + self """ for entry in self.shapes + self.subpatterns + self.labels: entry.offset[axis - 1] *= -1 @@ -541,10 +606,14 @@ class Pattern: def mirror_elements(self, axis: int) -> 'Pattern': """ Mirror each shape and subpattern across an axis, relative to its - center (offset) + offset - :param axis: Axis to mirror across - :return: self + Args: + axis: Axis to mirror across + (0: mirror across x axis, 1: mirror across y axis) + + Returns: + self """ for entry in self.shapes + self.subpatterns: entry.mirror(axis) @@ -554,22 +623,29 @@ class Pattern: """ Mirror the Pattern across an axis - :param axis: Axis to mirror across - :return: self + Args: + axis: Axis to mirror across + (0: mirror across x axis, 1: mirror across y axis) + + Returns: + self """ self.mirror_elements(axis) self.mirror_element_centers(axis) return self - def scale_element_doses(self, factor: float) -> 'Pattern': + def scale_element_doses(self, c: float) -> 'Pattern': """ Multiply all shape and subpattern doses by a factor - :param factor: Factor to multiply doses by - :return: self + Args: + c: Factor to multiply doses by + + Return: + self """ for entry in self.shapes + self.subpatterns: - entry.dose *= factor + entry.dose *= c return self def copy(self) -> 'Pattern': @@ -577,25 +653,26 @@ class Pattern: Return a copy of the Pattern, deep-copying shapes and copying subpattern entries, but not deep-copying any referenced patterns. - See also: Pattern.deepcopy() + See also: `Pattern.deepcopy()` - :return: A copy of the current Pattern. + Returns: + A copy of the current Pattern. """ return copy.copy(self) def deepcopy(self) -> 'Pattern': """ - Convenience method for copy.deepcopy(pattern) + Convenience method for `copy.deepcopy(pattern)` - :return: A deep copy of the current Pattern. + Returns: + A deep copy of the current Pattern. """ return copy.deepcopy(self) def is_empty(self) -> bool: """ - Returns true if the Pattern contains no shapes, labels, or subpatterns. - - :return: True if the pattern is empty. + Returns: + True if the pattern is contains no shapes, labels, or subpatterns. """ return (len(self.subpatterns) == 0 and len(self.shapes) == 0 and @@ -603,9 +680,11 @@ class Pattern: def lock(self) -> 'Pattern': """ - Lock the pattern + Lock the pattern, raising an exception if it is modified. + Also see `deeplock()`. - :return: self + Returns: + self """ object.__setattr__(self, 'locked', True) return self @@ -614,16 +693,18 @@ class Pattern: """ Unlock the pattern - :return: self + Returns: + self """ object.__setattr__(self, 'locked', False) return self def deeplock(self) -> 'Pattern': """ - Recursively lock the pattern, all referenced shapes, subpatterns, and labels + Recursively lock the pattern, all referenced shapes, subpatterns, and labels. - :return: self + Returns: + self """ self.lock() for ss in self.shapes + self.labels: @@ -634,11 +715,13 @@ class Pattern: def deepunlock(self) -> 'Pattern': """ - Recursively unlock the pattern, all referenced shapes, subpatterns, and labels + Recursively unlock the pattern, all referenced shapes, subpatterns, and labels. - This is dangerous unless you have just performed a deepcopy! + This is dangerous unless you have just performed a deepcopy, since anything + you change will be changed everywhere it is referenced! - :return: self + Return: + self """ self.unlock() for ss in self.shapes + self.labels: @@ -650,10 +733,13 @@ class Pattern: @staticmethod def load(filename: str) -> 'Pattern': """ - Load a Pattern from a file + Load a Pattern from a file using pickle - :param filename: Filename to load from - :return: Loaded Pattern + Args: + filename: Filename to load from + + Returns: + Loaded Pattern """ with open(filename, 'rb') as f: pattern = pickle.load(f) @@ -662,10 +748,13 @@ class Pattern: def save(self, filename: str) -> 'Pattern': """ - Save the Pattern to a file + Save the Pattern to a file using pickle - :param filename: Filename to save to - :return: self + Args: + filename: Filename to save to + + Returns: + self """ with open(filename, 'wb') as f: pickle.dump(self, f, protocol=pickle.HIGHEST_PROTOCOL) @@ -679,12 +768,16 @@ class Pattern: """ Draw a picture of the Pattern and wait for the user to inspect it - Imports matplotlib. + Imports `matplotlib`. - :param offset: Coordinates to offset by before drawing - :param line_color: Outlines are drawn with this color (passed to matplotlib PolyCollection) - :param fill_color: Interiors are drawn with this color (passed to matplotlib PolyCollection) - :param overdraw: Whether to create a new figure or draw on a pre-existing one + Note that this can be slow; it is often faster to export to GDSII and use + klayout or a different GDS viewer! + + Args: + offset: Coordinates to offset by before drawing + line_color: Outlines are drawn with this color (passed to `matplotlib.collections.PolyCollection`) + fill_color: Interiors are drawn with this color (passed to `matplotlib.collections.PolyCollection`) + overdraw: Whether to create a new figure or draw on a pre-existing one """ # TODO: add text labels to visualize() from matplotlib import pyplot diff --git a/masque/repetition.py b/masque/repetition.py index 5ed652e..924a756 100644 --- a/masque/repetition.py +++ b/masque/repetition.py @@ -20,8 +20,8 @@ __author__ = 'Jan Petykiewicz' class GridRepetition: """ - GridRepetition provides support for efficiently embedding multiple copies of a Pattern - into another Pattern at regularly-spaced offsets. + GridRepetition provides support for efficiently embedding multiple copies of a `Pattern` + into another `Pattern` at regularly-spaced offsets. """ __slots__ = ('pattern', '_offset', @@ -37,24 +37,49 @@ class GridRepetition: 'locked') pattern: 'Pattern' + """ The `Pattern` being instanced """ _offset: numpy.ndarray + """ (x, y) offset for the base instance """ + _dose: float + """ Dose factor """ _rotation: float - ''' Applies to individual instances in the grid, not the grid vectors ''' + """ Rotation of the individual instances in the grid (not the grid vectors). + Radians, counterclockwise. + """ + _scale: float - ''' Applies to individual instances in the grid, not the grid vectors ''' + """ Scaling factor applied to individual instances in the grid (not the grid vectors) """ + _mirrored: List[bool] - ''' Applies to individual instances in the grid, not the grid vectors ''' + """ Whether to mirror individual instances across the x and y axes + (Applies to individual instances in the grid, not the grid vectors) + """ _a_vector: numpy.ndarray - _b_vector: numpy.ndarray or None + """ Vector `[x, y]` specifying the first lattice vector of the grid. + Specifies center-to-center spacing between adjacent elements. + """ + _a_count: int + """ Number of instances along the direction specified by the `a_vector` """ + + _b_vector: numpy.ndarray or None + """ Vector `[x, y]` specifying a second lattice vector for the grid. + Specifies center-to-center spacing between adjacent elements. + Can be `None` for a 1D array. + """ + _b_count: int + """ Number of instances along the direction specified by the `b_vector` """ identifier: Tuple + """ Arbitrary identifier """ + locked: bool + """ If `True`, disallows changes to the GridRepetition """ def __init__(self, pattern: 'Pattern', @@ -69,17 +94,20 @@ class GridRepetition: scale: float = 1.0, locked: bool = False): """ - :param a_vector: First lattice vector, of the form [x, y]. - Specifies center-to-center spacing between adjacent elements. - :param a_count: Number of elements in the a_vector direction. - :param b_vector: Second lattice vector, of the form [x, y]. - Specifies center-to-center spacing between adjacent elements. - Can be omitted when specifying a 1D array. - :param b_count: Number of elements in the b_vector direction. - Should be omitted if b_vector was omitted. - :param locked: Whether the subpattern is locked after initialization. - :raises: PatternError if b_* inputs conflict with each other - or a_count < 1. + Args: + a_vector: First lattice vector, of the form `[x, y]`. + Specifies center-to-center spacing between adjacent elements. + a_count: Number of elements in the a_vector direction. + b_vector: Second lattice vector, of the form `[x, y]`. + Specifies center-to-center spacing between adjacent elements. + Can be omitted when specifying a 1D array. + b_count: Number of elements in the `b_vector` direction. + Should be omitted if `b_vector` was omitted. + locked: Whether the `GridRepetition` is locked after initialization. + + Raises: + PatternError if `b_*` inputs conflict with each other + or `a_count < 1`. """ if b_vector is None: if b_count > 1: @@ -254,9 +282,11 @@ class GridRepetition: def as_pattern(self) -> 'Pattern': """ Returns a copy of self.pattern which has been scaled, rotated, repeated, etc. - etc. according to this GridRepetitions's properties. - :return: Copy of self.pattern that has been repeated / altered as implied by - this object's other properties. + etc. according to this `GridRepetition`'s properties. + + Returns: + A copy of self.pattern which has been scaled, rotated, repeated, etc. + etc. according to this `GridRepetition`'s properties. """ patterns = [] @@ -283,8 +313,11 @@ class GridRepetition: """ Translate by the given offset - :param offset: Translate by this offset - :return: self + Args: + offset: `[x, y]` to translate by + + Returns: + self """ self.offset += offset return self @@ -293,9 +326,12 @@ class GridRepetition: """ Rotate the array around a point - :param pivot: Point to rotate around - :param rotation: Angle to rotate by (counterclockwise, radians) - :return: self + Args: + pivot: Point `[x, y]` to rotate around + rotation: Angle to rotate by (counterclockwise, radians) + + Returns: + self """ pivot = numpy.array(pivot, dtype=float) self.translate(-pivot) @@ -308,8 +344,11 @@ class GridRepetition: """ Rotate around (0, 0) - :param rotation: Angle to rotate by (counterclockwise, radians) - :return: self + Args: + rotation: Angle to rotate by (counterclockwise, radians) + + Returns: + self """ self.rotate_elements(rotation) self.a_vector = numpy.dot(rotation_matrix_2d(rotation), self.a_vector) @@ -321,8 +360,11 @@ class GridRepetition: """ Rotate each element around its origin - :param rotation: Angle to rotate by (counterclockwise, radians) - :return: self + Args: + rotation: Angle to rotate by (counterclockwise, radians) + + Returns: + self """ self.rotation += rotation return self @@ -331,8 +373,12 @@ class GridRepetition: """ Mirror the GridRepetition across an axis. - :param axis: Axis to mirror across. - :return: self + Args: + axis: Axis to mirror across. + (0: mirror across x-axis, 1: mirror across y-axis) + + Returns: + self """ self.mirror_elements(axis) self.a_vector[1-axis] *= -1 @@ -344,8 +390,12 @@ class GridRepetition: """ Mirror each element across an axis relative to its origin. - :param axis: Axis to mirror across. - :return: self + Args: + axis: Axis to mirror across. + (0: mirror across x-axis, 1: mirror across y-axis) + + Returns: + self """ self.mirrored[axis] = not self.mirrored[axis] self.rotation *= -1 @@ -353,11 +403,12 @@ class GridRepetition: def get_bounds(self) -> numpy.ndarray or None: """ - Return a numpy.ndarray containing [[x_min, y_min], [x_max, y_max]], corresponding to the - extent of the GridRepetition in each dimension. - Returns None if the contained Pattern is empty. + Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the + extent of the `GridRepetition` in each dimension. + Returns `None` if the contained `Pattern` is empty. - :return: [[x_min, y_min], [x_max, y_max]] or None + Returns: + `[[x_min, y_min], [x_max, y_max]]` or `None` """ return self.as_pattern().get_bounds() @@ -365,7 +416,11 @@ class GridRepetition: """ Scale the GridRepetition by a factor - :param c: scaling factor + Args: + c: scaling factor + + Returns: + self """ self.scale_elements_by(c) self.a_vector *= c @@ -377,7 +432,11 @@ class GridRepetition: """ Scale each element by a factor - :param c: scaling factor + Args: + c: scaling factor + + Returns: + self """ self.scale *= c return self @@ -386,7 +445,8 @@ class GridRepetition: """ Return a shallow copy of the repetition. - :return: copy.copy(self) + Returns: + `copy.copy(self)` """ return copy.copy(self) @@ -394,33 +454,37 @@ class GridRepetition: """ Return a deep copy of the repetition. - :return: copy.copy(self) + Returns: + `copy.deepcopy(self)` """ return copy.deepcopy(self) def lock(self) -> 'GridRepetition': """ - Lock the GridRepetition + Lock the `GridRepetition`, disallowing changes. - :return: self + Returns: + self """ object.__setattr__(self, 'locked', True) return self def unlock(self) -> 'GridRepetition': """ - Unlock the GridRepetition + Unlock the `GridRepetition` - :return: self + Returns: + self """ object.__setattr__(self, 'locked', False) return self def deeplock(self) -> 'GridRepetition': """ - Recursively lock the GridRepetition and its contained pattern + Recursively lock the `GridRepetition` and its contained pattern - :return: self + Returns: + self """ self.lock() self.pattern.deeplock() @@ -428,11 +492,13 @@ class GridRepetition: def deepunlock(self) -> 'GridRepetition': """ - Recursively unlock the GridRepetition and its contained pattern + Recursively unlock the `GridRepetition` and its contained pattern - This is dangerous unless you have just performed a deepcopy! + This is dangerous unless you have just performed a deepcopy, since + the component parts may be reused elsewhere. - :return: self + Returns: + self """ self.unlock() self.pattern.deepunlock() diff --git a/masque/shapes/arc.py b/masque/shapes/arc.py index 467db98..25aff16 100644 --- a/masque/shapes/arc.py +++ b/masque/shapes/arc.py @@ -24,19 +24,28 @@ class Arc(Shape): __slots__ = ('_radii', '_angles', '_width', '_rotation', 'poly_num_points', 'poly_max_arclen') _radii: numpy.ndarray - _angles: numpy.ndarray - _width: float + """ Two radii for defining an ellipse """ + _rotation: float + """ Rotation (ccw, radians) from the x axis to the first radius """ + + _angles: numpy.ndarray + """ Start and stop angles (ccw, radians) for choosing an arc from the ellipse, measured from the first radius """ + + _width: float + """ Width of the arc """ + poly_num_points: int + """ Sets the default number of points for `.polygonize()` """ + poly_max_arclen: float + """ Sets the default max segement length for `.polygonize()` """ # radius properties @property def radii(self) -> numpy.ndarray: """ - Return the radii [rx, ry] - - :return: [rx, ry] + Return the radii `[rx, ry]` """ return self._radii @@ -73,10 +82,11 @@ class Arc(Shape): @property def angles(self) -> vector2: """ - Return the start and stop angles [a_start, a_stop]. + Return the start and stop angles `[a_start, a_stop]`. Angles are measured from x-axis after rotation - :return: [a_start, a_stop] + Returns: + `[a_start, a_stop]` """ return self._angles @@ -109,7 +119,8 @@ class Arc(Shape): """ Rotation of radius_x from x_axis, counterclockwise, in radians. Stored mod 2*pi - :return: rotation counterclockwise in radians + Returns: + rotation counterclockwise in radians """ return self._rotation @@ -125,7 +136,8 @@ class Arc(Shape): """ Width of the arc (difference between inner and outer radii) - :return: width + Returns: + width """ return self._width @@ -225,12 +237,12 @@ class Arc(Shape): def get_bounds(self) -> numpy.ndarray: ''' Equation for rotated ellipse is - x = x0 + a * cos(t) * cos(rot) - b * sin(t) * sin(phi) - y = y0 + a * cos(t) * sin(rot) + b * sin(t) * cos(rot) - where t is our parameter. + `x = x0 + a * cos(t) * cos(rot) - b * sin(t) * sin(phi)` + `y = y0 + a * cos(t) * sin(rot) + b * sin(t) * cos(rot)` + where `t` is our parameter. - Differentiating and solving for 0 slope wrt. t, we find - tan(t) = -+ b/a cot(phi) + Differentiating and solving for 0 slope wrt. `t`, we find + `tan(t) = -+ b/a cot(phi)` where -+ is for x, y cases, so that's where the extrema are. If the extrema are innaccessible due to arc constraints, check the arc endpoints instead. @@ -329,8 +341,11 @@ class Arc(Shape): def get_cap_edges(self) -> numpy.ndarray: ''' - :returns: [[[x0, y0], [x1, y1]], array of 4 points, specifying the two cuts which - [[x2, y2], [x3, y3]]], would create this arc from its corresponding ellipse. + Returns: + ``` + [[[x0, y0], [x1, y1]], array of 4 points, specifying the two cuts which + [[x2, y2], [x3, y3]]], would create this arc from its corresponding ellipse. + ``` ''' a_ranges = self._angles_to_parameters() @@ -356,8 +371,9 @@ class Arc(Shape): def _angles_to_parameters(self) -> numpy.ndarray: ''' - :return: "Eccentric anomaly" parameter ranges for the inner and outer edges, in the form - [[a_min_inner, a_max_inner], [a_min_outer, a_max_outer]] + Returns: + "Eccentric anomaly" parameter ranges for the inner and outer edges, in the form + `[[a_min_inner, a_max_inner], [a_min_outer, a_max_outer]]` ''' a = [] for sgn in (-1, +1): diff --git a/masque/shapes/circle.py b/masque/shapes/circle.py index 8816787..8e47912 100644 --- a/masque/shapes/circle.py +++ b/masque/shapes/circle.py @@ -17,16 +17,19 @@ class Circle(Shape): """ __slots__ = ('_radius', 'poly_num_points', 'poly_max_arclen') _radius: float + """ Circle radius """ + poly_num_points: int + """ Sets the default number of points for `.polygonize()` """ + poly_max_arclen: float + """ Sets the default max segement length for `.polygonize()` """ # radius property @property def radius(self) -> float: """ Circle's radius (float, >= 0) - - :return: radius """ return self._radius diff --git a/masque/shapes/ellipse.py b/masque/shapes/ellipse.py index 0d8f084..931cf95 100644 --- a/masque/shapes/ellipse.py +++ b/masque/shapes/ellipse.py @@ -20,17 +20,22 @@ class Ellipse(Shape): __slots__ = ('_radii', '_rotation', 'poly_num_points', 'poly_max_arclen') _radii: numpy.ndarray + """ Ellipse radii """ + _rotation: float + """ Angle from x-axis to first radius (ccw, radians) """ + poly_num_points: int + """ Sets the default number of points for `.polygonize()` """ + poly_max_arclen: float + """ Sets the default max segement length for `.polygonize()` """ # radius properties @property def radii(self) -> numpy.ndarray: """ - Return the radii [rx, ry] - - :return: [rx, ry] + Return the radii `[rx, ry]` """ return self._radii @@ -70,7 +75,8 @@ class Ellipse(Shape): Rotation of rx from the x axis. Uses the interval [0, pi) in radians (counterclockwise is positive) - :return: counterclockwise rotation in radians + Returns: + counterclockwise rotation in radians """ return self._rotation diff --git a/masque/shapes/path.py b/masque/shapes/path.py index b31c5c5..7b0ff9c 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -37,8 +37,6 @@ class Path(Shape): def width(self) -> float: """ Path width (float, >= 0) - - :return: width """ return self._width @@ -55,8 +53,6 @@ class Path(Shape): def cap(self) -> 'Path.Cap': """ Path end-cap - - :return: Path.Cap enum """ return self._cap @@ -74,9 +70,10 @@ class Path(Shape): @property def cap_extensions(self) -> numpy.ndarray or None: """ - Path end-cap extensionf + Path end-cap extension - :return: 2-element ndarray or None + Returns: + 2-element ndarray or `None` """ return self._cap_extensions @@ -96,9 +93,7 @@ class Path(Shape): @property def vertices(self) -> numpy.ndarray: """ - Vertices of the path (Nx2 ndarray: [[x0, y0], [x1, y1], ...] - - :return: vertices + Vertices of the path (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`) """ return self._vertices @@ -194,22 +189,25 @@ class Path(Shape): Build a path by specifying the turn angles and travel distances rather than setting the distances directly. - :param travel_pairs: A list of (angle, distance) pairs that define - the path. Angles are counterclockwise, in radians, and are relative - to the previous segment's direction (the initial angle is relative - to the +x axis). - :param width: Path width, default 0 - :param cap: End-cap type, default Path.Cap.Flush (no end-cap) - :param cap_extensions: End-cap extension distances, when using Path.Cap.CustomSquare. - Default (0, 0) or None, depending on cap type - :param offset: Offset, default (0, 0) - :param rotation: Rotation counterclockwise, in radians. Default 0 - :param mirrored: Whether to mirror across the x or y axes. For example, - mirrored=(True, False) results in a reflection across the x-axis, - multiplying the path's y-coordinates by -1. Default (False, False) - :param layer: Layer, default 0 - :param dose: Dose, default 1.0 - :return: The resulting Path object + Args: + travel_pairs: A list of (angle, distance) pairs that define + the path. Angles are counterclockwise, in radians, and are relative + to the previous segment's direction (the initial angle is relative + to the +x axis). + width: Path width, default `0` + cap: End-cap type, default `Path.Cap.Flush` (no end-cap) + cap_extensions: End-cap extension distances, when using `Path.Cap.CustomSquare`. + Default `(0, 0)` or `None`, depending on cap type + offset: Offset, default `(0, 0)` + rotation: Rotation counterclockwise, in radians. Default `0` + mirrored: Whether to mirror across the x or y axes. For example, + `mirrored=(True, False)` results in a reflection across the x-axis, + multiplying the path's y-coordinates by -1. Default `(False, False)` + layer: Layer, default `0` + dose: Dose, default `1.0` + + Returns: + The resulting Path object """ #TODO: needs testing direction = numpy.array([1, 0]) @@ -359,7 +357,8 @@ class Path(Shape): """ Removes duplicate, co-linear and otherwise redundant vertices. - :returns: self + Returns: + self """ self.remove_colinear_vertices() return self @@ -368,7 +367,8 @@ class Path(Shape): ''' Removes all consecutive duplicate (repeated) vertices. - :returns: self + Returns: + self ''' self.vertices = remove_duplicate_vertices(self.vertices, closed_path=False) return self @@ -377,7 +377,8 @@ class Path(Shape): ''' Removes consecutive co-linear vertices. - :returns: self + Returns: + self ''' self.vertices = remove_colinear_vertices(self.vertices, closed_path=False) return self diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 3bbaeec..daef7fc 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -16,18 +16,17 @@ class Polygon(Shape): A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an implicitly-closed boundary, and an offset. - A normalized_form(...) is available, but can be quite slow with lots of vertices. + A `normalized_form(...)` is available, but can be quite slow with lots of vertices. """ __slots__ = ('_vertices',) _vertices: numpy.ndarray + """ Nx2 ndarray of vertices `[[x0, y0], [x1, y1], ...]` """ # vertices property @property def vertices(self) -> numpy.ndarray: """ - Vertices of the polygon (Nx2 ndarray: [[x0, y0], [x1, y1], ...] - - :return: vertices + Vertices of the polygon (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`) """ return self._vertices @@ -107,12 +106,15 @@ class Polygon(Shape): """ Draw a square given side_length, centered on the origin. - :param side_length: Length of one side - :param rotation: Rotation counterclockwise, in radians - :param offset: Offset, default (0, 0) - :param layer: Layer, default 0 - :param dose: Dose, default 1.0 - :return: A Polygon object containing the requested square + Args: + side_length: Length of one side + rotation: Rotation counterclockwise, in radians + offset: Offset, default `(0, 0)` + layer: Layer, default `0` + dose: Dose, default `1.0` + + Returns: + A Polygon object containing the requested square """ norm_square = numpy.array([[-1, -1], [-1, +1], @@ -134,13 +136,16 @@ class Polygon(Shape): """ Draw a rectangle with side lengths lx and ly, centered on the origin. - :param lx: Length along x (before rotation) - :param ly: Length along y (before rotation) - :param rotation: Rotation counterclockwise, in radians - :param offset: Offset, default (0, 0) - :param layer: Layer, default 0 - :param dose: Dose, default 1.0 - :return: A Polygon object containing the requested rectangle + Args: + lx: Length along x (before rotation) + ly: Length along y (before rotation) + rotation: Rotation counterclockwise, in radians + offset: Offset, default `(0, 0)` + layer: Layer, default `0` + dose: Dose, default `1.0` + + Returns: + A Polygon object containing the requested rectangle """ vertices = 0.5 * numpy.array([[-lx, -ly], [-lx, +ly], @@ -168,17 +173,20 @@ class Polygon(Shape): Must provide 2 of (xmin, xctr, xmax, lx), and 2 of (ymin, yctr, ymax, ly). - :param xmin: Minimum x coordinate - :param xctr: Center x coordinate - :param xmax: Maximum x coordinate - :param lx: Length along x direction - :param ymin: Minimum y coordinate - :param yctr: Center y coordinate - :param ymax: Maximum y coordinate - :param ly: Length along y direction - :param layer: Layer, default 0 - :param dose: Dose, default 1.0 - :return: A Polygon object containing the requested rectangle + Args: + xmin: Minimum x coordinate + xctr: Center x coordinate + xmax: Maximum x coordinate + lx: Length along x direction + ymin: Minimum y coordinate + yctr: Center y coordinate + ymax: Maximum y coordinate + ly: Length along y direction + layer: Layer, default `0` + dose: Dose, default `1.0` + + Returns: + A Polygon object containing the requested rectangle """ if lx is None: if xctr is None: @@ -278,7 +286,8 @@ class Polygon(Shape): """ Removes duplicate, co-linear and otherwise redundant vertices. - :returns: self + Returns: + self """ self.remove_colinear_vertices() return self @@ -287,7 +296,8 @@ class Polygon(Shape): ''' Removes all consecutive duplicate (repeated) vertices. - :returns: self + Returns: + self ''' self.vertices = remove_duplicate_vertices(self.vertices, closed_path=True) return self @@ -296,7 +306,8 @@ class Polygon(Shape): ''' Removes consecutive co-linear vertices. - :returns: self + Returns: + self ''' self.vertices = remove_colinear_vertices(self.vertices, closed_path=True) return self diff --git a/masque/shapes/shape.py b/masque/shapes/shape.py index 429e9dd..8c5b2cb 100644 --- a/masque/shapes/shape.py +++ b/masque/shapes/shape.py @@ -26,12 +26,20 @@ class Shape(metaclass=ABCMeta): """ __slots__ = ('_offset', '_layer', '_dose', 'identifier', 'locked') - _offset: numpy.ndarray # [x_offset, y_offset] - _layer: int or Tuple # Layer (integer >= 0 or tuple) - _dose: float # Dose - identifier: Tuple # An arbitrary identifier for the shape, - # usually empty but used by Pattern.flatten() - locked: bool # If True, any changes to the shape will raise a PatternLockedError + _offset: numpy.ndarray + """ `[x_offset, y_offset]` """ + + _layer: int or Tuple + """ Layer (integer >= 0 or tuple) """ + + _dose: float + """ Dose """ + + identifier: Tuple + """ An arbitrary identifier for the shape, usually empty but used by `Pattern.flatten()` """ + + locked: bool + """ If `True`, any changes to the shape will raise a `PatternLockedError` """ def __setattr__(self, name, value): if self.locked and name != 'locked': @@ -51,31 +59,35 @@ class Shape(metaclass=ABCMeta): """ Returns a list of polygons which approximate the shape. - :param num_vertices: Number of points to use for each polygon. Can be overridden by - max_arclen if that results in more points. Optional, defaults to shapes' - internal defaults. - :param max_arclen: Maximum arclength which can be approximated by a single line - segment. Optional, defaults to shapes' internal defaults. - :return: List of polygons equivalent to the shape + Args: + num_vertices: Number of points to use for each polygon. Can be overridden by + max_arclen if that results in more points. Optional, defaults to shapes' + internal defaults. + max_arclen: Maximum arclength which can be approximated by a single line + segment. Optional, defaults to shapes' internal defaults. + + Returns: + List of polygons equivalent to the shape """ pass @abstractmethod def get_bounds(self) -> numpy.ndarray: """ - Returns [[x_min, y_min], [x_max, y_max]] which specify a minimal bounding box for the shape. - - :return: [[x_min, y_min], [x_max, y_max]] + Returns `[[x_min, y_min], [x_max, y_max]]` which specify a minimal bounding box for the shape. """ pass @abstractmethod def rotate(self, theta: float) -> 'Shape': """ - Rotate the shape around its center (0, 0), ignoring its offset. + Rotate the shape around its origin (0, 0), ignoring its offset. - :param theta: Angle to rotate by (counterclockwise, radians) - :return: self + Args: + theta: Angle to rotate by (counterclockwise, radians) + + Returns: + self """ pass @@ -84,8 +96,12 @@ class Shape(metaclass=ABCMeta): """ Mirror the shape across an axis. - :param axis: Axis to mirror across. - :return: self + Args: + axis: Axis to mirror across. + (0: mirror across x axis, 1: mirror across y axis) + + Returns: + self """ pass @@ -94,8 +110,11 @@ class Shape(metaclass=ABCMeta): """ Scale the shape's size (eg. radius, for a circle) by a constant factor. - :param c: Factor to scale by - :return: self + Args: + c: Factor to scale by + + Returns: + self """ pass @@ -105,18 +124,21 @@ class Shape(metaclass=ABCMeta): Writes the shape in a standardized notation, with offset, scale, rotation, and dose information separated out from the remaining values. - :param norm_value: This value is used to normalize lengths intrinsic to the shape; + Args: + norm_value: This value is used to normalize lengths intrinsic to the shape; eg. for a circle, the returned intrinsic radius value will be (radius / norm_value), and - the returned callable will create a Circle(radius=norm_value, ...). This is useful + the returned callable will create a `Circle(radius=norm_value, ...)`. This is useful when you find it important for quantities to remain in a certain range, eg. for GDSII where vertex locations are stored as integers. - :return: The returned information takes the form of a 3-element tuple, - (intrinsic, extrinsic, constructor). These are further broken down as: - intrinsic: A tuple of basic types containing all information about the instance that - is not contained in 'extrinsic'. Usually, intrinsic[0] == type(self). - extrinsic: ([x_offset, y_offset], scale, rotation, mirror_across_x_axis, dose) - constructor: A callable (no arguments) which returns an instance of type(self) with - internal state equivalent to 'intrinsic'. + + Returns: + The returned information takes the form of a 3-element tuple, + `(intrinsic, extrinsic, constructor)`. These are further broken down as: + `intrinsic`: A tuple of basic types containing all information about the instance that + is not contained in 'extrinsic'. Usually, `intrinsic[0] == type(self)`. + `extrinsic`: `([x_offset, y_offset], scale, rotation, mirror_across_x_axis, dose)` + `constructor`: A callable (no arguments) which returns an instance of `type(self)` with + internal state equivalent to `intrinsic`. """ pass @@ -126,8 +148,6 @@ class Shape(metaclass=ABCMeta): def offset(self) -> numpy.ndarray: """ [x, y] offset - - :return: [x_offset, y_offset] """ return self._offset @@ -145,8 +165,6 @@ class Shape(metaclass=ABCMeta): def layer(self) -> int or Tuple[int]: """ Layer number (int or tuple of ints) - - :return: Layer """ return self._layer @@ -159,8 +177,6 @@ class Shape(metaclass=ABCMeta): def dose(self) -> float: """ Dose (float >= 0) - - :return: Dose value """ return self._dose @@ -177,7 +193,8 @@ class Shape(metaclass=ABCMeta): """ Returns a deep copy of the shape. - :return: Deep copy of self + Returns: + copy.deepcopy(self) """ return copy.deepcopy(self) @@ -185,8 +202,11 @@ class Shape(metaclass=ABCMeta): """ Translate the shape by the given offset - :param offset: [x_offset, y,offset] - :return: self + Args: + offset: [x_offset, y,offset] + + Returns: + self """ self.offset += offset return self @@ -195,9 +215,12 @@ class Shape(metaclass=ABCMeta): """ Rotate the shape around a point. - :param pivot: Point (x, y) to rotate around - :param rotation: Angle to rotate by (counterclockwise, radians) - :return: self + Args: + pivot: Point (x, y) to rotate around + rotation: Angle to rotate by (counterclockwise, radians) + + Returns: + self """ pivot = numpy.array(pivot, dtype=float) self.translate(-pivot) @@ -214,14 +237,17 @@ class Shape(metaclass=ABCMeta): Returns a list of polygons with grid-aligned ("Manhattan") edges approximating the shape. This function works by - 1) Converting the shape to polygons using .to_polygons() + 1) Converting the shape to polygons using `.to_polygons()` 2) Approximating each edge with an equivalent Manhattan edge This process results in a reasonable Manhattan representation of the shape, but is imprecise near non-Manhattan or off-grid corners. - :param grid_x: List of allowed x-coordinates for the Manhattanized polygon edges. - :param grid_y: List of allowed y-coordinates for the Manhattanized polygon edges. - :return: List of Polygon objects with grid-aligned edges. + Args: + grid_x: List of allowed x-coordinates for the Manhattanized polygon edges. + grid_y: List of allowed y-coordinates for the Manhattanized polygon edges. + + Returns: + List of `Polygon` objects with grid-aligned edges. """ from . import Polygon @@ -319,7 +345,7 @@ class Shape(metaclass=ABCMeta): Returns a list of polygons with grid-aligned ("Manhattan") edges approximating the shape. This function works by - 1) Converting the shape to polygons using .to_polygons() + 1) Converting the shape to polygons using `.to_polygons()` 2) Accurately rasterizing each polygon on a grid, where the edges of each grid cell correspond to the allowed coordinates 3) Thresholding the (anti-aliased) rasterized image @@ -328,7 +354,7 @@ class Shape(metaclass=ABCMeta): caveats include: a) If high accuracy is important, perform any polygonization and clipping operations prior to calling this function. This allows you to specify any arguments you may - need for .to_polygons(), and also avoids calling .manhattanize() multiple times for + need for `.to_polygons()`, and also avoids calling `.manhattanize()` multiple times for the same grid location (which causes inaccuracies in the final representation). b) If the shape is very large or the grid very fine, memory requirements can be reduced by breaking the shape apart into multiple, smaller shapes. @@ -336,19 +362,22 @@ class Shape(metaclass=ABCMeta): equidistant from allowed edge location. Implementation notes: - i) Rasterization is performed using float_raster, giving a high-precision anti-aliased + i) Rasterization is performed using `float_raster`, giving a high-precision anti-aliased rasterized image. ii) To find the exact polygon edges, the thresholded rasterized image is supersampled - prior to calling skimage.measure.find_contours(), which uses marching squares - to find the contours. This is done because find_contours() performs interpolation, + prior to calling `skimage.measure.find_contours()`, which uses marching squares + to find the contours. This is done because `find_contours()` performs interpolation, which has to be undone in order to regain the axis-aligned contours. A targetted - rewrite of find_contours() for this specific application, or use of a different + rewrite of `find_contours()` for this specific application, or use of a different boundary tracing method could remove this requirement, but for now this seems to be the most performant approach. - :param grid_x: List of allowed x-coordinates for the Manhattanized polygon edges. - :param grid_y: List of allowed y-coordinates for the Manhattanized polygon edges. - :return: List of Polygon objects with grid-aligned edges. + Args: + grid_x: List of allowed x-coordinates for the Manhattanized polygon edges. + grid_y: List of allowed y-coordinates for the Manhattanized polygon edges. + + Returns: + List of `Polygon` objects with grid-aligned edges. """ from . import Polygon import skimage.measure @@ -403,9 +432,10 @@ class Shape(metaclass=ABCMeta): def lock(self) -> 'Shape': """ - Lock the Shape + Lock the Shape, disallowing further changes - :return: self + Returns: + self """ object.__setattr__(self, 'locked', True) return self @@ -414,7 +444,8 @@ class Shape(metaclass=ABCMeta): """ Unlock the Shape - :return: self + Returns: + self """ object.__setattr__(self, 'locked', False) return self diff --git a/masque/shapes/text.py b/masque/shapes/text.py index 63ff2e1..53a9551 100644 --- a/masque/shapes/text.py +++ b/masque/shapes/text.py @@ -173,12 +173,15 @@ def get_char_as_polygons(font_path: str, The output is normalized so that the font size is 1 unit. - :param font_path: File path specifying a font loadable by freetype - :param char: Character to convert to polygons - :param resolution: Internal resolution setting (used for freetype - Face.set_font_size(resolution)). Modify at your own peril! - :return: List of polygons [[[x0, y0], [x1, y1], ...], ...] and 'advance' distance (distance - from the start of this glyph to the start of the next one) + Args: + font_path: File path specifying a font loadable by freetype + char: Character to convert to polygons + resolution: Internal resolution setting (used for freetype + `Face.set_font_size(resolution))`. Modify at your own peril! + + Returns: + List of polygons `[[[x0, y0], [x1, y1], ...], ...]` and + 'advance' distance (distance from the start of this glyph to the start of the next one) """ if len(char) != 1: raise Exception('get_char_as_polygons called with non-char') diff --git a/masque/subpattern.py b/masque/subpattern.py index 78df1b0..9887e83 100644 --- a/masque/subpattern.py +++ b/masque/subpattern.py @@ -24,13 +24,29 @@ class SubPattern: __slots__ = ('pattern', '_offset', '_rotation', '_dose', '_scale', '_mirrored', 'identifier', 'locked') pattern: 'Pattern' + """ The `Pattern` being instanced """ + _offset: numpy.ndarray + """ (x, y) offset for the instance """ + _rotation: float + """ rotation for the instance, radians counterclockwise """ + _dose: float + """ dose factor for the instance """ + _scale: float + """ scale factor for the instance """ + _mirrored: List[bool] + """ Whether to mirror the instanc across the x and/or y axes. """ + identifier: Tuple + """ An arbitrary identifier """ + locked: bool + """ If `True`, disallows changes to the GridRepetition """ + #TODO more documentation? def __init__(self, @@ -139,9 +155,9 @@ class SubPattern: def as_pattern(self) -> 'Pattern': """ - Returns a copy of self.pattern which has been scaled, rotated, etc. according to this - SubPattern's properties. - :return: Copy of self.pattern that has been altered to reflect the SubPattern's properties. + Returns: + A copy of self.pattern which has been scaled, rotated, etc. according to this + `SubPattern`'s properties. """ pattern = self.pattern.deepcopy().deepunlock() pattern.scale_by(self.scale) @@ -155,8 +171,11 @@ class SubPattern: """ Translate by the given offset - :param offset: Translate by this offset - :return: self + Args: + offset: Offset `[x, y]` to translate by + + Returns: + self """ self.offset += offset return self @@ -165,9 +184,12 @@ class SubPattern: """ Rotate around a point - :param pivot: Point to rotate around - :param rotation: Angle to rotate by (counterclockwise, radians) - :return: self + Args: + pivot: Point `[x, y]` to rotate around + rotation: Angle to rotate by (counterclockwise, radians) + + Returns: + self """ pivot = numpy.array(pivot, dtype=float) self.translate(-pivot) @@ -178,10 +200,13 @@ class SubPattern: def rotate(self, rotation: float) -> 'SubPattern': """ - Rotate around (0, 0) + Rotate the instance around it's origin - :param rotation: Angle to rotate by (counterclockwise, radians) - :return: self + Args: + rotation: Angle to rotate by (counterclockwise, radians) + + Returns: + self """ self.rotation += rotation return self @@ -190,8 +215,11 @@ class SubPattern: """ Mirror the subpattern across an axis. - :param axis: Axis to mirror across. - :return: self + Args: + axis: Axis to mirror across. + + Returns: + self """ self.mirrored[axis] = not self.mirrored[axis] self.rotation *= -1 @@ -199,11 +227,12 @@ class SubPattern: def get_bounds(self) -> numpy.ndarray or None: """ - Return a numpy.ndarray containing [[x_min, y_min], [x_max, y_max]], corresponding to the - extent of the SubPattern in each dimension. - Returns None if the contained Pattern is empty. + Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the + extent of the `SubPattern` in each dimension. + Returns `None` if the contained `Pattern` is empty. - :return: [[x_min, y_min], [x_max, y_max]] or None + Returns: + `[[x_min, y_min], [x_max, y_max]]` or `None` """ return self.as_pattern().get_bounds() @@ -211,7 +240,11 @@ class SubPattern: """ Scale the subpattern by a factor - :param c: scaling factor + Args: + c: scaling factor + + Returns: + self """ self.scale *= c return self @@ -220,7 +253,8 @@ class SubPattern: """ Return a shallow copy of the subpattern. - :return: copy.copy(self) + Returns: + `copy.copy(self)` """ return copy.copy(self) @@ -228,15 +262,17 @@ class SubPattern: """ Return a deep copy of the subpattern. - :return: copy.copy(self) + Returns: + `copy.deepcopy(self)` """ return copy.deepcopy(self) def lock(self) -> 'SubPattern': """ - Lock the SubPattern + Lock the SubPattern, disallowing changes - :return: self + Returns: + self """ object.__setattr__(self, 'locked', True) return self @@ -245,7 +281,8 @@ class SubPattern: """ Unlock the SubPattern - :return: self + Returns: + self """ object.__setattr__(self, 'locked', False) return self @@ -254,7 +291,8 @@ class SubPattern: """ Recursively lock the SubPattern and its contained pattern - :return: self + Returns: + self """ self.lock() self.pattern.deeplock() @@ -264,9 +302,11 @@ class SubPattern: """ Recursively unlock the SubPattern and its contained pattern - This is dangerous unless you have just performed a deepcopy! + This is dangerous unless you have just performed a deepcopy, since + the subpattern and its components may be used in more than one once! - :return: self + Returns: + self """ self.unlock() self.pattern.deepunlock() diff --git a/masque/utils.py b/masque/utils.py index b7c7b05..91ba8b2 100644 --- a/masque/utils.py +++ b/masque/utils.py @@ -14,30 +14,37 @@ def is_scalar(var: Any) -> bool: """ Alias for 'not hasattr(var, "__len__")' - :param var: Checks if var has a length. + Args: + var: Checks if `var` has a length. """ return not hasattr(var, "__len__") def get_bit(bit_string: Any, bit_id: int) -> bool: """ - Returns true iff bit number 'bit_id' from the right of 'bit_string' is 1 + Interprets bit number `bit_id` from the right (lsb) of `bit_string` as a boolean - :param bit_string: Bit string to test - :param bit_id: Bit number, 0-indexed from the right (lsb) - :return: value of the requested bit (bool) + Args: + bit_string: Bit string to test + bit_id: Bit number, 0-indexed from the right (lsb) + + Returns: + Boolean value of the requested bit """ return bit_string & (1 << bit_id) != 0 def set_bit(bit_string: Any, bit_id: int, value: bool) -> Any: """ - Returns 'bit_string' with bit number 'bit_id' set to 'value'. + Returns `bit_string`, with bit number `bit_id` set to boolean `value`. - :param bit_string: Bit string to alter - :param bit_id: Bit number, 0-indexed from right (lsb) - :param value: Boolean value to set bit to - :return: Altered 'bit_string' + Args: + bit_string: Bit string to alter + bit_id: Bit number, 0-indexed from right (lsb) + value: Boolean value to set bit to + + Returns: + Altered `bit_string` """ mask = (1 << bit_id) bit_string &= ~mask @@ -50,14 +57,29 @@ def rotation_matrix_2d(theta: float) -> numpy.ndarray: """ 2D rotation matrix for rotating counterclockwise around the origin. - :param theta: Angle to rotate, in radians - :return: rotation matrix + Args: + theta: Angle to rotate, in radians + + Returns: + rotation matrix """ return numpy.array([[numpy.cos(theta), -numpy.sin(theta)], [numpy.sin(theta), +numpy.cos(theta)]]) def normalize_mirror(mirrored: Tuple[bool, bool]) -> Tuple[bool, float]: + """ + Converts 0-2 mirror operations `(mirror_across_x_axis, mirror_across_y_axis)` + into 0-1 mirror operations and a rotation + + Args: + mirrored: `(mirror_across_x_axis, mirror_across_y_axis)` + + Returns: + `mirror_across_x_axis` (bool) and + `angle_to_rotate` in radians + """ + mirrored_x, mirrored_y = mirrored mirror_x = (mirrored_x != mirrored_y) #XOR angle = numpy.pi if mirrored_y else 0 @@ -65,34 +87,48 @@ def normalize_mirror(mirrored: Tuple[bool, bool]) -> Tuple[bool, float]: def remove_duplicate_vertices(vertices: numpy.ndarray, closed_path: bool = True) -> numpy.ndarray: - duplicates = (vertices == numpy.roll(vertices, 1, axis=0)).all(axis=1) - if not closed_path: - duplicates[0] = False - return vertices[~duplicates] + """ + Given a list of vertices, remove any consecutive duplicates. + + Args: + vertices: `[[x0, y0], [x1, y1], ...]` + closed_path: If True, `vertices` is interpreted as an implicity-closed path + (i.e. the last vertex will be removed if it is the same as the first) + + Returns: + `vertices` with no consecutive duplicates. + """ + duplicates = (vertices == numpy.roll(vertices, 1, axis=0)).all(axis=1) + if not closed_path: + duplicates[0] = False + return vertices[~duplicates] def remove_colinear_vertices(vertices: numpy.ndarray, closed_path: bool = True) -> numpy.ndarray: - ''' - Given a list of vertices, remove any superflous vertices (i.e. - those which lie along the line formed by their neighbors) + """ + Given a list of vertices, remove any superflous vertices (i.e. + those which lie along the line formed by their neighbors) - :param vertices: Nx2 ndarray of vertices - :param closed_path: If True, the vertices are assumed to represent an implicitly - closed path. If False, the path is assumed to be open. Default True. - :return: - ''' - vertices = numpy.array(vertices) + Args: + vertices: Nx2 ndarray of vertices + closed_path: If `True`, the vertices are assumed to represent an implicitly + closed path. If `False`, the path is assumed to be open. Default `True`. - # Check for dx0/dy0 == dx1/dy1 + Returns: + `vertices` with colinear (superflous) vertices removed. + """ + vertices = numpy.array(vertices) - dv = numpy.roll(vertices, -1, axis=0) - vertices # [y1-y0, y2-y1, ...] - dxdy = dv * numpy.roll(dv, 1, axis=0)[:, ::-1] #[[dx0*(dy_-1), (dx_-1)*dy0], dx1*dy0, dy1*dy0]] + # Check for dx0/dy0 == dx1/dy1 - dxdy_diff = numpy.abs(numpy.diff(dxdy, axis=1))[:, 0] - err_mult = 2 * numpy.abs(dxdy).sum(axis=1) + 1e-40 + dv = numpy.roll(vertices, -1, axis=0) - vertices # [y1-y0, y2-y1, ...] + dxdy = dv * numpy.roll(dv, 1, axis=0)[:, ::-1] #[[dx0*(dy_-1), (dx_-1)*dy0], dx1*dy0, dy1*dy0]] - slopes_equal = (dxdy_diff / err_mult) < 1e-15 - if not closed_path: - slopes_equal[[0, -1]] = False + dxdy_diff = numpy.abs(numpy.diff(dxdy, axis=1))[:, 0] + err_mult = 2 * numpy.abs(dxdy).sum(axis=1) + 1e-40 - return vertices[~slopes_equal] + slopes_equal = (dxdy_diff / err_mult) < 1e-15 + if not closed_path: + slopes_equal[[0, -1]] = False + + return vertices[~slopes_equal]