Compare commits

...

4 Commits

Author SHA1 Message Date
jan
73ce794fec import pack2d by default 2023-10-15 23:07:37 -07:00
jan
3a6807707b Add more docs 2023-10-15 23:07:28 -07:00
jan
1bdb998085 Generalize underscore into SINGLE_USE_PREFIX 2023-10-15 23:01:47 -07:00
jan
668d4b5d8b docstring updates 2023-10-15 18:31:58 -07:00
6 changed files with 89 additions and 44 deletions

View File

@ -33,9 +33,12 @@ pip install 'masque[oasis,dxf,svg,visualization,text]'
## Overview ## Overview
A layout consists of a hierarchy of `Pattern`s stored in a single `Library`. 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: `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. - 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]`). 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 * 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, * 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. 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 ## Glossary
- `Library`: A collection of named cells. OASIS or GDS "library" or file. - `Library`: A collection of named cells. OASIS or GDS "library" or file.

View File

@ -1,7 +1,7 @@
""" """
masque 2D CAD library 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, 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 but with some additional vectorized element types (eg. ellipses, not just polygons), and the
ability to interface with multiple file formats. ability to interface with multiple file formats.
@ -20,11 +20,12 @@
NOTES ON INTERNALS NOTES ON INTERNALS
========================== ==========================
- Many of `masque`'s classes make use of `__slots__` to make them faster / smaller. - 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` Since `__slots__` doesn't play well with multiple inheritance, often they are left
metaclass is used to auto-generate slots based on superclass type annotations. empty for superclasses and it is the subclass's responsibility to set them correctly.
- File I/O submodules are imported by `masque.file` to avoid creating hard dependencies on - File I/O submodules are not imported by `masque.file` to avoid creating hard dependencies
external file-format reader/writers 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 from .utils import layer_t, annotations_t, SupportsBool

View File

@ -11,7 +11,7 @@ from numpy import pi
from numpy.typing import ArrayLike from numpy.typing import ArrayLike
from ..pattern import Pattern from ..pattern import Pattern
from ..library import ILibrary from ..library import ILibrary, SINGLE_USE_PREFIX
from ..error import PortError, BuildError from ..error import PortError, BuildError
from ..ports import PortList, Port from ..ports import PortList, Port
from ..abstract import Abstract from ..abstract import Abstract
@ -422,7 +422,7 @@ class Pather(Builder):
set_rotation: float | None = None, set_rotation: float | None = None,
tool_port_names: tuple[str, str] = ('A', 'B'), tool_port_names: tuple[str, str] = ('A', 'B'),
force_container: bool = False, force_container: bool = False,
base_name: str = '_mpath', base_name: str = SINGLE_USE_PREFIX + 'mpath',
**kwargs, **kwargs,
) -> Self: ) -> Self:
""" """

View File

@ -15,7 +15,7 @@ from ..utils import SupportsBool, rotation_matrix_2d, layer_t
from ..ports import Port from ..ports import Port
from ..pattern import Pattern from ..pattern import Pattern
from ..abstract import Abstract from ..abstract import Abstract
from ..library import ILibrary, Library from ..library import ILibrary, Library, SINGLE_USE_PREFIX
from ..error import BuildError from ..error import BuildError
@ -288,13 +288,13 @@ class BasicTool(Tool, metaclass=ABCMeta):
) )
gen_straight, sport_in, sport_out = self.straight 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) pat.add_port_pair(names=port_names)
if data.in_transition: if data.in_transition:
ipat, iport_theirs, _iport_ours = data.in_transition ipat, iport_theirs, _iport_ours = data.in_transition
pat.plug(ipat, {port_names[1]: iport_theirs}) pat.plug(ipat, {port_names[1]: iport_theirs})
if not numpy.isclose(data.straight_length, 0): 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}) pat.plug(straight, {port_names[1]: sport_in})
if data.ccw is not None: if data.ccw is not None:
bend, bport_in, bport_out = self.bend bend, bport_in, bport_out = self.bend
@ -391,7 +391,7 @@ class BasicTool(Tool, metaclass=ABCMeta):
**kwargs, **kwargs,
) -> ILibrary: ) -> 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])) pat.add_port_pair(names=(port_names[0], port_names[1]))
gen_straight, sport_in, _sport_out = self.straight gen_straight, sport_in, _sport_out = self.straight
@ -408,7 +408,7 @@ class BasicTool(Tool, metaclass=ABCMeta):
if append: if append:
pat.plug(straight_pat, {port_names[1]: sport_in}, append=True) pat.plug(straight_pat, {port_names[1]: sport_in}, append=True)
else: else:
straight = tree <= {'_straight': straight_pat} straight = tree <= {SINGLE_USE_PREFIX + 'straight': straight_pat}
pat.plug(straight, {port_names[1]: sport_in}, append=True) pat.plug(straight, {port_names[1]: sport_in}, append=True)
if ccw is not None: if ccw is not None:
bend, bport_in, bport_out = self.bend bend, bport_in, bport_out = self.bend
@ -463,7 +463,7 @@ class PathTool(Tool, metaclass=ABCMeta):
out_ptype=out_ptype, 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)]) pat.path(layer=self.layer, width=self.width, vertices=[(0, 0), (length, 0)])
if ccw is None: 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 # If the path ends in a bend, we need to add the final vertex
path_vertices.append(batch[-1].end_port.offset) 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.path(layer=self.layer, width=self.width, vertices=path_vertices)
pat.ports = { pat.ports = {
port_names[0]: batch[0].start_port.copy().rotate(pi), port_names[0]: batch[0].start_port.copy().rotate(pi),

View File

@ -1,9 +1,18 @@
""" """
Library classes for managing unique name->pattern mappings and Library classes for managing unique name->pattern mappings and deferred loading or execution.
deferred loading or creation.
# TODO documennt all library classes Classes include:
# TODO toplevel documentation of library, classes, and abstracts - `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 Callable, Self, Type, TYPE_CHECKING, cast
from typing import Iterator, Mapping, MutableMapping, Sequence from typing import Iterator, Mapping, MutableMapping, Sequence
@ -35,15 +44,24 @@ logger = logging.getLogger(__name__)
visitor_function_t = Callable[..., 'Pattern'] 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: def _rename_patterns(lib: 'ILibraryView', name: str) -> str:
""" """
The default `rename_theirs` function for `ILibrary.add`. The default `rename_theirs` function for `ILibrary.add`.
Treats names starting with an underscore as "one-offs" for which name conflicts Treats names starting with `SINGLE_USE_PREFIX` (default: one underscore) as
should be automatically resolved. Conflicts are resolved by calling "one-offs" for which name conflicts should be automatically resolved.
`lib.get_name(name.split('$')[0])`. Conflicts are resolved by calling `lib.get_name(SINGLE_USE_PREFIX + stem)`
Names without a leading underscore are directly returned. where `stem = name.removeprefix(SINGLE_USE_PREFIX).split('$')[0]`.
Names lacking the prefix are directly returned (not renamed).
Args: Args:
lib: The library into which `name` is to be added (but is presumed to conflict) lib: The library into which `name` is to be added (but is presumed to conflict)
@ -52,11 +70,11 @@ def _rename_patterns(lib: 'ILibraryView', name: str) -> str:
Returns: Returns:
The new name, not guaranteed to be conflict-free! 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 return name
stem = name.split('$')[0] stem = name.removeprefix(SINGLE_USE_PREFIX).split('$')[0]
return lib.get_name(stem) return lib.get_name(SINGLE_USE_PREFIX + stem)
class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta): class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
@ -284,7 +302,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
def get_name( def get_name(
self, self,
name: str = '__', name: str = SINGLE_USE_PREFIX * 2,
sanitize: bool = True, sanitize: bool = True,
max_length: int = 32, max_length: int = 32,
quiet: bool | None = None, quiet: bool | None = None,
@ -295,17 +313,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. This function may be overridden in a subclass or monkey-patched to fit the caller's requirements.
Args: 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. sanitize: Allows only alphanumeric charaters and _?$. Replaces invalid characters with underscores.
max_length: Names longer than this will be truncated. max_length: Names longer than this will be truncated.
quiet: If `True`, suppress log messages. Default `None` suppresses messages only if 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: Returns:
Name, unique within this library. Name, unique within this library.
""" """
if quiet is None: if quiet is None:
quiet = name.startswith('_') quiet = name.startswith(SINGLE_USE_PREFIX)
if sanitize: if sanitize:
# Remove invalid characters # Remove invalid characters
@ -316,7 +334,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
ii = 0 ii = 0
suffixed_name = sanitized_name suffixed_name = sanitized_name
while suffixed_name in self or suffixed_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') suffixed_name = sanitized_name + '$' + suffix[:-1].lstrip('A')
ii += 1 ii += 1
@ -602,16 +620,14 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
If `mutate_other=False` (default), all changes are made to a deepcopy of `other`. 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 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 '$' name starts with `SINGLE_USE_PREFIX`. Prefixed names are truncated to before their first
and then passed to `self.get_name()` to create a new unique name. non-prefix '$' and then passed to `self.get_name()` to create a new unique name.
Args: 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 rename_theirs: Called as rename_theirs(self, name) for each duplicate name
encountered in `other`. Should return the new name for the pattern in encountered in `other`. Should return the new name for the pattern in
`other`. `other`. See above for default behavior.
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 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 (e.g. when renaming patterns and updating refs). Otherwise, operate on a deepcopy
(default). (default).
@ -703,7 +719,8 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
exclude_types: Shape types passed in this argument are always left untouched, for exclude_types: Shape types passed in this argument are always left untouched, for
speed or convenience. Default: `(shapes.Polygon,)` speed or convenience. Default: `(shapes.Polygon,)`
label2name: Given a label tuple as returned by `shape.normalized_form(...)`, pick 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 threshold: Only replace shapes with refs if there will be at least this many
instances. instances.
@ -720,8 +737,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
if label2name is None: if label2name is None:
def label2name(label): def label2name(label):
return self.get_name('_shape') return self.get_name(SINGLE_USE_PREFIX + 'shape')
#label2name = lambda label: self.get_name('_shape')
shape_counts: MutableMapping[tuple, int] = defaultdict(int) shape_counts: MutableMapping[tuple, int] = defaultdict(int)
shape_funcs = {} shape_funcs = {}
@ -801,7 +817,8 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
Args: Args:
name_func: Function f(this_pattern, shape) which generates a name for the 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: Returns:
self self
@ -810,8 +827,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
if name_func is None: if name_func is None:
def name_func(_pat, _shape): def name_func(_pat, _shape):
return self.get_name('_rep') return self.get_name(SINGLE_USE_PREFIX = 'rep')
#name_func = lambda _pat, _shape: self.get_name('_rep')
for pat in tuple(self.values()): for pat in tuple(self.values()):
for layer in pat.shapes: for layer in pat.shapes:

View File

@ -15,4 +15,4 @@ from .transform import rotation_matrix_2d, normalize_mirror, rotate_offsets_arou
from . import ports2data from . import ports2data
#from . import pack2d from . import pack2d