diff --git a/README.md b/README.md index 4565cad..e23059d 100644 --- a/README.md +++ b/README.md @@ -33,12 +33,9 @@ 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, `Label`s, and `Port`s. - +Each `Pattern` can contain `Ref`s pointing at other patterns, `Shape`s, and `Label`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 @@ -73,31 +70,6 @@ Each `Pattern` can contain `Ref`s pointing at other patterns, `Shape`s, `Label`s * 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 051af54..e38119d 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 compact library for designing lithography + masque is an attempt to make a relatively small 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,12 +20,11 @@ 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, 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 -- 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. + 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 + """ from .utils import layer_t, annotations_t, SupportsBool diff --git a/masque/builder/pather.py b/masque/builder/pather.py index 8b4bb54..a793308 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, SINGLE_USE_PREFIX +from ..library import ILibrary 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 = SINGLE_USE_PREFIX + 'mpath', + base_name: str = '_mpath', **kwargs, ) -> Self: """ diff --git a/masque/builder/tools.py b/masque/builder/tools.py index 3733ad1..b0a71db 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, SINGLE_USE_PREFIX +from ..library import ILibrary, Library 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(SINGLE_USE_PREFIX + 'path') + tree, pat = Library.mktree('_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 <= {SINGLE_USE_PREFIX + 'straight': gen_straight(data.straight_length)} + straight = tree <= {'_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(SINGLE_USE_PREFIX + 'path') + tree, pat = Library.mktree('_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 <= {SINGLE_USE_PREFIX + 'straight': straight_pat} + straight = tree <= {'_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(SINGLE_USE_PREFIX + 'path') + tree, pat = Library.mktree('_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(SINGLE_USE_PREFIX + 'path') + tree, pat = Library.mktree('_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 6f36d40..92d302d 100644 --- a/masque/library.py +++ b/masque/library.py @@ -1,18 +1,9 @@ """ -Library classes for managing unique name->pattern mappings and deferred loading or execution. +Library classes for managing unique name->pattern mappings and + deferred loading or creation. -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()`. +# TODO documennt all library classes +# TODO toplevel documentation of library, classes, and abstracts """ from typing import Callable, Self, Type, TYPE_CHECKING, cast from typing import Iterator, Mapping, MutableMapping, Sequence @@ -44,24 +35,15 @@ 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 `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). + 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) @@ -70,11 +52,11 @@ def _rename_patterns(lib: 'ILibraryView', name: str) -> str: Returns: The new name, not guaranteed to be conflict-free! """ - if not name.startswith(SINGLE_USE_PREFIX): + if not name.startswith('_'): # TODO what are the consequences of making '_' special? maybe we can make this decision everywhere? return name - stem = name.removeprefix(SINGLE_USE_PREFIX).split('$')[0] - return lib.get_name(SINGLE_USE_PREFIX + stem) + stem = name.split('$')[0] + return lib.get_name(stem) class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta): @@ -302,7 +284,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta): def get_name( self, - name: str = SINGLE_USE_PREFIX * 2, + name: str = '__', sanitize: bool = True, max_length: int = 32, quiet: bool | None = None, @@ -313,17 +295,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 is `SINGLE_USE_PREFIX * 2`. + name: Preferred name for the pattern. Default '__'. 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 `SINGLE_USE_PREFIX`. + the name starts with an underscore. Returns: Name, unique within this library. """ if quiet is None: - quiet = name.startswith(SINGLE_USE_PREFIX) + quiet = name.startswith('_') if sanitize: # Remove invalid characters @@ -334,7 +316,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), altchars=b'$?').decode('ASCII') + suffix = base64.b64encode(struct.pack('>Q', ii), b'$?').decode('ASCII') suffixed_name = sanitized_name + '$' + suffix[:-1].lstrip('A') ii += 1 @@ -620,14 +602,16 @@ 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 `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. + 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. 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`. See above for default behavior. + `other`. + Default is effectively + `self.get_name(name.split('$')[0]) if name.startswith('_') else name` 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). @@ -719,8 +703,7 @@ 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(SINGLE_USE_PREIX + 'shape')`. + a name for the generated pattern. Default `self.get_name('_shape')`. threshold: Only replace shapes with refs if there will be at least this many instances. @@ -737,7 +720,8 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta): if label2name is None: def label2name(label): - return self.get_name(SINGLE_USE_PREFIX + 'shape') + return self.get_name('_shape') + #label2name = lambda label: self.get_name('_shape') shape_counts: MutableMapping[tuple, int] = defaultdict(int) shape_funcs = {} @@ -817,8 +801,7 @@ 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(SINGLE_USE_PREFIX + 'rep')`. + wrapping pattern. Default is `self.get_name('_rep')`. Returns: self @@ -827,7 +810,8 @@ 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('_rep') + #name_func = lambda _pat, _shape: self.get_name('_rep') for pat in tuple(self.values()): for layer in pat.shapes: diff --git a/masque/utils/__init__.py b/masque/utils/__init__.py index f4a7805..4df9f9a 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