Lots of progress on tutorials

This commit is contained in:
Jan Petykiewicz 2023-01-24 23:25:10 -08:00 committed by jan
commit f4537a0feb
16 changed files with 579 additions and 510 deletions

View file

@ -1,27 +1,38 @@
from typing import Dict, Union, Optional
from typing import MutableMapping, TYPE_CHECKING
from typing import Dict
#from typing import Union, Optional, MutableMapping, TYPE_CHECKING
import copy
import logging
from .pattern import Pattern
#from .pattern import Pattern
from .ports import PortList, Port
if TYPE_CHECKING:
from .builder import Builder, Tool
from .library import MutableLibrary
#if TYPE_CHECKING:
# from .builder import Builder, Tool
# from .library import MutableLibrary
logger = logging.getLogger(__name__)
AA = TypeVar('AA', bound='Abstract')
class Abstract(PortList):
__slots__ = ('name', 'ports')
__slots__ = ('name', '_ports')
name: str
""" Name of the pattern this device references """
ports: Dict[str, Port]
""" Uniquely-named ports which can be used to snap instances together"""
_ports: Dict[str, Port]
""" Uniquely-named ports which can be used to instances together"""
@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,
@ -31,22 +42,22 @@ class Abstract(PortList):
self.name = name
self.ports = copy.deepcopy(ports)
def build(
self,
library: 'MutableLibrary',
tools: Union[None, 'Tool', MutableMapping[Optional[str], 'Tool']] = None,
) -> 'Builder':
"""
Begin building a new device around an instance of the current device
(rather than modifying the current device).
Returns:
The new `Builder` object.
"""
pat = Pattern(ports=self.ports)
pat.ref(self.name)
new = Builder(library=library, pattern=pat, tools=tools) # TODO should Abstract have tools?
return new
# def build(
# self,
# library: 'MutableLibrary',
# tools: Union[None, 'Tool', MutableMapping[Optional[str], 'Tool']] = None,
# ) -> 'Builder':
# """
# Begin building a new device around an instance of the current device
# (rather than modifying the current device).
#
# Returns:
# The new `Builder` object.
# """
# pat = Pattern(ports=self.ports)
# pat.ref(self.name)
# new = Builder(library=library, pattern=pat, tools=tools) # TODO should Abstract have tools?
# return new
# TODO do we want to store a Ref instead of just a name? then we can translate/rotate/mirror...
@ -56,3 +67,164 @@ class Abstract(PortList):
s += f'\n\t{name}: {port}'
s += ']>'
return s
def translate_ports(self: AA, offset: ArrayLike) -> AA:
"""
Translates all ports by the given offset.
Args:
offset: (x, y) to translate by
Returns:
self
"""
for port in self.ports.values():
port.translate(offset)
return self
def scale_by(self: AA, c: float) -> AA:
"""
Scale this Abstract by the given value
(all port offsets are scaled)
Args:
c: factor to scale by
Returns:
self
"""
for port in self.ports.values():
port.offset *= c
return self
def rotate_around(self: AA, pivot: ArrayLike, rotation: float) -> AA:
"""
Rotate the Abstract around the a location.
Args:
pivot: (x, y) location to rotate around
rotation: Angle to rotate by (counter-clockwise, radians)
Returns:
self
"""
pivot = numpy.array(pivot)
self.translate_ports(-pivot)
self.rotate_ports(rotation)
self.rotate_port_offsets(rotation)
self.translate_ports(+pivot)
return self
def rotate_port_offsets(self: AA, rotation: float) -> AA:
"""
Rotate the offsets of all ports around (0, 0)
Args:
rotation: Angle to rotate by (counter-clockwise, radians)
Returns:
self
"""
for port in self.ports.values():
port.offset = rotation_matrix_2d(rotation) @ port.offset
return self
def rotate_ports(self: AA, rotation: float) -> AA:
"""
Rotate each port around its offset (i.e. in place)
Args:
rotation: Angle to rotate by (counter-clockwise, radians)
Returns:
self
"""
for port in self.ports.values():
port.rotate(rotation)
return self
def mirror_port_offsets(self: AA, across_axis: int) -> AA:
"""
Mirror the offsets of all shapes, labels, and refs across an axis
Args:
across_axis: Axis to mirror across
(0: mirror across x axis, 1: mirror across y axis)
Returns:
self
"""
for port in self.ports.values():
port.offset[across_axis - 1] *= -1
return self
def mirror_ports(self: AA, across_axis: int) -> AA:
"""
Mirror each port's rotation across an axis, relative to its
offset
Args:
across_axis: Axis to mirror across
(0: mirror across x axis, 1: mirror across y axis)
Returns:
self
"""
for port in self.ports.values():
port.mirror(across_axis)
return self
def mirror(self: AA, across_axis: int) -> AA:
"""
Mirror the Pattern across an axis
Args:
axis: Axis to mirror across
(0: mirror across x axis, 1: mirror across y axis)
Returns:
self
"""
self.mirror_ports(across_axis)
self.mirror_port_offsets(across_axis)
return self
def apply_ref_transform(self: AA, ref: Ref) -> AA:
"""
Apply the transform from a `Ref` to the ports of this `Abstract`.
This changes the port locations to where they would be in the Ref's parent pattern.
Args:
ref: The ref whose transform should be applied.
Returns:
self
"""
mirror_across_x, angle = normalize_mirror(ref.mirrored)
if mirrored_across_x:
self.mirror(across_axis=0)
self.rotate_ports(angle + ref.rotation)
self.rotate_port_offsets(angle + ref.rotation)
self.translate_ports(ref.offset)
return self
def undo_ref_transform(self: AA, ref: Ref) -> AA:
"""
Apply the inverse transform from a `Ref` to the ports of this `Abstract`.
This changes the port locations to where they would be in the Ref's target (from the parent).
Args:
ref: The ref whose (inverse) transform should be applied.
Returns:
self
# TODO test undo_ref_transform
"""
mirror_across_x, angle = normalize_mirror(ref.mirrored)
self.translate_ports(-ref.offset)
self.rotate_port_offsets(-angle - ref.rotation)
self.rotate_ports(-angle - ref.rotation)
if mirrored_across_x:
self.mirror(across_axis=0)
return self

View file

@ -97,19 +97,30 @@ class Builder(PortList):
_dead: bool
""" If True, plug()/place() are skipped (for debugging)"""
@property
def ports(self) -> Dict[str, Port]:
return self.pattern.ports
@ports.setter
def ports(self, value: Dict[str, Port]) -> None:
self.pattern.ports = value
def __init__(
self,
library: MutableLibrary,
*,
pattern: Optional[Pattern] = None,
ports: Optional[Mapping[str, Port]] = None,
ports: Union[None, str, Mapping[str, Port]] = None,
tools: Union[None, Tool, MutableMapping[Optional[str], Tool]] = None,
) -> None:
"""
If `ports` is `None`, two default ports ('A' and 'B') are created.
Both are placed at (0, 0) and have default `ptype`, but 'A' has rotation 0
(attached devices will be placed to the left) and 'B' has rotation
pi (attached devices will be placed to the right).
# TODO documentation for Builder() constructor
# TODO MOVE THE BELOW DOCS to PortList
# If `ports` is `None`, two default ports ('A' and 'B') are created.
# Both are placed at (0, 0) and have default `ptype`, but 'A' has rotation 0
# (attached devices will be placed to the left) and 'B' has rotation
# pi (attached devices will be placed to the right).
"""
self.library = library
if pattern is not None:
@ -120,7 +131,10 @@ class Builder(PortList):
if ports is not None:
if self.pattern.ports:
raise BuildError('Ports supplied for pattern with pre-existing ports!')
self.pattern.ports.update(copy.deepcopy(ports))
if isinstance(ports, str):
ports = library.abstract(ports).ports
self.pattern.ports.update(copy.deepcopy(dict(ports)))
if tools is None:
self.tools = {}
@ -509,7 +523,7 @@ class Builder(PortList):
in_ptype = self.pattern[portspec].ptype
pat = tool.path(ccw, length, in_ptype=in_ptype, port_names=tool_port_names, **kwargs)
name = self.library.get_name(base_name)
self.library._set(name, pat)
self.library.set_const(name, pat)
return self.plug(Abstract(name, pat.ports), {portspec: tool_port_names[0]})
def path_to(
@ -592,7 +606,7 @@ class Builder(PortList):
for port_name, length in extensions.items():
bld.path(port_name, ccw, length, tool_port_names=tool_port_names)
name = self.library.get_name(base_name)
self.library._set(name, bld.pattern)
self.library.set_const(name, bld.pattern)
return self.plug(Abstract(name, bld.pattern.ports), {sp: 'in_' + sp for sp in ports.keys()}) # TODO safe to use 'in_'?
# TODO def path_join() and def bus_join()?

View file

@ -5,10 +5,11 @@ Functions for writing port data into a Pattern (`dev2pat`) and retrieving it (`p
the port locations. This particular approach is just a sensible default; feel free to
to write equivalent functions for your own format or alternate storage methods.
"""
from typing import Sequence, Optional, Mapping
from typing import Sequence, Optional, Mapping, Tuple, Dict
import logging
import numpy
from numpy.typing import NDArray
from ..pattern import Pattern
from ..label import Label
@ -50,13 +51,16 @@ def dev2pat(pattern: Pattern, layer: layer_t) -> Pattern:
def pat2dev(
library: Mapping[str, Pattern],
top: str,
layers: Sequence[layer_t],
library: Mapping[str, Pattern],
pattern: Pattern,
name: Optional[str] = None,
max_depth: int = 999_999,
skip_subcells: bool = True,
) -> Pattern:
"""
# TODO fixup documentation in port_utils
# TODO move port_utils to utils.file?
Examine `pattern` for labels specifying port info, and use that info
to fill out its `ports` attribute.
@ -64,8 +68,8 @@ def pat2dev(
'name:ptype angle_deg'
Args:
pattern: Pattern object to scan for labels.
layers: Search for labels on all the given layers.
pattern: Pattern object to scan for labels.
max_depth: Maximum hierarcy depth to search. Default 999_999.
Reduce this to 0 to avoid ever searching subcells.
skip_subcells: If port labels are found at a given hierarcy level,
@ -73,52 +77,51 @@ def pat2dev(
to contain their own port info without interfering with supercells'
port data.
Default True.
blacklist: If a cell name appears in the blacklist, do not ea
Returns:
The updated `pattern`. Port labels are not removed.
"""
print(f'TODO pat2dev {name}')
if pattern.ports:
logger.warning(f'Pattern {name if name else pattern} already had ports, skipping pat2dev')
return pattern
if not isinstance(library, Library):
library = WrapROLibrary(library)
ports = {}
annotated_cells = set()
pat2dev_flat(layers, pattern, name)
if (skip_subcells and pattern.ports) or max_depth == 0:
return pattern
def find_ports_each(pat, hierarchy, transform, memo) -> Pattern:
if len(hierarchy) > max_depth:
if max_depth >= 999_999:
logger.warning(f'pat2dev reached max depth ({max_depth})')
return pat
# Load ports for all subpatterns
for target in set(rr.target for rr in pat.refs):
pp = pat2dev(
layers=layers,
library=library,
pattern=library[target],
name=target,
max_depth=max_depth-1,
skip_subcells=skip_subcells,
blacklist=blacklist + {name},
)
found_ports |= bool(pp.ports)
if skip_subcells and any(parent in annotated_cells for parent in hierarchy):
return pat
for ref in pat.refs:
aa = library.abstract(ref.target)
if not aa.ports:
continue
cell_name = hierarchy[-1]
pat2dev_flat(pat, cell_name)
aa.apply_ref_transform(ref)
if skip_subcells:
annotated_cells.add(cell_name)
mirr_factor = numpy.array((1, -1)) ** transform[3]
rot_matrix = rotation_matrix_2d(transform[2])
for name, port in pat.ports.items():
port.offset = transform[:2] + rot_matrix @ (port.offset * mirr_factor)
port.rotation = port.rotation * mirr_factor[0] * mirr_factor[1] + transform[2]
ports[name] = port
return pat
# update `ports`
library.dfs(top=top, visit_before=find_ports_each, transform=True)
pattern = library[top]
pattern.check_ports(other_names=ports.keys())
pattern.ports.update(ports)
pattern.check_ports(other_names=aa.ports.keys())
pattern.ports.update(aa.ports)
return pattern
def pat2dev_flat(
pattern: Pattern,
layers: Sequence[layer_t],
pattern: Pattern,
cell_name: Optional[str] = None,
) -> Pattern:
"""
@ -131,8 +134,8 @@ def pat2dev_flat(
The pattern is assumed to be flat (have no `refs`) and have no pre-existing ports.
Args:
pattern: Pattern object to scan for labels.
layers: Search for labels on all the given layers.
pattern: Pattern object to scan for labels.
cell_name: optional, used for warning message only
Returns:

View file

@ -10,11 +10,11 @@ from ..utils import rotation_matrix_2d
from ..error import BuildError
if TYPE_CHECKING:
from ..ports import Port, PortList
from ..ports import Port
def ell(
ports: Union[Mapping[str, 'Port'], 'PortList'],
ports: Mapping[str, 'Port'],
ccw: Optional[bool],
bound_type: str,
bound: Union[float, ArrayLike],
@ -83,9 +83,6 @@ def ell(
if not ports:
raise BuildError('Empty port list passed to `ell()`')
if isinstance(ports, PortList):
ports = PortList.ports
if ccw is None:
if spacing is not None and not numpy.isclose(spacing, 0):
raise BuildError('Spacing must be 0 or None when ccw=None')

View file

@ -6,11 +6,8 @@ Notes:
* ezdxf sets creation time, write time, $VERSIONGUID, and $FINGERPRINTGUID
to unique values, so byte-for-byte reproducibility is not achievable for now
"""
from typing import List, Any, Dict, Tuple, Callable, Union, Iterable, Mapping, TextIO
import re
from typing import List, Any, Dict, Tuple, Callable, Union, Mapping, TextIO
import io
import base64
import struct
import logging
import pathlib
import gzip
@ -60,7 +57,7 @@ def write(
Other functions you may want to call:
- `masque.file.oasis.check_valid_names(library.keys())` to check for invalid names
- `library.dangling_references()` to check for references to missing patterns
- `library.dangling_refs()` to check for references to missing patterns
- `pattern.polygonize()` for any patterns with shapes other
than `masque.shapes.Polygon` or `masque.shapes.Path`

View file

@ -19,8 +19,8 @@ Notes:
* GDS creation/modification/access times are set to 1900-01-01 for reproducibility.
* Gzip modification time is set to 0 (start of current epoch, usually 1970-01-01)
"""
from typing import List, Any, Dict, Tuple, Callable, Union, Iterable
from typing import BinaryIO, Mapping, cast
from typing import List, Dict, Tuple, Callable, Union, Iterable, Mapping
from typing import BinaryIO, cast, Optional, Any
import io
import mmap
import logging
@ -39,7 +39,7 @@ from .. import Pattern, Ref, PatternError, LibraryError, Label, Shape
from ..shapes import Polygon, Path
from ..repetition import Grid
from ..utils import layer_t, normalize_mirror, annotations_t
from ..library import LazyLibrary, WrapLibrary, MutableLibrary
from ..library import LazyLibrary, WrapLibrary, MutableLibrary, Library
logger = logging.getLogger(__name__)
@ -84,7 +84,7 @@ def write(
Other functions you may want to call:
- `masque.file.gdsii.check_valid_names(library.keys())` to check for invalid names
- `library.dangling_references()` to check for references to missing patterns
- `library.dangling_refs()` to check for references to missing patterns
- `pattern.polygonize()` for any patterns with shapes other
than `masque.shapes.Polygon` or `masque.shapes.Path`
@ -188,6 +188,7 @@ def read(
raw_mode: bool = True,
) -> Tuple[Dict[str, Pattern], Dict[str, Any]]:
"""
# TODO check GDSII file for cycles!
Read a gdsii file and translate it into a dict of Pattern objects. GDSII structures are
translated into Pattern objects; boundaries are translated into polygons, and srefs and arefs
are translated into Ref objects.
@ -511,6 +512,7 @@ def load_library(
stream: BinaryIO,
*,
full_load: bool = False,
postprocess: Optional[Callable[[Library, str, Pattern], Pattern]] = None
) -> Tuple[LazyLibrary, Dict[str, Any]]:
"""
Scan a GDSII stream to determine what structures are present, and create
@ -526,6 +528,8 @@ def load_library(
full_load: If True, force all structures to be read immediately rather
than as-needed. Since data is read sequentially from the file, this
will be faster than using the resulting library's `precache` method.
postprocess: If given, this function is used to post-process each
pattern *upon first load only*.
Returns:
LazyLibrary object, allowing for deferred load of structures.
@ -548,9 +552,14 @@ def load_library(
for name_bytes, pos in structs.items():
name = name_bytes.decode('ASCII')
def mkstruct(pos: int = pos) -> Pattern:
def mkstruct(pos: int = pos, name: str = name) -> Pattern:
logger.error(f'mkstruct {name} @ {pos:x}')
stream.seek(pos)
return read_elements(stream, raw_mode=True)
pat = read_elements(stream, raw_mode=True)
if postprocess is not None:
pat = postprocess(lib, name, pat)
logger.error(f'mkstruct post {name} @ {pos:x}')
return pat
lib[name] = mkstruct
@ -562,6 +571,7 @@ def load_libraryfile(
*,
use_mmap: bool = True,
full_load: bool = False,
postprocess: Optional[Callable[[Library, str], Pattern]] = None
) -> Tuple[LazyLibrary, Dict[str, Any]]:
"""
Wrapper for `load_library()` that takes a filename or path instead of a stream.
@ -578,6 +588,7 @@ def load_libraryfile(
is decompressed into a python `bytes` object in memory
and reopened as an `io.BytesIO` stream.
full_load: If `True`, immediately loads all data. See `load_library`.
postprocess: Passed to `load_library`
Returns:
LazyLibrary object, allowing for deferred load of structures.
@ -599,7 +610,7 @@ def load_libraryfile(
stream = mmap.mmap(base_stream.fileno(), 0, access=mmap.ACCESS_READ) # type: ignore
else:
stream = open(path, mode='rb')
return load_library(stream, full_load=full_load)
return load_library(stream, full_load=full_load, postprocess=postprocess)
def check_valid_names(

View file

@ -78,7 +78,7 @@ def build(
Other functions you may want to call:
- `masque.file.oasis.check_valid_names(library.keys())` to check for invalid names
- `library.dangling_references()` to check for references to missing patterns
- `library.dangling_refs()` to check for references to missing patterns
- `pattern.polygonize()` for any patterns with shapes other
than `masque.shapes.Polygon`, `masque.shapes.Path`, or `masque.shapes.Circle`

View file

@ -63,7 +63,7 @@ class Library(Mapping[str, 'Pattern'], metaclass=ABCMeta):
def __repr__(self) -> str:
return '<Library with keys\n' + pformat(list(self.keys())) + '>'
def dangling_references(
def dangling_refs(
self,
tops: Union[None, str, Sequence[str]] = None,
) -> Set[Optional[str]]:
@ -304,11 +304,11 @@ class Library(Mapping[str, 'Pattern'], metaclass=ABCMeta):
def dfs(
self: L,
top: str,
pattern: 'Pattern',
visit_before: Optional[visitor_function_t] = None,
visit_after: Optional[visitor_function_t] = None,
*,
hierarchy: Tuple[str, ...] = (),
hierarchy: Tuple[Optional[str], ...] = (None,),
transform: Union[ArrayLike, bool, None] = False,
memo: Optional[Dict] = None,
) -> L:
@ -317,23 +317,23 @@ class Library(Mapping[str, 'Pattern'], metaclass=ABCMeta):
Performs a depth-first traversal of a pattern and its referenced patterns.
At each pattern in the tree, the following sequence is called:
```
hierarchy += (top,)
current_pattern = visit_before(current_pattern, **vist_args)
for sp in current_pattern.refs]
self.dfs(sp.target, visit_before, visit_after,
hierarchy, updated_transform, memo)
hierarchy + (sp.target,), updated_transform, memo)
current_pattern = visit_after(current_pattern, **visit_args)
```
where `visit_args` are
`hierarchy`: (top_pattern, L1_pattern, L2_pattern, ..., parent_pattern, current_pattern)
tuple of all parent-and-higher pattern names
`hierarchy`: (top_pattern_or_None, L1_pattern, L2_pattern, ..., parent_pattern)
tuple of all parent-and-higher pattern names. Top pattern name may be
`None` if not provided in first call to .dfs()
`transform`: numpy.ndarray containing cumulative
[x_offset, y_offset, rotation (rad), mirror_x (0 or 1)]
for the instance being visited
`memo`: Arbitrary dict (not altered except by `visit_before()` and `visit_after()`)
Args:
top: Name of the pattern to start at (root node of the tree).
pattern: Pattern object to start at ("top"/root node of the tree).
visit_before: Function to call before traversing refs.
Should accept a `Pattern` and `**visit_args`, and return the (possibly modified)
pattern. Default `None` (not called).
@ -345,8 +345,8 @@ class Library(Mapping[str, 'Pattern'], metaclass=ABCMeta):
`True` or `None` is interpreted as `[0, 0, 0, 0]`.
memo: Arbitrary dict for use by `visit_*()` functions. Default `None` (empty dict).
hierarchy: Tuple of patterns specifying the hierarchy above the current pattern.
Appended to the start of the generated `visit_args['hierarchy']`.
Default is an empty tuple.
Default is (None,), which will be used as a placeholder for the top pattern's
name if not overridden.
Returns:
self
@ -359,16 +359,12 @@ class Library(Mapping[str, 'Pattern'], metaclass=ABCMeta):
elif transform is not False:
transform = numpy.array(transform)
if top in hierarchy:
raise LibraryError('.dfs() called on pattern with circular reference')
original_pattern = pattern
hierarchy += (top,)
pat = self[top]
if visit_before is not None:
pat = visit_before(pat, hierarchy=hierarchy, memo=memo, transform=transform)
pattern = visit_before(pattern, hierarchy=hierarchy, memo=memo, transform=transform)
for ref in pat.refs:
for ref in pattern.refs:
if transform is not False:
sign = numpy.ones(2)
if transform[3]:
@ -376,32 +372,38 @@ class Library(Mapping[str, 'Pattern'], metaclass=ABCMeta):
xy = numpy.dot(rotation_matrix_2d(transform[2]), ref.offset * sign)
mirror_x, angle = normalize_mirror(ref.mirrored)
angle += ref.rotation
sp_transform = transform + (xy[0], xy[1], angle, mirror_x)
sp_transform[3] %= 2
ref_transform = transform + (xy[0], xy[1], angle, mirror_x)
ref_transform[3] %= 2
else:
sp_transform = False
ref_transform = False
if ref.target is None:
continue
if ref.target in hierarchy:
raise LibraryError(f'.dfs() called on pattern with circular reference to "{ref.target}"')
self.dfs(
top=ref.target,
pattern=self[ref.target],
visit_before=visit_before,
visit_after=visit_after,
transform=sp_transform,
hierarchy=hierarchy + (ref.target,),
transform=ref_transform,
memo=memo,
hierarchy=hierarchy,
)
if visit_after is not None:
pat = visit_after(pat, hierarchy=hierarchy, memo=memo, transform=transform)
pattern = visit_after(pattern, hierarchy=hierarchy, memo=memo, transform=transform)
if self[top] is not pat:
if isinstance(self, MutableLibrary):
self._set(top, pat)
else:
if pattern is not original_pattern:
name = hierarchy[-1]
if not isintance(self, MutableLibrary):
raise LibraryError('visit_* functions returned a new `Pattern` object'
' but the library is immutable')
if name is None:
raise LibraryError('visit_* functions returned a new `Pattern` object'
' but no top-level name was provided in `hierarchy`')
self.set_const(name, pattern)
return self
@ -424,7 +426,7 @@ class MutableLibrary(Library, Generic[VVV], metaclass=ABCMeta):
pass
@abstractmethod
def _set(self, key: str, value: 'Pattern') -> None:
def set_const(self, key: str, value: 'Pattern') -> None:
pass
@abstractmethod
@ -564,7 +566,7 @@ class MutableLibrary(Library, Generic[VVV], metaclass=ABCMeta):
del pat.shapes[i]
for ll, pp in shape_pats.items():
self._set(label2name(ll), pp)
self.set_const(label2name(ll), pp)
return self
@ -599,7 +601,7 @@ class MutableLibrary(Library, Generic[VVV], metaclass=ABCMeta):
continue
name = name_func(pat, shape)
self._set(name, Pattern(shapes=[shape]))
self.set_const(name, Pattern(shapes=[shape]))
pat.ref(name, repetition=shape.repetition)
shape.repetition = None
pat.shapes = new_shapes
@ -610,7 +612,7 @@ class MutableLibrary(Library, Generic[VVV], metaclass=ABCMeta):
new_labels.append(label)
continue
name = name_func(pat, label)
self._set(name, Pattern(labels=[label]))
self.set_const(name, Pattern(labels=[label]))
pat.ref(name, repetition=label.repetition)
label.repetition = None
pat.labels = new_labels
@ -692,7 +694,7 @@ class WrapLibrary(MutableLibrary):
def __delitem__(self, key: str) -> None:
del self.mapping[key]
def _set(self, key: str, value: 'Pattern') -> None:
def set_const(self, key: str, value: 'Pattern') -> None:
self[key] = value
def _merge(self, other: Mapping[str, 'Pattern'], key: str) -> None:
@ -744,7 +746,7 @@ class LazyLibrary(MutableLibrary):
def __len__(self) -> int:
return len(self.dict)
def _set(self, key: str, value: 'Pattern') -> None:
def set_const(self, key: str, value: 'Pattern') -> None:
self[key] = lambda: value
def _merge(self, other: Mapping[str, 'Pattern'], key: str) -> None:
@ -753,7 +755,7 @@ class LazyLibrary(MutableLibrary):
if key in other.cache:
self.cache[key] = other.cache[key]
else:
self._set(key, other[key])
self.set_const(key, other[key])
def __repr__(self) -> str:
return '<LazyLibrary with keys\n' + pformat(list(self.keys())) + '>'

View file

@ -30,9 +30,9 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
(via Ref). Shapes are assumed to inherit from masque.shapes.Shape or provide equivalent functions.
"""
__slots__ = (
'shapes', 'labels', 'refs', 'ports',
'shapes', 'labels', 'refs', '_ports',
# inherited
'_offset', '_annotations'
'_offset', '_annotations',
)
shapes: List[Shape]
@ -49,9 +49,17 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
(i.e. multiple instances of the same object).
"""
ports: Dict[str, Port]
_ports: Dict[str, Port]
""" Uniquely-named ports which can be used to snap to other Pattern instances"""
@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,
*,
@ -331,7 +339,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
def translate_elements(self: P, offset: ArrayLike) -> P:
"""
Translates all shapes, label, and refs by the given offset.
Translates all shapes, label, refs, and ports by the given offset.
Args:
offset: (x, y) to translate by
@ -339,7 +347,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
Returns:
self
"""
for entry in chain(self.shapes, self.refs, self.labels, self.ports):
for entry in chain(self.shapes, self.refs, self.labels, self.ports.values()):
cast(Positionable, entry).translate(offset)
return self
@ -360,7 +368,8 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
def scale_by(self: P, c: float) -> P:
"""
Scale this Pattern by the given value
(all shapes and refs and their offsets are scaled)
(all shapes and refs and their offsets are scaled,
as are all label and port offsets)
Args:
c: factor to scale by
@ -407,7 +416,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
def rotate_element_centers(self: P, rotation: float) -> P:
"""
Rotate the offsets of all shapes, labels, and refs around (0, 0)
Rotate the offsets of all shapes, labels, refs, and ports around (0, 0)
Args:
rotation: Angle to rotate by (counter-clockwise, radians)
@ -415,14 +424,14 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
Returns:
self
"""
for entry in chain(self.shapes, self.refs, self.labels, self.ports):
for entry in chain(self.shapes, self.refs, self.labels, self.ports.values()):
old_offset = cast(Positionable, entry).offset
cast(Positionable, entry).offset = numpy.dot(rotation_matrix_2d(rotation), old_offset)
return self
def rotate_elements(self: P, rotation: float) -> P:
"""
Rotate each shape and refs around its center (offset)
Rotate each shape, ref, and port around its origin (offset)
Args:
rotation: Angle to rotate by (counter-clockwise, radians)
@ -430,54 +439,54 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
Returns:
self
"""
for entry in chain(self.shapes, self.refs):
for entry in chain(self.shapes, self.refs, self.ports.values()):
cast(Rotatable, entry).rotate(rotation)
return self
def mirror_element_centers(self: P, axis: int) -> P:
def mirror_element_centers(self: P, across_axis: int) -> P:
"""
Mirror the offsets of all shapes, labels, and refs across an axis
Args:
axis: Axis to mirror across
across_axis: Axis to mirror across
(0: mirror across x axis, 1: mirror across y axis)
Returns:
self
"""
for entry in chain(self.shapes, self.refs, self.labels, self.ports):
cast(Positionable, entry).offset[axis - 1] *= -1
for entry in chain(self.shapes, self.refs, self.labels, self.ports.values()):
cast(Positionable, entry).offset[across_axis - 1] *= -1
return self
def mirror_elements(self: P, axis: int) -> P:
def mirror_elements(self: P, across_axis: int) -> P:
"""
Mirror each shape and refs across an axis, relative to its
offset
Mirror each shape, ref, and pattern across an axis, relative
to its offset
Args:
axis: Axis to mirror across
across_axis: Axis to mirror across
(0: mirror across x axis, 1: mirror across y axis)
Returns:
self
"""
for entry in chain(self.shapes, self.refs):
cast(Mirrorable, entry).mirror(axis)
for entry in chain(self.shapes, self.refs, self.ports.values()):
cast(Mirrorable, entry).mirror(across_axis)
return self
def mirror(self: P, axis: int) -> P:
def mirror(self: P, across_axis: int) -> P:
"""
Mirror the Pattern across an axis
Args:
axis: Axis to mirror across
across_axis: Axis to mirror across
(0: mirror across x axis, 1: mirror across y axis)
Returns:
self
"""
self.mirror_elements(axis)
self.mirror_element_centers(axis)
self.mirror_elements(across_axis)
self.mirror_element_centers(across_axis)
return self
def copy(self: P) -> P:

View file

@ -4,7 +4,7 @@ import warnings
import traceback
import logging
from collections import Counter
from abc import ABCMeta
from abc import ABCMeta, abstractmethod
import numpy
from numpy import pi
@ -103,10 +103,18 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable, met
class PortList(metaclass=ABCMeta):
__slots__ = () # For use with AutoSlots
__slots__ = () # Allow subclasses to use __slots__
ports: Dict[str, Port]
""" Uniquely-named ports which can be used to snap to other Device instances"""
@property
@abstractmethod
def ports(self) -> Dict[str, Port]:
""" Uniquely-named ports which can be used to snap to other Device instances"""
pass
@ports.setter
@abstractmethod
def ports(self, value: Dict[str, Port]) -> None:
pass
@overload
def __getitem__(self, key: str) -> Port: