wip
parent
d9ae8dd6e3
commit
9efb6f0eeb
@ -1,280 +0,0 @@
|
|||||||
"""
|
|
||||||
DeviceLibrary class for managing unique name->device mappings and
|
|
||||||
deferred loading or creation.
|
|
||||||
"""
|
|
||||||
from typing import Dict, Callable, TypeVar, TYPE_CHECKING
|
|
||||||
from typing import Any, Tuple, Union, Iterator, Mapping
|
|
||||||
import logging
|
|
||||||
from pprint import pformat
|
|
||||||
from abc import ABCMeta, abstractmethod
|
|
||||||
|
|
||||||
from ..error import DeviceLibraryError
|
|
||||||
from ..library import Library, LazyLibrary
|
|
||||||
from ..builder import Device, DeviceRef
|
|
||||||
from .. import Pattern
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
DL = TypeVar('DL', bound='LazyDeviceLibrary')
|
|
||||||
DL2 = TypeVar('DL2', bound='LazyDeviceLibrary')
|
|
||||||
LDL = TypeVar('LDL', bound='LibDeviceLibrary')
|
|
||||||
|
|
||||||
|
|
||||||
class LazyDeviceLibrary(Mapping[str, DeviceRef]):
|
|
||||||
"""
|
|
||||||
This class maps names to functions which generate or load the
|
|
||||||
relevant `Device` object.
|
|
||||||
|
|
||||||
This class largely functions the same way as `Library`, but
|
|
||||||
operates on `Device`s rather than `Patterns` and thus has no
|
|
||||||
need for distinctions between primary/secondary devices (as
|
|
||||||
there is no inter-`Device` hierarchy).
|
|
||||||
|
|
||||||
Each device is cached the first time it is used. The cache can
|
|
||||||
be disabled by setting the `enable_cache` attribute to `False`.
|
|
||||||
"""
|
|
||||||
generators: Dict[str, Callable[[], Device]]
|
|
||||||
cache: Dict[Union[str, Tuple[str, str]], Device]
|
|
||||||
enable_cache: bool = True
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.generators = {}
|
|
||||||
self.cache = {}
|
|
||||||
|
|
||||||
def __setitem__(self, key: str, value: Callable[[], Device]) -> None:
|
|
||||||
self.generators[key] = value
|
|
||||||
if key in self.cache:
|
|
||||||
del self.cache[key]
|
|
||||||
|
|
||||||
def __delitem__(self, key: str) -> None:
|
|
||||||
del self.generators[key]
|
|
||||||
if key in self.cache:
|
|
||||||
del self.cache[key]
|
|
||||||
|
|
||||||
def __getitem__(self, key: str) -> DeviceRef:
|
|
||||||
dev = self.get_device(key)
|
|
||||||
return DeviceRef(name=key, ports=dev.ports)
|
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[str]:
|
|
||||||
return iter(self.keys())
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return '<LazyDeviceLibrary with keys ' + repr(list(self.keys())) + '>'
|
|
||||||
|
|
||||||
def get_device(self, key: str) -> Device:
|
|
||||||
if self.enable_cache and key in self.cache:
|
|
||||||
logger.debug(f'found {key} in cache')
|
|
||||||
dev = self.cache[key]
|
|
||||||
return dev
|
|
||||||
|
|
||||||
logger.debug(f'loading {key}')
|
|
||||||
dev = self.generators[key]()
|
|
||||||
self.cache[key] = dev
|
|
||||||
return dev
|
|
||||||
|
|
||||||
def clear_cache(self: LDL) -> LDL:
|
|
||||||
"""
|
|
||||||
Clear the cache of this library.
|
|
||||||
This is usually used before modifying or deleting cells, e.g. when merging
|
|
||||||
with another library.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
self.cache.clear()
|
|
||||||
return self
|
|
||||||
|
|
||||||
def add_device(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
fn: Callable[[], Device],
|
|
||||||
dev2pat: Callable[[Device], Pattern],
|
|
||||||
prefix: str = '',
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Convenience function for adding a device to the library.
|
|
||||||
|
|
||||||
- The device is generated with the provided `fn()`
|
|
||||||
- Port info is written to the pattern using the provied dev2pat
|
|
||||||
- The pattern is renamed to match the provided `prefix + name`
|
|
||||||
- If `prefix` is non-empty, a wrapped copy is also added, named
|
|
||||||
`name` (no prefix). See `wrap_device()` for details.
|
|
||||||
|
|
||||||
Adding devices with this function helps to
|
|
||||||
- Make sure Pattern names are reflective of what the devices are named
|
|
||||||
- Ensure port info is written into the `Pattern`, so that the `Device`
|
|
||||||
can be reconstituted from the layout.
|
|
||||||
- Simplify adding a prefix to all device names, to make it easier to
|
|
||||||
track their provenance and purpose, while also allowing for
|
|
||||||
generic device names which can later be swapped out with different
|
|
||||||
underlying implementations.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: Base name for the device. If a prefix is used, this is the
|
|
||||||
"generic" name (e.g. "L3_cavity" vs "2022_02_02_L3_cavity").
|
|
||||||
fn: Function which is called to generate the device.
|
|
||||||
dev2pat: Post-processing function which is called to add the port
|
|
||||||
info into the device's pattern.
|
|
||||||
prefix: If present, the actual device is named `prefix + name`, and
|
|
||||||
a second device with name `name` is also added (containing only
|
|
||||||
this one).
|
|
||||||
"""
|
|
||||||
def build_dev() -> Device:
|
|
||||||
dev = fn()
|
|
||||||
dev.pattern = dev2pat(dev)
|
|
||||||
return dev
|
|
||||||
|
|
||||||
self[prefix + name] = build_dev
|
|
||||||
if prefix:
|
|
||||||
self.wrap_device(name, prefix + name)
|
|
||||||
|
|
||||||
def wrap_device(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
old_name: str,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Create a new device which simply contains an instance of an already-existing device.
|
|
||||||
|
|
||||||
This is useful for assigning an alternate name to a device, while still keeping
|
|
||||||
the original name available for traceability.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: Name for the wrapped device.
|
|
||||||
old_name: Name of the existing device to wrap.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def build_wrapped_dev() -> Device:
|
|
||||||
old_dev = self[old_name]
|
|
||||||
wrapper = Pattern()
|
|
||||||
wrapper.addsp(old_name)
|
|
||||||
return Device(wrapper, old_dev.ports)
|
|
||||||
|
|
||||||
self[name] = build_wrapped_dev
|
|
||||||
|
|
||||||
def add(
|
|
||||||
self: DL,
|
|
||||||
other: DL2,
|
|
||||||
use_ours: Callable[[str], bool] = lambda name: False,
|
|
||||||
use_theirs: Callable[[str], bool] = lambda name: False,
|
|
||||||
) -> DL:
|
|
||||||
"""
|
|
||||||
Add keys from another library into this one.
|
|
||||||
|
|
||||||
There must be no conflicting keys.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
other: The library to insert keys from
|
|
||||||
use_ours: Decision function for name conflicts. Will be called with duplicate cell names.
|
|
||||||
Should return `True` if the value from `self` should be used.
|
|
||||||
use_theirs: Decision function for name conflicts. Same format as `use_ours`.
|
|
||||||
Should return `True` if the value from `other` should be used.
|
|
||||||
`use_ours` takes priority over `use_theirs`.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
duplicates = set(self.keys()) & set(other.keys())
|
|
||||||
keep_ours = set(name for name in duplicates if use_ours(name))
|
|
||||||
keep_theirs = set(name for name in duplicates - keep_ours if use_theirs(name))
|
|
||||||
conflicts = duplicates - keep_ours - keep_theirs
|
|
||||||
if conflicts:
|
|
||||||
raise DeviceLibraryError('Duplicate keys encountered in DeviceLibrary merge: '
|
|
||||||
+ pformat(conflicts))
|
|
||||||
|
|
||||||
for name in set(other.keys()) - keep_ours:
|
|
||||||
self.generators[name] = other.generators[name]
|
|
||||||
if name in other.cache:
|
|
||||||
self.cache[name] = other.cache[name]
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
class LibDeviceLibrary(LazyDeviceLibrary):
|
|
||||||
"""
|
|
||||||
Extends `LazyDeviceLibrary`, enabling it to ingest `Library` objects
|
|
||||||
(e.g. obtained by loading a GDS file).
|
|
||||||
|
|
||||||
Each `Library` object must be accompanied by a `pat2dev` function,
|
|
||||||
which takes in the `Pattern` and returns a full `Device` (including
|
|
||||||
port info). This is usually accomplished by scanning the `Pattern` for
|
|
||||||
port-related geometry, but could also bake in external info.
|
|
||||||
|
|
||||||
`Library` objects are ingested into `underlying`, which is a
|
|
||||||
`Library` which is kept in sync with the `DeviceLibrary` when
|
|
||||||
devices are removed (or new libraries added via `add_library()`).
|
|
||||||
"""
|
|
||||||
underlying: LazyLibrary
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
LazyDeviceLibrary.__init__(self)
|
|
||||||
self.underlying = LazyLibrary()
|
|
||||||
|
|
||||||
def __setitem__(self, key: str, value: Callable[[], Device]) -> None:
|
|
||||||
self.generators[key] = value
|
|
||||||
if key in self.cache:
|
|
||||||
del self.cache[key]
|
|
||||||
|
|
||||||
# If any `Library` that has been (or will be) added has an entry for `key`,
|
|
||||||
# it will be added to `self.underlying` and then returned by it during subpattern
|
|
||||||
# resolution for other entries, and will conflict with the name for our
|
|
||||||
# wrapped device. To avoid that, we need to set ourselves as the "true" source of
|
|
||||||
# the `Pattern` named `key`.
|
|
||||||
if key in self.underlying:
|
|
||||||
raise DeviceLibraryError(f'Device name {key} already exists in underlying Library!')
|
|
||||||
|
|
||||||
# NOTE that this means the `Device` may be cached without the `Pattern` being in
|
|
||||||
# the `underlying` cache yet!
|
|
||||||
self.underlying[key] = lambda: self.get_device(key).pattern
|
|
||||||
|
|
||||||
def __delitem__(self, key: str) -> None:
|
|
||||||
LazyDeviceLibrary.__delitem__(self, key)
|
|
||||||
if key in self.underlying:
|
|
||||||
del self.underlying[key]
|
|
||||||
|
|
||||||
def add_library(
|
|
||||||
self: LDL,
|
|
||||||
lib: Mapping[str, Pattern],
|
|
||||||
pat2dev: Callable[[Pattern], Device],
|
|
||||||
use_ours: Callable[[Union[str, Tuple[str, str]]], bool] = lambda name: False,
|
|
||||||
use_theirs: Callable[[Union[str, Tuple[str, str]]], bool] = lambda name: False,
|
|
||||||
) -> LDL:
|
|
||||||
"""
|
|
||||||
Add a pattern `Library` into this `LibDeviceLibrary`.
|
|
||||||
|
|
||||||
This requires a `pat2dev` function which can transform each `Pattern`
|
|
||||||
into a `Device`. For example, this can be accomplished by scanning
|
|
||||||
the `Pattern` data for port location info or by looking up port info
|
|
||||||
based on the pattern name or other characteristics in a hardcoded or
|
|
||||||
user-supplied dictionary.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
lib: Pattern library to add.
|
|
||||||
pat2dev: Function for transforming each `Pattern` object from `lib`
|
|
||||||
into a `Device` which will be returned by this device library.
|
|
||||||
use_ours: Decision function for name conflicts. Will be called with
|
|
||||||
duplicate cell names, and (name, tag) tuples from the underlying library.
|
|
||||||
Should return `True` if the value from `self` should be used.
|
|
||||||
use_theirs: Decision function for name conflicts. Same format as `use_ours`.
|
|
||||||
Should return `True` if the value from `other` should be used.
|
|
||||||
`use_ours` takes priority over `use_theirs`.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
duplicates = set(lib.keys()) & set(self.keys())
|
|
||||||
keep_ours = set(name for name in duplicates if use_ours(name))
|
|
||||||
keep_theirs = set(name for name in duplicates - keep_ours if use_theirs(name))
|
|
||||||
bad_duplicates = duplicates - keep_ours - keep_theirs
|
|
||||||
if bad_duplicates:
|
|
||||||
raise DeviceLibraryError('Duplicate devices (no action specified): ' + pformat(bad_duplicates))
|
|
||||||
|
|
||||||
self.underlying.add(lib, use_ours, use_theirs)
|
|
||||||
|
|
||||||
for name in lib:
|
|
||||||
def gen(name=name):
|
|
||||||
return pat2dev(self.underlying[name])
|
|
||||||
|
|
||||||
self.generators[name] = gen
|
|
||||||
return self
|
|
@ -1,2 +0,0 @@
|
|||||||
# FOr backwards compatibility
|
|
||||||
from .gdsii import *
|
|
@ -1,560 +0,0 @@
|
|||||||
"""
|
|
||||||
GDSII file format readers and writers using python-gdsii
|
|
||||||
|
|
||||||
Note that GDSII references follow the same convention as `masque`,
|
|
||||||
with this order of operations:
|
|
||||||
1. Mirroring
|
|
||||||
2. Rotation
|
|
||||||
3. Scaling
|
|
||||||
4. Offset and array expansion (no mirroring/rotation/scaling applied to offsets)
|
|
||||||
|
|
||||||
Scaling, rotation, and mirroring apply to individual instances, not grid
|
|
||||||
vectors or offsets.
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
* absolute positioning is not supported
|
|
||||||
* PLEX is not supported
|
|
||||||
* ELFLAGS are not supported
|
|
||||||
* GDS does not support library- or structure-level annotations
|
|
||||||
"""
|
|
||||||
from typing import List, Any, Dict, Tuple, Callable, Union, Iterable, Optional
|
|
||||||
from typing import Sequence, Mapping
|
|
||||||
import re
|
|
||||||
import io
|
|
||||||
import copy
|
|
||||||
import base64
|
|
||||||
import struct
|
|
||||||
import logging
|
|
||||||
import pathlib
|
|
||||||
import gzip
|
|
||||||
|
|
||||||
import numpy
|
|
||||||
from numpy.typing import NDArray, ArrayLike
|
|
||||||
# python-gdsii
|
|
||||||
import gdsii.library #type: ignore
|
|
||||||
import gdsii.structure #type: ignore
|
|
||||||
import gdsii.elements #type: ignore
|
|
||||||
|
|
||||||
from .utils import clean_pattern_vertices, is_gzipped
|
|
||||||
from .. import Pattern, SubPattern, PatternError, Label, Shape
|
|
||||||
from ..shapes import Polygon, Path
|
|
||||||
from ..repetition import Grid
|
|
||||||
from ..utils import get_bit, set_bit, layer_t, normalize_mirror, annotations_t
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
path_cap_map = {
|
|
||||||
None: Path.Cap.Flush,
|
|
||||||
0: Path.Cap.Flush,
|
|
||||||
1: Path.Cap.Circle,
|
|
||||||
2: Path.Cap.Square,
|
|
||||||
4: Path.Cap.SquareCustom,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def rint_cast(val: ArrayLike) -> NDArray[numpy.int32]:
|
|
||||||
return numpy.rint(val, dtype=numpy.int32, casting='unsafe')
|
|
||||||
|
|
||||||
|
|
||||||
def build(
|
|
||||||
library: Mapping[str, Pattern],
|
|
||||||
meters_per_unit: float,
|
|
||||||
logical_units_per_unit: float = 1,
|
|
||||||
library_name: str = 'masque-gdsii-write',
|
|
||||||
*,
|
|
||||||
modify_originals: bool = False,
|
|
||||||
) -> gdsii.library.Library:
|
|
||||||
"""
|
|
||||||
Convert a `Pattern` or list of patterns to a GDSII stream, by first calling
|
|
||||||
`.polygonize()` to change the shapes into polygons, and then writing patterns
|
|
||||||
as GDSII structures, polygons as boundary elements, and subpatterns as structure
|
|
||||||
references (sref).
|
|
||||||
|
|
||||||
For each shape,
|
|
||||||
layer is chosen to be equal to `shape.layer` if it is an int,
|
|
||||||
or `shape.layer[0]` if it is a tuple
|
|
||||||
datatype is chosen to be `shape.layer[1]` if available,
|
|
||||||
otherwise `0`
|
|
||||||
|
|
||||||
It is often a good idea to run `pattern.subpatternize()` prior to calling this function,
|
|
||||||
especially if calling `.polygonize()` will result in very many vertices.
|
|
||||||
|
|
||||||
If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()`
|
|
||||||
prior to calling this function.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
library: A {name: Pattern} mapping of patterns to write.
|
|
||||||
meters_per_unit: Written into the GDSII file, meters per (database) length unit.
|
|
||||||
All distances are assumed to be an integer multiple of this unit, and are stored as such.
|
|
||||||
logical_units_per_unit: Written into the GDSII file. Allows the GDSII to specify a
|
|
||||||
"logical" unit which is different from the "database" unit, for display purposes.
|
|
||||||
Default `1`.
|
|
||||||
library_name: Library name written into the GDSII file.
|
|
||||||
Default 'masque-gdsii-write'.
|
|
||||||
modify_originals: If `True`, the original pattern is modified as part of the writing
|
|
||||||
process. Otherwise, a copy is made.
|
|
||||||
Default `False`.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
`gdsii.library.Library`
|
|
||||||
"""
|
|
||||||
# TODO check name errors
|
|
||||||
bad_keys = check_valid_names(library.keys())
|
|
||||||
|
|
||||||
# TODO check all hierarchy present
|
|
||||||
|
|
||||||
|
|
||||||
if not modify_originals:
|
|
||||||
library = library.deepcopy() #TODO figure out best approach e.g. if lazy
|
|
||||||
|
|
||||||
library.wrap_repeated_shapes()
|
|
||||||
|
|
||||||
old_names = list(library.keys())
|
|
||||||
new_names = disambiguate_func(old_names)
|
|
||||||
renamed_lib = {new_name: library[old_name]
|
|
||||||
for old_name, new_name in zip(old_names, new_names)}
|
|
||||||
|
|
||||||
# Create library
|
|
||||||
lib = gdsii.library.Library(version=600,
|
|
||||||
name=library_name.encode('ASCII'),
|
|
||||||
logical_unit=logical_units_per_unit,
|
|
||||||
physical_unit=meters_per_unit)
|
|
||||||
|
|
||||||
# Now create a structure for each pattern, and add in any Boundary and SREF elements
|
|
||||||
for name, pat in renamed_lib.items():
|
|
||||||
structure = gdsii.structure.Structure(name=name.encode('ASCII'))
|
|
||||||
lib.append(structure)
|
|
||||||
|
|
||||||
structure += _shapes_to_elements(pat.shapes)
|
|
||||||
structure += _labels_to_texts(pat.labels)
|
|
||||||
structure += _subpatterns_to_refs(pat.subpatterns)
|
|
||||||
|
|
||||||
return lib
|
|
||||||
|
|
||||||
|
|
||||||
def write(
|
|
||||||
library: Mapping[str, Pattern],
|
|
||||||
stream: io.BufferedIOBase,
|
|
||||||
*args,
|
|
||||||
**kwargs,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Write a `Pattern` or list of patterns to a GDSII file.
|
|
||||||
See `masque.file.gdsii.build()` for details.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
library: A {name: Pattern} mapping of patterns to write.
|
|
||||||
stream: Stream to write to.
|
|
||||||
*args: passed to `masque.file.gdsii.build()`
|
|
||||||
**kwargs: passed to `masque.file.gdsii.build()`
|
|
||||||
"""
|
|
||||||
lib = build(library, *args, **kwargs)
|
|
||||||
lib.save(stream)
|
|
||||||
return
|
|
||||||
|
|
||||||
def writefile(
|
|
||||||
library: Mapping[str, Pattern],
|
|
||||||
filename: Union[str, pathlib.Path],
|
|
||||||
*args,
|
|
||||||
**kwargs,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Wrapper for `write()` that takes a filename or path instead of a stream.
|
|
||||||
|
|
||||||
Will automatically compress the file if it has a .gz suffix.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
library: {name: Pattern} pairs to save.
|
|
||||||
filename: Filename to save to.
|
|
||||||
*args: passed to `write()`
|
|
||||||
**kwargs: passed to `write()`
|
|
||||||
"""
|
|
||||||
path = pathlib.Path(filename)
|
|
||||||
if path.suffix == '.gz':
|
|
||||||
open_func: Callable = gzip.open
|
|
||||||
else:
|
|
||||||
open_func = open
|
|
||||||
|
|
||||||
with io.BufferedWriter(open_func(path, mode='wb')) as stream:
|
|
||||||
write(library, stream, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def readfile(
|
|
||||||
filename: Union[str, pathlib.Path],
|
|
||||||
*args,
|
|
||||||
**kwargs,
|
|
||||||
) -> Tuple[Dict[str, Pattern], Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Wrapper for `read()` that takes a filename or path instead of a stream.
|
|
||||||
|
|
||||||
Will automatically decompress gzipped files.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filename: Filename to save to.
|
|
||||||
*args: passed to `read()`
|
|
||||||
**kwargs: passed to `read()`
|
|
||||||
"""
|
|
||||||
path = pathlib.Path(filename)
|
|
||||||
if is_gzipped(path):
|
|
||||||
open_func: Callable = gzip.open
|
|
||||||
else:
|
|
||||||
open_func = open
|
|
||||||
|
|
||||||
with io.BufferedReader(open_func(path, mode='rb')) as stream:
|
|
||||||
results = read(stream, *args, **kwargs)
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def read(
|
|
||||||
stream: io.BufferedIOBase,
|
|
||||||
clean_vertices: bool = True,
|
|
||||||
) -> Tuple[Dict[str, Pattern], Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
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 SubPattern objects.
|
|
||||||
|
|
||||||
Additional library info is returned in a dict, containing:
|
|
||||||
'name': name of the library
|
|
||||||
'meters_per_unit': number of meters per database unit (all values are in database units)
|
|
||||||
'logical_units_per_unit': number of "logical" units displayed by layout tools (typically microns)
|
|
||||||
per database unit
|
|
||||||
|
|
||||||
Args:
|
|
||||||
stream: Stream to read from.
|
|
||||||
clean_vertices: If `True`, remove any redundant vertices when loading polygons.
|
|
||||||
The cleaning process removes any polygons with zero area or <3 vertices.
|
|
||||||
Default `True`.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
- Dict of pattern_name:Patterns generated from GDSII structures
|
|
||||||
- Dict of GDSII library info
|
|
||||||
"""
|
|
||||||
|
|
||||||
lib = gdsii.library.Library.load(stream)
|
|
||||||
|
|
||||||
library_info = {'name': lib.name.decode('ASCII'),
|
|
||||||
'meters_per_unit': lib.physical_unit,
|
|
||||||
'logical_units_per_unit': lib.logical_unit,
|
|
||||||
}
|
|
||||||
|
|
||||||
raw_mode = True # Whether to construct shapes in raw mode (less error checking)
|
|
||||||
|
|
||||||
patterns_dict = {}
|
|
||||||
for structure in lib:
|
|
||||||
pat = Pattern()
|
|
||||||
name = structure.name.decode('ASCII')
|
|
||||||
for element in structure:
|
|
||||||
# Switch based on element type:
|
|
||||||
if isinstance(element, gdsii.elements.Boundary):
|
|
||||||
poly = _boundary_to_polygon(element, raw_mode)
|
|
||||||
pat.shapes.append(poly)
|
|
||||||
|
|
||||||
if isinstance(element, gdsii.elements.Path):
|
|
||||||
path = _gpath_to_mpath(element, raw_mode)
|
|
||||||
pat.shapes.append(path)
|
|
||||||
|
|
||||||
elif isinstance(element, gdsii.elements.Text):
|
|
||||||
label = Label(
|
|
||||||
offset=element.xy.astype(float),
|
|
||||||
layer=(element.layer, element.text_type),
|
|
||||||
string=element.string.decode('ASCII'),
|
|
||||||
)
|
|
||||||
pat.labels.append(label)
|
|
||||||
|
|
||||||
elif isinstance(element, (gdsii.elements.SRef, gdsii.elements.ARef)):
|
|
||||||
pat.subpatterns.append(_ref_to_subpat(element))
|
|
||||||
|
|
||||||
if clean_vertices:
|
|
||||||
clean_pattern_vertices(pat)
|
|
||||||
patterns_dict[name] = pat
|
|
||||||
|
|
||||||
return patterns_dict, library_info
|
|
||||||
|
|
||||||
|
|
||||||
def _mlayer2gds(mlayer: layer_t) -> Tuple[int, int]:
|
|
||||||
""" Helper to turn a layer tuple-or-int into a layer and datatype"""
|
|
||||||
if isinstance(mlayer, int):
|
|
||||||
layer = mlayer
|
|
||||||
data_type = 0
|
|
||||||
elif isinstance(mlayer, tuple):
|
|
||||||
layer = mlayer[0]
|
|
||||||
if len(mlayer) > 1:
|
|
||||||
data_type = mlayer[1]
|
|
||||||
else:
|
|
||||||
data_type = 0
|
|
||||||
else:
|
|
||||||
raise PatternError(f'Invalid layer for gdsii: {mlayer}. Note that gdsii layers cannot be strings.')
|
|
||||||
return layer, data_type
|
|
||||||
|
|
||||||
|
|
||||||
def _ref_to_subpat(
|
|
||||||
element: Union[gdsii.elements.SRef,
|
|
||||||
gdsii.elements.ARef]
|
|
||||||
) -> SubPattern:
|
|
||||||
"""
|
|
||||||
Helper function to create a SubPattern from an SREF or AREF. Sets `subpat.target` to `element.struct_name`.
|
|
||||||
|
|
||||||
NOTE: "Absolute" means not affected by parent elements.
|
|
||||||
That's not currently supported by masque at all (and not planned).
|
|
||||||
"""
|
|
||||||
rotation = 0.0
|
|
||||||
offset = numpy.array(element.xy[0], dtype=float)
|
|
||||||
scale = 1.0
|
|
||||||
mirror_across_x = False
|
|
||||||
repetition = None
|
|
||||||
|
|
||||||
if element.strans is not None:
|
|
||||||
if element.mag is not None:
|
|
||||||
scale = element.mag
|
|
||||||
# Bit 13 means absolute scale
|
|
||||||
if get_bit(element.strans, 15 - 13):
|
|
||||||
raise PatternError('Absolute scale is not implemented in masque!')
|
|
||||||
if element.angle is not None:
|
|
||||||
rotation = numpy.deg2rad(element.angle)
|
|
||||||
# Bit 14 means absolute rotation
|
|
||||||
if get_bit(element.strans, 15 - 14):
|
|
||||||
raise PatternError('Absolute rotation is not implemented in masque!')
|
|
||||||
# Bit 0 means mirror x-axis
|
|
||||||
if get_bit(element.strans, 15 - 0):
|
|
||||||
mirror_across_x = True
|
|
||||||
|
|
||||||
if isinstance(element, gdsii.elements.ARef):
|
|
||||||
a_count = element.cols
|
|
||||||
b_count = element.rows
|
|
||||||
a_vector = (element.xy[1] - offset) / a_count
|
|
||||||
b_vector = (element.xy[2] - offset) / b_count
|
|
||||||
repetition = Grid(a_vector=a_vector, b_vector=b_vector,
|
|
||||||
a_count=a_count, b_count=b_count)
|
|
||||||
|
|
||||||
subpat = SubPattern(
|
|
||||||
target=element.struct_name,
|
|
||||||
offset=offset,
|
|
||||||
rotation=rotation,
|
|
||||||
scale=scale,
|
|
||||||
mirrored=(mirror_across_x, False),
|
|
||||||
annotations=_properties_to_annotations(element.properties),
|
|
||||||
repetition=repetition,
|
|
||||||
)
|
|
||||||
return subpat
|
|
||||||
|
|
||||||
|
|
||||||
def _gpath_to_mpath(element: gdsii.elements.Path, raw_mode: bool) -> Path:
|
|
||||||
if element.path_type in path_cap_map:
|
|
||||||
cap = path_cap_map[element.path_type]
|
|
||||||
else:
|
|
||||||
raise PatternError(f'Unrecognized path type: {element.path_type}')
|
|
||||||
|
|
||||||
args = {
|
|
||||||
'vertices': element.xy.astype(float),
|
|
||||||
'layer': (element.layer, element.data_type),
|
|
||||||
'width': element.width if element.width is not None else 0.0,
|
|
||||||
'cap': cap,
|
|
||||||
'offset': numpy.zeros(2),
|
|
||||||
'annotations': _properties_to_annotations(element.properties),
|
|
||||||
'raw': raw_mode,
|
|
||||||
}
|
|
||||||
|
|
||||||
if cap == Path.Cap.SquareCustom:
|
|
||||||
args['cap_extensions'] = numpy.zeros(2)
|
|
||||||
if element.bgn_extn is not None:
|
|
||||||
args['cap_extensions'][0] = element.bgn_extn
|
|
||||||
if element.end_extn is not None:
|
|
||||||
args['cap_extensions'][1] = element.end_extn
|
|
||||||
|
|
||||||
return Path(**args)
|
|
||||||
|
|
||||||
|
|
||||||
def _boundary_to_polygon(element: gdsii.elements.Boundary, raw_mode: bool) -> Polygon:
|
|
||||||
args = {'vertices': element.xy[:-1].astype(float),
|
|
||||||
'layer': (element.layer, element.data_type),
|
|
||||||
'offset': numpy.zeros(2),
|
|
||||||
'annotations': _properties_to_annotations(element.properties),
|
|
||||||
'raw': raw_mode,
|
|
||||||
}
|
|
||||||
return Polygon(**args)
|
|
||||||
|
|
||||||
|
|
||||||
def _subpatterns_to_refs(
|
|
||||||
subpatterns: List[SubPattern],
|
|
||||||
) -> List[Union[gdsii.elements.ARef, gdsii.elements.SRef]]:
|
|
||||||
refs = []
|
|
||||||
for subpat in subpatterns:
|
|
||||||
if subpat.target is None:
|
|
||||||
continue
|
|
||||||
encoded_name = subpat.target.encode('ASCII')
|
|
||||||
|
|
||||||
# Note: GDS mirrors first and rotates second
|
|
||||||
mirror_across_x, extra_angle = normalize_mirror(subpat.mirrored)
|
|
||||||
rep = subpat.repetition
|
|
||||||
|
|
||||||
new_refs: List[Union[gdsii.elements.SRef, gdsii.elements.ARef]]
|
|
||||||
ref: Union[gdsii.elements.SRef, gdsii.elements.ARef]
|
|
||||||
if isinstance(rep, Grid):
|
|
||||||
b_vector = rep.b_vector if rep.b_vector is not None else numpy.zeros(2)
|
|
||||||
b_count = rep.b_count if rep.b_count is not None else 1
|
|
||||||
xy: NDArray[numpy.float64] = numpy.array(subpat.offset) + [
|
|
||||||
[0, 0],
|
|
||||||
rep.a_vector * rep.a_count,
|
|
||||||
b_vector * b_count,
|
|
||||||
]
|
|
||||||
ref = gdsii.elements.ARef(
|
|
||||||
struct_name=encoded_name,
|
|
||||||
xy=rint_cast(xy),
|
|
||||||
cols=rint_cast(rep.a_count),
|
|
||||||
rows=rint_cast(rep.b_count),
|
|
||||||
)
|
|
||||||
new_refs = [ref]
|
|
||||||
elif rep is None:
|
|
||||||
ref = gdsii.elements.SRef(
|
|
||||||
struct_name=encoded_name,
|
|
||||||
xy=rint_cast([subpat.offset]),
|
|
||||||
)
|
|
||||||
new_refs = [ref]
|
|
||||||
else:
|
|
||||||
new_refs = [gdsii.elements.SRef(
|
|
||||||
struct_name=encoded_name,
|
|
||||||
xy=rint_cast([subpat.offset + dd]),
|
|
||||||
)
|
|
||||||
for dd in rep.displacements]
|
|
||||||
|
|
||||||
for ref in new_refs:
|
|
||||||
ref.angle = numpy.rad2deg(subpat.rotation + extra_angle) % 360
|
|
||||||
# strans must be non-None for angle and mag to take effect
|
|
||||||
ref.strans = set_bit(0, 15 - 0, mirror_across_x)
|
|
||||||
ref.mag = subpat.scale
|
|
||||||
ref.properties = _annotations_to_properties(subpat.annotations, 512)
|
|
||||||
|
|
||||||
refs += new_refs
|
|
||||||
return refs
|
|
||||||
|
|
||||||
|
|
||||||
def _properties_to_annotations(properties: List[Tuple[int, bytes]]) -> annotations_t:
|
|
||||||
return {str(k): [v.decode()] for k, v in properties}
|
|
||||||
|
|
||||||
|
|
||||||
def _annotations_to_properties(annotations: annotations_t, max_len: int = 126) -> List[Tuple[int, bytes]]:
|
|
||||||
cum_len = 0
|
|
||||||
props = []
|
|
||||||
for key, vals in annotations.items():
|
|
||||||
try:
|
|
||||||
i = int(key)
|
|
||||||
except ValueError:
|
|
||||||
raise PatternError(f'Annotation key {key} is not convertable to an integer')
|
|
||||||
if not (0 < i < 126):
|
|
||||||
raise PatternError(f'Annotation key {key} converts to {i} (must be in the range [1,125])')
|
|
||||||
|
|
||||||
val_strings = ' '.join(str(val) for val in vals)
|
|
||||||
b = val_strings.encode()
|
|
||||||
if len(b) > 126:
|
|
||||||
raise PatternError(f'Annotation value {b!r} is longer than 126 characters!')
|
|
||||||
cum_len += numpy.ceil(len(b) / 2) * 2 + 2
|
|
||||||
if cum_len > max_len:
|
|
||||||
raise PatternError(f'Sum of annotation data will be longer than {max_len} bytes! Generated bytes were {b!r}')
|
|
||||||
props.append((i, b))
|
|
||||||
return props
|
|
||||||
|
|
||||||
|
|
||||||
def _shapes_to_elements(
|
|
||||||
shapes: List[Shape],
|
|
||||||
polygonize_paths: bool = False,
|
|
||||||
) -> List[Union[gdsii.elements.Boundary, gdsii.elements.Path]]:
|
|
||||||
elements: List[Union[gdsii.elements.Boundary, gdsii.elements.Path]] = []
|
|
||||||
# Add a Boundary element for each shape, and Path elements if necessary
|
|
||||||
for shape in shapes:
|
|
||||||
layer, data_type = _mlayer2gds(shape.layer)
|
|
||||||
properties = _annotations_to_properties(shape.annotations, 128)
|
|
||||||
if isinstance(shape, Path) and not polygonize_paths:
|
|
||||||
xy = rint_cast(shape.vertices + shape.offset)
|
|
||||||
width = rint_cast(shape.width)
|
|
||||||
path_type = next(k for k, v in path_cap_map.items() if v == shape.cap) # reverse lookup
|
|
||||||
path = gdsii.elements.Path(layer=layer,
|
|
||||||
data_type=data_type,
|
|
||||||
xy=xy)
|
|
||||||
path.path_type = path_type
|
|
||||||
path.width = width
|
|
||||||
path.properties = properties
|
|
||||||
elements.append(path)
|
|
||||||
else:
|
|
||||||
for polygon in shape.to_polygons():
|
|
||||||
xy_closed = numpy.empty((polygon.vertices.shape[0] + 1, 2), dtype=numpy.int32)
|
|
||||||
numpy.rint(polygon.vertices + polygon.offset, out=xy_closed[:-1], casting='unsafe')
|
|
||||||
xy_closed[-1] = xy_closed[0]
|
|
||||||
boundary = gdsii.elements.Boundary(
|
|
||||||
layer=layer,
|
|
||||||
data_type=data_type,
|
|
||||||
xy=xy_closed,
|
|
||||||
)
|
|
||||||
boundary.properties = properties
|
|
||||||
elements.append(boundary)
|
|
||||||
return elements
|
|
||||||
|
|
||||||
|
|
||||||
def _labels_to_texts(labels: List[Label]) -> List[gdsii.elements.Text]:
|
|
||||||
texts = []
|
|
||||||
for label in labels:
|
|
||||||
properties = _annotations_to_properties(label.annotations, 128)
|
|
||||||
layer, text_type = _mlayer2gds(label.layer)
|
|
||||||
xy = rint_cast([label.offset])
|
|
||||||
text = gdsii.elements.Text(
|
|
||||||
layer=layer,
|
|
||||||
text_type=text_type,
|
|
||||||
xy=xy,
|
|
||||||
string=label.string.encode('ASCII'),
|
|
||||||
)
|
|
||||||
text.properties = properties
|
|
||||||
texts.append(text)
|
|
||||||
return texts
|
|
||||||
|
|
||||||
|
|
||||||
def disambiguate_pattern_names(
|
|
||||||
names: Iterable[str],
|
|
||||||
max_name_length: int = 32,
|
|
||||||
suffix_length: int = 6,
|
|
||||||
) -> List[str]:
|
|
||||||
"""
|
|
||||||
Args:
|
|
||||||
names: List of pattern names to disambiguate
|
|
||||||
max_name_length: Names longer than this will be truncated
|
|
||||||
suffix_length: Names which get truncated are truncated by this many extra characters. This is to
|
|
||||||
leave room for a suffix if one is necessary.
|
|
||||||
"""
|
|
||||||
new_names = []
|
|
||||||
for name in names:
|
|
||||||
# Shorten names which already exceed max-length
|
|
||||||
if len(name) > max_name_length:
|
|
||||||
shortened_name = name[:max_name_length - suffix_length]
|
|
||||||
logger.warning(f'Pattern name "{name}" is too long ({len(name)}/{max_name_length} chars),\n'
|
|
||||||
+ f' shortening to "{shortened_name}" before generating suffix')
|
|
||||||
else:
|
|
||||||
shortened_name = name
|
|
||||||
|
|
||||||
# Remove invalid characters
|
|
||||||
sanitized_name = re.compile(r'[^A-Za-z0-9_\?\$]').sub('_', shortened_name)
|
|
||||||
|
|
||||||
# Add a suffix that makes the name unique
|
|
||||||
i = 0
|
|
||||||
suffixed_name = sanitized_name
|
|
||||||
while suffixed_name in new_names or suffixed_name == '':
|
|
||||||
suffix = base64.b64encode(struct.pack('>Q', i), b'$?').decode('ASCII')
|
|
||||||
|
|
||||||
suffixed_name = sanitized_name + '$' + suffix[:-1].lstrip('A')
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
if sanitized_name == '':
|
|
||||||
logger.warning(f'Empty pattern name saved as "{suffixed_name}"')
|
|
||||||
|
|
||||||
# Encode into a byte-string and perform some final checks
|
|
||||||
encoded_name = suffixed_name.encode('ASCII')
|
|
||||||
if len(encoded_name) == 0:
|
|
||||||
# Should never happen since zero-length names are replaced
|
|
||||||
raise PatternError(f'Zero-length name after sanitize+encode,\n originally "{name}"')
|
|
||||||
if len(encoded_name) > max_name_length:
|
|
||||||
raise PatternError(f'Pattern name "{encoded_name!r}" length > {max_name_length} after encode,\n'
|
|
||||||
+ f' originally "{name}"')
|
|
||||||
|
|
||||||
new_names.append(suffixed_name)
|
|
||||||
return new_names
|
|
||||||
|
|
@ -0,0 +1,413 @@
|
|||||||
|
from typing import Dict, Iterable, List, Tuple, Union, TypeVar, Any, Iterator, Optional, Sequence
|
||||||
|
from typing import overload, KeysView, ValuesView, ItemsView
|
||||||
|
import copy
|
||||||
|
import warnings
|
||||||
|
import traceback
|
||||||
|
import logging
|
||||||
|
from collections import Counter
|
||||||
|
from abc import ABCMeta
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
from numpy import pi
|
||||||
|
from numpy.typing import ArrayLike, NDArray
|
||||||
|
|
||||||
|
from .traits import PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable
|
||||||
|
from .utils import AutoSlots, rotate_offsets_around
|
||||||
|
from .error import DeviceError
|
||||||
|
from .library import MutableLibrary
|
||||||
|
from .builder import Tool
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
P = TypeVar('P', bound='Port')
|
||||||
|
PL = TypeVar('PL', bound='PortList')
|
||||||
|
PL2 = TypeVar('PL2', bound='PortList')
|
||||||
|
|
||||||
|
|
||||||
|
class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable, metaclass=AutoSlots):
|
||||||
|
"""
|
||||||
|
A point at which a `Device` can be snapped to another `Device`.
|
||||||
|
|
||||||
|
Each port has an `offset` ((x, y) position) and may also have a
|
||||||
|
`rotation` (orientation) and a `ptype` (port type).
|
||||||
|
|
||||||
|
The `rotation` is an angle, in radians, measured counterclockwise
|
||||||
|
from the +x axis, pointing inwards into the device which owns the port.
|
||||||
|
The rotation may be set to `None`, indicating that any orientation is
|
||||||
|
allowed (e.g. for a DC electrical port). It is stored modulo 2pi.
|
||||||
|
|
||||||
|
The `ptype` is an arbitrary string, default of `unk` (unknown).
|
||||||
|
"""
|
||||||
|
__slots__ = ('ptype', '_rotation')
|
||||||
|
|
||||||
|
_rotation: Optional[float]
|
||||||
|
""" radians counterclockwise from +x, pointing into device body.
|
||||||
|
Can be `None` to signify undirected port """
|
||||||
|
|
||||||
|
ptype: str
|
||||||
|
""" Port types must match to be plugged together if both are non-zero """
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
offset: ArrayLike,
|
||||||
|
rotation: Optional[float],
|
||||||
|
ptype: str = 'unk',
|
||||||
|
) -> None:
|
||||||
|
self.offset = offset
|
||||||
|
self.rotation = rotation
|
||||||
|
self.ptype = ptype
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rotation(self) -> Optional[float]:
|
||||||
|
""" Rotation, radians counterclockwise, pointing into device body. Can be None. """
|
||||||
|
return self._rotation
|
||||||
|
|
||||||
|
@rotation.setter
|
||||||
|
def rotation(self, val: float) -> None:
|
||||||
|
if val is None:
|
||||||
|
self._rotation = None
|
||||||
|
else:
|
||||||
|
if not numpy.size(val) == 1:
|
||||||
|
raise DeviceError('Rotation must be a scalar')
|
||||||
|
self._rotation = val % (2 * pi)
|
||||||
|
|
||||||
|
def get_bounds(self):
|
||||||
|
return numpy.vstack((self.offset, self.offset))
|
||||||
|
|
||||||
|
def set_ptype(self: P, ptype: str) -> P:
|
||||||
|
""" Chainable setter for `ptype` """
|
||||||
|
self.ptype = ptype
|
||||||
|
return self
|
||||||
|
|
||||||
|
def mirror(self: P, axis: int) -> P:
|
||||||
|
self.offset[1 - axis] *= -1
|
||||||
|
if self.rotation is not None:
|
||||||
|
self.rotation *= -1
|
||||||
|
self.rotation += axis * pi
|
||||||
|
return self
|
||||||
|
|
||||||
|
def rotate(self: P, rotation: float) -> P:
|
||||||
|
if self.rotation is not None:
|
||||||
|
self.rotation += rotation
|
||||||
|
return self
|
||||||
|
|
||||||
|
def set_rotation(self: P, rotation: Optional[float]) -> P:
|
||||||
|
self.rotation = rotation
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
if self.rotation is None:
|
||||||
|
rot = 'any'
|
||||||
|
else:
|
||||||
|
rot = str(numpy.rad2deg(self.rotation))
|
||||||
|
return f'<{self.offset}, {rot}, [{self.ptype}]>'
|
||||||
|
|
||||||
|
|
||||||
|
class PortList(metaclass=ABCMeta):
|
||||||
|
__slots__ = () # For use with AutoSlots
|
||||||
|
|
||||||
|
ports: Dict[str, Port]
|
||||||
|
""" Uniquely-named ports which can be used to snap to other Device instances"""
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def __getitem__(self, key: str) -> Port:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def __getitem__(self, key: Union[List[str], Tuple[str, ...], KeysView[str], ValuesView[str]]) -> PortList:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __getitem__(self, key: Union[str, Iterable[str]]) -> Union[Port, PortList]:
|
||||||
|
"""
|
||||||
|
For convenience, ports can be read out using square brackets:
|
||||||
|
- `pattern['A'] == Port((0, 0), 0)`
|
||||||
|
- ```
|
||||||
|
pattern[['A', 'B']] == {
|
||||||
|
'A': Port((0, 0), 0),
|
||||||
|
'B': Port((0, 0), pi),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
if isinstance(key, str):
|
||||||
|
return self.ports[key]
|
||||||
|
else:
|
||||||
|
return {k: self.ports[k] for k in key}
|
||||||
|
|
||||||
|
# TODO add Mapping stuff to PortsList
|
||||||
|
def keys(self) -> KeysView[Port]:
|
||||||
|
return self.ports.keys()
|
||||||
|
|
||||||
|
def values(self) -> ValuesView[Port]:
|
||||||
|
return self.ports.values()
|
||||||
|
|
||||||
|
def items(self) -> ItemsView[str, Port]:
|
||||||
|
return self.ports.items()
|
||||||
|
|
||||||
|
def rename_ports(
|
||||||
|
self: PL,
|
||||||
|
mapping: Dict[str, Optional[str]],
|
||||||
|
overwrite: bool = False,
|
||||||
|
) -> PL:
|
||||||
|
"""
|
||||||
|
Renames ports as specified by `mapping`.
|
||||||
|
Ports can be explicitly deleted by mapping them to `None`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mapping: Dict of `{'old_name': 'new_name'}` pairs. Names can be mapped
|
||||||
|
to `None` to perform an explicit deletion. `'new_name'` can also
|
||||||
|
overwrite an existing non-renamed port to implicitly delete it if
|
||||||
|
`overwrite` is set to `True`.
|
||||||
|
overwrite: Allows implicit deletion of ports if set to `True`; see `mapping`.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
"""
|
||||||
|
if not overwrite:
|
||||||
|
duplicates = (set(self.ports.keys()) - set(mapping.keys())) & set(mapping.values())
|
||||||
|
if duplicates:
|
||||||
|
raise DeviceError(f'Unrenamed ports would be overwritten: {duplicates}')
|
||||||
|
|
||||||
|
renamed = {mapping[k]: self.ports.pop(k) for k in mapping.keys()}
|
||||||
|
if None in renamed:
|
||||||
|
del renamed[None]
|
||||||
|
|
||||||
|
self.ports.update(renamed) # type: ignore
|
||||||
|
return self
|
||||||
|
|
||||||
|
def check_ports(
|
||||||
|
self: PL,
|
||||||
|
other_names: Iterable[str],
|
||||||
|
map_in: Optional[Dict[str, str]] = None,
|
||||||
|
map_out: Optional[Dict[str, Optional[str]]] = None,
|
||||||
|
) -> PL:
|
||||||
|
"""
|
||||||
|
Given the provided port mappings, check that:
|
||||||
|
- All of the ports specified in the mappings exist
|
||||||
|
- There are no duplicate port names after all the mappings are performed
|
||||||
|
|
||||||
|
Args:
|
||||||
|
other_names: List of port names being considered for inclusion into
|
||||||
|
`self.ports` (before mapping)
|
||||||
|
map_in: Dict of `{'self_port': 'other_port'}` mappings, specifying
|
||||||
|
port connections between the two devices.
|
||||||
|
map_out: Dict of `{'old_name': 'new_name'}` mappings, specifying
|
||||||
|
new names for unconnected `other_names` ports.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
`DeviceError` if any ports specified in `map_in` or `map_out` do not
|
||||||
|
exist in `self.ports` or `other_names`.
|
||||||
|
`DeviceError` if there are any duplicate names after `map_in` and `map_out`
|
||||||
|
are applied.
|
||||||
|
"""
|
||||||
|
if map_in is None:
|
||||||
|
map_in = {}
|
||||||
|
|
||||||
|
if map_out is None:
|
||||||
|
map_out = {}
|
||||||
|
|
||||||
|
other = set(other_names)
|
||||||
|
|
||||||
|
missing_inkeys = set(map_in.keys()) - set(self.ports.keys())
|
||||||
|
if missing_inkeys:
|
||||||
|
raise DeviceError(f'`map_in` keys not present in device: {missing_inkeys}')
|
||||||
|
|
||||||
|
missing_invals = set(map_in.values()) - other
|
||||||
|
if missing_invals:
|
||||||
|
raise DeviceError(f'`map_in` values not present in other device: {missing_invals}')
|
||||||
|
|
||||||
|
missing_outkeys = set(map_out.keys()) - other
|
||||||
|
if missing_outkeys:
|
||||||
|
raise DeviceError(f'`map_out` keys not present in other device: {missing_outkeys}')
|
||||||
|
|
||||||
|
orig_remaining = set(self.ports.keys()) - set(map_in.keys())
|
||||||
|
other_remaining = other - set(map_out.keys()) - set(map_in.values())
|
||||||
|
mapped_vals = set(map_out.values())
|
||||||
|
mapped_vals.discard(None)
|
||||||
|
|
||||||
|
conflicts_final = orig_remaining & (other_remaining | mapped_vals)
|
||||||
|
if conflicts_final:
|
||||||
|
raise DeviceError(f'Device ports conflict with existing ports: {conflicts_final}')
|
||||||
|
|
||||||
|
conflicts_partial = other_remaining & mapped_vals
|
||||||
|
if conflicts_partial:
|
||||||
|
raise DeviceError(f'`map_out` targets conflict with non-mapped outputs: {conflicts_partial}')
|
||||||
|
|
||||||
|
map_out_counts = Counter(map_out.values())
|
||||||
|
map_out_counts[None] = 0
|
||||||
|
conflicts_out = {k for k, v in map_out_counts.items() if v > 1}
|
||||||
|
if conflicts_out:
|
||||||
|
raise DeviceError(f'Duplicate targets in `map_out`: {conflicts_out}')
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def as_interface(
|
||||||
|
self,
|
||||||
|
library: MutableLibrary,
|
||||||
|
*,
|
||||||
|
tools: Optional[Dict[str, Tool]] = None,
|
||||||
|
in_prefix: str = 'in_',
|
||||||
|
out_prefix: str = '',
|
||||||
|
port_map: Optional[Union[Dict[str, str], Sequence[str]]] = None,
|
||||||
|
) -> 'Builder':
|
||||||
|
"""
|
||||||
|
Begin building a new device based on all or some of the ports in the
|
||||||
|
current device. Do not include the current device; instead use it
|
||||||
|
to define ports (the "interface") for the new device.
|
||||||
|
|
||||||
|
The ports specified by `port_map` (default: all ports) are copied to
|
||||||
|
new device, and additional (input) ports are created facing in the
|
||||||
|
opposite directions. The specified `in_prefix` and `out_prefix` are
|
||||||
|
prepended to the port names to differentiate them.
|
||||||
|
|
||||||
|
By default, the flipped ports are given an 'in_' prefix and unflipped
|
||||||
|
ports keep their original names, enabling intuitive construction of
|
||||||
|
a device that will "plug into" the current device; the 'in_*' ports
|
||||||
|
are used for plugging the devices together while the original port
|
||||||
|
names are used for building the new device.
|
||||||
|
|
||||||
|
Another use-case could be to build the new device using the 'in_'
|
||||||
|
ports, creating a new device which could be used in place of the
|
||||||
|
current device.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
in_prefix: Prepended to port names for newly-created ports with
|
||||||
|
reversed directions compared to the current device.
|
||||||
|
out_prefix: Prepended to port names for ports which are directly
|
||||||
|
copied from the current device.
|
||||||
|
port_map: Specification for ports to copy into the new device:
|
||||||
|
- If `None`, all ports are copied.
|
||||||
|
- If a sequence, only the listed ports are copied
|
||||||
|
- If a mapping, the listed ports (keys) are copied and
|
||||||
|
renamed (to the values).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The new device, with an empty pattern and 2x as many ports as
|
||||||
|
listed in port_map.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
`DeviceError` if `port_map` contains port names not present in the
|
||||||
|
current device.
|
||||||
|
`DeviceError` if applying the prefixes results in duplicate port
|
||||||
|
names.
|
||||||
|
"""
|
||||||
|
if port_map:
|
||||||
|
if isinstance(port_map, dict):
|
||||||
|
missing_inkeys = set(port_map.keys()) - set(self.ports.keys())
|
||||||
|
orig_ports = {port_map[k]: v for k, v in self.ports.items() if k in port_map}
|
||||||
|
else:
|
||||||
|
port_set = set(port_map)
|
||||||
|
missing_inkeys = port_set - set(self.ports.keys())
|
||||||
|
orig_ports = {k: v for k, v in self.ports.items() if k in port_set}
|
||||||
|
|
||||||
|
if missing_inkeys:
|
||||||
|
raise DeviceError(f'`port_map` keys not present in device: {missing_inkeys}')
|
||||||
|
else:
|
||||||
|
orig_ports = self.ports
|
||||||
|
|
||||||
|
ports_in = {f'{in_prefix}{name}': port.deepcopy().rotate(pi)
|
||||||
|
for name, port in orig_ports.items()}
|
||||||
|
ports_out = {f'{out_prefix}{name}': port.deepcopy()
|
||||||
|
for name, port in orig_ports.items()}
|
||||||
|
|
||||||
|
duplicates = set(ports_out.keys()) & set(ports_in.keys())
|
||||||
|
if duplicates:
|
||||||
|
raise DeviceError(f'Duplicate keys after prefixing, try a different prefix: {duplicates}')
|
||||||
|
|
||||||
|
new = Builder(library=library, ports={**ports_in, **ports_out}, tools=tools)
|
||||||
|
return new
|
||||||
|
|
||||||
|
def find_transform(
|
||||||
|
self: PL,
|
||||||
|
other: PL2,
|
||||||
|
map_in: Dict[str, str],
|
||||||
|
*,
|
||||||
|
mirrored: Tuple[bool, bool] = (False, False),
|
||||||
|
set_rotation: Optional[bool] = None,
|
||||||
|
) -> Tuple[NDArray[numpy.float64], float, NDArray[numpy.float64]]:
|
||||||
|
"""
|
||||||
|
Given a device `other` and a mapping `map_in` specifying port connections,
|
||||||
|
find the transform which will correctly align the specified ports.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
other: a device
|
||||||
|
map_in: Dict of `{'self_port': 'other_port'}` mappings, specifying
|
||||||
|
port connections between the two devices.
|
||||||
|
mirrored: Mirrors `other` across the x or y axes prior to
|
||||||
|
connecting any ports.
|
||||||
|
set_rotation: If the necessary rotation cannot be determined from
|
||||||
|
the ports being connected (i.e. all pairs have at least one
|
||||||
|
port with `rotation=None`), `set_rotation` must be provided
|
||||||
|
to indicate how much `other` should be rotated. Otherwise,
|
||||||
|
`set_rotation` must remain `None`.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- The (x, y) translation (performed last)
|
||||||
|
- The rotation (radians, counterclockwise)
|
||||||
|
- The (x, y) pivot point for the rotation
|
||||||
|
|
||||||
|
The rotation should be performed before the translation.
|
||||||
|
"""
|
||||||
|
s_ports = self[map_in.keys()]
|
||||||
|
o_ports = other[map_in.values()]
|
||||||
|
|
||||||
|
s_offsets = numpy.array([p.offset for p in s_ports.values()])
|
||||||
|
o_offsets = numpy.array([p.offset for p in o_ports.values()])
|
||||||
|
s_types = [p.ptype for p in s_ports.values()]
|
||||||
|
o_types = [p.ptype for p in o_ports.values()]
|
||||||
|
|
||||||
|
s_rotations = numpy.array([p.rotation if p.rotation is not None else 0 for p in s_ports.values()])
|
||||||
|
o_rotations = numpy.array([p.rotation if p.rotation is not None else 0 for p in o_ports.values()])
|
||||||
|
s_has_rot = numpy.array([p.rotation is not None for p in s_ports.values()], dtype=bool)
|
||||||
|
o_has_rot = numpy.array([p.rotation is not None for p in o_ports.values()], dtype=bool)
|
||||||
|
has_rot = s_has_rot & o_has_rot
|
||||||
|
|
||||||
|
if mirrored[0]:
|
||||||
|
o_offsets[:, 1] *= -1
|
||||||
|
o_rotations *= -1
|
||||||
|
if mirrored[1]:
|
||||||
|
o_offsets[:, 0] *= -1
|
||||||
|
o_rotations *= -1
|
||||||
|
o_rotations += pi
|
||||||
|
|
||||||
|
type_conflicts = numpy.array([st != ot and st != 'unk' and ot != 'unk'
|
||||||
|
for st, ot in zip(s_types, o_types)])
|
||||||
|
if type_conflicts.any():
|
||||||
|
ports = numpy.where(type_conflicts)
|
||||||
|
msg = 'Ports have conflicting types:\n'
|
||||||
|
for nn, (k, v) in enumerate(map_in.items()):
|
||||||
|
if type_conflicts[nn]:
|
||||||
|
msg += f'{k} | {s_types[nn]}:{o_types[nn]} | {v}\n'
|
||||||
|
msg = ''.join(traceback.format_stack()) + '\n' + msg
|
||||||
|
warnings.warn(msg, stacklevel=2)
|
||||||
|
|
||||||
|
rotations = numpy.mod(s_rotations - o_rotations - pi, 2 * pi)
|
||||||
|
if not has_rot.any():
|
||||||
|
if set_rotation is None:
|
||||||
|
DeviceError('Must provide set_rotation if rotation is indeterminate')
|
||||||
|
rotations[:] = set_rotation
|
||||||
|
else:
|
||||||
|
rotations[~has_rot] = rotations[has_rot][0]
|
||||||
|
|
||||||
|
if not numpy.allclose(rotations[:1], rotations):
|
||||||
|
rot_deg = numpy.rad2deg(rotations)
|
||||||
|
msg = f'Port orientations do not match:\n'
|
||||||
|
for nn, (k, v) in enumerate(map_in.items()):
|
||||||
|
msg += f'{k} | {rot_deg[nn]:g} | {v}\n'
|
||||||
|
raise DeviceError(msg)
|
||||||
|
|
||||||
|
pivot = o_offsets[0].copy()
|
||||||
|
rotate_offsets_around(o_offsets, pivot, rotations[0])
|
||||||
|
translations = s_offsets - o_offsets
|
||||||
|
if not numpy.allclose(translations[:1], translations):
|
||||||
|
msg = f'Port translations do not match:\n'
|
||||||
|
for nn, (k, v) in enumerate(map_in.items()):
|
||||||
|
msg += f'{k} | {translations[nn]} | {v}\n'
|
||||||
|
raise DeviceError(msg)
|
||||||
|
|
||||||
|
return translations[0], rotations[0], o_offsets[0]
|
Loading…
Reference in New Issue