From 9a7a5583edf29925915ec832a95ca022dfcfc9cc Mon Sep 17 00:00:00 2001 From: jan Date: Fri, 20 Oct 2023 23:14:47 -0700 Subject: [PATCH 1/5] Add Tree/TreeView and allow Builder to ingest them --- README.md | 7 ++++++- masque/__init__.py | 2 +- masque/builder/builder.py | 34 ++++++++++++++++++++++++++-------- masque/library.py | 12 ++++++++++-- 4 files changed, 43 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 4565cad..6448b26 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ References are accomplished by listing the target's name, not its `Pattern` obje ## Glossary - `Library`: A collection of named cells. OASIS or GDS "library" or file. -- "tree": Any Library which has only one topcell. +- `Tree`: Any `{name: pattern}` mapping which has only one topcell. - `Pattern`: A collection of geometry, text labels, and reference to other patterns. OASIS or GDS "Cell", DXF "Block". - `Ref`: A reference to another pattern. GDS "AREF/SREF", OASIS "Placement". @@ -142,6 +142,11 @@ my_pattern.ref(new_name, ...) # instantiate the cell # In practice, you may do lots of my_pattern.ref(lib << make_tree(...), ...) + +# With a `Builder` and `place()`/`plug()` the `lib <<` portion can be implicit: +my_builder = Builder(library=lib, ...) +... +my_builder.place(make_tree(...)) ``` We can also use this shorthand to quickly add and reference a single flat (as yet un-named) pattern: diff --git a/masque/__init__.py b/masque/__init__.py index 3ed2a6f..15210ba 100644 --- a/masque/__init__.py +++ b/masque/__init__.py @@ -38,7 +38,7 @@ from .pattern import Pattern, map_layers, map_targets, chain_elements from .library import ( ILibraryView, ILibrary, LibraryView, Library, LazyLibrary, - AbstractView, + AbstractView, TreeView, Tree, ) from .ports import Port, PortList from .abstract import Abstract diff --git a/masque/builder/builder.py b/masque/builder/builder.py index 68de9e6..a401abc 100644 --- a/masque/builder/builder.py +++ b/masque/builder/builder.py @@ -8,7 +8,7 @@ import logging from numpy.typing import ArrayLike from ..pattern import Pattern -from ..library import ILibrary +from ..library import ILibrary, TreeView from ..error import BuildError from ..ports import PortList, Port from ..abstract import Abstract @@ -190,7 +190,7 @@ class Builder(PortList): def plug( self, - other: Abstract | str | Pattern, + other: Abstract | str | Pattern | TreeView, map_in: dict[str, str], map_out: dict[str, str | None] | None = None, *, @@ -201,11 +201,16 @@ class Builder(PortList): ) -> Self: """ Wrapper around `Pattern.plug` which allows a string for `other`. + The `Builder`'s library is used to dereference the string (or `Abstract`, if - one is passed with `append=True`). + one is passed with `append=True`). If a `TreeView` is passed, it is first + added into `self.library`. Args: - other: An `Abstract`, string, or `Pattern` describing the device to be instatiated. + other: An `Abstract`, string, `Pattern`, or `TreeView` describing the + device to be instatiated. If it is a `TreeView`, it is first + added into `self.library`, after which the topcell is plugged; + an equivalent statement is `self.plug(self.library << other, ...)`. map_in: dict of `{'self_port': 'other_port'}` mappings, specifying port connections between the two devices. map_out: dict of `{'old_name': 'new_name'}` mappings, specifying @@ -243,6 +248,10 @@ class Builder(PortList): logger.error('Skipping plug() since device is dead') return self + if not isinstance(other, (str, Abstract, Pattern)): + # We got a Tree; add it into self.library and grab an Abstract for it + other = self.library << other + if isinstance(other, str): other = self.library.abstract(other) if append and isinstance(other, Abstract): @@ -261,7 +270,7 @@ class Builder(PortList): def place( self, - other: Abstract | str | Pattern, + other: Abstract | str | Pattern | TreeView, *, offset: ArrayLike = (0, 0), rotation: float = 0, @@ -272,12 +281,17 @@ class Builder(PortList): append: bool = False, ) -> Self: """ - Wrapper around `Pattern.place` which allows a string for `other`. + Wrapper around `Pattern.place` which allows a string or `TreeView` for `other`. + The `Builder`'s library is used to dereference the string (or `Abstract`, if - one is passed with `append=True`). + one is passed with `append=True`). If a `TreeView` is passed, it is first + added into `self.library`. Args: - other: An `Abstract`, string, or `Pattern` describing the device to be instatiated. + other: An `Abstract`, string, `Pattern`, or `TreeView` describing the + device to be instatiated. If it is a `TreeView`, it is first + added into `self.library`, after which the topcell is plugged; + an equivalent statement is `self.plug(self.library << other, ...)`. offset: Offset at which to place the instance. Default (0, 0). rotation: Rotation applied to the instance before placement. Default 0. pivot: Rotation is applied around this pivot point (default (0, 0)). @@ -306,6 +320,10 @@ class Builder(PortList): logger.error('Skipping place() since device is dead') return self + if not isinstance(other, (str, Abstract, Pattern)): + # We got a Tree; add it into self.library and grab an Abstract for it + other = self.library << other + if isinstance(other, str): other = self.library.abstract(other) if append and isinstance(other, Abstract): diff --git a/masque/library.py b/masque/library.py index c0d8c93..bee7537 100644 --- a/masque/library.py +++ b/masque/library.py @@ -14,7 +14,7 @@ Classes include: - `AbstractView`: Provides a way to use []-indexing to generate abstracts for patterns in the linked library. Generated with `ILibraryView.abstract_view()`. """ -from typing import Callable, Self, Type, TYPE_CHECKING, cast +from typing import Callable, Self, Type, TYPE_CHECKING, cast, TypeAlias, Protocol, Literal from typing import Iterator, Mapping, MutableMapping, Sequence import logging import base64 @@ -44,6 +44,14 @@ logger = logging.getLogger(__name__) visitor_function_t = Callable[..., 'Pattern'] + +TreeView: TypeAlias = Mapping[str, 'Pattern'] +""" A name-to-`Pattern` mapping which is expected to have only one top-level cell """ + +Tree: TypeAlias = MutableMapping[str, 'Pattern'] +""" A mutable name-to-`Pattern` mapping which is expected to have only one top-level cell """ + + SINGLE_USE_PREFIX = '_' """ Names starting with this prefix are assumed to refer to single-use patterns, @@ -674,7 +682,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): return rename_map - def __lshift__(self, other: Mapping[str, 'Pattern']) -> str: + def __lshift__(self, other: TreeView) -> str: if len(other) == 1: name = next(iter(other)) else: From e710fa44b5e4994dfabb6ba104f7698b8b556e4f Mon Sep 17 00:00:00 2001 From: jan Date: Fri, 20 Oct 2023 23:15:13 -0700 Subject: [PATCH 2/5] improve type annotations --- masque/library.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/masque/library.py b/masque/library.py index bee7537..96a6de4 100644 --- a/masque/library.py +++ b/masque/library.py @@ -26,7 +26,7 @@ from collections import defaultdict from abc import ABCMeta, abstractmethod import numpy -from numpy.typing import ArrayLike +from numpy.typing import ArrayLike, NDArray from .error import LibraryError, PatternError from .utils import rotation_matrix_2d, layer_t @@ -42,7 +42,16 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) -visitor_function_t = Callable[..., 'Pattern'] +class visitor_function_t(Protocol): + """ Signature for `Library.dfs()` visitor functions. """ + def __call__( + self, + pattern: 'Pattern', + hierarchy: tuple[str | None, ...], + memo: dict, + transform: NDArray[numpy.float64] | Literal[False], + ) -> Pattern: + ... TreeView: TypeAlias = Mapping[str, 'Pattern'] From ed433861e3e9586fa0f551f164d7ff641fd15ce7 Mon Sep 17 00:00:00 2001 From: jan Date: Fri, 20 Oct 2023 23:15:27 -0700 Subject: [PATCH 3/5] make sure transform is float-typed --- masque/library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/library.py b/masque/library.py index 96a6de4..e4e60e7 100644 --- a/masque/library.py +++ b/masque/library.py @@ -455,7 +455,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta): if transform is None or transform is True: transform = numpy.zeros(4) elif transform is not False: - transform = numpy.array(transform) + transform = numpy.array(transform, dtype=float, copy=False) original_pattern = pattern From 8ef5e2e8525352e83414b93afe6bd42f7a386fa2 Mon Sep 17 00:00:00 2001 From: jan Date: Fri, 20 Oct 2023 23:16:02 -0700 Subject: [PATCH 4/5] improve docs --- masque/library.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/masque/library.py b/masque/library.py index e4e60e7..bb6af9c 100644 --- a/masque/library.py +++ b/masque/library.py @@ -387,6 +387,9 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta): def top(self) -> str: """ Return the name of the topcell, or raise an exception if there isn't a single topcell + + Raises: + LibraryError if there is not exactly one topcell. """ tops = self.tops() if len(tops) != 1: @@ -396,6 +399,9 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta): def top_pattern(self) -> 'Pattern': """ Shorthand for self[self.top()] + + Raises: + LibraryError if there is not exactly one topcell. """ return self[self.top()] @@ -692,6 +698,14 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): return rename_map def __lshift__(self, other: TreeView) -> str: + """ + `add()` items from a tree (single-topcell name: pattern mapping) into this one, + and return the name of the tree's topcell (in this library; it may have changed + based on `add()`'s default `rename_theirs` argument). + + Raises: + LibraryError if there is more than one topcell in `other`. + """ if len(other) == 1: name = next(iter(other)) else: @@ -709,6 +723,13 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): return new_name def __le__(self, other: Mapping[str, 'Pattern']) -> Abstract: + """ + Perform the same operation as `__lshift__` / `<<`, but return an `Abstract` instead + of just the pattern's name. + + Raises: + LibraryError if there is more than one topcell in `other`. + """ new_name = self << other return self.abstract(new_name) From f28c31fe2917f480584a9ebb921b7690cf63acdc Mon Sep 17 00:00:00 2001 From: jan Date: Fri, 20 Oct 2023 23:16:39 -0700 Subject: [PATCH 5/5] = should have been + --- masque/library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/library.py b/masque/library.py index bb6af9c..947f6b9 100644 --- a/masque/library.py +++ b/masque/library.py @@ -865,7 +865,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): if name_func is None: def name_func(_pat, _shape): - return self.get_name(SINGLE_USE_PREFIX = 'rep') + return self.get_name(SINGLE_USE_PREFIX + 'rep') for pat in tuple(self.values()): for layer in pat.shapes: