From 668d4b5d8ba7e47b1d3144391b772d846ea1cb22 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 15 Oct 2023 18:31:58 -0700 Subject: [PATCH 1/4] docstring updates --- masque/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/masque/__init__.py b/masque/__init__.py index e38119d..99c0201 100644 --- a/masque/__init__.py +++ b/masque/__init__.py @@ -1,7 +1,7 @@ """ masque 2D CAD library - masque is an attempt to make a relatively small library for designing lithography + masque is an attempt to make a relatively compact library for designing lithography masks. The general idea is to implement something resembling the GDSII and OASIS file-formats, but with some additional vectorized element types (eg. ellipses, not just polygons), and the ability to interface with multiple file formats. @@ -20,10 +20,10 @@ NOTES ON INTERNALS ========================== - Many of `masque`'s classes make use of `__slots__` to make them faster / smaller. - Since `__slots__` doesn't play well with multiple inheritance, the `masque.utils.AutoSlots` - metaclass is used to auto-generate slots based on superclass type annotations. - - File I/O submodules are imported by `masque.file` to avoid creating hard dependencies on - external file-format reader/writers + Since `__slots__` doesn't play well with multiple inheritance, often they are left + empty for superclasses and it is the subclass's responsibility to set them correctly. + - File I/O submodules are not imported by `masque.file` to avoid creating hard dependencies + on external file-format reader/writers """ From 1bdb998085f91bb4d9189115a34166943b5a0583 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 15 Oct 2023 23:01:47 -0700 Subject: [PATCH 2/4] Generalize underscore into SINGLE_USE_PREFIX --- masque/builder/pather.py | 4 +-- masque/builder/tools.py | 14 +++++------ masque/library.py | 53 +++++++++++++++++++++++----------------- 3 files changed, 39 insertions(+), 32 deletions(-) diff --git a/masque/builder/pather.py b/masque/builder/pather.py index a793308..8b4bb54 100644 --- a/masque/builder/pather.py +++ b/masque/builder/pather.py @@ -11,7 +11,7 @@ from numpy import pi from numpy.typing import ArrayLike from ..pattern import Pattern -from ..library import ILibrary +from ..library import ILibrary, SINGLE_USE_PREFIX from ..error import PortError, BuildError from ..ports import PortList, Port from ..abstract import Abstract @@ -422,7 +422,7 @@ class Pather(Builder): set_rotation: float | None = None, tool_port_names: tuple[str, str] = ('A', 'B'), force_container: bool = False, - base_name: str = '_mpath', + base_name: str = SINGLE_USE_PREFIX + 'mpath', **kwargs, ) -> Self: """ diff --git a/masque/builder/tools.py b/masque/builder/tools.py index b0a71db..3733ad1 100644 --- a/masque/builder/tools.py +++ b/masque/builder/tools.py @@ -15,7 +15,7 @@ from ..utils import SupportsBool, rotation_matrix_2d, layer_t from ..ports import Port from ..pattern import Pattern from ..abstract import Abstract -from ..library import ILibrary, Library +from ..library import ILibrary, Library, SINGLE_USE_PREFIX from ..error import BuildError @@ -288,13 +288,13 @@ class BasicTool(Tool, metaclass=ABCMeta): ) gen_straight, sport_in, sport_out = self.straight - tree, pat = Library.mktree('_path') + tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path') pat.add_port_pair(names=port_names) if data.in_transition: ipat, iport_theirs, _iport_ours = data.in_transition pat.plug(ipat, {port_names[1]: iport_theirs}) if not numpy.isclose(data.straight_length, 0): - straight = tree <= {'_straight': gen_straight(data.straight_length)} + straight = tree <= {SINGLE_USE_PREFIX + 'straight': gen_straight(data.straight_length)} pat.plug(straight, {port_names[1]: sport_in}) if data.ccw is not None: bend, bport_in, bport_out = self.bend @@ -391,7 +391,7 @@ class BasicTool(Tool, metaclass=ABCMeta): **kwargs, ) -> ILibrary: - tree, pat = Library.mktree('_path') + tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path') pat.add_port_pair(names=(port_names[0], port_names[1])) gen_straight, sport_in, _sport_out = self.straight @@ -408,7 +408,7 @@ class BasicTool(Tool, metaclass=ABCMeta): if append: pat.plug(straight_pat, {port_names[1]: sport_in}, append=True) else: - straight = tree <= {'_straight': straight_pat} + straight = tree <= {SINGLE_USE_PREFIX + 'straight': straight_pat} pat.plug(straight, {port_names[1]: sport_in}, append=True) if ccw is not None: bend, bport_in, bport_out = self.bend @@ -463,7 +463,7 @@ class PathTool(Tool, metaclass=ABCMeta): out_ptype=out_ptype, ) - tree, pat = Library.mktree('_path') + tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path') pat.path(layer=self.layer, width=self.width, vertices=[(0, 0), (length, 0)]) if ccw is None: @@ -543,7 +543,7 @@ class PathTool(Tool, metaclass=ABCMeta): # If the path ends in a bend, we need to add the final vertex path_vertices.append(batch[-1].end_port.offset) - tree, pat = Library.mktree('_path') + tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path') pat.path(layer=self.layer, width=self.width, vertices=path_vertices) pat.ports = { port_names[0]: batch[0].start_port.copy().rotate(pi), diff --git a/masque/library.py b/masque/library.py index 92d302d..871a0e4 100644 --- a/masque/library.py +++ b/masque/library.py @@ -35,15 +35,24 @@ logger = logging.getLogger(__name__) visitor_function_t = Callable[..., 'Pattern'] +SINGLE_USE_PREFIX = '_' +""" +Names starting with this prefix are assumed to refer to single-use patterns, +which may be renamed automatically by `ILibrary.add()` (via +`rename_theirs=_rename_patterns()` ) +""" +# TODO what are the consequences of making '_' special? maybe we can make this decision everywhere? + def _rename_patterns(lib: 'ILibraryView', name: str) -> str: """ 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. + Treats names starting with `SINGLE_USE_PREFIX` (default: one underscore) as + "one-offs" for which name conflicts should be automatically resolved. + Conflicts are resolved by calling `lib.get_name(SINGLE_USE_PREFIX + stem)` + where `stem = name.removeprefix(SINGLE_USE_PREFIX).split('$')[0]`. + Names lacking the prefix are directly returned (not renamed). Args: lib: The library into which `name` is to be added (but is presumed to conflict) @@ -52,11 +61,11 @@ def _rename_patterns(lib: 'ILibraryView', name: str) -> str: 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? + if not name.startswith(SINGLE_USE_PREFIX): return name - stem = name.split('$')[0] - return lib.get_name(stem) + stem = name.removeprefix(SINGLE_USE_PREFIX).split('$')[0] + return lib.get_name(SINGLE_USE_PREFIX + stem) class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta): @@ -284,7 +293,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta): def get_name( self, - name: str = '__', + name: str = SINGLE_USE_PREFIX * 2, sanitize: bool = True, max_length: int = 32, quiet: bool | None = None, @@ -295,17 +304,17 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta): This function may be overridden in a subclass or monkey-patched to fit the caller's requirements. Args: - name: Preferred name for the pattern. Default '__'. + name: Preferred name for the pattern. Default is `SINGLE_USE_PREFIX * 2`. sanitize: Allows only alphanumeric charaters and _?$. Replaces invalid characters with underscores. max_length: Names longer than this will be truncated. quiet: If `True`, suppress log messages. Default `None` suppresses messages only if - the name starts with an underscore. + the name starts with `SINGLE_USE_PREFIX`. Returns: Name, unique within this library. """ if quiet is None: - quiet = name.startswith('_') + quiet = name.startswith(SINGLE_USE_PREFIX) if sanitize: # Remove invalid characters @@ -316,7 +325,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta): ii = 0 suffixed_name = sanitized_name while suffixed_name in self or suffixed_name == '': - suffix = base64.b64encode(struct.pack('>Q', ii), b'$?').decode('ASCII') + suffix = base64.b64encode(struct.pack('>Q', ii), altchars=b'$?').decode('ASCII') suffixed_name = sanitized_name + '$' + suffix[:-1].lstrip('A') ii += 1 @@ -602,16 +611,14 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): If `mutate_other=False` (default), all changes are made to a deepcopy of `other`. 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. + name starts with `SINGLE_USE_PREFIX`. Prefixed names are truncated to before their first + non-prefix '$' and then passed to `self.get_name()` to create a new unique name. Args: 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 - `self.get_name(name.split('$')[0]) if name.startswith('_') else name` + `other`. See above for default behavior. mutate_other: If `True`, modify the original library and its contained patterns (e.g. when renaming patterns and updating refs). Otherwise, operate on a deepcopy (default). @@ -703,7 +710,8 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): exclude_types: Shape types passed in this argument are always left untouched, for speed or convenience. Default: `(shapes.Polygon,)` label2name: Given a label tuple as returned by `shape.normalized_form(...)`, pick - a name for the generated pattern. Default `self.get_name('_shape')`. + a name for the generated pattern. + Default `self.get_name(SINGLE_USE_PREIX + 'shape')`. threshold: Only replace shapes with refs if there will be at least this many instances. @@ -720,8 +728,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): if label2name is None: def label2name(label): - return self.get_name('_shape') - #label2name = lambda label: self.get_name('_shape') + return self.get_name(SINGLE_USE_PREFIX + 'shape') shape_counts: MutableMapping[tuple, int] = defaultdict(int) shape_funcs = {} @@ -801,7 +808,8 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): Args: name_func: Function f(this_pattern, shape) which generates a name for the - wrapping pattern. Default is `self.get_name('_rep')`. + wrapping pattern. + Default is `self.get_name(SINGLE_USE_PREFIX + 'rep')`. Returns: self @@ -810,8 +818,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): if name_func is None: def name_func(_pat, _shape): - return self.get_name('_rep') - #name_func = lambda _pat, _shape: self.get_name('_rep') + return self.get_name(SINGLE_USE_PREFIX = 'rep') for pat in tuple(self.values()): for layer in pat.shapes: From 3a6807707beb708ea2743b18799f19d95053717a Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 15 Oct 2023 23:07:28 -0700 Subject: [PATCH 3/4] Add more docs --- README.md | 30 +++++++++++++++++++++++++++++- masque/__init__.py | 3 ++- masque/library.py | 17 +++++++++++++---- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e23059d..4565cad 100644 --- a/README.md +++ b/README.md @@ -33,9 +33,12 @@ pip install 'masque[oasis,dxf,svg,visualization,text]' ## Overview A layout consists of a hierarchy of `Pattern`s stored in a single `Library`. -Each `Pattern` can contain `Ref`s pointing at other patterns, `Shape`s, and `Label`s. +Each `Pattern` can contain `Ref`s pointing at other patterns, `Shape`s, `Label`s, and `Port`s. + `masque` departs from several "classic" GDSII paradigms: +- A `Pattern` object does not store its own name. A name is only assigned when the pattern is placed + into a `Library`, which is effectively a name->`Pattern` mapping. - Layer info for `Shape`ss and `Label`s is not stored in the individual shape and label objects. Instead, the layer is determined by the key for the container dict (e.g. `pattern.shapes[layer]`). * This simplifies many common tasks: filtering `Shape`s by layer, remapping layers, and checking if @@ -70,6 +73,31 @@ Each `Pattern` can contain `Ref`s pointing at other patterns, `Shape`s, and `Lab * Ports can be exported into/imported from `Label`s stored directly in the layout, editable from standard tools (e.g. KLayout). A default format is provided. +In one important way, `masque` stays very orthodox: +References are accomplished by listing the target's name, not its `Pattern` object. + +- The main downside of this is that any operations that traverse the hierarchy require + both the `Pattern` and the `Library` which is contains its reference targets. +- This guarantees that names within a `Library` remain unique at all times. + * Since this can be tedious in cases where you don't actually care about the name of a + pattern, patterns whose names start with `SINGLE_USE_PREFIX` (default: an underscore) + may be silently renamed in order to maintain uniqueness. + See `masque.library.SINGLE_USE_PREFIX`, `masque.library._rename_patterns()`, + and `ILibrary.add()` for more details. +- Having all patterns accessible through the `Library` avoids having to perform a + tree traversal for every operation which needs to touch all `Pattern` objects + (e.g. deleting a layer everywhere or scaling all patterns). +- Since `Pattern` doesn't know its own name, you can't create a reference by passing in + a `Pattern` object -- you need to know its name. +- You *can* reference a `Pattern` before it is created, so long as you have already decided + on its name. +- Functions like `Pattern.place()` and `Pattern.plug()` need to receive a pattern's name + in order to create a reference, but they also need to access the pattern's ports. + * One way to provide this data is through an `Abstract`, generated via + `Library.abstract()` or through a `Library.abstract_view()`. + * Another way is use `Builder.place()` or `Builder.plug()`, which automatically creates + an `Abstract` from its internally-referenced `Library`. + ## Glossary - `Library`: A collection of named cells. OASIS or GDS "library" or file. diff --git a/masque/__init__.py b/masque/__init__.py index 99c0201..051af54 100644 --- a/masque/__init__.py +++ b/masque/__init__.py @@ -24,7 +24,8 @@ empty for superclasses and it is the subclass's responsibility to set them correctly. - File I/O submodules are not imported by `masque.file` to avoid creating hard dependencies on external file-format reader/writers - +- Try to accept the broadest-possible inputs: e.g., don't demand an `ILibraryView` if you + can accept a `Mapping[str, Pattern]` and wrap it in a `LibraryView` internally. """ from .utils import layer_t, annotations_t, SupportsBool diff --git a/masque/library.py b/masque/library.py index 871a0e4..6f36d40 100644 --- a/masque/library.py +++ b/masque/library.py @@ -1,9 +1,18 @@ """ -Library classes for managing unique name->pattern mappings and - deferred loading or creation. +Library classes for managing unique name->pattern mappings and deferred loading or execution. -# TODO documennt all library classes -# TODO toplevel documentation of library, classes, and abstracts +Classes include: +- `ILibraryView`: Defines a general interface for read-only name->pattern mappings. +- `LibraryView`: An implementation of `ILibraryView` backed by an arbitrary `Mapping`. + Can be used to wrap any arbitrary `Mapping` to give it all the functionality in `ILibraryView` +- `ILibrary`: Defines a general interface for mutable name->pattern mappings. +- `Library`: An implementation of `ILibrary` backed by an arbitrary `MutableMapping`. + Can be used to wrap any arbitrary `MutableMapping` to give it all the functionality in `ILibrary`. + By default, uses a `dict` as the underylingmapping. +- `LazyLibrary`: An implementation of `ILibrary` which enables on-demand loading or generation + of patterns. +- `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 Iterator, Mapping, MutableMapping, Sequence From 73ce794fec1882591bee23a9d4131767d6786cc4 Mon Sep 17 00:00:00 2001 From: jan Date: Sun, 15 Oct 2023 23:07:37 -0700 Subject: [PATCH 4/4] import pack2d by default --- masque/utils/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/masque/utils/__init__.py b/masque/utils/__init__.py index 4df9f9a..f4a7805 100644 --- a/masque/utils/__init__.py +++ b/masque/utils/__init__.py @@ -15,4 +15,4 @@ from .transform import rotation_matrix_2d, normalize_mirror, rotate_offsets_arou from . import ports2data -#from . import pack2d +from . import pack2d