diff --git a/masque/builder/tools.py b/masque/builder/tools.py index b411295..ea9bd9d 100644 --- a/masque/builder/tools.py +++ b/masque/builder/tools.py @@ -1,5 +1,7 @@ """ Tools are objects which dynamically generate simple single-use devices (e.g. wires or waveguides) + +# TODO document all tools """ from typing import Sequence, Literal, Callable, Any from abc import ABCMeta, abstractmethod # TODO any way to make Tool ok with implementing only one method? diff --git a/masque/library.py b/masque/library.py index 6957031..a5db368 100644 --- a/masque/library.py +++ b/masque/library.py @@ -36,7 +36,21 @@ visitor_function_t = Callable[..., 'Pattern'] def _rename_patterns(lib: 'ILibraryView', name: str) -> str: - # TODO document rename function + """ + The default `rename_theirs` function for `ILibrary.add`. + + Treats names starting with an underscore as "one-offs" for which name conflicts + should be automatically resolved. Conflicts are resolved by calling + `lib.get_name(name.split('$')[0])`. + Names without a leading underscore are directly returned. + + Args: + lib: The library into which `name` is to be added (but is presumed to conflict) + name: The original name, to be modified + + Returns: + The new name, not guaranteed to be conflict-free! + """ if not name.startswith('_'): # TODO what are the consequences of making '_' special? maybe we can make this decision everywhere? return name @@ -45,6 +59,11 @@ def _rename_patterns(lib: 'ILibraryView', name: str) -> str: class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta): + """ + Interface for a read-only library. + + A library is a mapping from unique names (str) to collections of geometry (`Pattern`). + """ # inherited abstract functions #def __getitem__(self, key: str) -> 'Pattern': #def __iter__(self) -> Iterator[str]: @@ -53,6 +72,10 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta): #__contains__, keys, items, values, get, __eq__, __ne__ supplied by Mapping def abstract_view(self) -> 'AbstractView': + """ + Returns: + An AbstractView into this library + """ return AbstractView(self) def abstract(self, name: str) -> Abstract: @@ -205,14 +228,19 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta): def flatten( self, tops: str | Sequence[str], - flatten_ports: bool = False, # TODO document + flatten_ports: bool = False, ) -> dict[str, 'Pattern']: """ - Removes all refs and adds equivalent shapes. - Also flattens all referenced patterns. + Returns copies of all `tops` patterns with all refs + removed and replaced with equivalent shapes. + Also returns flattened copies of all referenced patterns. + The originals in the calling `Library` are not modified. + For an in-place variant, see `Pattern.flatten`. Args: tops: The pattern(s) to flattern. + flatten_ports: If `True`, keep ports from any referenced + patterns; otherwise discard them. Returns: {name: flat_pattern} mapping for all flattened patterns. @@ -222,7 +250,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta): flattened: dict[str, 'Pattern | None'] = {} - def flatten_single(name) -> None: + def flatten_single(name: str) -> None: flattened[name] = None pat = self[name].deepcopy() @@ -428,11 +456,12 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta): pattern = visit_after(pattern, hierarchy=hierarchy, memo=memo, transform=transform) if pattern is not original_pattern: - name = hierarchy[-1] # TODO what is name=None? + name = hierarchy[-1] if not isinstance(self, ILibrary): raise LibraryError('visit_* functions returned a new `Pattern` object' ' but the library is immutable') if name is None: + # The top pattern is not the original pattern, but we don't know what to call it! raise LibraryError('visit_* functions returned a new `Pattern` object' ' but no top-level name was provided in `hierarchy`') @@ -442,6 +471,11 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta): class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): + """ + Interface for a writeable library. + + A library is a mapping from unique names (str) to collections of geometry (`Pattern`). + """ # inherited abstract functions #def __getitem__(self, key: str) -> 'Pattern': #def __iter__(self) -> Iterator[str]: @@ -477,7 +511,8 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): Args: old_name: Current name for the pattern new_name: New name for the pattern - #TODO move_Reference + move_references: If `True`, any refs in this library pointing to `old_name` + will be updated to point to `new_name`. Returns: self @@ -534,19 +569,31 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): rename_theirs: Callable[['ILibraryView', str], str] = _rename_patterns, ) -> dict[str, str]: """ - Add keys from another library into this one. + Add items from another library into this one. - # TODO explain reference renaming and return + If any name in `other` is already present in `self`, `rename_theirs(self, name)` is called + to pick a new name for the newly-added pattern. If the new name still conflicts with a name + in `self` a `LibraryError` is raised. All references to the original name (within `other)` + are updated to the new name. + + By default, `rename_theirs` makes no changes to the name (causing a `LibraryError`) unless the + name starts with an underscore. Underscored names are truncated to before their first '$' + and then passed to `self.get_name()` to create a new unique name. Args: - other: The library to insert keys from + other: The library to insert keys from. rename_theirs: Called as rename_theirs(self, name) for each duplicate name encountered in `other`. Should return the new name for the pattern in `other`. Default is effectively - `name.split('$')[0] if name.startswith('_') else name` + `self.get_name(name.split('$')[0]) if name.startswith('_') else name` + Returns: - self + A mapping of `{old_name: new_name}` for all `old_name`s in `other`. Unchanged + names map to themselves. + + Raises: + `LibraryError` if a duplicate name is encountered even after applying `rename_theirs()`. """ from .pattern import map_targets duplicates = set(self.keys()) & set(other.keys()) @@ -785,7 +832,15 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): self, repeat: bool = True, ) -> set[str]: - # TODO doc prune_empty + """ + Delete any empty patterns (i.e. where `Pattern.is_empty` returns `True`). + + Args: + repeat: Also recursively delete any patterns which only contain(ed) empty patterns. + + Returns: + A set containing the names of all deleted patterns + """ trimmed = set() while empty := set(name for name, pat in self.items() if pat.is_empty()): for name in empty: @@ -807,7 +862,13 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): key: str, delete_refs: bool = True, ) -> Self: - # TODO doc delete() + """ + Delete a pattern and (optionally) all refs pointing to that pattern. + + Args: + key: Name of the pattern to be deleted. + delete_refs: If `True` (default), also delete all refs pointing to the pattern. + """ del self[key] if delete_refs: for pat in self.values(): @@ -817,6 +878,12 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): class LibraryView(ILibraryView): + """ + Default implementation for a read-only library. + + A library is a mapping from unique names (str) to collections of geometry (`Pattern`). + This library is backed by an arbitrary python object which implements the `Mapping` interface. + """ mapping: Mapping[str, 'Pattern'] def __init__( @@ -842,6 +909,12 @@ class LibraryView(ILibraryView): class Library(ILibrary): + """ + Default implementation for a writeable library. + + A library is a mapping from unique names (str) to collections of geometry (`Pattern`). + This library is backed by an arbitrary python object which implements the `MutableMapping` interface. + """ mapping: MutableMapping[str, 'Pattern'] def __init__( @@ -892,6 +965,12 @@ class Library(ILibrary): def mktree(cls, name: str) -> tuple[Self, 'Pattern']: """ Create a new Library and immediately add a pattern + + Args: + name: The name for the new pattern (usually the name of the topcell). + + Returns: + The newly created `Library` and the newly created `Pattern` """ from .pattern import Pattern tree = cls() @@ -1041,6 +1120,11 @@ class LazyLibrary(ILibrary): class AbstractView(Mapping[str, Abstract]): + """ + A read-only mapping from names to `Abstract` objects. + + This is usually just used as a shorthand for repeated calls to `library.abstract()`. + """ library: ILibraryView def __init__(self, library: ILibraryView) -> None: diff --git a/masque/pattern.py b/masque/pattern.py index 34f1a95..c8c2c30 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -318,7 +318,12 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): Returns `None` if the Pattern is empty. Args: - TODO docs for get_bounds + library: If `recurse=True`, any referenced patterns are loaded from this library. + recurse: If `False`, do not evaluate the bounds of any refs (i.e. assume they are empty). + If `True`, evaluate the bounds of all refs and their conained geometry recursively. + Default `True`. + cache: Mapping of `{name: bounds}` for patterns for which the bounds have already been calculated. + Modified during the run (any referenced pattern's bounds are added). Returns: `[[x_min, y_min], [x_max, y_max]]` or `None` @@ -401,7 +406,12 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): Convenience wrapper for `get_bounds()` which asserts that the Pattern as non-None bounds. Args: - TODO docs for get_bounds + library: If `recurse=True`, any referenced patterns are loaded from this library. + recurse: If `False`, do not evaluate the bounds of any refs (i.e. assume they are empty). + If `True`, evaluate the bounds of all refs and their conained geometry recursively. + Default `True`. + cache: Mapping of `{name: bounds}` for patterns for which the bounds have already been calculated. + Modified during the run (any referenced pattern's bounds are added). Returns: `[[x_min, y_min], [x_max, y_max]]` @@ -590,12 +600,24 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): return not (self.has_refs() or self.has_shapes() or self.has_labels()) def has_refs(self) -> bool: + """ + Returns: + True if the pattern contains any refs. + """ return any(True for _ in chain.from_iterable(self.refs.values())) def has_shapes(self) -> bool: + """ + Returns: + True if the pattern contains any shapes. + """ return any(True for _ in chain.from_iterable(self.shapes.values())) def has_labels(self) -> bool: + """ + Returns: + True if the pattern contains any labels. + """ return any(True for _ in chain.from_iterable(self.labels.values())) def ref(self, target: str | None, *args: Any, **kwargs: Any) -> Self: @@ -708,21 +730,23 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable): def flatten( self, library: Mapping[str, 'Pattern'], - flatten_ports: bool = False, # TODO document + flatten_ports: bool = False, ) -> 'Pattern': """ Removes all refs (recursively) and adds equivalent shapes. - Alters the current pattern in-place + Alters the current pattern in-place. + For a version which creates copies, see `Library.flatten`. Args: library: Source for referenced patterns. + flatten_ports: If `True`, keep ports from any referenced + patterns; otherwise discard them. Returns: self """ flattened: dict[str | None, 'Pattern | None'] = {} - # TODO both Library and Pattern have flatten()... pattern is in-place? def flatten_single(name: str | None) -> None: if name is None: pat = self diff --git a/masque/ref.py b/masque/ref.py index a3239a3..26aeaba 100644 --- a/masque/ref.py +++ b/masque/ref.py @@ -2,7 +2,7 @@ Ref provides basic support for nesting Pattern objects within each other, by adding offset, rotation, scaling, and other such properties to the reference. """ -#TODO more top-level documentation +#TODO more top-level documentation for ref from typing import Mapping, TYPE_CHECKING, Self import copy diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index 9c9a671..c9e4c48 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -17,6 +17,9 @@ class Polygon(Shape): A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an implicitly-closed boundary, and an offset. + Note that the setter for `Polygon.vertices` may (but may not) create a copy of the + passed vertex coordinates. See `numpy.array()` for details. + A `normalized_form(...)` is available, but can be quite slow with lots of vertices. """ __slots__ = ( @@ -38,7 +41,7 @@ class Polygon(Shape): @vertices.setter def vertices(self, val: ArrayLike) -> None: - val = numpy.array(val, dtype=float) # TODO document that these might not be copied + val = numpy.array(val, dtype=float) # note that this hopefully won't create a copy if len(val.shape) < 2 or val.shape[1] != 2: raise PatternError('Vertices must be an Nx2 array') if val.shape[0] < 3: diff --git a/masque/utils/ports2data.py b/masque/utils/ports2data.py index 1ef03da..2843e0d 100644 --- a/masque/utils/ports2data.py +++ b/masque/utils/ports2data.py @@ -60,8 +60,8 @@ def data_to_ports( # TODO missing ok? ) -> Pattern: """ - # TODO fixup documentation in port_utils - # TODO move port_utils to utils.file? + # TODO fixup documentation in ports2data + # TODO move to utils.file? Examine `pattern` for labels specifying port info, and use that info to fill out its `ports` attribute.