diff --git a/README.md b/README.md index c9274b8..0ab5897 100644 --- a/README.md +++ b/README.md @@ -15,32 +15,65 @@ to output to multiple formats. Requirements: * python >= 3.11 * numpy -* klamath (optional, used for `gdsii` i/o) -* matplotlib (optional, used for `visualization` functions and `text`) -* ezdxf (optional, used for `dxf` i/o) -* fatamorgana (optional, used for `oasis` i/o) -* svgwrite (optional, used for `svg` output) -* freetype (optional, used for `text`) +* klamath (used for GDSII i/o) + +Optional requirements: +* `ezdxf` (DXF i/o): ezdxf +* `oasis` (OASIS i/o): fatamorgana +* `svg` (SVG output): svgwrite +* `visualization` (shape plotting): matplotlib +* `text` (`Text` shape): matplotlib, freetype Install with pip: ```bash -pip install 'masque[visualization,oasis,dxf,svg,text]' +pip install 'masque[oasis,dxf,svg,visualization,text]' ``` -Alternatively, install from git -```bash -pip install git+https://mpxd.net/code/jan/masque.git@release -``` +## 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. + +`masque` departs from several "classic" GDSII paradigms: +- 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 + a layer is empty. + * Technically, this allows reusing the same shape or label object across multiple layers. This isn't + part of the standard workflow since a mixture of single-use and multi-use shapes could be confusing. + * This is similar to the approach used in [KLayout](https://www.klayout.de) +- `Ref` target names are also determined in the key of the container dict (e.g. `pattern.refs[target_name]`). + * This similarly simplifies filtering `Ref`s by target name, updating to a new target, and checking + if a given `Pattern` is referenced. +- `Pattern` names are set by their containing `Library` and are not stored in the `Pattern` objects. + * This guarantees that there are no duplicate pattern names within any given `Library`. + * Likewise, enumerating all the names (and all the `Pattern`s) in a `Library` is straightforward. +- Each `Ref`, `Shape`, or `Label` can be repeated multiple times by attaching a `repetition` object to it. + * This is similar to how OASIS reptitions are handled, and provides extra flexibility over the GDSII + approach of only allowing arrays through AREF (`Ref` + `repetition`). +- `Label`s do not have an orientation or presentation + * This is in line with how they are used in practice, and how they are represented in OASIS. +- Non-polygonal `Shape`s are allowed. For example, elliptical arcs are a basic shape type. + * This enables compatibility with OASIS (e.g. circles) and other formats. + * `Shape`s provide a `.to_polygons()` method for GDSII compatibility. +- Most coordinate values are stored as 64-bit floats internally. + * 1 earth radii in nanometers (6e15) is still represented without approximation (53 bit mantissa -> 2^53 > 9e15) + * Operations that would otherwise clip/round on are still represented approximately. + * Memory usage is usually dominated by other Python overhead. + ## Glossary -- `Library`: OASIS or GDS "library" or file (a collection of named cells) -- `Pattern`: OASIS or GDS "Cell", DXF "Block" -- `Ref`: GDS "AREF/SREF", OASIS "Placement" -- `Shape`: OASIS or GDS "Geometry element", DXF "LWPolyline" or "Polyline" -- `repetition`: OASIS "repetition". GDS "AREF" is a `Ref` combined with a `Grid` repetition. -- `Label`: OASIS, GDS, DXF "Text". -- `annotation`: OASIS or GDS "property" +- `Library`: A collection of named cells. OASIS or GDS "library" or file. +- `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". +- `Shape`: Individual geometric entity. OASIS or GDS "Geometry element", DXF "LWPolyline" or "Polyline". +- `repetition`: Repetition operation. OASIS "repetition". + GDS "AREF" is a `Ref` combined with a `Grid` repetition. +- `Label`: Text label. Not rendered into geometry. OASIS, GDS, DXF "Text". +- `annotation`: Additional metadata. OASIS or GDS "property". + ## TODO @@ -48,7 +81,6 @@ pip install git+https://mpxd.net/code/jan/masque.git@release * Better interface for polygon operations (e.g. with `pyclipper`) - de-embedding - boolean ops -* DOCS DOCS DOCS * Tests tests tests * check renderpather * pather and renderpather examples diff --git a/masque/file/gdsii.py b/masque/file/gdsii.py index e4175fa..cd2c6c5 100644 --- a/masque/file/gdsii.py +++ b/masque/file/gdsii.py @@ -495,12 +495,12 @@ def _labels_to_texts(labels: dict[layer_t, list[Label]]) -> list[klamath.element xy=xy, string=label.string.encode('ASCII'), properties=properties, - presentation=0, # TODO maybe set some of these? - angle_deg=0, - invert_y=False, - width=0, - path_type=0, - mag=1, + presentation=0, # font number & alignment -- unused by us + angle_deg=0, # rotation -- unused by us + invert_y=False, # inversion -- unused by us + width=0, # stroke width -- unused by us + path_type=0, # text path endcaps, unused + mag=1, # size -- unused by us ) texts.append(text) return texts diff --git a/masque/library.py b/masque/library.py index a5b4b79..c662b3a 100644 --- a/masque/library.py +++ b/masque/library.py @@ -2,7 +2,7 @@ Library classes for managing unique name->pattern mappings and deferred loading or creation. -# TODO documentn all library classes +# TODO documennt all library classes # TODO toplevel documentation of library, classes, and abstracts """ from typing import Callable, Self, Type, TYPE_CHECKING, cast diff --git a/masque/pattern.py b/masque/pattern.py index c80284c..4b85a3a 100644 --- a/masque/pattern.py +++ b/masque/pattern.py @@ -1,5 +1,6 @@ """ - Base object representing a lithography mask. + Object representing a one multi-layer lithographic layout. + A single level of hierarchical references is included. """ from typing import Callable, Sequence, cast, Mapping, Self, Any, Iterable, TypeVar, MutableMapping import copy @@ -27,7 +28,7 @@ logger = logging.getLogger(__name__) class Pattern(PortList, AnnotatableImpl, Mirrorable): """ 2D layout consisting of some set of shapes, labels, and references to other Pattern objects - (via Ref). Shapes are assumed to inherit from masque.shapes.Shape or provide equivalent functions. + (via Ref). Shapes are assumed to inherit from `masque.shapes.Shape` or provide equivalent functions. """ __slots__ = ( 'shapes', 'labels', 'refs', '_ports', @@ -864,6 +865,19 @@ TT = TypeVar('TT') def chain_elements(*args: Mapping[Any, Iterable[TT]]) -> Iterable[TT]: + """ + Iterate over each element in one or more {layer: elements} mappings. + + Useful when you want to do some operation on all shapes and/or labels, + disregarding which layer they are on. + + Args: + *args: One or more {layer: [element0, ...]} mappings. + Can also be applied to e.g. {target: [ref0, ...]} mappings. + + Returns: + An iterable containing all elements, regardless of layer. + """ return chain(*(chain.from_iterable(aa.values()) for aa in args)) @@ -871,6 +885,20 @@ def map_layers( elements: Mapping[layer_t, Sequence[TT]], map_layer: Callable[[layer_t], layer_t], ) -> defaultdict[layer_t, list[TT]]: + """ + Move all the elements from one layer onto a different layer. + Can also handle multiple such mappings simultaneously. + + Args: + elements: Mapping of {old_layer: geometry_or_labels}. + map_layer: Callable which may be called with each layer present in `elements`, + and should return the new layer to which it will be mapped. + A simple example which maps `old_layer` to `new_layer` and leaves all others + as-is would look like `lambda layer: {old_layer: new_layer}.get(layer, layer)` + + Returns: + Mapping of {new_layer: geometry_or_labels} + """ new_elements: defaultdict[layer_t, list[TT]] = defaultdict(list) for old_layer, seq in elements.items(): new_layer = map_layer(old_layer) @@ -882,6 +910,20 @@ def map_targets( refs: Mapping[str | None, Sequence[Ref]], map_target: Callable[[str | None], str | None], ) -> defaultdict[str | None, list[Ref]]: + """ + Change the target of all references to a given cell. + Can also handle multiple such mappings simultaneously. + + Args: + refs: Mapping of {old_target: ref_objects}. + map_target: Callable which may be called with each target present in `refs`, + and should return the new target to which it will be mapped. + A simple example which maps `old_target` to `new_target` and leaves all others + as-is would look like `lambda target: {old_target: new_target}.get(target, target)` + + Returns: + Mapping of {new_target: ref_objects} + """ new_refs: defaultdict[str | None, list[Ref]] = defaultdict(list) for old_target, seq in refs.items(): new_target = map_target(old_target) diff --git a/masque/ref.py b/masque/ref.py index 2034eee..817eb68 100644 --- a/masque/ref.py +++ b/masque/ref.py @@ -1,9 +1,7 @@ """ - Ref provides basic support for nesting Pattern objects within each other, by adding - offset, rotation, scaling, and other such properties to the reference. + Ref provides basic support for nesting Pattern objects within each other. + It carries offset, rotation, mirroring, and scaling data for each individual instance. """ -#TODO more top-level documentation for ref - from typing import Mapping, TYPE_CHECKING, Self import copy @@ -28,10 +26,15 @@ class Ref( PivotableImpl, Copyable, RepeatableImpl, AnnotatableImpl, ): """ - `Ref` provides basic support for nesting Pattern objects within each other, by adding - offset, rotation, scaling, and associated methods. + `Ref` provides basic support for nesting Pattern objects within each other. - Note: Order is (mirror, rotate, scale, translate, repeat) + It containts the transformation (mirror, rotation, scale, offset, repetition) + and annotations for a single instantiation of a `Pattern`. + + Note that the target (i.e. which pattern a `Ref` instantiates) is not stored within the + `Ref` itself, but is specified by the containing `Pattern`. + + Order of operations is (mirror, rotate, scale, translate, repeat). """ __slots__ = ( '_mirrored', diff --git a/masque/shapes/path.py b/masque/shapes/path.py index d8037ad..abb4973 100644 --- a/masque/shapes/path.py +++ b/masque/shapes/path.py @@ -26,6 +26,9 @@ class Path(Shape): A path, consisting of a bunch of vertices (Nx2 ndarray), a width, an end-cap shape, and an offset. + Note that the setter for `Path.vertices` may (but may not) create a copy of the + passed vertex coordinates. See `numpy.array(..., copy=False)` for details. + A normalized_form(...) is available, but can be quite slow with lots of vertices. """ __slots__ = ( @@ -61,12 +64,14 @@ class Path(Shape): def cap(self) -> PathCap: """ Path end-cap + + Note that `cap_extensions` will be reset to default values if + `cap` is changed away from `PathCap.SquareCustom`. """ return self._cap @cap.setter def cap(self, val: PathCap) -> None: - # TODO: Document that setting cap can change cap_extensions self._cap = PathCap(val) if self.cap != PathCap.SquareCustom: self.cap_extensions = None @@ -80,6 +85,9 @@ class Path(Shape): """ Path end-cap extension + Note that `cap_extensions` will be reset to default values if + `cap` is changed away from `PathCap.SquareCustom`. + Returns: 2-element ndarray or `None` """ @@ -101,13 +109,16 @@ class Path(Shape): @property def vertices(self) -> Any: # mypy#3004 NDArray[numpy.float64]]: """ - Vertices of the path (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`) + Vertices of the path (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]` + + When setting, note that a copy of the provided vertices may or may not be made, + following the rules from `numpy.array(.., copy=False)`. """ return self._vertices @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) if len(val.shape) < 2 or val.shape[1] != 2: raise PatternError('Vertices must be an Nx2 array') if val.shape[0] < 2: @@ -218,7 +229,7 @@ class Path(Shape): Returns: The resulting Path object """ - # TODO: needs testing + # TODO: Path.travel() needs testing direction = numpy.array([1, 0]) verts = [numpy.zeros(2)] diff --git a/masque/shapes/polygon.py b/masque/shapes/polygon.py index c9e4c48..ef1ba35 100644 --- a/masque/shapes/polygon.py +++ b/masque/shapes/polygon.py @@ -18,7 +18,7 @@ class Polygon(Shape): 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. + passed vertex coordinates. See `numpy.array(..., copy=False)` for details. A `normalized_form(...)` is available, but can be quite slow with lots of vertices. """ @@ -36,12 +36,15 @@ class Polygon(Shape): def vertices(self) -> Any: # mypy#3004 NDArray[numpy.float64]: """ Vertices of the polygon (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`) + + When setting, note that a copy of the provided vertices may or may not be made, + following the rules from `numpy.array(.., copy=False)`. """ return self._vertices @vertices.setter def vertices(self, val: ArrayLike) -> None: - val = numpy.array(val, dtype=float) # note that this hopefully won't create a copy + val = numpy.array(val, dtype=float) if len(val.shape) < 2 or val.shape[1] != 2: raise PatternError('Vertices must be an Nx2 array') if val.shape[0] < 3: