masque/masque/pattern.py

724 lines
23 KiB
Python
Raw Normal View History

2016-03-15 19:12:39 -07:00
"""
2020-05-11 19:10:00 -07:00
Base object representing a lithography mask.
2016-03-15 19:12:39 -07:00
"""
2023-01-23 22:27:26 -08:00
from typing import List, Callable, Dict, Union, Set, Sequence, Optional, cast
from typing import Mapping, TypeVar, Any
2016-03-15 19:12:39 -07:00
import copy
2020-09-18 19:46:57 -07:00
from itertools import chain
2016-03-15 19:12:39 -07:00
import numpy
from numpy import inf
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-01-25 23:19:25 -08:00
from .shapes import Shape, Polygon
2018-08-30 23:06:31 -07:00
from .label import Label
2023-01-25 23:19:25 -08:00
from .utils import rotation_matrix_2d, annotations_t
from .error import PatternError
2023-01-19 22:20:16 -08:00
from .traits import AnnotatableImpl, Scalable, Mirrorable, Rotatable, Positionable, Repeatable
from .ports import Port, PortList
2016-03-15 19:12:39 -07:00
P = TypeVar('P', bound='Pattern')
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-01-21 21:22:11 -08: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
)
shapes: List[Shape]
""" List of all shapes in this Pattern.
Elements in this list are assumed to inherit from Shape or provide equivalent functions.
"""
labels: List[Label]
""" List of all labels in this Pattern. """
2023-01-21 21:22:11 -08:00
refs: List[Ref]
""" 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-01-24 23:25:10 -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
def ports(self) -> Dict[str, Port]:
return self._ports
@ports.setter
def ports(self, value: Dict[str, Port]) -> None:
self._ports = value
def __init__(
self,
*,
shapes: Sequence[Shape] = (),
labels: Sequence[Label] = (),
2023-01-21 21:22:11 -08:00
refs: Sequence[Ref] = (),
annotations: Optional[annotations_t] = None,
2023-01-21 23:38:53 -08:00
ports: Optional[Mapping[str, 'Port']] = 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
"""
if isinstance(shapes, list):
self.shapes = shapes
else:
self.shapes = list(shapes)
2018-08-30 23:06:31 -07:00
if isinstance(labels, list):
self.labels = labels
else:
self.labels = list(labels)
2023-01-21 21:22:11 -08:00
if isinstance(refs, list):
self.refs = refs
2016-03-15 19:12:39 -07:00
else:
2023-01-21 21:22:11 -08:00
self.refs = list(refs)
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-01-21 21:22:11 -08:00
s = f'<Pattern: sh{len(self.shapes)} sp{len(self.refs)} la{len(self.labels)} ['
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':
return Pattern(
shapes=copy.deepcopy(self.shapes),
labels=copy.deepcopy(self.labels),
2023-01-21 21:22:11 -08:00
refs=[copy.copy(sp) for sp in self.refs],
annotations=copy.deepcopy(self.annotations),
2023-01-19 22:20:16 -08:00
ports=copy.deepcopy(self.ports),
)
2016-03-15 19:12:39 -07:00
2023-01-13 20:33:14 -08:00
def __deepcopy__(self, memo: Optional[Dict] = None) -> 'Pattern':
memo = {} if memo is None else memo
new = Pattern(
shapes=copy.deepcopy(self.shapes, memo),
labels=copy.deepcopy(self.labels, memo),
2023-01-21 21:22:11 -08:00
refs=copy.deepcopy(self.refs, memo),
annotations=copy.deepcopy(self.annotations, memo),
2023-01-19 22:20:16 -08:00
ports=copy.deepcopy(self.ports),
)
return new
2023-01-23 22:27:26 -08:00
def append(self: P, other_pattern: 'Pattern') -> P:
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-01-21 21:22:11 -08:00
self.refs += other_pattern.refs
2016-03-15 19:12:39 -07:00
self.shapes += other_pattern.shapes
2018-08-30 23:06:31 -07:00
self.labels += other_pattern.labels
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-01-13 20:33:14 -08:00
shapes: Optional[Callable[[Shape], bool]] = None,
labels: Optional[Callable[[Label], bool]] = None,
2023-01-21 21:22:11 -08:00
refs: Optional[Callable[[Ref], bool]] = None,
2023-01-21 23:38:53 -08:00
annotations: Optional[Callable[[str, List[Union[int, float, str]]], bool]] = None,
ports: Optional[Callable[[str, Port], bool]] = 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-01-19 22:20:16 -08:00
shapes: Given a shape, returns a boolean denoting whether the shape is a member of the subset.
labels: Given a label, returns a boolean denoting whether the label is a member of the subset.
2023-01-21 21:22:11 -08:00
refs: Given a 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:
pat.shapes = [s for s in self.shapes if shapes(s)]
2023-01-19 22:20:16 -08:00
elif default_keep:
pat.shapes = copy.copy(self.shapes)
if labels is not None:
pat.labels = [s for s in self.labels if labels(s)]
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:
pat.refs = [s for s in self.refs if refs(s)]
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(
self: P,
2023-02-23 11:25:40 -08:00
num_points: Optional[int] = None,
max_arclen: Optional[float] = None,
) -> P:
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-02-23 11:25:40 -08:00
num_points: Number of points to use for each polygon. Can be overridden by
`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
"""
old_shapes = self.shapes
self.shapes = list(chain.from_iterable((
2023-02-23 11:25:40 -08:00
shape.to_polygons(num_points, max_arclen)
for shape in old_shapes)))
2016-03-15 19:12:39 -07:00
return self
def manhattanize(
self: P,
grid_x: ArrayLike,
grid_y: ArrayLike,
) -> P:
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()
2017-09-06 01:16:24 -07:00
old_shapes = self.shapes
2020-09-18 19:46:57 -07:00
self.shapes = list(chain.from_iterable(
(shape.manhattanize(grid_x, grid_y) for shape in old_shapes)))
2017-09-06 01:16:24 -07:00
return self
2023-01-12 23:04:59 -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)
return [shape.vertices + shape.offset for shape in pat.shapes] # type: ignore # mypy can't figure out that shapes are all Polygons now
2016-03-15 19:12:39 -07:00
def referenced_patterns(self) -> Set[Optional[str]]:
"""
Get all pattern namers referenced by this pattern. Non-recursive.
Returns:
A set of all pattern names referenced by this pattern.
"""
2023-01-21 21:22:11 -08:00
return set(sp.target for sp in self.refs)
def get_bounds(
self,
library: Optional[Mapping[str, 'Pattern']] = None,
2023-01-26 23:47:16 -08:00
recurse: bool = True,
) -> Optional[NDArray[numpy.float64]]:
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:
TODO docs for get_bounds
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
min_bounds = numpy.array((+inf, +inf))
2019-06-09 23:57:36 -07:00
max_bounds = numpy.array((-inf, -inf))
for entry in chain(self.shapes, self.labels):
2016-03-15 19:12:39 -07:00
bounds = entry.get_bounds()
if bounds is None:
continue
2016-03-15 19:12:39 -07:00
min_bounds = numpy.minimum(min_bounds, bounds[0, :])
max_bounds = numpy.maximum(max_bounds, bounds[1, :])
2023-01-21 21:22:11 -08:00
if self.refs and (library is None):
raise PatternError('Must provide a library to get_bounds() to resolve refs')
2023-01-26 23:47:16 -08:00
if recurse:
for entry in self.refs:
bounds = entry.get_bounds(library=library)
if bounds is None:
continue
min_bounds = numpy.minimum(min_bounds, bounds[0, :])
max_bounds = numpy.maximum(max_bounds, bounds[1, :])
if (max_bounds < min_bounds).any():
return None
else:
return numpy.vstack((min_bounds, max_bounds))
2016-03-15 19:12:39 -07:00
def get_bounds_nonempty(
self,
library: Optional[Mapping[str, 'Pattern']] = 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:
TODO docs for get_bounds
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
def translate_elements(self: P, offset: ArrayLike) -> P:
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-01-24 23:25:10 -08:00
for entry in chain(self.shapes, self.refs, self.labels, 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
def scale_elements(self: P, c: float) -> P:
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-01-21 21:22:11 -08:00
for entry in chain(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
def scale_by(self: P, c: float) -> P:
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-01-21 21:22:11 -08:00
for entry in chain(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)
2020-05-11 19:01:02 -07:00
for label in 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
def rotate_around(self: P, pivot: ArrayLike, rotation: float) -> P:
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
def rotate_element_centers(self: P, rotation: float) -> P:
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-01-24 23:25:10 -08:00
for entry in chain(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
def rotate_elements(self: P, rotation: float) -> P:
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-01-24 23:25:10 -08:00
for entry in chain(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
2023-01-24 23:25:10 -08:00
def mirror_element_centers(self: P, across_axis: int) -> P:
"""
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-01-24 23:25:10 -08:00
for entry in chain(self.shapes, self.refs, self.labels, self.ports.values()):
cast(Positionable, entry).offset[across_axis - 1] *= -1
return self
2023-01-24 23:25:10 -08:00
def mirror_elements(self: P, across_axis: int) -> P:
"""
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-01-24 23:25:10 -08:00
for entry in chain(self.shapes, self.refs, self.ports.values()):
cast(Mirrorable, entry).mirror(across_axis)
return self
2023-01-24 23:25:10 -08:00
def mirror(self: P, across_axis: int) -> P:
"""
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
def copy(self: P) -> P:
2016-03-15 19:12:39 -07:00
"""
2023-01-21 21:22:11 -08:00
Return a copy of the Pattern, deep-copying shapes and copying refs
2019-05-18 15:05:38 -07:00
entries, but not deep-copying any referenced patterns.
2016-03-15 19:12:39 -07:00
See also: `Pattern.deepcopy()`
Returns:
A copy of the current Pattern.
2016-03-15 19:12:39 -07:00
"""
2019-12-12 00:38:11 -08:00
return copy.copy(self)
2016-03-15 19:12:39 -07:00
def deepcopy(self: P) -> P:
"""
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:
"""
2023-02-09 16:38:33 -08:00
# TODO is_empty doesn't include ports... maybe there should be an equivalent?
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-01-21 21:22:11 -08:00
return (len(self.refs) == 0
and len(self.shapes) == 0
and len(self.labels) == 0)
2019-05-15 00:11:44 -07:00
2023-01-21 21:22:11 -08:00
def ref(self: P, *args: Any, **kwargs: Any) -> P:
"""
2023-01-21 21:22:11 -08:00
Convenience function which constructs a `Ref` object and adds it
to this pattern.
Args:
2023-01-21 21:22:11 -08:00
*args: Passed to `Ref()`
**kwargs: Passed to `Ref()`
Returns:
self
"""
2023-01-21 21:22:11 -08:00
self.refs.append(Ref(*args, **kwargs))
return self
2023-01-25 23:19:25 -08:00
def rect(self: P, *args: Any, **kwargs: Any) -> P:
"""
Convenience function which calls `Polygon.rect` to construct a
rectangle and adds it to this pattern.
Args:
*args: Passed to `Polygon.rect()`
**kwargs: Passed to `Polygon.rect()`
Returns:
self
"""
self.shapes.append(Polygon.rect(*args, **kwargs))
return self
def flatten(
self: P,
library: Mapping[str, P],
2023-02-08 09:26:44 -08:00
flatten_ports: bool = False, # TODO document
) -> '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.
Alters the current pattern in-place
2019-12-12 00:38:11 -08:00
Args:
library: Source for referenced patterns.
2019-12-12 00:38:11 -08:00
Returns:
self
2019-12-12 00:38:11 -08:00
"""
flattened: Dict[Optional[str], Optional[P]] = {}
2019-12-12 00:38:11 -08:00
2023-02-09 16:38:33 -08:00
# TODO both Library and Pattern have flatten()... pattern is in-place?
def flatten_single(name: Optional[str]) -> None:
if name is None:
pat = self
else:
pat = library[name].deepcopy()
flattened[name] = None
2023-01-21 21:22:11 -08:00
for ref in pat.refs:
target = ref.target
if target is None:
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]
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-01-21 21:22:11 -08:00
p = ref.as_pattern(pattern=flattened[target])
2023-02-08 09:26:44 -08:00
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(
self: P,
library: Optional[Mapping[str, P]] = 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-01-21 21:22:11 -08:00
if self.refs and library is None:
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 = []
for shape in self.shapes:
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-01-21 21:22:11 -08:00
for ref in self.refs:
ref.as_pattern(library=library).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-01-27 10:07:39 -08:00
class NamedPattern(Pattern):
"""
TODO: Document NamedPattern
"""
__slots__ = ('_name',)
_name: str
""" The pattern's name """
@property
def name(self) -> str:
return self._name
def __init__(self, name: str) -> None:
"""
Creates an empty NamedPattern
Args:
name: The pattern's name. Immutable.
"""
Pattern.__init__(self)
self._name = name
def __repr__(self) -> str:
s = f'<NamedPattern "{self.name}":'
s += f' sh{len(self.shapes)} sp{len(self.refs)} la{len(self.labels)} ['
for name, port in self.ports.items():
s += f'\n\t{name}: {port}'
s += ']>'
return s
def __copy__(self) -> Pattern:
return Pattern.__copy__(self)
def __deepcopy__(self, memo: Optional[Dict] = None) -> Pattern:
return Pattern.__deepcopy__(self, memo)
2023-02-23 11:25:40 -08:00
def as_pattern(self) -> Pattern:
2023-01-27 10:07:39 -08:00
return Pattern(
shapes=self.shapes,
labels=self.labels,
refs=self.refs,
annotations=self.annotations,
ports=self.ports,
)