various doc updates

This commit is contained in:
jan 2023-09-17 21:33:22 -07:00
parent 04e15f7c85
commit e2c7f8c8cc
7 changed files with 132 additions and 41 deletions

View File

@ -15,32 +15,65 @@ to output to multiple formats.
Requirements: Requirements:
* python >= 3.11 * python >= 3.11
* numpy * numpy
* klamath (optional, used for `gdsii` i/o) * klamath (used for GDSII i/o)
* matplotlib (optional, used for `visualization` functions and `text`)
* ezdxf (optional, used for `dxf` i/o) Optional requirements:
* fatamorgana (optional, used for `oasis` i/o) * `ezdxf` (DXF i/o): ezdxf
* svgwrite (optional, used for `svg` output) * `oasis` (OASIS i/o): fatamorgana
* freetype (optional, used for `text`) * `svg` (SVG output): svgwrite
* `visualization` (shape plotting): matplotlib
* `text` (`Text` shape): matplotlib, freetype
Install with pip: Install with pip:
```bash ```bash
pip install 'masque[visualization,oasis,dxf,svg,text]' pip install 'masque[oasis,dxf,svg,visualization,text]'
``` ```
Alternatively, install from git ## Overview
```bash
pip install git+https://mpxd.net/code/jan/masque.git@release 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 ## Glossary
- `Library`: OASIS or GDS "library" or file (a collection of named cells) - `Library`: A collection of named cells. OASIS or GDS "library" or file.
- `Pattern`: OASIS or GDS "Cell", DXF "Block" - `Pattern`: A collection of geometry, text labels, and reference to other patterns.
- `Ref`: GDS "AREF/SREF", OASIS "Placement" OASIS or GDS "Cell", DXF "Block".
- `Shape`: OASIS or GDS "Geometry element", DXF "LWPolyline" or "Polyline" - `Ref`: A reference to another pattern. GDS "AREF/SREF", OASIS "Placement".
- `repetition`: OASIS "repetition". GDS "AREF" is a `Ref` combined with a `Grid` repetition. - `Shape`: Individual geometric entity. OASIS or GDS "Geometry element", DXF "LWPolyline" or "Polyline".
- `Label`: OASIS, GDS, DXF "Text". - `repetition`: Repetition operation. OASIS "repetition".
- `annotation`: OASIS or GDS "property" 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 ## 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`) * Better interface for polygon operations (e.g. with `pyclipper`)
- de-embedding - de-embedding
- boolean ops - boolean ops
* DOCS DOCS DOCS
* Tests tests tests * Tests tests tests
* check renderpather * check renderpather
* pather and renderpather examples * pather and renderpather examples

View File

@ -495,12 +495,12 @@ def _labels_to_texts(labels: dict[layer_t, list[Label]]) -> list[klamath.element
xy=xy, xy=xy,
string=label.string.encode('ASCII'), string=label.string.encode('ASCII'),
properties=properties, properties=properties,
presentation=0, # TODO maybe set some of these? presentation=0, # font number & alignment -- unused by us
angle_deg=0, angle_deg=0, # rotation -- unused by us
invert_y=False, invert_y=False, # inversion -- unused by us
width=0, width=0, # stroke width -- unused by us
path_type=0, path_type=0, # text path endcaps, unused
mag=1, mag=1, # size -- unused by us
) )
texts.append(text) texts.append(text)
return texts return texts

View File

@ -2,7 +2,7 @@
Library classes for managing unique name->pattern mappings and Library classes for managing unique name->pattern mappings and
deferred loading or creation. deferred loading or creation.
# TODO documentn all library classes # TODO documennt all library classes
# TODO toplevel documentation of library, classes, and abstracts # TODO toplevel documentation of library, classes, and abstracts
""" """
from typing import Callable, Self, Type, TYPE_CHECKING, cast from typing import Callable, Self, Type, TYPE_CHECKING, cast

View File

@ -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 from typing import Callable, Sequence, cast, Mapping, Self, Any, Iterable, TypeVar, MutableMapping
import copy import copy
@ -27,7 +28,7 @@ logger = logging.getLogger(__name__)
class Pattern(PortList, AnnotatableImpl, Mirrorable): class Pattern(PortList, AnnotatableImpl, Mirrorable):
""" """
2D layout consisting of some set of shapes, labels, and references to other Pattern objects 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__ = ( __slots__ = (
'shapes', 'labels', 'refs', '_ports', 'shapes', 'labels', 'refs', '_ports',
@ -864,6 +865,19 @@ TT = TypeVar('TT')
def chain_elements(*args: Mapping[Any, Iterable[TT]]) -> Iterable[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)) return chain(*(chain.from_iterable(aa.values()) for aa in args))
@ -871,6 +885,20 @@ def map_layers(
elements: Mapping[layer_t, Sequence[TT]], elements: Mapping[layer_t, Sequence[TT]],
map_layer: Callable[[layer_t], layer_t], map_layer: Callable[[layer_t], layer_t],
) -> defaultdict[layer_t, list[TT]]: ) -> 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) new_elements: defaultdict[layer_t, list[TT]] = defaultdict(list)
for old_layer, seq in elements.items(): for old_layer, seq in elements.items():
new_layer = map_layer(old_layer) new_layer = map_layer(old_layer)
@ -882,6 +910,20 @@ def map_targets(
refs: Mapping[str | None, Sequence[Ref]], refs: Mapping[str | None, Sequence[Ref]],
map_target: Callable[[str | None], str | None], map_target: Callable[[str | None], str | None],
) -> defaultdict[str | None, list[Ref]]: ) -> 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) new_refs: defaultdict[str | None, list[Ref]] = defaultdict(list)
for old_target, seq in refs.items(): for old_target, seq in refs.items():
new_target = map_target(old_target) new_target = map_target(old_target)

View File

@ -1,9 +1,7 @@
""" """
Ref provides basic support for nesting Pattern objects within each other, by adding Ref provides basic support for nesting Pattern objects within each other.
offset, rotation, scaling, and other such properties to the reference. 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 from typing import Mapping, TYPE_CHECKING, Self
import copy import copy
@ -28,10 +26,15 @@ class Ref(
PivotableImpl, Copyable, RepeatableImpl, AnnotatableImpl, PivotableImpl, Copyable, RepeatableImpl, AnnotatableImpl,
): ):
""" """
`Ref` provides basic support for nesting Pattern objects within each other, by adding `Ref` provides basic support for nesting Pattern objects within each other.
offset, rotation, scaling, and associated methods.
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__ = ( __slots__ = (
'_mirrored', '_mirrored',

View File

@ -26,6 +26,9 @@ class Path(Shape):
A path, consisting of a bunch of vertices (Nx2 ndarray), a width, an end-cap shape, A path, consisting of a bunch of vertices (Nx2 ndarray), a width, an end-cap shape,
and an offset. 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. A normalized_form(...) is available, but can be quite slow with lots of vertices.
""" """
__slots__ = ( __slots__ = (
@ -61,12 +64,14 @@ class Path(Shape):
def cap(self) -> PathCap: def cap(self) -> PathCap:
""" """
Path end-cap Path end-cap
Note that `cap_extensions` will be reset to default values if
`cap` is changed away from `PathCap.SquareCustom`.
""" """
return self._cap return self._cap
@cap.setter @cap.setter
def cap(self, val: PathCap) -> None: def cap(self, val: PathCap) -> None:
# TODO: Document that setting cap can change cap_extensions
self._cap = PathCap(val) self._cap = PathCap(val)
if self.cap != PathCap.SquareCustom: if self.cap != PathCap.SquareCustom:
self.cap_extensions = None self.cap_extensions = None
@ -80,6 +85,9 @@ class Path(Shape):
""" """
Path end-cap extension Path end-cap extension
Note that `cap_extensions` will be reset to default values if
`cap` is changed away from `PathCap.SquareCustom`.
Returns: Returns:
2-element ndarray or `None` 2-element ndarray or `None`
""" """
@ -101,13 +109,16 @@ class Path(Shape):
@property @property
def vertices(self) -> Any: # mypy#3004 NDArray[numpy.float64]]: 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 return self._vertices
@vertices.setter @vertices.setter
def vertices(self, val: ArrayLike) -> None: 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: if len(val.shape) < 2 or val.shape[1] != 2:
raise PatternError('Vertices must be an Nx2 array') raise PatternError('Vertices must be an Nx2 array')
if val.shape[0] < 2: if val.shape[0] < 2:
@ -218,7 +229,7 @@ class Path(Shape):
Returns: Returns:
The resulting Path object The resulting Path object
""" """
# TODO: needs testing # TODO: Path.travel() needs testing
direction = numpy.array([1, 0]) direction = numpy.array([1, 0])
verts = [numpy.zeros(2)] verts = [numpy.zeros(2)]

View File

@ -18,7 +18,7 @@ class Polygon(Shape):
implicitly-closed boundary, and an offset. implicitly-closed boundary, and an offset.
Note that the setter for `Polygon.vertices` may (but may not) create a copy of the 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. 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]: def vertices(self) -> Any: # mypy#3004 NDArray[numpy.float64]:
""" """
Vertices of the polygon (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`) 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 return self._vertices
@vertices.setter @vertices.setter
def vertices(self, val: ArrayLike) -> None: 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: if len(val.shape) < 2 or val.shape[1] != 2:
raise PatternError('Vertices must be an Nx2 array') raise PatternError('Vertices must be an Nx2 array')
if val.shape[0] < 3: if val.shape[0] < 3: