masque/masque/pattern.py

932 lines
32 KiB
Python
Raw Normal View History

2016-03-15 19:12:39 -07:00
"""
2023-09-17 21:33:22 -07:00
Object representing a one multi-layer lithographic layout.
A single level of hierarchical references is included.
2016-03-15 19:12:39 -07:00
"""
2023-04-12 18:43:44 -07:00
from typing import Callable, Sequence, cast, Mapping, Self, Any, Iterable, TypeVar, MutableMapping
2016-03-15 19:12:39 -07:00
import copy
2023-04-12 13:56:50 -07:00
import logging
2020-09-18 19:46:57 -07:00
from itertools import chain
2023-04-12 13:56:50 -07:00
from collections import defaultdict
2016-03-15 19:12:39 -07:00
import numpy
from numpy import inf, pi, nan
from numpy.typing import NDArray, ArrayLike
2016-03-15 19:12:39 -07:00
# .visualize imports matplotlib and matplotlib.collections
2023-01-21 23:38:53 -08:00
from .ref import Ref
2023-04-12 13:56:50 -07:00
from .shapes import Shape, Polygon, Path, DEFAULT_POLY_NUM_VERTICES
2018-08-30 23:06:31 -07:00
from .label import Label
from .utils import rotation_matrix_2d, annotations_t, layer_t
from .error import PatternError
2023-04-12 13:56:50 -07:00
from .traits import AnnotatableImpl, Scalable, Mirrorable, Rotatable, Positionable, Repeatable, Bounded
2023-01-19 22:20:16 -08:00
from .ports import Port, PortList
2016-03-15 19:12:39 -07:00
2023-04-12 13:56:50 -07:00
logger = logging.getLogger(__name__)
2023-01-25 23:19:25 -08:00
class Pattern(PortList, AnnotatableImpl, Mirrorable):
2016-03-15 19:12:39 -07:00
"""
2D layout consisting of some set of shapes, labels, and references to other Pattern objects
2023-09-17 21:33:22 -07:00
(via Ref). Shapes are assumed to inherit from `masque.shapes.Shape` or provide equivalent functions.
2016-03-15 19:12:39 -07:00
"""
2023-01-23 22:27:26 -08:00
__slots__ = (
2023-01-24 23:25:10 -08:00
'shapes', 'labels', 'refs', '_ports',
2023-01-23 22:27:26 -08:00
# inherited
2023-01-24 23:25:10 -08:00
'_offset', '_annotations',
2023-01-23 22:27:26 -08:00
)
2023-04-12 13:56:50 -07:00
shapes: defaultdict[layer_t, list[Shape]]
""" Stores of all shapes in this Pattern, indexed by layer.
Elements in this list are assumed to inherit from Shape or provide equivalent functions.
"""
2023-04-12 13:56:50 -07:00
labels: defaultdict[layer_t, list[Label]]
""" List of all labels in this Pattern. """
2023-04-12 13:56:50 -07:00
refs: defaultdict[str | None, list[Ref]]
2023-01-21 21:22:11 -08:00
""" List of all references to other patterns (`Ref`s) in this `Pattern`.
Multiple objects in this list may reference the same Pattern object
(i.e. multiple instances of the same object).
"""
2023-02-23 13:15:32 -08:00
_ports: dict[str, Port]
2023-01-19 22:20:16 -08:00
""" Uniquely-named ports which can be used to snap to other Pattern instances"""
2023-01-24 23:25:10 -08:00
@property
2023-02-23 13:15:32 -08:00
def ports(self) -> dict[str, Port]:
2023-01-24 23:25:10 -08:00
return self._ports
@ports.setter
2023-02-23 13:15:32 -08:00
def ports(self, value: dict[str, Port]) -> None:
2023-01-24 23:25:10 -08:00
self._ports = value
def __init__(
self,
*,
2023-04-12 13:56:50 -07:00
shapes: Mapping[layer_t, Sequence[Shape]] | None = None,
labels: Mapping[layer_t, Sequence[Label]] | None = None,
refs: Mapping[str | None, Sequence[Ref]] | None = None,
2023-02-23 13:15:32 -08:00
annotations: annotations_t | None = None,
ports: Mapping[str, 'Port'] | None = None
) -> None:
2016-03-15 19:12:39 -07:00
"""
Basic init; arguments get assigned to member variables.
2023-01-21 21:22:11 -08:00
Non-list inputs for shapes and refs get converted to lists.
2016-03-15 19:12:39 -07:00
Args:
shapes: Initial shapes in the Pattern
labels: Initial labels in the Pattern
2023-01-21 21:22:11 -08:00
refs: Initial refs in the Pattern
2023-01-19 22:20:16 -08:00
annotations: Initial annotations for the pattern
ports: Any ports in the pattern
2016-03-15 19:12:39 -07:00
"""
2023-04-12 13:56:50 -07:00
self.shapes = defaultdict(list)
self.labels = defaultdict(list)
self.refs = defaultdict(list)
if shapes:
for layer, sseq in shapes.items():
self.shapes[layer].extend(sseq)
if labels:
for layer, lseq in labels.items():
self.labels[layer].extend(lseq)
if refs:
for target, rseq in refs.items():
self.refs[target].extend(rseq)
2016-03-15 19:12:39 -07:00
2023-01-19 22:20:16 -08:00
if ports is not None:
2023-01-24 12:45:44 -08:00
self.ports = dict(copy.deepcopy(ports))
else:
self.ports = {}
2023-01-19 22:20:16 -08:00
self.annotations = annotations if annotations is not None else {}
2019-12-12 00:38:11 -08:00
2023-01-19 22:20:16 -08:00
def __repr__(self) -> str:
2023-04-12 13:56:50 -07:00
nshapes = sum(len(seq) for seq in self.shapes.values())
nrefs = sum(len(seq) for seq in self.refs.values())
nlabels = sum(len(seq) for seq in self.labels.values())
s = f'<Pattern: s{nshapes} r{nrefs} l{nlabels} ['
2023-01-19 22:20:16 -08:00
for name, port in self.ports.items():
s += f'\n\t{name}: {port}'
s += ']>'
return s
2023-01-13 20:33:14 -08:00
def __copy__(self) -> 'Pattern':
2023-04-12 13:56:50 -07:00
logger.warning('Making a shallow copy of a Pattern... old shapes are re-referenced!')
new = Pattern(
annotations=copy.deepcopy(self.annotations),
2023-01-19 22:20:16 -08:00
ports=copy.deepcopy(self.ports),
)
2023-04-12 13:56:50 -07:00
for target, rseq in self.refs.items():
new.refs[target].extend(rseq)
for layer, sseq in self.shapes.items():
new.shapes[layer].extend(sseq)
for layer, lseq in self.labels.items():
new.labels[layer].extend(lseq)
2016-03-15 19:12:39 -07:00
return new
2023-04-12 13:56:50 -07:00
# def __deepcopy__(self, memo: dict | None = None) -> 'Pattern':
# memo = {} if memo is None else memo
# new = Pattern(
# shapes=copy.deepcopy(self.shapes, memo),
# labels=copy.deepcopy(self.labels, memo),
# refs=copy.deepcopy(self.refs, memo),
# annotations=copy.deepcopy(self.annotations, memo),
# ports=copy.deepcopy(self.ports),
# )
# return new
2023-02-23 13:37:34 -08:00
def append(self, other_pattern: 'Pattern') -> Self:
2016-03-15 19:12:39 -07:00
"""
2023-01-21 21:22:11 -08:00
Appends all shapes, labels and refs from other_pattern to self's shapes,
2018-08-30 23:06:31 -07:00
labels, and supbatterns.
2016-03-15 19:12:39 -07:00
Args:
other_pattern: The Pattern to append
Returns:
self
2016-03-15 19:12:39 -07:00
"""
2023-04-12 13:56:50 -07:00
for target, rseq in other_pattern.refs.items():
self.refs[target].extend(rseq)
for layer, sseq in other_pattern.shapes.items():
self.shapes[layer].extend(sseq)
for layer, lseq in other_pattern.labels.items():
self.labels[layer].extend(lseq)
2023-01-21 23:38:53 -08:00
annotation_conflicts = set(self.annotations.keys()) & set(other_pattern.annotations.keys())
if annotation_conflicts:
raise PatternError(f'Annotation keys overlap: {annotation_conflicts}')
self.annotations.update(other_pattern.annotations)
port_conflicts = set(self.ports.keys()) & set(other_pattern.ports.keys())
if port_conflicts:
raise PatternError(f'Port names overlap: {port_conflicts}')
self.ports.update(other_pattern.ports)
2016-03-15 19:12:39 -07:00
return self
def subset(
self,
2023-04-12 13:56:50 -07:00
shapes: Callable[[layer_t, Shape], bool] | None = None,
labels: Callable[[layer_t, Label], bool] | None = None,
refs: Callable[[str | None, Ref], bool] | None = None,
2023-02-23 13:15:32 -08:00
annotations: Callable[[str, list[int | float | str]], bool] | None = None,
ports: Callable[[str, Port], bool] | None = None,
2023-01-19 22:20:16 -08:00
default_keep: bool = False
) -> 'Pattern':
2016-03-15 19:12:39 -07:00
"""
2018-08-30 23:06:31 -07:00
Returns a Pattern containing only the entities (e.g. shapes) for which the
given entity_func returns True.
2023-01-21 21:22:11 -08:00
Self is _not_ altered, but shapes, labels, and refs are _not_ copied, just referenced.
2016-03-15 19:12:39 -07:00
Args:
2023-04-12 13:56:50 -07:00
shapes: Given a layer and shape, returns a boolean denoting whether the shape is a
member of the subset.
labels: Given a layer and label, returns a boolean denoting whether the label is a
member of the subset.
refs: Given a target and ref, returns a boolean denoting if it is a member of the subset.
2023-01-19 22:20:16 -08:00
annotations: Given an annotation, returns a boolean denoting if it is a member of the subset.
ports: Given a port, returns a boolean denoting if it is a member of the subset.
default_keep: If `True`, keeps all elements of a given type if no function is supplied.
Default `False` (discards all elements).
Returns:
2023-01-21 21:22:11 -08:00
A Pattern containing all the shapes and refs for which the parameter
functions return True
2016-03-15 19:12:39 -07:00
"""
pat = Pattern()
2023-01-19 22:20:16 -08:00
if shapes is not None:
2023-04-12 13:56:50 -07:00
for layer in self.shapes:
pat.shapes[layer] = [ss for ss in self.shapes[layer] if shapes(layer, ss)]
2023-01-19 22:20:16 -08:00
elif default_keep:
pat.shapes = copy.copy(self.shapes)
if labels is not None:
2023-04-12 13:56:50 -07:00
for layer in self.labels:
pat.labels[layer] = [ll for ll in self.labels[layer] if labels(layer, ll)]
2023-01-19 22:20:16 -08:00
elif default_keep:
pat.labels = copy.copy(self.labels)
2023-01-21 21:22:11 -08:00
if refs is not None:
2023-04-12 13:56:50 -07:00
for target in self.refs:
pat.refs[target] = [rr for rr in self.refs[target] if refs(target, rr)]
2023-01-19 22:20:16 -08:00
elif default_keep:
2023-01-21 21:22:11 -08:00
pat.refs = copy.copy(self.refs)
2023-01-19 22:20:16 -08:00
if annotations is not None:
2023-01-21 23:38:53 -08:00
pat.annotations = {k: v for k, v in self.annotations.items() if annotations(k, v)}
2023-01-19 22:20:16 -08:00
elif default_keep:
pat.annotations = copy.copy(self.annotations)
if ports is not None:
2023-01-21 23:38:53 -08:00
pat.ports = {k: v for k, v in self.ports.items() if ports(k, v)}
2023-01-19 22:20:16 -08:00
elif default_keep:
pat.ports = copy.copy(self.ports)
return pat
def polygonize(
2023-02-23 13:37:34 -08:00
self,
2023-03-19 10:17:09 -07:00
num_vertices: int | None = DEFAULT_POLY_NUM_VERTICES,
2023-02-23 13:15:32 -08:00
max_arclen: float | None = None,
2023-02-23 13:37:34 -08:00
) -> Self:
2016-03-15 19:12:39 -07:00
"""
Calls `.to_polygons(...)` on all the shapes in this Pattern, replacing them with the returned polygons.
Arguments are passed directly to `shape.to_polygons(...)`.
2016-03-15 19:12:39 -07:00
Args:
2023-03-19 10:17:09 -07:00
num_vertices: Number of points to use for each polygon. Can be overridden by
2023-02-23 11:25:40 -08:00
`max_arclen` if that results in more points. Optional, defaults to shapes'
internal defaults.
2023-02-23 11:25:40 -08:00
max_arclen: Maximum arclength which can be approximated by a single line
2018-04-15 16:42:00 -07:00
segment. Optional, defaults to shapes' internal defaults.
Returns:
self
2016-03-15 19:12:39 -07:00
"""
2023-04-12 13:56:50 -07:00
for layer in self.shapes:
self.shapes[layer] = list(chain.from_iterable(
ss.to_polygons(num_vertices, max_arclen)
for ss in self.shapes[layer]
))
2016-03-15 19:12:39 -07:00
return self
def manhattanize(
2023-02-23 13:37:34 -08:00
self,
grid_x: ArrayLike,
grid_y: ArrayLike,
2023-02-23 13:37:34 -08:00
) -> Self:
2017-09-06 01:16:24 -07:00
"""
Calls `.polygonize()` on the pattern, then calls `.manhattanize()` on all the
2017-09-06 01:16:24 -07:00
resulting shapes, replacing them with the returned Manhattan polygons.
Args:
grid_x: List of allowed x-coordinates for the Manhattanized polygon edges.
grid_y: List of allowed y-coordinates for the Manhattanized polygon edges.
Returns:
self
2017-09-06 01:16:24 -07:00
"""
self.polygonize()
2023-04-12 13:56:50 -07:00
for layer in self.shapes:
self.shapes[layer] = list(chain.from_iterable((
ss.manhattanize(grid_x, grid_y)
for ss in self.shapes[layer]
)))
2017-09-06 01:16:24 -07:00
return self
2023-02-23 13:15:32 -08:00
def as_polygons(self, library: Mapping[str, 'Pattern']) -> list[NDArray[numpy.float64]]:
2016-03-15 19:12:39 -07:00
"""
Represents the pattern as a list of polygons.
Deep-copies the pattern, then calls `.polygonize()` and `.flatten()` on the copy in order to
2016-03-15 19:12:39 -07:00
generate the list of polygons.
Returns:
A list of `(Ni, 2)` `numpy.ndarray`s specifying vertices of the polygons. Each ndarray
is of the form `[[x0, y0], [x1, y1],...]`.
2016-03-15 19:12:39 -07:00
"""
pat = self.deepcopy().polygonize().flatten(library=library)
2023-04-12 13:56:50 -07:00
polys = [
cast(Polygon, shape).vertices + cast(Polygon, shape).offset
for shape in chain_elements(pat.shapes)
]
return polys
2016-03-15 19:12:39 -07:00
2023-02-23 13:15:32 -08:00
def referenced_patterns(self) -> set[str | None]:
"""
Get all pattern namers referenced by this pattern. Non-recursive.
Returns:
A set of all pattern names referenced by this pattern.
"""
2023-04-12 13:56:50 -07:00
return set(self.refs.keys())
def get_bounds(
self,
2023-02-23 13:15:32 -08:00
library: Mapping[str, 'Pattern'] | None = None,
2023-01-26 23:47:16 -08:00
recurse: bool = True,
cache: MutableMapping[str, NDArray[numpy.float64] | None] | None = None,
2023-02-23 13:15:32 -08:00
) -> NDArray[numpy.float64] | None:
2016-03-15 19:12:39 -07:00
"""
Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the
2016-03-15 19:12:39 -07:00
extent of the Pattern's contents in each dimension.
Returns `None` if the Pattern is empty.
2016-03-15 19:12:39 -07:00
2023-01-26 23:47:16 -08:00
Args:
2023-07-17 21:28:42 -07:00
library: If `recurse=True`, any referenced patterns are loaded from this library.
recurse: If `False`, do not evaluate the bounds of any refs (i.e. assume they are empty).
If `True`, evaluate the bounds of all refs and their conained geometry recursively.
Default `True`.
cache: Mapping of `{name: bounds}` for patterns for which the bounds have already been calculated.
Modified during the run (any referenced pattern's bounds are added).
2023-01-26 23:47:16 -08:00
Returns:
`[[x_min, y_min], [x_max, y_max]]` or `None`
2016-03-15 19:12:39 -07:00
"""
2020-09-18 19:46:57 -07:00
if self.is_empty():
2016-03-15 19:12:39 -07:00
return None
n_elems = sum(1 for _ in chain_elements(self.shapes, self.labels))
ebounds = numpy.full((n_elems, 2, 2), nan)
for ee, entry in enumerate(chain_elements(self.shapes, self.labels)):
maybe_ebounds = cast(Bounded, entry).get_bounds()
if maybe_ebounds is not None:
ebounds[ee] = maybe_ebounds
mask = ~numpy.isnan(ebounds[:, 0, 0])
if mask.any():
cbounds = numpy.vstack((
2023-04-14 22:27:44 -07:00
numpy.min(ebounds[mask, 0, :], axis=0),
numpy.max(ebounds[mask, 1, :], axis=0),
))
else:
cbounds = numpy.array((
(+inf, +inf),
(-inf, -inf),
))
2023-04-12 13:56:50 -07:00
if recurse and self.has_refs():
if library is None:
raise PatternError('Must provide a library to get_bounds() to resolve refs')
2023-04-12 18:21:37 -07:00
if cache is None:
cache = {}
2023-04-12 13:56:50 -07:00
for target, refs in self.refs.items():
if target is None:
continue
if not refs:
2023-01-26 23:47:16 -08:00
continue
2023-04-12 18:21:37 -07:00
if target in cache:
unrot_bounds = cache[target]
2023-04-12 21:41:18 -07:00
elif any(numpy.isclose(ref.rotation % (pi / 2), 0) for ref in refs):
2023-04-12 18:21:37 -07:00
unrot_bounds = library[target].get_bounds(library=library, recurse=recurse, cache=cache)
cache[target] = unrot_bounds
2023-04-12 13:56:50 -07:00
for ref in refs:
2023-04-12 18:43:44 -07:00
if numpy.isclose(ref.rotation % (pi / 2), 0):
2023-04-12 18:21:37 -07:00
if unrot_bounds is None:
bounds = None
else:
ubounds = unrot_bounds.copy()
if ref.mirrored:
2023-04-12 18:21:37 -07:00
ubounds[:, 1] *= -1
2023-04-12 22:40:08 -07:00
corners = (rotation_matrix_2d(ref.rotation) @ ubounds.T).T
2023-04-12 22:40:08 -07:00
bounds = numpy.vstack((numpy.min(corners, axis=0),
numpy.max(corners, axis=0))) * ref.scale + [ref.offset]
2023-04-12 19:31:17 -07:00
2023-04-12 18:21:37 -07:00
else:
# Non-manhattan rotation, have to figure out bounds by rotating the pattern
bounds = ref.get_bounds(library[target], library=library)
2023-04-12 13:56:50 -07:00
if bounds is None:
continue
2023-04-12 19:31:17 -07:00
cbounds[0] = numpy.minimum(cbounds[0], bounds[0])
cbounds[1] = numpy.maximum(cbounds[1], bounds[1])
2023-04-12 18:21:37 -07:00
2023-04-12 19:31:17 -07:00
if (cbounds[1] < cbounds[0]).any():
return None
else:
2023-04-12 19:31:17 -07:00
return cbounds
2016-03-15 19:12:39 -07:00
def get_bounds_nonempty(
self,
2023-02-23 13:15:32 -08:00
library: Mapping[str, 'Pattern'] | None = None,
2023-01-26 23:47:16 -08:00
recurse: bool = True,
) -> NDArray[numpy.float64]:
2022-02-27 21:21:34 -08:00
"""
Convenience wrapper for `get_bounds()` which asserts that the Pattern as non-None bounds.
2023-01-26 23:47:16 -08:00
Args:
2023-07-17 21:28:42 -07:00
library: If `recurse=True`, any referenced patterns are loaded from this library.
recurse: If `False`, do not evaluate the bounds of any refs (i.e. assume they are empty).
If `True`, evaluate the bounds of all refs and their conained geometry recursively.
Default `True`.
cache: Mapping of `{name: bounds}` for patterns for which the bounds have already been calculated.
Modified during the run (any referenced pattern's bounds are added).
2023-01-26 23:47:16 -08:00
2022-02-27 21:21:34 -08:00
Returns:
`[[x_min, y_min], [x_max, y_max]]`
"""
bounds = self.get_bounds(library)
2023-01-23 22:27:26 -08:00
assert bounds is not None
2022-02-27 21:21:34 -08:00
return bounds
2023-02-23 13:37:34 -08:00
def translate_elements(self, offset: ArrayLike) -> Self:
2016-03-15 19:12:39 -07:00
"""
2023-01-24 23:25:10 -08:00
Translates all shapes, label, refs, and ports by the given offset.
2016-03-15 19:12:39 -07:00
Args:
offset: (x, y) to translate by
Returns:
self
2016-03-15 19:12:39 -07:00
"""
2023-04-12 13:56:50 -07:00
for entry in chain(chain_elements(self.shapes, self.labels, self.refs), self.ports.values()):
2023-01-19 22:20:16 -08:00
cast(Positionable, entry).translate(offset)
2016-03-15 19:12:39 -07:00
return self
2023-02-23 13:37:34 -08:00
def scale_elements(self, c: float) -> Self:
2016-03-15 19:12:39 -07:00
""""
2023-01-21 21:22:11 -08:00
Scales all shapes and refs by the given value.
2016-03-15 19:12:39 -07:00
Args:
c: factor to scale by
Returns:
self
2016-03-15 19:12:39 -07:00
"""
2023-04-12 13:56:50 -07:00
for entry in chain_elements(self.shapes, self.refs):
2023-01-19 22:20:16 -08:00
cast(Scalable, entry).scale_by(c)
2016-03-15 19:12:39 -07:00
return self
2023-02-23 13:37:34 -08:00
def scale_by(self, c: float) -> Self:
2016-03-15 19:12:39 -07:00
"""
Scale this Pattern by the given value
2023-01-24 23:25:10 -08:00
(all shapes and refs and their offsets are scaled,
as are all label and port offsets)
2016-03-15 19:12:39 -07:00
Args:
c: factor to scale by
Returns:
self
2016-03-15 19:12:39 -07:00
"""
2023-04-12 13:56:50 -07:00
for entry in chain_elements(self.shapes, self.refs):
2023-01-19 22:20:16 -08:00
cast(Positionable, entry).offset *= c
cast(Scalable, entry).scale_by(c)
rep = cast(Repeatable, entry).repetition
if rep:
rep.scale_by(c)
2023-04-12 13:56:50 -07:00
for label in chain_elements(self.labels):
2023-01-19 22:20:16 -08:00
cast(Positionable, label).offset *= c
rep = cast(Repeatable, label).repetition
if rep:
rep.scale_by(c)
for port in self.ports.values():
port.offset *= c
2016-03-15 19:12:39 -07:00
return self
2023-02-23 13:37:34 -08:00
def rotate_around(self, pivot: ArrayLike, rotation: float) -> Self:
2016-03-15 19:12:39 -07:00
"""
Rotate the Pattern around the a location.
Args:
pivot: (x, y) location to rotate around
rotation: Angle to rotate by (counter-clockwise, radians)
Returns:
self
2016-03-15 19:12:39 -07:00
"""
pivot = numpy.array(pivot)
self.translate_elements(-pivot)
self.rotate_elements(rotation)
self.rotate_element_centers(rotation)
self.translate_elements(+pivot)
return self
2023-02-23 13:37:34 -08:00
def rotate_element_centers(self, rotation: float) -> Self:
2016-03-15 19:12:39 -07:00
"""
2023-01-24 23:25:10 -08:00
Rotate the offsets of all shapes, labels, refs, and ports around (0, 0)
2016-03-15 19:12:39 -07:00
Args:
rotation: Angle to rotate by (counter-clockwise, radians)
Returns:
self
2016-03-15 19:12:39 -07:00
"""
2023-04-12 13:56:50 -07:00
for entry in chain(chain_elements(self.shapes, self.refs, self.labels), self.ports.values()):
2023-01-19 22:20:16 -08:00
old_offset = cast(Positionable, entry).offset
cast(Positionable, entry).offset = numpy.dot(rotation_matrix_2d(rotation), old_offset)
2016-03-15 19:12:39 -07:00
return self
2023-02-23 13:37:34 -08:00
def rotate_elements(self, rotation: float) -> Self:
2016-03-15 19:12:39 -07:00
"""
2023-01-24 23:25:10 -08:00
Rotate each shape, ref, and port around its origin (offset)
2016-03-15 19:12:39 -07:00
Args:
rotation: Angle to rotate by (counter-clockwise, radians)
Returns:
self
2016-03-15 19:12:39 -07:00
"""
2023-04-12 13:56:50 -07:00
for entry in chain(chain_elements(self.shapes, self.refs), self.ports.values()):
2020-11-09 22:04:04 -08:00
cast(Rotatable, entry).rotate(rotation)
2016-03-15 19:12:39 -07:00
return self
def mirror_element_centers(self, across_axis: int = 0) -> Self:
"""
2023-01-21 21:22:11 -08:00
Mirror the offsets of all shapes, labels, and refs across an axis
Args:
2023-01-24 23:25:10 -08:00
across_axis: Axis to mirror across
(0: mirror across x axis, 1: mirror across y axis)
Returns:
self
"""
2023-04-12 13:56:50 -07:00
for entry in chain(chain_elements(self.shapes, self.refs, self.labels), self.ports.values()):
2023-01-24 23:25:10 -08:00
cast(Positionable, entry).offset[across_axis - 1] *= -1
return self
def mirror_elements(self, across_axis: int = 0) -> Self:
"""
2023-01-24 23:25:10 -08:00
Mirror each shape, ref, and pattern across an axis, relative
to its offset
Args:
2023-01-24 23:25:10 -08:00
across_axis: Axis to mirror across
(0: mirror across x axis, 1: mirror across y axis)
Returns:
self
"""
2023-04-12 13:56:50 -07:00
for entry in chain(chain_elements(self.shapes, self.refs), self.ports.values()):
2023-01-24 23:25:10 -08:00
cast(Mirrorable, entry).mirror(across_axis)
return self
def mirror(self, across_axis: int = 0) -> Self:
"""
Mirror the Pattern across an axis
Args:
2023-01-24 23:25:10 -08:00
across_axis: Axis to mirror across
(0: mirror across x axis, 1: mirror across y axis)
Returns:
self
"""
2023-01-24 23:25:10 -08:00
self.mirror_elements(across_axis)
self.mirror_element_centers(across_axis)
return self
2023-02-23 13:37:34 -08:00
def copy(self) -> Self:
2016-03-15 19:12:39 -07:00
"""
2023-04-12 21:43:58 -07:00
Convenience method for `copy.deepcopy(pattern)` (same as `Pattern.deepcopy()`).
See also: `Pattern.deepcopy()`
Returns:
2023-04-12 21:43:58 -07:00
A deep copy of the current Pattern.
2016-03-15 19:12:39 -07:00
"""
2023-04-12 21:43:58 -07:00
return copy.deepcopy(self)
2016-03-15 19:12:39 -07:00
2023-02-23 13:37:34 -08:00
def deepcopy(self) -> Self:
"""
Convenience method for `copy.deepcopy(pattern)`
Returns:
A deep copy of the current Pattern.
"""
return copy.deepcopy(self)
2019-05-15 00:11:44 -07:00
def is_empty(self) -> bool:
"""
Returns:
2023-01-21 21:22:11 -08:00
True if the pattern is contains no shapes, labels, or refs.
2019-05-15 00:11:44 -07:00
"""
2023-04-12 13:56:50 -07:00
return not (self.has_refs() or self.has_shapes() or self.has_labels())
2019-05-15 00:11:44 -07:00
2023-04-12 13:56:50 -07:00
def has_refs(self) -> bool:
2023-07-17 21:28:42 -07:00
"""
Returns:
True if the pattern contains any refs.
"""
2023-04-12 13:56:50 -07:00
return any(True for _ in chain.from_iterable(self.refs.values()))
def has_shapes(self) -> bool:
2023-07-17 21:28:42 -07:00
"""
Returns:
True if the pattern contains any shapes.
"""
2023-04-12 13:56:50 -07:00
return any(True for _ in chain.from_iterable(self.shapes.values()))
def has_labels(self) -> bool:
2023-07-17 21:28:42 -07:00
"""
Returns:
True if the pattern contains any labels.
"""
2023-04-12 13:56:50 -07:00
return any(True for _ in chain.from_iterable(self.labels.values()))
2023-07-17 21:29:12 -07:00
def has_ports(self) -> bool:
"""
Returns:
True if the pattern contains any ports.
"""
return bool(self.ports)
2023-04-12 13:56:50 -07:00
def ref(self, target: str | None, *args: Any, **kwargs: Any) -> Self:
"""
2023-01-21 21:22:11 -08:00
Convenience function which constructs a `Ref` object and adds it
to this pattern.
Args:
2023-04-12 13:56:50 -07:00
target: Target for the ref
2023-01-21 21:22:11 -08:00
*args: Passed to `Ref()`
**kwargs: Passed to `Ref()`
Returns:
self
"""
2023-04-12 13:56:50 -07:00
self.refs[target].append(Ref(*args, **kwargs))
return self
2023-04-12 13:56:50 -07:00
def polygon(self, layer: layer_t, *args: Any, **kwargs: Any) -> Self:
"""
Convenience function which constructs a `Polygon` object and adds it
to this pattern.
Args:
2023-04-12 13:56:50 -07:00
layer: Layer for the polygon
*args: Passed to `Polygon()`
**kwargs: Passed to `Polygon()`
Returns:
self
"""
2023-04-12 13:56:50 -07:00
self.shapes[layer].append(Polygon(*args, **kwargs))
return self
2023-04-12 13:56:50 -07:00
def rect(self, layer: layer_t, *args: Any, **kwargs: Any) -> Self:
2023-01-25 23:19:25 -08:00
"""
Convenience function which calls `Polygon.rect` to construct a
rectangle and adds it to this pattern.
Args:
2023-04-12 13:56:50 -07:00
layer: Layer for the rectangle
2023-01-25 23:19:25 -08:00
*args: Passed to `Polygon.rect()`
**kwargs: Passed to `Polygon.rect()`
Returns:
self
"""
2023-04-12 13:56:50 -07:00
self.shapes[layer].append(Polygon.rect(*args, **kwargs))
return self
def path(self, layer: layer_t, *args: Any, **kwargs: Any) -> Self:
"""
Convenience function which constructs a `Path` object and adds it
to this pattern.
Args:
layer: Layer for the path
*args: Passed to `Path()`
**kwargs: Passed to `Path()`
Returns:
self
"""
self.shapes[layer].append(Path(*args, **kwargs))
2023-01-25 23:19:25 -08:00
return self
2023-04-12 13:56:50 -07:00
def label(self, layer: layer_t, *args: Any, **kwargs: Any) -> Self:
"""
Convenience function which constructs a `Label` object
and adds it to this pattern.
Args:
2023-04-12 13:56:50 -07:00
layer: Layer for the label
*args: Passed to `Label()`
**kwargs: Passed to `Label()`
Returns:
self
"""
2023-04-12 13:56:50 -07:00
self.labels[layer].append(Label(*args, **kwargs))
return self
2023-06-02 21:15:16 -07:00
def prune_layers(self) -> Self:
"""
Removes empty layers (empty lists) in `self.shapes` and `self.labels`.
Returns:
self
"""
for layer in list(self.shapes):
if not self.shapes[layer]:
del self.shapes[layer]
for layer in list(self.labels):
if not self.labels[layer]:
del self.labels[layer]
return self
def prune_refs(self) -> Self:
"""
Remove empty ref lists in `self.refs`.
Returns:
self
"""
for target in list(self.refs):
if not self.refs[target]:
del self.refs[target]
return self
def flatten(
2023-02-23 13:37:34 -08:00
self,
library: Mapping[str, 'Pattern'],
2023-07-17 21:28:42 -07:00
flatten_ports: bool = False,
) -> 'Pattern':
2019-12-12 00:38:11 -08:00
"""
2023-01-21 21:22:11 -08:00
Removes all refs (recursively) and adds equivalent shapes.
2023-07-17 21:28:42 -07:00
Alters the current pattern in-place.
For a version which creates copies, see `Library.flatten`.
2019-12-12 00:38:11 -08:00
Args:
library: Source for referenced patterns.
2023-07-17 21:28:42 -07:00
flatten_ports: If `True`, keep ports from any referenced
patterns; otherwise discard them.
2019-12-12 00:38:11 -08:00
Returns:
self
2019-12-12 00:38:11 -08:00
"""
flattened: dict[str | None, 'Pattern | None'] = {}
2019-12-12 00:38:11 -08:00
2023-02-23 13:15:32 -08:00
def flatten_single(name: str | None) -> None:
if name is None:
pat = self
else:
pat = library[name].deepcopy()
flattened[name] = None
2023-04-12 13:56:50 -07:00
for target, refs in pat.refs.items():
if target is None:
continue
2023-04-12 13:56:50 -07:00
if not refs:
continue
2016-03-15 19:12:39 -07:00
if target not in flattened:
flatten_single(target)
2023-02-09 16:38:33 -08:00
target_pat = flattened[target]
2023-04-12 13:56:50 -07:00
2023-02-09 16:38:33 -08:00
if target_pat is None:
raise PatternError(f'Circular reference in {name} to {target}')
2023-02-09 16:43:06 -08:00
if target_pat.is_empty(): # avoid some extra allocations
2023-02-09 16:38:33 -08:00
continue
2016-03-15 19:12:39 -07:00
2023-04-12 13:56:50 -07:00
for ref in refs:
p = ref.as_pattern(pattern=target_pat)
if not flatten_ports:
p.ports.clear()
pat.append(p)
2023-01-21 21:22:11 -08:00
pat.refs.clear()
flattened[name] = pat
2016-03-15 19:12:39 -07:00
flatten_single(None)
2016-03-15 19:12:39 -07:00
return self
def visualize(
2023-02-23 13:37:34 -08:00
self,
library: Mapping[str, 'Pattern'] | None = None,
offset: ArrayLike = (0., 0.),
line_color: str = 'k',
fill_color: str = 'none',
overdraw: bool = False,
) -> None:
2016-03-15 19:12:39 -07:00
"""
Draw a picture of the Pattern and wait for the user to inspect it
Imports `matplotlib`.
Note that this can be slow; it is often faster to export to GDSII and use
klayout or a different GDS viewer!
2016-03-15 19:12:39 -07:00
Args:
offset: Coordinates to offset by before drawing
line_color: Outlines are drawn with this color (passed to `matplotlib.collections.PolyCollection`)
fill_color: Interiors are drawn with this color (passed to `matplotlib.collections.PolyCollection`)
overdraw: Whether to create a new figure or draw on a pre-existing one
2016-03-15 19:12:39 -07:00
"""
2018-08-30 23:06:31 -07:00
# TODO: add text labels to visualize()
2020-09-18 19:47:31 -07:00
from matplotlib import pyplot # type: ignore
import matplotlib.collections # type: ignore
2016-03-15 19:12:39 -07:00
2023-04-12 13:56:50 -07:00
if self.has_refs() and library is None:
2023-01-21 21:22:11 -08:00
raise PatternError('Must provide a library when visualizing a pattern with refs')
2016-03-15 19:12:39 -07:00
offset = numpy.array(offset, dtype=float)
if not overdraw:
figure = pyplot.figure()
pyplot.axis('equal')
else:
figure = pyplot.gcf()
axes = figure.gca()
polygons = []
2023-04-12 13:56:50 -07:00
for shape in chain.from_iterable(self.shapes.values()):
2016-03-15 19:12:39 -07:00
polygons += [offset + s.offset + s.vertices for s in shape.to_polygons()]
mpl_poly_collection = matplotlib.collections.PolyCollection(
polygons,
facecolors=fill_color,
edgecolors=line_color,
)
2016-03-15 19:12:39 -07:00
axes.add_collection(mpl_poly_collection)
pyplot.axis('equal')
2023-04-12 13:56:50 -07:00
for target, refs in self.refs.items():
if target is None:
continue
if not refs:
continue
assert library is not None
target_pat = library[target]
for ref in refs:
ref.as_pattern(target_pat).visualize(
library=library,
offset=offset,
overdraw=True,
line_color=line_color,
fill_color=fill_color,
)
2016-03-15 19:12:39 -07:00
if not overdraw:
2021-01-08 21:20:03 -08:00
pyplot.xlabel('x')
pyplot.ylabel('y')
2016-03-15 19:12:39 -07:00
pyplot.show()
2023-04-12 13:56:50 -07:00
TT = TypeVar('TT')
def chain_elements(*args: Mapping[Any, Iterable[TT]]) -> Iterable[TT]:
2023-09-17 21:33:22 -07:00
"""
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.
"""
2023-04-12 13:56:50 -07:00
return chain(*(chain.from_iterable(aa.values()) for aa in args))
def map_layers(
elements: Mapping[layer_t, Sequence[TT]],
2023-04-12 14:47:10 -07:00
map_layer: Callable[[layer_t], layer_t],
2023-04-12 13:56:50 -07:00
) -> defaultdict[layer_t, list[TT]]:
2023-09-17 21:33:22 -07:00
"""
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}
"""
2023-04-12 13:56:50 -07:00
new_elements: defaultdict[layer_t, list[TT]] = defaultdict(list)
for old_layer, seq in elements.items():
2023-04-12 14:47:10 -07:00
new_layer = map_layer(old_layer)
2023-04-12 13:56:50 -07:00
new_elements[new_layer].extend(seq)
return new_elements
def map_targets(
refs: Mapping[str | None, Sequence[Ref]],
2023-04-12 14:47:10 -07:00
map_target: Callable[[str | None], str | None],
2023-04-12 13:56:50 -07:00
) -> defaultdict[str | None, list[Ref]]:
2023-09-17 21:33:22 -07:00
"""
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}
"""
2023-04-12 13:56:50 -07:00
new_refs: defaultdict[str | None, list[Ref]] = defaultdict(list)
for old_target, seq in refs.items():
2023-04-12 14:47:10 -07:00
new_target = map_target(old_target)
2023-04-12 13:56:50 -07:00
new_refs[new_target].extend(seq)
return new_refs