wip
This commit is contained in:
parent
d9ae8dd6e3
commit
9efb6f0eeb
@ -35,9 +35,9 @@ pip3 install git+https://mpxd.net/code/jan/masque.git@release
|
|||||||
|
|
||||||
## Translation
|
## Translation
|
||||||
- `Pattern`: OASIS or GDS "Cell", DXF "Block"
|
- `Pattern`: OASIS or GDS "Cell", DXF "Block"
|
||||||
- `SubPattern`: GDS "AREF/SREF", OASIS "Placement"
|
- `Ref`: GDS "AREF/SREF", OASIS "Placement"
|
||||||
- `Shape`: OASIS or GDS "Geometry element", DXF "LWPolyline" or "Polyline"
|
- `Shape`: OASIS or GDS "Geometry element", DXF "LWPolyline" or "Polyline"
|
||||||
- `repetition`: OASIS "repetition". GDS "AREF" is a `SubPattern` combined with a `Grid` repetition.
|
- `repetition`: OASIS "repetition". GDS "AREF" is a `Ref` combined with a `Grid` repetition.
|
||||||
- `Label`: OASIS, GDS, DXF "Text".
|
- `Label`: OASIS, GDS, DXF "Text".
|
||||||
- `annotation`: OASIS or GDS "property"
|
- `annotation`: OASIS or GDS "property"
|
||||||
|
|
||||||
@ -54,4 +54,4 @@ pip3 install git+https://mpxd.net/code/jan/masque.git@release
|
|||||||
- need to add the other device by name
|
- need to add the other device by name
|
||||||
- need to know the other device's ports
|
- need to know the other device's ports
|
||||||
-
|
-
|
||||||
- also: device doesn't know its own name, can't wrap itself into a subpattern
|
- also: device doesn't know its own name, can't wrap itself into a ref
|
||||||
|
@ -8,9 +8,9 @@
|
|||||||
|
|
||||||
`Pattern` is a basic object containing a 2D lithography mask, composed of a list of `Shape`
|
`Pattern` is a basic object containing a 2D lithography mask, composed of a list of `Shape`
|
||||||
objects, a list of `Label` objects, and a list of references to other `Patterns` (using
|
objects, a list of `Label` objects, and a list of references to other `Patterns` (using
|
||||||
`SubPattern`).
|
`Ref`).
|
||||||
|
|
||||||
`SubPattern` provides basic support for nesting `Pattern` objects within each other, by adding
|
`Ref` provides basic support for nesting `Pattern` objects within each other, by adding
|
||||||
offset, rotation, scaling, repetition, and other such properties to a Pattern reference.
|
offset, rotation, scaling, repetition, and other such properties to a Pattern reference.
|
||||||
|
|
||||||
Note that the methods for these classes try to avoid copying wherever possible, so unless
|
Note that the methods for these classes try to avoid copying wherever possible, so unless
|
||||||
@ -30,12 +30,12 @@
|
|||||||
from .error import PatternError
|
from .error import PatternError
|
||||||
from .shapes import Shape
|
from .shapes import Shape
|
||||||
from .label import Label
|
from .label import Label
|
||||||
from .subpattern import SubPattern
|
from .ref import Ref
|
||||||
from .pattern import Pattern
|
from .pattern import Pattern
|
||||||
from .utils import layer_t, annotations_t
|
from .utils import layer_t, annotations_t
|
||||||
from .library import Library, MutableLibrary, WrapROLibrary, WrapLibrary, LazyLibrary
|
from .library import Library, MutableLibrary, WrapROLibrary, WrapLibrary, LazyLibrary
|
||||||
from .builder import LazyDeviceLibrary, LibDeviceLibrary, Device, DeviceRef, Port, PortList
|
from .ports import Port, PortList
|
||||||
|
from .builder import Builder, PortsRef, Tool
|
||||||
|
|
||||||
__author__ = 'Jan Petykiewicz'
|
__author__ = 'Jan Petykiewicz'
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
from .devices import Port, PortList, Device, DeviceRef
|
from .devices import Builder, PortsRef
|
||||||
from .utils import ell
|
from .utils import ell
|
||||||
from .tools import Tool
|
from .tools import Tool
|
||||||
from .device_library import LazyDeviceLibrary, LibDeviceLibrary
|
|
||||||
|
@ -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
|
|
@ -11,9 +11,10 @@ import numpy
|
|||||||
from numpy import pi
|
from numpy import pi
|
||||||
from numpy.typing import ArrayLike, NDArray
|
from numpy.typing import ArrayLike, NDArray
|
||||||
|
|
||||||
from ..pattern import Pattern
|
|
||||||
from ..subpattern import SubPattern
|
|
||||||
from ..traits import PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable
|
from ..traits import PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable
|
||||||
|
from ..pattern import Pattern
|
||||||
|
from ..ref import Ref
|
||||||
|
from ..library import MutableLibrary
|
||||||
from ..utils import AutoSlots
|
from ..utils import AutoSlots
|
||||||
from ..error import DeviceError
|
from ..error import DeviceError
|
||||||
from ..ports import PortList, Port
|
from ..ports import PortList, Port
|
||||||
@ -24,18 +25,18 @@ from .utils import ell
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
D = TypeVar('D', bound='Device')
|
B = TypeVar('B', bound='Builder')
|
||||||
DR = TypeVar('DR', bound='DeviceRef')
|
PR = TypeVar('PR', bound='PortsRef')
|
||||||
|
|
||||||
|
|
||||||
class DeviceRef(PortList):
|
class PortsRef(PortList):
|
||||||
__slots__ = ('name',)
|
__slots__ = ('name', 'ports')
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
""" Name of the pattern this device references """
|
""" Name of the pattern this device references """
|
||||||
|
|
||||||
ports: Dict[str, Port]
|
ports: Dict[str, Port]
|
||||||
""" Uniquely-named ports which can be used to snap to other Device instances"""
|
""" Uniquely-named ports which can be used to snap instances together"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -45,31 +46,32 @@ class DeviceRef(PortList):
|
|||||||
self.name = name
|
self.name = name
|
||||||
self.ports = copy.deepcopy(ports)
|
self.ports = copy.deepcopy(ports)
|
||||||
|
|
||||||
def build(self) -> 'Device':
|
def build(self, library: MutableLibrary) -> 'Builder':
|
||||||
"""
|
"""
|
||||||
Begin building a new device around an instance of the current device
|
Begin building a new device around an instance of the current device
|
||||||
(rather than modifying the current device).
|
(rather than modifying the current device).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The new `Device` object.
|
The new `Builder` object.
|
||||||
"""
|
"""
|
||||||
pat = Pattern()
|
pat = Pattern(ports=self.ports)
|
||||||
pat.addsp(self.name)
|
pat.ref(self.name)
|
||||||
new = Device(pat, ports=self.ports, tools=self.tools) # TODO should DeviceRef have tools?
|
new = Builder(library=library, pattern=pat, tools=self.tools) # TODO should Ref have tools?
|
||||||
return new
|
return new
|
||||||
|
|
||||||
# TODO do we want to store a SubPattern instead of just a name? then we can translate/rotate/mirror...
|
# TODO do we want to store a Ref instead of just a name? then we can translate/rotate/mirror...
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
s = f'<DeviceRef {self.name} ['
|
s = f'<PortsRef {self.name} ['
|
||||||
for name, port in self.ports.items():
|
for name, port in self.ports.items():
|
||||||
s += f'\n\t{name}: {port}'
|
s += f'\n\t{name}: {port}'
|
||||||
s += ']>'
|
s += ']>'
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
class Device(PortList):
|
class Builder(PortList):
|
||||||
"""
|
"""
|
||||||
|
TODO DOCUMENT Builder
|
||||||
A `Device` is a combination of a `Pattern` with a set of named `Port`s
|
A `Device` is a combination of a `Pattern` with a set of named `Port`s
|
||||||
which can be used to "snap" devices together to make complex layouts.
|
which can be used to "snap" devices together to make complex layouts.
|
||||||
|
|
||||||
@ -121,13 +123,16 @@ class Device(PortList):
|
|||||||
renamed to 'gnd' so that further routing can use this signal or net name
|
renamed to 'gnd' so that further routing can use this signal or net name
|
||||||
rather than the port name on the original `pad` device.
|
rather than the port name on the original `pad` device.
|
||||||
"""
|
"""
|
||||||
__slots__ = ('pattern', 'tools', '_dead')
|
__slots__ = ('pattern', 'library', 'tools', '_dead')
|
||||||
|
|
||||||
pattern: Pattern
|
pattern: Pattern
|
||||||
""" Layout of this device """
|
""" Layout of this device """
|
||||||
|
|
||||||
ports: Dict[str, Port]
|
library: MutableLibrary
|
||||||
""" Uniquely-named ports which can be used to snap to other Device instances"""
|
"""
|
||||||
|
Library from which existing patterns should be referenced, and to which
|
||||||
|
new ones should be added
|
||||||
|
"""
|
||||||
|
|
||||||
tools: Dict[Optional[str], Tool]
|
tools: Dict[Optional[str], Tool]
|
||||||
"""
|
"""
|
||||||
@ -140,8 +145,8 @@ class Device(PortList):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
library: MutableLibrary,
|
||||||
pattern: Optional[Pattern] = None,
|
pattern: Optional[Pattern] = None,
|
||||||
ports: Optional[Dict[str, Port]] = None,
|
|
||||||
*,
|
*,
|
||||||
tools: Union[None, Tool, Dict[Optional[str], Tool]] = None,
|
tools: Union[None, Tool, Dict[Optional[str], Tool]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -151,15 +156,17 @@ class Device(PortList):
|
|||||||
(attached devices will be placed to the left) and 'B' has rotation
|
(attached devices will be placed to the left) and 'B' has rotation
|
||||||
pi (attached devices will be placed to the right).
|
pi (attached devices will be placed to the right).
|
||||||
"""
|
"""
|
||||||
|
self.library = library
|
||||||
self.pattern = pattern or Pattern()
|
self.pattern = pattern or Pattern()
|
||||||
|
|
||||||
if ports is None:
|
## TODO add_port_pair function to add ports at location with rotation
|
||||||
self.ports = {
|
#if ports is None:
|
||||||
'A': Port([0, 0], rotation=0),
|
# self.ports = {
|
||||||
'B': Port([0, 0], rotation=pi),
|
# 'A': Port([0, 0], rotation=0),
|
||||||
}
|
# 'B': Port([0, 0], rotation=pi),
|
||||||
else:
|
# }
|
||||||
self.ports = copy.deepcopy(ports)
|
#else:
|
||||||
|
# self.ports = copy.deepcopy(ports)
|
||||||
|
|
||||||
if tools is None:
|
if tools is None:
|
||||||
self.tools = {}
|
self.tools = {}
|
||||||
@ -175,9 +182,9 @@ class Device(PortList):
|
|||||||
in_prefix: str = 'in_',
|
in_prefix: str = 'in_',
|
||||||
out_prefix: str = '',
|
out_prefix: str = '',
|
||||||
port_map: Optional[Union[Dict[str, str], Sequence[str]]] = None
|
port_map: Optional[Union[Dict[str, str], Sequence[str]]] = None
|
||||||
) -> 'Device':
|
) -> 'Builder':
|
||||||
new = PortList.as_interface(
|
new = self.pattern.as_interface(
|
||||||
self,
|
library=self.library,
|
||||||
in_prefix=in_prefix,
|
in_prefix=in_prefix,
|
||||||
out_prefix=out_prefix,
|
out_prefix=out_prefix,
|
||||||
port_map=port_map,
|
port_map=port_map,
|
||||||
@ -186,15 +193,15 @@ class Device(PortList):
|
|||||||
return new
|
return new
|
||||||
|
|
||||||
def plug(
|
def plug(
|
||||||
self: D,
|
self: B,
|
||||||
other: DR,
|
other: PR,
|
||||||
map_in: Dict[str, str],
|
map_in: Dict[str, str],
|
||||||
map_out: Optional[Dict[str, Optional[str]]] = None,
|
map_out: Optional[Dict[str, Optional[str]]] = None,
|
||||||
*,
|
*,
|
||||||
mirrored: Tuple[bool, bool] = (False, False),
|
mirrored: Tuple[bool, bool] = (False, False),
|
||||||
inherit_name: bool = True,
|
inherit_name: bool = True,
|
||||||
set_rotation: Optional[bool] = None,
|
set_rotation: Optional[bool] = None,
|
||||||
) -> D:
|
) -> B:
|
||||||
"""
|
"""
|
||||||
Instantiate a device `library[name]` into the current device, connecting
|
Instantiate a device `library[name]` into the current device, connecting
|
||||||
the ports specified by `map_in` and renaming the unconnected
|
the ports specified by `map_in` and renaming the unconnected
|
||||||
@ -264,8 +271,12 @@ class Device(PortList):
|
|||||||
map_out = copy.deepcopy(map_out)
|
map_out = copy.deepcopy(map_out)
|
||||||
|
|
||||||
self.check_ports(other.ports.keys(), map_in, map_out)
|
self.check_ports(other.ports.keys(), map_in, map_out)
|
||||||
translation, rotation, pivot = self.find_transform(other, map_in, mirrored=mirrored,
|
translation, rotation, pivot = self.find_transform(
|
||||||
set_rotation=set_rotation)
|
other,
|
||||||
|
map_in,
|
||||||
|
mirrored=mirrored,
|
||||||
|
set_rotation=set_rotation,
|
||||||
|
)
|
||||||
|
|
||||||
# get rid of plugged ports
|
# get rid of plugged ports
|
||||||
for ki, vi in map_in.items():
|
for ki, vi in map_in.items():
|
||||||
@ -277,8 +288,8 @@ class Device(PortList):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def place(
|
def place(
|
||||||
self: D,
|
self: B,
|
||||||
other: DR,
|
other: PR,
|
||||||
*,
|
*,
|
||||||
offset: ArrayLike = (0, 0),
|
offset: ArrayLike = (0, 0),
|
||||||
rotation: float = 0,
|
rotation: float = 0,
|
||||||
@ -286,7 +297,7 @@ class Device(PortList):
|
|||||||
mirrored: Tuple[bool, bool] = (False, False),
|
mirrored: Tuple[bool, bool] = (False, False),
|
||||||
port_map: Optional[Dict[str, Optional[str]]] = None,
|
port_map: Optional[Dict[str, Optional[str]]] = None,
|
||||||
skip_port_check: bool = False,
|
skip_port_check: bool = False,
|
||||||
) -> D:
|
) -> B:
|
||||||
"""
|
"""
|
||||||
Instantiate the device `other` into the current device, adding its
|
Instantiate the device `other` into the current device, adding its
|
||||||
ports to those of the current device (but not connecting any ports).
|
ports to those of the current device (but not connecting any ports).
|
||||||
@ -348,13 +359,13 @@ class Device(PortList):
|
|||||||
p.translate(offset)
|
p.translate(offset)
|
||||||
self.ports[name] = p
|
self.ports[name] = p
|
||||||
|
|
||||||
sp = SubPattern(other.name, mirrored=mirrored)
|
sp = Ref(other.name, mirrored=mirrored)
|
||||||
sp.rotate_around(pivot, rotation)
|
sp.rotate_around(pivot, rotation)
|
||||||
sp.translate(offset)
|
sp.translate(offset)
|
||||||
self.pattern.subpatterns.append(sp)
|
self.pattern.refs.append(sp)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def translate(self: D, offset: ArrayLike) -> D:
|
def translate(self: B, offset: ArrayLike) -> B:
|
||||||
"""
|
"""
|
||||||
Translate the pattern and all ports.
|
Translate the pattern and all ports.
|
||||||
|
|
||||||
@ -369,7 +380,7 @@ class Device(PortList):
|
|||||||
port.translate(offset)
|
port.translate(offset)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def rotate_around(self: D, pivot: ArrayLike, angle: float) -> D:
|
def rotate_around(self: B, pivot: ArrayLike, angle: float) -> B:
|
||||||
"""
|
"""
|
||||||
Rotate the pattern and all ports.
|
Rotate the pattern and all ports.
|
||||||
|
|
||||||
@ -385,7 +396,7 @@ class Device(PortList):
|
|||||||
port.rotate_around(pivot, angle)
|
port.rotate_around(pivot, angle)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def mirror(self: D, axis: int) -> D:
|
def mirror(self: B, axis: int) -> B:
|
||||||
"""
|
"""
|
||||||
Mirror the pattern and all ports across the specified axis.
|
Mirror the pattern and all ports across the specified axis.
|
||||||
|
|
||||||
@ -400,7 +411,7 @@ class Device(PortList):
|
|||||||
p.mirror(axis)
|
p.mirror(axis)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def set_dead(self: D) -> D:
|
def set_dead(self: B) -> B:
|
||||||
"""
|
"""
|
||||||
Disallows further changes through `plug()` or `place()`.
|
Disallows further changes through `plug()` or `place()`.
|
||||||
This is meant for debugging:
|
This is meant for debugging:
|
||||||
@ -419,17 +430,18 @@ class Device(PortList):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
s = f'<Device {self.pattern} ['
|
s = f'<Builder {self.pattern} >'
|
||||||
for name, port in self.ports.items():
|
# '['
|
||||||
s += f'\n\t{name}: {port}'
|
# for name, port in self.ports.items():
|
||||||
s += ']>'
|
# s += f'\n\t{name}: {port}'
|
||||||
|
# s += ']>'
|
||||||
return s
|
return s
|
||||||
|
|
||||||
def retool(
|
def retool(
|
||||||
self: D,
|
self: B,
|
||||||
tool: Tool,
|
tool: Tool,
|
||||||
keys: Union[Optional[str], Sequence[Optional[str]]] = None,
|
keys: Union[Optional[str], Sequence[Optional[str]]] = None,
|
||||||
) -> D:
|
) -> B:
|
||||||
if keys is None or isinstance(keys, str):
|
if keys is None or isinstance(keys, str):
|
||||||
self.tools[keys] = tool
|
self.tools[keys] = tool
|
||||||
else:
|
else:
|
||||||
@ -438,37 +450,41 @@ class Device(PortList):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def path(
|
def path(
|
||||||
self: D,
|
self: B,
|
||||||
portspec: str,
|
portspec: str,
|
||||||
ccw: Optional[bool],
|
ccw: Optional[bool],
|
||||||
length: float,
|
length: float,
|
||||||
*,
|
*,
|
||||||
tool_port_names: Sequence[str] = ('A', 'B'),
|
tool_port_names: Sequence[str] = ('A', 'B'),
|
||||||
|
base_name: str = '_path_',
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> D:
|
) -> B:
|
||||||
if self._dead:
|
if self._dead:
|
||||||
logger.error('Skipping path() since device is dead')
|
logger.error('Skipping path() since device is dead')
|
||||||
return self
|
return self
|
||||||
|
|
||||||
tool = self.tools.get(portspec, self.tools[None])
|
tool = self.tools.get(portspec, self.tools[None])
|
||||||
in_ptype = self.ports[portspec].ptype
|
in_ptype = self.pattern[portspec].ptype
|
||||||
dev = tool.path(ccw, length, in_ptype=in_ptype, port_names=tool_port_names, **kwargs)
|
pat = tool.path(ccw, length, in_ptype=in_ptype, port_names=tool_port_names, **kwargs)
|
||||||
return self.plug(dev, {portspec: tool_port_names[0]})
|
name = self.library.get_name(base_name)
|
||||||
|
self.library._set(name, pat)
|
||||||
|
return self.plug(PortsRef(name, pat.ports), {portspec: tool_port_names[0]})
|
||||||
|
|
||||||
def path_to(
|
def path_to(
|
||||||
self: D,
|
self: B,
|
||||||
portspec: str,
|
portspec: str,
|
||||||
ccw: Optional[bool],
|
ccw: Optional[bool],
|
||||||
position: float,
|
position: float,
|
||||||
*,
|
*,
|
||||||
tool_port_names: Sequence[str] = ('A', 'B'),
|
tool_port_names: Sequence[str] = ('A', 'B'),
|
||||||
|
base_name: str = '_pathto_',
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> D:
|
) -> B:
|
||||||
if self._dead:
|
if self._dead:
|
||||||
logger.error('Skipping path_to() since device is dead')
|
logger.error('Skipping path_to() since device is dead')
|
||||||
return self
|
return self
|
||||||
|
|
||||||
port = self.ports[portspec]
|
port = self.pattern[portspec]
|
||||||
x, y = port.offset
|
x, y = port.offset
|
||||||
if port.rotation is None:
|
if port.rotation is None:
|
||||||
raise DeviceError(f'Port {portspec} has no rotation and cannot be used for path_to()')
|
raise DeviceError(f'Port {portspec} has no rotation and cannot be used for path_to()')
|
||||||
@ -486,10 +502,10 @@ class Device(PortList):
|
|||||||
raise DeviceError(f'path_to routing to behind source port: y={y:g} to {position:g}')
|
raise DeviceError(f'path_to routing to behind source port: y={y:g} to {position:g}')
|
||||||
length = numpy.abs(position - y)
|
length = numpy.abs(position - y)
|
||||||
|
|
||||||
return self.path(portspec, ccw, length, tool_port_names=tool_port_names, **kwargs)
|
return self.path(portspec, ccw, length, tool_port_names=tool_port_names, base_name=base_name, **kwargs)
|
||||||
|
|
||||||
def mpath(
|
def mpath(
|
||||||
self: D,
|
self: B,
|
||||||
portspec: Union[str, Sequence[str]],
|
portspec: Union[str, Sequence[str]],
|
||||||
ccw: Optional[bool],
|
ccw: Optional[bool],
|
||||||
*,
|
*,
|
||||||
@ -497,8 +513,9 @@ class Device(PortList):
|
|||||||
set_rotation: Optional[float] = None,
|
set_rotation: Optional[float] = None,
|
||||||
tool_port_names: Sequence[str] = ('A', 'B'),
|
tool_port_names: Sequence[str] = ('A', 'B'),
|
||||||
force_container: bool = False,
|
force_container: bool = False,
|
||||||
|
base_name: str = '_mpath_',
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> D:
|
) -> B:
|
||||||
if self._dead:
|
if self._dead:
|
||||||
logger.error('Skipping mpath() since device is dead')
|
logger.error('Skipping mpath() since device is dead')
|
||||||
return self
|
return self
|
||||||
@ -520,7 +537,7 @@ class Device(PortList):
|
|||||||
|
|
||||||
if isinstance(portspec, str):
|
if isinstance(portspec, str):
|
||||||
portspec = [portspec]
|
portspec = [portspec]
|
||||||
ports = self[tuple(portspec)]
|
ports = self.pattern[tuple(portspec)]
|
||||||
|
|
||||||
extensions = ell(ports, ccw, spacing=spacing, bound=bound, bound_type=bound_type, set_rotation=set_rotation)
|
extensions = ell(ports, ccw, spacing=spacing, bound=bound, bound_type=bound_type, set_rotation=set_rotation)
|
||||||
|
|
||||||
@ -529,10 +546,12 @@ class Device(PortList):
|
|||||||
port_name = tuple(portspec)[0]
|
port_name = tuple(portspec)[0]
|
||||||
return self.path(port_name, ccw, extensions[port_name], tool_port_names=tool_port_names)
|
return self.path(port_name, ccw, extensions[port_name], tool_port_names=tool_port_names)
|
||||||
else:
|
else:
|
||||||
dev = Device(name='', ports=ports, tools=self.tools).as_interface()
|
bld = ports.as_interface(self.library, tools=self.tools)
|
||||||
for name, length in extensions.items():
|
for port_name, length in extensions.items():
|
||||||
dev.path(name, ccw, length, tool_port_names=tool_port_names)
|
bld.path(port_name, ccw, length, tool_port_names=tool_port_names)
|
||||||
return self.plug(dev, {sp: 'in_' + sp for sp in ports.keys()}) # TODO safe to use 'in_'?
|
name = self.library.get_name(base_name)
|
||||||
|
self.library._set(name, bld.pattern)
|
||||||
|
return self.plug(PortsRef(name, pat.ports), {sp: 'in_' + sp for sp in ports.keys()}) # TODO safe to use 'in_'?
|
||||||
|
|
||||||
# TODO def path_join() and def bus_join()?
|
# TODO def path_join() and def bus_join()?
|
||||||
|
|
||||||
|
@ -9,11 +9,11 @@ from ..utils import rotation_matrix_2d
|
|||||||
from ..error import BuildError
|
from ..error import BuildError
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .devices import Port
|
from ..ports import Port, PortList
|
||||||
|
|
||||||
|
|
||||||
def ell(
|
def ell(
|
||||||
ports: Dict[str, 'Port'],
|
ports: Union[Mapping[str, 'Port'], 'PortList'],
|
||||||
ccw: Optional[bool],
|
ccw: Optional[bool],
|
||||||
bound_type: str,
|
bound_type: str,
|
||||||
bound: Union[float, ArrayLike],
|
bound: Union[float, ArrayLike],
|
||||||
|
@ -13,7 +13,7 @@ import gzip
|
|||||||
import numpy
|
import numpy
|
||||||
import ezdxf # type: ignore
|
import ezdxf # type: ignore
|
||||||
|
|
||||||
from .. import Pattern, SubPattern, PatternError, Label, Shape
|
from .. import Pattern, Ref, PatternError, Label, Shape
|
||||||
from ..shapes import Polygon, Path
|
from ..shapes import Polygon, Path
|
||||||
from ..repetition import Grid
|
from ..repetition import Grid
|
||||||
from ..utils import rotation_matrix_2d, layer_t
|
from ..utils import rotation_matrix_2d, layer_t
|
||||||
@ -39,7 +39,7 @@ def write(
|
|||||||
"""
|
"""
|
||||||
Write a `Pattern` to a DXF file, by first calling `.polygonize()` to change the shapes
|
Write a `Pattern` to a DXF file, by first calling `.polygonize()` to change the shapes
|
||||||
into polygons, and then writing patterns as DXF `Block`s, polygons as `LWPolyline`s,
|
into polygons, and then writing patterns as DXF `Block`s, polygons as `LWPolyline`s,
|
||||||
and subpatterns as `Insert`s.
|
and refs as `Insert`s.
|
||||||
|
|
||||||
The top level pattern's name is not written to the DXF file. Nested patterns keep their
|
The top level pattern's name is not written to the DXF file. Nested patterns keep their
|
||||||
names.
|
names.
|
||||||
@ -49,7 +49,7 @@ def write(
|
|||||||
tuple: (1, 2) -> '1.2'
|
tuple: (1, 2) -> '1.2'
|
||||||
str: '1.2' -> '1.2' (no change)
|
str: '1.2' -> '1.2' (no change)
|
||||||
|
|
||||||
It is often a good idea to run `pattern.subpatternize()` prior to calling this function,
|
It is often a good idea to run `pattern.dedup()` prior to calling this function,
|
||||||
especially if calling `.polygonize()` will result in very many vertices.
|
especially if calling `.polygonize()` will result in very many vertices.
|
||||||
|
|
||||||
If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()`
|
If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()`
|
||||||
@ -86,7 +86,7 @@ def write(
|
|||||||
msp = lib.modelspace()
|
msp = lib.modelspace()
|
||||||
_shapes_to_elements(msp, pattern.shapes)
|
_shapes_to_elements(msp, pattern.shapes)
|
||||||
_labels_to_texts(msp, pattern.labels)
|
_labels_to_texts(msp, pattern.labels)
|
||||||
_subpatterns_to_refs(msp, pattern.subpatterns)
|
_mrefs_to_drefs(msp, pattern.refs)
|
||||||
|
|
||||||
# Now create a block for each referenced pattern, and add in any shapes
|
# Now create a block for each referenced pattern, and add in any shapes
|
||||||
for name, pat in library.items():
|
for name, pat in library.items():
|
||||||
@ -95,7 +95,7 @@ def write(
|
|||||||
|
|
||||||
_shapes_to_elements(block, pat.shapes)
|
_shapes_to_elements(block, pat.shapes)
|
||||||
_labels_to_texts(block, pat.labels)
|
_labels_to_texts(block, pat.labels)
|
||||||
_subpatterns_to_refs(block, pat.subpatterns)
|
_mrefs_to_drefs(block, pat.refs)
|
||||||
|
|
||||||
lib.write(stream)
|
lib.write(stream)
|
||||||
|
|
||||||
@ -163,7 +163,7 @@ def read(
|
|||||||
"""
|
"""
|
||||||
Read a dxf file and translate it into a dict of `Pattern` objects. DXF `Block`s are
|
Read a dxf file and translate it into a dict of `Pattern` objects. DXF `Block`s are
|
||||||
translated into `Pattern` objects; `LWPolyline`s are translated into polygons, and `Insert`s
|
translated into `Pattern` objects; `LWPolyline`s are translated into polygons, and `Insert`s
|
||||||
are translated into `SubPattern` objects.
|
are translated into `Ref` objects.
|
||||||
|
|
||||||
If an object has no layer it is set to this module's `DEFAULT_LAYER` ("DEFAULT").
|
If an object has no layer it is set to this module's `DEFAULT_LAYER` ("DEFAULT").
|
||||||
|
|
||||||
@ -268,57 +268,57 @@ def _read_block(block, clean_vertices: bool) -> Tuple[str, Pattern]:
|
|||||||
b_vector=(0, attr['row_spacing']),
|
b_vector=(0, attr['row_spacing']),
|
||||||
a_count=attr['column_count'],
|
a_count=attr['column_count'],
|
||||||
b_count=attr['row_count'])
|
b_count=attr['row_count'])
|
||||||
pat.subpatterns.append(SubPattern(**args))
|
pat.ref(**args)
|
||||||
else:
|
else:
|
||||||
logger.warning(f'Ignoring DXF element {element.dxftype()} (not implemented).')
|
logger.warning(f'Ignoring DXF element {element.dxftype()} (not implemented).')
|
||||||
return name, pat
|
return name, pat
|
||||||
|
|
||||||
|
|
||||||
def _subpatterns_to_refs(
|
def _mrefs_to_drefs(
|
||||||
block: Union[ezdxf.layouts.BlockLayout, ezdxf.layouts.Modelspace],
|
block: Union[ezdxf.layouts.BlockLayout, ezdxf.layouts.Modelspace],
|
||||||
subpatterns: List[SubPattern],
|
refs: List[Ref],
|
||||||
) -> None:
|
) -> None:
|
||||||
for subpat in subpatterns:
|
for ref in refs:
|
||||||
if subpat.target is None:
|
if ref.target is None:
|
||||||
continue
|
continue
|
||||||
encoded_name = subpat.target
|
encoded_name = ref.target
|
||||||
|
|
||||||
rotation = (subpat.rotation * 180 / numpy.pi) % 360
|
rotation = (ref.rotation * 180 / numpy.pi) % 360
|
||||||
attribs = {
|
attribs = {
|
||||||
'xscale': subpat.scale * (-1 if subpat.mirrored[1] else 1),
|
'xscale': ref.scale * (-1 if ref.mirrored[1] else 1),
|
||||||
'yscale': subpat.scale * (-1 if subpat.mirrored[0] else 1),
|
'yscale': ref.scale * (-1 if ref.mirrored[0] else 1),
|
||||||
'rotation': rotation,
|
'rotation': rotation,
|
||||||
}
|
}
|
||||||
|
|
||||||
rep = subpat.repetition
|
rep = ref.repetition
|
||||||
if rep is None:
|
if rep is None:
|
||||||
block.add_blockref(encoded_name, subpat.offset, dxfattribs=attribs)
|
block.add_blockref(encoded_name, ref.offset, dxfattribs=attribs)
|
||||||
elif isinstance(rep, Grid):
|
elif isinstance(rep, Grid):
|
||||||
a = rep.a_vector
|
a = rep.a_vector
|
||||||
b = rep.b_vector if rep.b_vector is not None else numpy.zeros(2)
|
b = rep.b_vector if rep.b_vector is not None else numpy.zeros(2)
|
||||||
rotated_a = rotation_matrix_2d(-subpat.rotation) @ a
|
rotated_a = rotation_matrix_2d(-ref.rotation) @ a
|
||||||
rotated_b = rotation_matrix_2d(-subpat.rotation) @ b
|
rotated_b = rotation_matrix_2d(-ref.rotation) @ b
|
||||||
if rotated_a[1] == 0 and rotated_b[0] == 0:
|
if rotated_a[1] == 0 and rotated_b[0] == 0:
|
||||||
attribs['column_count'] = rep.a_count
|
attribs['column_count'] = rep.a_count
|
||||||
attribs['row_count'] = rep.b_count
|
attribs['row_count'] = rep.b_count
|
||||||
attribs['column_spacing'] = rotated_a[0]
|
attribs['column_spacing'] = rotated_a[0]
|
||||||
attribs['row_spacing'] = rotated_b[1]
|
attribs['row_spacing'] = rotated_b[1]
|
||||||
block.add_blockref(encoded_name, subpat.offset, dxfattribs=attribs)
|
block.add_blockref(encoded_name, ref.offset, dxfattribs=attribs)
|
||||||
elif rotated_a[0] == 0 and rotated_b[1] == 0:
|
elif rotated_a[0] == 0 and rotated_b[1] == 0:
|
||||||
attribs['column_count'] = rep.b_count
|
attribs['column_count'] = rep.b_count
|
||||||
attribs['row_count'] = rep.a_count
|
attribs['row_count'] = rep.a_count
|
||||||
attribs['column_spacing'] = rotated_b[0]
|
attribs['column_spacing'] = rotated_b[0]
|
||||||
attribs['row_spacing'] = rotated_a[1]
|
attribs['row_spacing'] = rotated_a[1]
|
||||||
block.add_blockref(encoded_name, subpat.offset, dxfattribs=attribs)
|
block.add_blockref(encoded_name, ref.offset, dxfattribs=attribs)
|
||||||
else:
|
else:
|
||||||
#NOTE: We could still do non-manhattan (but still orthogonal) grids by getting
|
#NOTE: We could still do non-manhattan (but still orthogonal) grids by getting
|
||||||
# creative with counter-rotated nested patterns, but probably not worth it.
|
# creative with counter-rotated nested patterns, but probably not worth it.
|
||||||
# Instead, just break appart the grid into individual elements:
|
# Instead, just break appart the grid into individual elements:
|
||||||
for dd in rep.displacements:
|
for dd in rep.displacements:
|
||||||
block.add_blockref(encoded_name, subpat.offset + dd, dxfattribs=attribs)
|
block.add_blockref(encoded_name, ref.offset + dd, dxfattribs=attribs)
|
||||||
else:
|
else:
|
||||||
for dd in rep.displacements:
|
for dd in rep.displacements:
|
||||||
block.add_blockref(encoded_name, subpat.offset + dd, dxfattribs=attribs)
|
block.add_blockref(encoded_name, ref.offset + dd, dxfattribs=attribs)
|
||||||
|
|
||||||
|
|
||||||
def _shapes_to_elements(
|
def _shapes_to_elements(
|
||||||
|
@ -36,7 +36,7 @@ import klamath
|
|||||||
from klamath import records
|
from klamath import records
|
||||||
|
|
||||||
from .utils import is_gzipped
|
from .utils import is_gzipped
|
||||||
from .. import Pattern, SubPattern, PatternError, Label, Shape
|
from .. import Pattern, Ref, PatternError, Label, Shape
|
||||||
from ..shapes import Polygon, Path
|
from ..shapes import Polygon, Path
|
||||||
from ..repetition import Grid
|
from ..repetition import Grid
|
||||||
from ..utils import layer_t, normalize_mirror, annotations_t
|
from ..utils import layer_t, normalize_mirror, annotations_t
|
||||||
@ -70,7 +70,7 @@ def write(
|
|||||||
"""
|
"""
|
||||||
Convert a library to a GDSII stream, mapping data as follows:
|
Convert a library to a GDSII stream, mapping data as follows:
|
||||||
Pattern -> GDSII structure
|
Pattern -> GDSII structure
|
||||||
SubPattern -> GDSII SREF or AREF
|
Ref -> GDSII SREF or AREF
|
||||||
Path -> GSDII path
|
Path -> GSDII path
|
||||||
Shape (other than path) -> GDSII boundary/ies
|
Shape (other than path) -> GDSII boundary/ies
|
||||||
Label -> GDSII text
|
Label -> GDSII text
|
||||||
@ -82,7 +82,7 @@ def write(
|
|||||||
datatype is chosen to be `shape.layer[1]` if available,
|
datatype is chosen to be `shape.layer[1]` if available,
|
||||||
otherwise `0`
|
otherwise `0`
|
||||||
|
|
||||||
It is often a good idea to run `pattern.subpatternize()` prior to calling this function,
|
It is often a good idea to run `pattern.dedup()` prior to calling this function,
|
||||||
especially if calling `.polygonize()` will result in very many vertices.
|
especially if calling `.polygonize()` will result in very many vertices.
|
||||||
|
|
||||||
If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()`
|
If you want pattern polygonized with non-default arguments, just call `pattern.polygonize()`
|
||||||
@ -107,7 +107,7 @@ def write(
|
|||||||
# TODO check all hierarchy present
|
# TODO check all hierarchy present
|
||||||
|
|
||||||
if not modify_originals:
|
if not modify_originals:
|
||||||
library = library.deepcopy() #TODO figure out best approach e.g. if lazy
|
library = copy.deepcopy(library) #TODO figure out best approach e.g. if lazy
|
||||||
|
|
||||||
if not isinstance(library, MutableLibrary):
|
if not isinstance(library, MutableLibrary):
|
||||||
if isinstance(library, dict):
|
if isinstance(library, dict):
|
||||||
@ -130,7 +130,7 @@ def write(
|
|||||||
elements: List[klamath.elements.Element] = []
|
elements: List[klamath.elements.Element] = []
|
||||||
elements += _shapes_to_elements(pat.shapes)
|
elements += _shapes_to_elements(pat.shapes)
|
||||||
elements += _labels_to_texts(pat.labels)
|
elements += _labels_to_texts(pat.labels)
|
||||||
elements += _subpatterns_to_refs(pat.subpatterns)
|
elements += _mrefs_to_grefs(pat.refs)
|
||||||
|
|
||||||
klamath.library.write_struct(stream, name=name.encode('ASCII'), elements=elements)
|
klamath.library.write_struct(stream, name=name.encode('ASCII'), elements=elements)
|
||||||
records.ENDLIB.write(stream, None)
|
records.ENDLIB.write(stream, None)
|
||||||
@ -196,7 +196,7 @@ def read(
|
|||||||
"""
|
"""
|
||||||
Read a gdsii file and translate it into a dict of Pattern objects. GDSII structures are
|
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
|
translated into Pattern objects; boundaries are translated into polygons, and srefs and arefs
|
||||||
are translated into SubPattern objects.
|
are translated into Ref objects.
|
||||||
|
|
||||||
Additional library info is returned in a dict, containing:
|
Additional library info is returned in a dict, containing:
|
||||||
'name': name of the library
|
'name': name of the library
|
||||||
@ -273,7 +273,7 @@ def read_elements(
|
|||||||
)
|
)
|
||||||
pat.labels.append(label)
|
pat.labels.append(label)
|
||||||
elif isinstance(element, klamath.elements.Reference):
|
elif isinstance(element, klamath.elements.Reference):
|
||||||
pat.subpatterns.append(_ref_to_subpat(element))
|
pat.refs.append(_gref_to_mref(element))
|
||||||
return pat
|
return pat
|
||||||
|
|
||||||
|
|
||||||
@ -293,9 +293,9 @@ def _mlayer2gds(mlayer: layer_t) -> Tuple[int, int]:
|
|||||||
return layer, data_type
|
return layer, data_type
|
||||||
|
|
||||||
|
|
||||||
def _ref_to_subpat(ref: klamath.library.Reference) -> SubPattern:
|
def _gref_to_mref(ref: klamath.library.Reference) -> Ref:
|
||||||
"""
|
"""
|
||||||
Helper function to create a SubPattern from an SREF or AREF. Sets subpat.target to struct_name.
|
Helper function to create a Ref from an SREF or AREF. Sets ref.target to struct_name.
|
||||||
"""
|
"""
|
||||||
xy = ref.xy.astype(float)
|
xy = ref.xy.astype(float)
|
||||||
offset = xy[0]
|
offset = xy[0]
|
||||||
@ -307,7 +307,7 @@ def _ref_to_subpat(ref: klamath.library.Reference) -> SubPattern:
|
|||||||
repetition = Grid(a_vector=a_vector, b_vector=b_vector,
|
repetition = Grid(a_vector=a_vector, b_vector=b_vector,
|
||||||
a_count=a_count, b_count=b_count)
|
a_count=a_count, b_count=b_count)
|
||||||
|
|
||||||
subpat = SubPattern(
|
ref = Ref(
|
||||||
target=ref.struct_name.decode('ASCII'),
|
target=ref.struct_name.decode('ASCII'),
|
||||||
offset=offset,
|
offset=offset,
|
||||||
rotation=numpy.deg2rad(ref.angle_deg),
|
rotation=numpy.deg2rad(ref.angle_deg),
|
||||||
@ -316,7 +316,7 @@ def _ref_to_subpat(ref: klamath.library.Reference) -> SubPattern:
|
|||||||
annotations=_properties_to_annotations(ref.properties),
|
annotations=_properties_to_annotations(ref.properties),
|
||||||
repetition=repetition,
|
repetition=repetition,
|
||||||
)
|
)
|
||||||
return subpat
|
return ref
|
||||||
|
|
||||||
|
|
||||||
def _gpath_to_mpath(gpath: klamath.library.Path, raw_mode: bool) -> Path:
|
def _gpath_to_mpath(gpath: klamath.library.Path, raw_mode: bool) -> Path:
|
||||||
@ -349,45 +349,45 @@ def _boundary_to_polygon(boundary: klamath.library.Boundary, raw_mode: bool) ->
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _subpatterns_to_refs(subpatterns: List[SubPattern]) -> List[klamath.library.Reference]:
|
def _mrefs_to_grefs(refs: List[Ref]) -> List[klamath.library.Reference]:
|
||||||
refs = []
|
refs = []
|
||||||
for subpat in subpatterns:
|
for ref in refs:
|
||||||
if subpat.target is None:
|
if ref.target is None:
|
||||||
continue
|
continue
|
||||||
encoded_name = subpat.target.encode('ASCII')
|
encoded_name = ref.target.encode('ASCII')
|
||||||
|
|
||||||
# Note: GDS mirrors first and rotates second
|
# Note: GDS mirrors first and rotates second
|
||||||
mirror_across_x, extra_angle = normalize_mirror(subpat.mirrored)
|
mirror_across_x, extra_angle = normalize_mirror(ref.mirrored)
|
||||||
rep = subpat.repetition
|
rep = ref.repetition
|
||||||
angle_deg = numpy.rad2deg(subpat.rotation + extra_angle) % 360
|
angle_deg = numpy.rad2deg(ref.rotation + extra_angle) % 360
|
||||||
properties = _annotations_to_properties(subpat.annotations, 512)
|
properties = _annotations_to_properties(ref.annotations, 512)
|
||||||
|
|
||||||
if isinstance(rep, Grid):
|
if isinstance(rep, Grid):
|
||||||
b_vector = rep.b_vector if rep.b_vector is not None else numpy.zeros(2)
|
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
|
b_count = rep.b_count if rep.b_count is not None else 1
|
||||||
xy: NDArray[numpy.float64] = numpy.array(subpat.offset) + [
|
xy = numpy.array(ref.offset) + numpy.array([
|
||||||
[0, 0],
|
[0.0, 0.0],
|
||||||
rep.a_vector * rep.a_count,
|
rep.a_vector * rep.a_count,
|
||||||
b_vector * b_count,
|
b_vector * b_count,
|
||||||
]
|
])
|
||||||
aref = klamath.library.Reference(
|
aref = klamath.library.Reference(
|
||||||
struct_name=encoded_name,
|
struct_name=encoded_name,
|
||||||
xy=rint_cast(xy),
|
xy=rint_cast(xy),
|
||||||
colrow=(numpy.rint(rep.a_count), numpy.rint(rep.b_count)),
|
colrow=(numpy.rint(rep.a_count), numpy.rint(rep.b_count)),
|
||||||
angle_deg=angle_deg,
|
angle_deg=angle_deg,
|
||||||
invert_y=mirror_across_x,
|
invert_y=mirror_across_x,
|
||||||
mag=subpat.scale,
|
mag=ref.scale,
|
||||||
properties=properties,
|
properties=properties,
|
||||||
)
|
)
|
||||||
refs.append(aref)
|
refs.append(aref)
|
||||||
elif rep is None:
|
elif rep is None:
|
||||||
ref = klamath.library.Reference(
|
ref = klamath.library.Reference(
|
||||||
struct_name=encoded_name,
|
struct_name=encoded_name,
|
||||||
xy=rint_cast([subpat.offset]),
|
xy=rint_cast([ref.offset]),
|
||||||
colrow=None,
|
colrow=None,
|
||||||
angle_deg=angle_deg,
|
angle_deg=angle_deg,
|
||||||
invert_y=mirror_across_x,
|
invert_y=mirror_across_x,
|
||||||
mag=subpat.scale,
|
mag=ref.scale,
|
||||||
properties=properties,
|
properties=properties,
|
||||||
)
|
)
|
||||||
refs.append(ref)
|
refs.append(ref)
|
||||||
@ -395,11 +395,11 @@ def _subpatterns_to_refs(subpatterns: List[SubPattern]) -> List[klamath.library.
|
|||||||
new_srefs = [
|
new_srefs = [
|
||||||
klamath.library.Reference(
|
klamath.library.Reference(
|
||||||
struct_name=encoded_name,
|
struct_name=encoded_name,
|
||||||
xy=rint_cast([subpat.offset + dd]),
|
xy=rint_cast([ref.offset + dd]),
|
||||||
colrow=None,
|
colrow=None,
|
||||||
angle_deg=angle_deg,
|
angle_deg=angle_deg,
|
||||||
invert_y=mirror_across_x,
|
invert_y=mirror_across_x,
|
||||||
mag=subpat.scale,
|
mag=ref.scale,
|
||||||
properties=properties,
|
properties=properties,
|
||||||
)
|
)
|
||||||
for dd in rep.displacements]
|
for dd in rep.displacements]
|
||||||
@ -636,6 +636,7 @@ def load_libraryfile(
|
|||||||
Additional library info (dict, same format as from `read`).
|
Additional library info (dict, same format as from `read`).
|
||||||
"""
|
"""
|
||||||
path = pathlib.Path(filename)
|
path = pathlib.Path(filename)
|
||||||
|
stream: BinaryIO
|
||||||
if is_gzipped(path):
|
if is_gzipped(path):
|
||||||
if mmap:
|
if mmap:
|
||||||
logger.info('Asked to mmap a gzipped file, reading into memory instead...')
|
logger.info('Asked to mmap a gzipped file, reading into memory instead...')
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
# FOr backwards compatibility
|
|
||||||
from .gdsii import *
|
|
@ -28,7 +28,7 @@ import fatamorgana.records as fatrec
|
|||||||
from fatamorgana.basic import PathExtensionScheme, AString, NString, PropStringReference
|
from fatamorgana.basic import PathExtensionScheme, AString, NString, PropStringReference
|
||||||
|
|
||||||
from .utils import is_gzipped
|
from .utils import is_gzipped
|
||||||
from .. import Pattern, SubPattern, PatternError, Label, Shape
|
from .. import Pattern, Ref, PatternError, Label, Shape
|
||||||
from ..library import WrapLibrary, MutableLibrary
|
from ..library import WrapLibrary, MutableLibrary
|
||||||
from ..shapes import Polygon, Path, Circle
|
from ..shapes import Polygon, Path, Circle
|
||||||
from ..repetition import Grid, Arbitrary, Repetition
|
from ..repetition import Grid, Arbitrary, Repetition
|
||||||
@ -62,7 +62,7 @@ def build(
|
|||||||
) -> fatamorgana.OasisLayout:
|
) -> fatamorgana.OasisLayout:
|
||||||
"""
|
"""
|
||||||
Convert a collection of {name: Pattern} pairs to an OASIS stream, writing patterns
|
Convert a collection of {name: Pattern} pairs to an OASIS stream, writing patterns
|
||||||
as OASIS cells, subpatterns as Placement records, and mapping other shapes and labels
|
as OASIS cells, refs as Placement records, and mapping other shapes and labels
|
||||||
to equivalent record types (Polygon, Path, Circle, Text).
|
to equivalent record types (Polygon, Path, Circle, Text).
|
||||||
Other shape types may be converted to polygons if no equivalent
|
Other shape types may be converted to polygons if no equivalent
|
||||||
record type exists (or is not implemented here yet).
|
record type exists (or is not implemented here yet).
|
||||||
@ -148,7 +148,7 @@ def build(
|
|||||||
|
|
||||||
structure.geometry += _shapes_to_elements(pat.shapes, layer2oas)
|
structure.geometry += _shapes_to_elements(pat.shapes, layer2oas)
|
||||||
structure.geometry += _labels_to_texts(pat.labels, layer2oas)
|
structure.geometry += _labels_to_texts(pat.labels, layer2oas)
|
||||||
structure.placements += _subpatterns_to_placements(pat.subpatterns)
|
structure.placements += _refs_to_placements(pat.refs)
|
||||||
|
|
||||||
return lib
|
return lib
|
||||||
|
|
||||||
@ -232,7 +232,7 @@ def read(
|
|||||||
"""
|
"""
|
||||||
Read a OASIS file and translate it into a dict of Pattern objects. OASIS cells are
|
Read a OASIS file and translate it into a dict of Pattern objects. OASIS cells are
|
||||||
translated into Pattern objects; Polygons are translated into polygons, and Placements
|
translated into Pattern objects; Polygons are translated into polygons, and Placements
|
||||||
are translated into SubPattern objects.
|
are translated into Ref objects.
|
||||||
|
|
||||||
Additional library info is returned in a dict, containing:
|
Additional library info is returned in a dict, containing:
|
||||||
'units_per_micrometer': number of database units per micrometer (all values are in database units)
|
'units_per_micrometer': number of database units per micrometer (all values are in database units)
|
||||||
@ -456,7 +456,7 @@ def read(
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
for placement in cell.placements:
|
for placement in cell.placements:
|
||||||
pat.subpatterns.append(_placement_to_subpat(placement, lib))
|
pat.refs.append(_placement_to_ref(placement, lib))
|
||||||
|
|
||||||
patterns_dict[cell_name] = pat
|
patterns_dict[cell_name] = pat
|
||||||
|
|
||||||
@ -480,9 +480,9 @@ def _mlayer2oas(mlayer: layer_t) -> Tuple[int, int]:
|
|||||||
return layer, data_type
|
return layer, data_type
|
||||||
|
|
||||||
|
|
||||||
def _placement_to_subpat(placement: fatrec.Placement, lib: fatamorgana.OasisLayout) -> SubPattern:
|
def _placement_to_ref(placement: fatrec.Placement, lib: fatamorgana.OasisLayout) -> Ref:
|
||||||
"""
|
"""
|
||||||
Helper function to create a SubPattern from a placment. Sets subpat.target to the placement name.
|
Helper function to create a Ref from a placment. Sets ref.target to the placement name.
|
||||||
"""
|
"""
|
||||||
assert(not isinstance(placement.repetition, fatamorgana.ReuseRepetition))
|
assert(not isinstance(placement.repetition, fatamorgana.ReuseRepetition))
|
||||||
xy = numpy.array((placement.x, placement.y))
|
xy = numpy.array((placement.x, placement.y))
|
||||||
@ -494,7 +494,7 @@ def _placement_to_subpat(placement: fatrec.Placement, lib: fatamorgana.OasisLayo
|
|||||||
rotation = 0
|
rotation = 0
|
||||||
else:
|
else:
|
||||||
rotation = numpy.deg2rad(float(placement.angle))
|
rotation = numpy.deg2rad(float(placement.angle))
|
||||||
subpat = SubPattern(
|
ref = Ref(
|
||||||
target=name,
|
target=name,
|
||||||
offset=xy,
|
offset=xy,
|
||||||
mirrored=(placement.flip, False),
|
mirrored=(placement.flip, False),
|
||||||
@ -503,29 +503,29 @@ def _placement_to_subpat(placement: fatrec.Placement, lib: fatamorgana.OasisLayo
|
|||||||
repetition=repetition_fata2masq(placement.repetition),
|
repetition=repetition_fata2masq(placement.repetition),
|
||||||
annotations=annotations,
|
annotations=annotations,
|
||||||
)
|
)
|
||||||
return subpat
|
return ref
|
||||||
|
|
||||||
|
|
||||||
def _subpatterns_to_placements(
|
def _refs_to_placements(
|
||||||
subpatterns: List[SubPattern],
|
refs: List[Ref],
|
||||||
) -> List[fatrec.Placement]:
|
) -> List[fatrec.Placement]:
|
||||||
refs = []
|
refs = []
|
||||||
for subpat in subpatterns:
|
for ref in refs:
|
||||||
if subpat.target is None:
|
if ref.target is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Note: OASIS mirrors first and rotates second
|
# Note: OASIS mirrors first and rotates second
|
||||||
mirror_across_x, extra_angle = normalize_mirror(subpat.mirrored)
|
mirror_across_x, extra_angle = normalize_mirror(ref.mirrored)
|
||||||
frep, rep_offset = repetition_masq2fata(subpat.repetition)
|
frep, rep_offset = repetition_masq2fata(ref.repetition)
|
||||||
|
|
||||||
offset = rint_cast(subpat.offset + rep_offset)
|
offset = rint_cast(ref.offset + rep_offset)
|
||||||
angle = numpy.rad2deg(subpat.rotation + extra_angle) % 360
|
angle = numpy.rad2deg(ref.rotation + extra_angle) % 360
|
||||||
ref = fatrec.Placement(
|
ref = fatrec.Placement(
|
||||||
name=subpat.target,
|
name=ref.target,
|
||||||
flip=mirror_across_x,
|
flip=mirror_across_x,
|
||||||
angle=angle,
|
angle=angle,
|
||||||
magnification=subpat.scale,
|
magnification=ref.scale,
|
||||||
properties=annotations_to_properties(subpat.annotations),
|
properties=annotations_to_properties(ref.annotations),
|
||||||
x=offset[0],
|
x=offset[0],
|
||||||
y=offset[1],
|
y=offset[1],
|
||||||
repetition=frep,
|
repetition=frep,
|
||||||
|
@ -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
|
|
||||||
|
|
@ -21,7 +21,7 @@ def writefile(
|
|||||||
"""
|
"""
|
||||||
Write a Pattern to an SVG file, by first calling .polygonize() on it
|
Write a Pattern to an SVG file, by first calling .polygonize() on it
|
||||||
to change the shapes into polygons, and then writing patterns as SVG
|
to change the shapes into polygons, and then writing patterns as SVG
|
||||||
groups (<g>, inside <defs>), polygons as paths (<path>), and subpatterns
|
groups (<g>, inside <defs>), polygons as paths (<path>), and refs
|
||||||
as <use> elements.
|
as <use> elements.
|
||||||
|
|
||||||
Note that this function modifies the Pattern.
|
Note that this function modifies the Pattern.
|
||||||
@ -29,7 +29,7 @@ def writefile(
|
|||||||
If `custom_attributes` is `True`, a non-standard `pattern_layer` attribute
|
If `custom_attributes` is `True`, a non-standard `pattern_layer` attribute
|
||||||
is written to the relevant elements.
|
is written to the relevant elements.
|
||||||
|
|
||||||
It is often a good idea to run `pattern.subpatternize()` on pattern prior to
|
It is often a good idea to run `pattern.dedup()` on pattern prior to
|
||||||
calling this function, especially if calling `.polygonize()` will result in very
|
calling this function, especially if calling `.polygonize()` will result in very
|
||||||
many vertices.
|
many vertices.
|
||||||
|
|
||||||
@ -75,11 +75,11 @@ def writefile(
|
|||||||
|
|
||||||
svg_group.add(path)
|
svg_group.add(path)
|
||||||
|
|
||||||
for subpat in pat.subpatterns:
|
for ref in pat.refs:
|
||||||
if subpat.target is None:
|
if ref.target is None:
|
||||||
continue
|
continue
|
||||||
transform = f'scale({subpat.scale:g}) rotate({subpat.rotation:g}) translate({subpat.offset[0]:g},{subpat.offset[1]:g})'
|
transform = f'scale({ref.scale:g}) rotate({ref.rotation:g}) translate({ref.offset[0]:g},{ref.offset[1]:g})'
|
||||||
use = svg.use(href='#' + mangle_name(subpat.target), transform=transform)
|
use = svg.use(href='#' + mangle_name(ref.target), transform=transform)
|
||||||
svg_group.add(use)
|
svg_group.add(use)
|
||||||
|
|
||||||
svg.defs.add(svg_group)
|
svg.defs.add(svg_group)
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
Library class for managing unique name->pattern mappings and
|
Library class for managing unique name->pattern mappings and
|
||||||
deferred loading or creation.
|
deferred loading or creation.
|
||||||
"""
|
"""
|
||||||
from typing import List, Dict, Callable, TypeVar, Type, TYPE_CHECKING
|
from typing import List, Dict, Callable, TypeVar, Generic, Type, TYPE_CHECKING
|
||||||
from typing import Any, Tuple, Union, Iterator, Mapping, MutableMapping, Set, Optional, Sequence
|
from typing import Any, Tuple, Union, Iterator, Mapping, MutableMapping, Set, Optional, Sequence
|
||||||
import logging
|
import logging
|
||||||
import copy
|
import copy
|
||||||
@ -31,7 +31,7 @@ logger = logging.getLogger(__name__)
|
|||||||
visitor_function_t = Callable[['Pattern', Tuple['Pattern'], Dict, NDArray[numpy.float64]], 'Pattern']
|
visitor_function_t = Callable[['Pattern', Tuple['Pattern'], Dict, NDArray[numpy.float64]], 'Pattern']
|
||||||
L = TypeVar('L', bound='Library')
|
L = TypeVar('L', bound='Library')
|
||||||
ML = TypeVar('ML', bound='MutableLibrary')
|
ML = TypeVar('ML', bound='MutableLibrary')
|
||||||
#LL = TypeVar('LL', bound='LazyLibrary')
|
LL = TypeVar('LL', bound='LazyLibrary')
|
||||||
|
|
||||||
|
|
||||||
class Library(Mapping[str, Pattern], metaclass=ABCMeta):
|
class Library(Mapping[str, Pattern], metaclass=ABCMeta):
|
||||||
@ -51,7 +51,7 @@ class Library(Mapping[str, Pattern], metaclass=ABCMeta):
|
|||||||
skip: Optional[Set[Optional[str]]] = None,
|
skip: Optional[Set[Optional[str]]] = None,
|
||||||
) -> Set[Optional[str]]:
|
) -> Set[Optional[str]]:
|
||||||
"""
|
"""
|
||||||
Get the set of all pattern names referenced by `top`. Recursively traverses into any subpatterns.
|
Get the set of all pattern names referenced by `top`. Recursively traverses into any refs.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
top: Name of the top pattern(s) to check.
|
top: Name of the top pattern(s) to check.
|
||||||
@ -83,7 +83,7 @@ class Library(Mapping[str, Pattern], metaclass=ABCMeta):
|
|||||||
def subtree(
|
def subtree(
|
||||||
self,
|
self,
|
||||||
tops: Union[str, Sequence[str]],
|
tops: Union[str, Sequence[str]],
|
||||||
) -> WrapLibrary:
|
) -> Library:
|
||||||
"""
|
"""
|
||||||
Return a new `Library`, containing only the specified patterns and the patterns they
|
Return a new `Library`, containing only the specified patterns and the patterns they
|
||||||
reference (recursively).
|
reference (recursively).
|
||||||
@ -92,12 +92,12 @@ class Library(Mapping[str, Pattern], metaclass=ABCMeta):
|
|||||||
tops: Name(s) of patterns to keep
|
tops: Name(s) of patterns to keep
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A `Library` containing only `tops` and the patterns they reference.
|
A `WrapROLibrary` containing only `tops` and the patterns they reference.
|
||||||
"""
|
"""
|
||||||
keep: Set[str] = self.referenced_patterns(tops) - set((None,)) # type: ignore
|
keep: Set[str] = self.referenced_patterns(tops) - set((None,)) # type: ignore
|
||||||
|
|
||||||
filtered = {kk: vv for kk, vv in self.items() if kk in keep}
|
filtered = {kk: vv for kk, vv in self.items() if kk in keep}
|
||||||
new = WrapLibrary(filtered)
|
new = WrapROLibrary(filtered)
|
||||||
return new
|
return new
|
||||||
|
|
||||||
def polygonize(
|
def polygonize(
|
||||||
@ -147,8 +147,8 @@ class Library(Mapping[str, Pattern], metaclass=ABCMeta):
|
|||||||
tops: Union[str, Sequence[str]],
|
tops: Union[str, Sequence[str]],
|
||||||
) -> Dict[str, 'Pattern']:
|
) -> Dict[str, 'Pattern']:
|
||||||
"""
|
"""
|
||||||
Removes all subpatterns and adds equivalent shapes.
|
Removes all refs and adds equivalent shapes.
|
||||||
Also flattens all subpatterns.
|
Also flattens all referenced patterns.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
tops: The pattern(s) to flattern.
|
tops: The pattern(s) to flattern.
|
||||||
@ -165,8 +165,8 @@ class Library(Mapping[str, Pattern], metaclass=ABCMeta):
|
|||||||
flattened[name] = None
|
flattened[name] = None
|
||||||
pat = self[name].deepcopy()
|
pat = self[name].deepcopy()
|
||||||
|
|
||||||
for subpat in pat.subpatterns:
|
for ref in pat.refs:
|
||||||
target = subpat.target
|
target = ref.target
|
||||||
if target is None:
|
if target is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -175,10 +175,10 @@ class Library(Mapping[str, Pattern], metaclass=ABCMeta):
|
|||||||
if flattened[target] is None:
|
if flattened[target] is None:
|
||||||
raise PatternError(f'Circular reference in {name} to {target}')
|
raise PatternError(f'Circular reference in {name} to {target}')
|
||||||
|
|
||||||
p = subpat.as_pattern(pattern=flattened[target])
|
p = ref.as_pattern(pattern=flattened[target])
|
||||||
pat.append(p)
|
pat.append(p)
|
||||||
|
|
||||||
pat.subpatterns.clear()
|
pat.refs.clear()
|
||||||
flattened[name] = pat
|
flattened[name] = pat
|
||||||
|
|
||||||
for top in tops:
|
for top in tops:
|
||||||
@ -245,13 +245,16 @@ class Library(Mapping[str, Pattern], metaclass=ABCMeta):
|
|||||||
names = set(self.keys())
|
names = set(self.keys())
|
||||||
not_toplevel: Set[Optional[str]] = set()
|
not_toplevel: Set[Optional[str]] = set()
|
||||||
for name in names:
|
for name in names:
|
||||||
not_toplevel |= set(sp.target for sp in self[name].subpatterns)
|
not_toplevel |= set(sp.target for sp in self[name].refs)
|
||||||
|
|
||||||
toplevel = list(names - not_toplevel)
|
toplevel = list(names - not_toplevel)
|
||||||
return toplevel
|
return toplevel
|
||||||
|
|
||||||
|
|
||||||
class MutableLibrary(Library, metaclass=ABCMeta):
|
VVV = TypeVar('VVV')
|
||||||
|
|
||||||
|
|
||||||
|
class MutableLibrary(Generic[VVV], Library, metaclass=ABCMeta):
|
||||||
# inherited abstract functions
|
# inherited abstract functions
|
||||||
#def __getitem__(self, key: str) -> 'Pattern':
|
#def __getitem__(self, key: str) -> 'Pattern':
|
||||||
#def __iter__(self) -> Iterator[str]:
|
#def __iter__(self) -> Iterator[str]:
|
||||||
@ -317,11 +320,11 @@ class MutableLibrary(Library, metaclass=ABCMeta):
|
|||||||
) -> ML:
|
) -> ML:
|
||||||
"""
|
"""
|
||||||
Convenience function.
|
Convenience function.
|
||||||
Performs a depth-first traversal of a pattern and its subpatterns.
|
Performs a depth-first traversal of a pattern and its referenced patterns.
|
||||||
At each pattern in the tree, the following sequence is called:
|
At each pattern in the tree, the following sequence is called:
|
||||||
```
|
```
|
||||||
current_pattern = visit_before(current_pattern, **vist_args)
|
current_pattern = visit_before(current_pattern, **vist_args)
|
||||||
for sp in current_pattern.subpatterns]
|
for sp in current_pattern.refs]
|
||||||
self.dfs(sp.target, visit_before, visit_after, updated_transform,
|
self.dfs(sp.target, visit_before, visit_after, updated_transform,
|
||||||
memo, (current_pattern,) + hierarchy)
|
memo, (current_pattern,) + hierarchy)
|
||||||
current_pattern = visit_after(current_pattern, **visit_args)
|
current_pattern = visit_after(current_pattern, **visit_args)
|
||||||
@ -336,10 +339,10 @@ class MutableLibrary(Library, metaclass=ABCMeta):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
top: Name of the pattern to start at (root node of the tree).
|
top: Name of the pattern to start at (root node of the tree).
|
||||||
visit_before: Function to call before traversing subpatterns.
|
visit_before: Function to call before traversing refs.
|
||||||
Should accept a `Pattern` and `**visit_args`, and return the (possibly modified)
|
Should accept a `Pattern` and `**visit_args`, and return the (possibly modified)
|
||||||
pattern. Default `None` (not called).
|
pattern. Default `None` (not called).
|
||||||
visit_after: Function to call after traversing subpatterns.
|
visit_after: Function to call after traversing refs.
|
||||||
Should accept a `Pattern` and `**visit_args`, and return the (possibly modified)
|
Should accept a `Pattern` and `**visit_args`, and return the (possibly modified)
|
||||||
pattern. Default `None` (not called).
|
pattern. Default `None` (not called).
|
||||||
transform: Initial value for `visit_args['transform']`.
|
transform: Initial value for `visit_args['transform']`.
|
||||||
@ -368,24 +371,24 @@ class MutableLibrary(Library, metaclass=ABCMeta):
|
|||||||
if visit_before is not None:
|
if visit_before is not None:
|
||||||
pat = visit_before(pat, hierarchy=hierarchy, memo=memo, transform=transform) # type: ignore
|
pat = visit_before(pat, hierarchy=hierarchy, memo=memo, transform=transform) # type: ignore
|
||||||
|
|
||||||
for subpattern in pat.subpatterns:
|
for ref in pat.refs:
|
||||||
if transform is not False:
|
if transform is not False:
|
||||||
sign = numpy.ones(2)
|
sign = numpy.ones(2)
|
||||||
if transform[3]:
|
if transform[3]:
|
||||||
sign[1] = -1
|
sign[1] = -1
|
||||||
xy = numpy.dot(rotation_matrix_2d(transform[2]), subpattern.offset * sign)
|
xy = numpy.dot(rotation_matrix_2d(transform[2]), ref.offset * sign)
|
||||||
mirror_x, angle = normalize_mirror(subpattern.mirrored)
|
mirror_x, angle = normalize_mirror(ref.mirrored)
|
||||||
angle += subpattern.rotation
|
angle += ref.rotation
|
||||||
sp_transform = transform + (xy[0], xy[1], angle, mirror_x)
|
sp_transform = transform + (xy[0], xy[1], angle, mirror_x)
|
||||||
sp_transform[3] %= 2
|
sp_transform[3] %= 2
|
||||||
else:
|
else:
|
||||||
sp_transform = False
|
sp_transform = False
|
||||||
|
|
||||||
if subpattern.target is None:
|
if ref.target is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self.dfs(
|
self.dfs(
|
||||||
top=subpattern.target,
|
top=ref.target,
|
||||||
visit_before=visit_before,
|
visit_before=visit_before,
|
||||||
visit_after=visit_after,
|
visit_after=visit_after,
|
||||||
transform=sp_transform,
|
transform=sp_transform,
|
||||||
@ -399,7 +402,7 @@ class MutableLibrary(Library, metaclass=ABCMeta):
|
|||||||
self._set(top, pat)
|
self._set(top, pat)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def subpatternize(
|
def dedup(
|
||||||
self: ML,
|
self: ML,
|
||||||
norm_value: int = int(1e6),
|
norm_value: int = int(1e6),
|
||||||
exclude_types: Tuple[Type] = (Polygon,),
|
exclude_types: Tuple[Type] = (Polygon,),
|
||||||
@ -410,7 +413,7 @@ class MutableLibrary(Library, metaclass=ABCMeta):
|
|||||||
Iterates through all `Pattern`s. Within each `Pattern`, it iterates
|
Iterates through all `Pattern`s. Within each `Pattern`, it iterates
|
||||||
over all shapes, calling `.normalized_form(norm_value)` on them to retrieve a scale-,
|
over all shapes, calling `.normalized_form(norm_value)` on them to retrieve a scale-,
|
||||||
offset-, and rotation-independent form. Each shape whose normalized form appears
|
offset-, and rotation-independent form. Each shape whose normalized form appears
|
||||||
more than once is removed and re-added using subpattern objects referencing a newly-created
|
more than once is removed and re-added using `Ref` objects referencing a newly-created
|
||||||
`Pattern` containing only the normalized form of the shape.
|
`Pattern` containing only the normalized form of the shape.
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
@ -424,14 +427,14 @@ class MutableLibrary(Library, metaclass=ABCMeta):
|
|||||||
speed or convenience. Default: `(shapes.Polygon,)`
|
speed or convenience. Default: `(shapes.Polygon,)`
|
||||||
label2name: Given a label tuple as returned by `shape.normalized_form(...)`, pick
|
label2name: Given a label tuple as returned by `shape.normalized_form(...)`, pick
|
||||||
a name for the generated pattern. Default `self.get_name('_shape')`.
|
a name for the generated pattern. Default `self.get_name('_shape')`.
|
||||||
threshold: Only replace shapes with subpatterns if there will be at least this many
|
threshold: Only replace shapes with refs if there will be at least this many
|
||||||
instances.
|
instances.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
# This currently simplifies globally (same shape in different patterns is
|
# This currently simplifies globally (same shape in different patterns is
|
||||||
# merged into the same subpattern target).
|
# merged into the same ref target).
|
||||||
|
|
||||||
from .pattern import Pattern
|
from .pattern import Pattern
|
||||||
|
|
||||||
@ -483,18 +486,18 @@ class MutableLibrary(Library, metaclass=ABCMeta):
|
|||||||
shape_table[label].append((i, values))
|
shape_table[label].append((i, values))
|
||||||
|
|
||||||
# For repeated shapes, create a `Pattern` holding a normalized shape object,
|
# For repeated shapes, create a `Pattern` holding a normalized shape object,
|
||||||
# and add `pat.subpatterns` entries for each occurrence in pat. Also, note down that
|
# and add `pat.refs` entries for each occurrence in pat. Also, note down that
|
||||||
# we should delete the `pat.shapes` entries for which we made SubPatterns.
|
# we should delete the `pat.shapes` entries for which we made `Ref`s.
|
||||||
shapes_to_remove = []
|
shapes_to_remove = []
|
||||||
for label in shape_table:
|
for label in shape_table:
|
||||||
target = label2name(label)
|
target = label2name(label)
|
||||||
for i, values in shape_table[label]:
|
for i, values in shape_table[label]:
|
||||||
offset, scale, rotation, mirror_x = values
|
offset, scale, rotation, mirror_x = values
|
||||||
pat.addsp(target=target, offset=offset, scale=scale,
|
pat.ref(target=target, offset=offset, scale=scale,
|
||||||
rotation=rotation, mirrored=(mirror_x, False))
|
rotation=rotation, mirrored=(mirror_x, False))
|
||||||
shapes_to_remove.append(i)
|
shapes_to_remove.append(i)
|
||||||
|
|
||||||
# Remove any shapes for which we have created subpatterns.
|
# Remove any shapes for which we have created refs.
|
||||||
for i in sorted(shapes_to_remove, reverse=True):
|
for i in sorted(shapes_to_remove, reverse=True):
|
||||||
del pat.shapes[i]
|
del pat.shapes[i]
|
||||||
|
|
||||||
@ -509,8 +512,8 @@ class MutableLibrary(Library, metaclass=ABCMeta):
|
|||||||
) -> ML:
|
) -> ML:
|
||||||
"""
|
"""
|
||||||
Wraps all shapes and labels with a non-`None` `repetition` attribute
|
Wraps all shapes and labels with a non-`None` `repetition` attribute
|
||||||
into a `SubPattern`/`Pattern` combination, and applies the `repetition`
|
into a `Ref`/`Pattern` combination, and applies the `repetition`
|
||||||
to each `SubPattern` instead of its contained shape.
|
to each `Ref` instead of its contained shape.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name_func: Function f(this_pattern, shape) which generates a name for the
|
name_func: Function f(this_pattern, shape) which generates a name for the
|
||||||
@ -533,7 +536,7 @@ class MutableLibrary(Library, metaclass=ABCMeta):
|
|||||||
|
|
||||||
name = name_func(pat, shape)
|
name = name_func(pat, shape)
|
||||||
self._set(name, Pattern(shapes=[shape]))
|
self._set(name, Pattern(shapes=[shape]))
|
||||||
pat.addsp(name, repetition=shape.repetition)
|
pat.ref(name, repetition=shape.repetition)
|
||||||
shape.repetition = None
|
shape.repetition = None
|
||||||
pat.shapes = new_shapes
|
pat.shapes = new_shapes
|
||||||
|
|
||||||
@ -544,7 +547,7 @@ class MutableLibrary(Library, metaclass=ABCMeta):
|
|||||||
continue
|
continue
|
||||||
name = name_func(pat, label)
|
name = name_func(pat, label)
|
||||||
self._set(name, Pattern(labels=[label]))
|
self._set(name, Pattern(labels=[label]))
|
||||||
pat.addsp(name, repetition=label.repetition)
|
pat.ref(name, repetition=label.repetition)
|
||||||
label.repetition = None
|
label.repetition = None
|
||||||
pat.labels = new_labels
|
pat.labels = new_labels
|
||||||
|
|
||||||
@ -682,7 +685,7 @@ class LazyLibrary(MutableLibrary):
|
|||||||
self._set(key, other[key])
|
self._set(key, other[key])
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return '<LazyLibrary with keys ' + repr(list(self.dict.keys())) + '>'
|
return '<LazyLibrary with keys ' + repr(list(self.keys())) + '>'
|
||||||
|
|
||||||
def precache(self: LL) -> LL:
|
def precache(self: LL) -> LL:
|
||||||
"""
|
"""
|
||||||
|
@ -13,7 +13,7 @@ from numpy import inf
|
|||||||
from numpy.typing import NDArray, ArrayLike
|
from numpy.typing import NDArray, ArrayLike
|
||||||
# .visualize imports matplotlib and matplotlib.collections
|
# .visualize imports matplotlib and matplotlib.collections
|
||||||
|
|
||||||
from .subpattern import SubPattern
|
from .refs import Ref
|
||||||
from .shapes import Shape, Polygon
|
from .shapes import Shape, Polygon
|
||||||
from .label import Label
|
from .label import Label
|
||||||
from .utils import rotation_matrix_2d, normalize_mirror, AutoSlots, annotations_t
|
from .utils import rotation_matrix_2d, normalize_mirror, AutoSlots, annotations_t
|
||||||
@ -28,9 +28,9 @@ P = TypeVar('P', bound='Pattern')
|
|||||||
class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
||||||
"""
|
"""
|
||||||
2D layout consisting of some set of shapes, labels, and references to other Pattern objects
|
2D layout consisting of some set of shapes, labels, and references to other Pattern objects
|
||||||
(via SubPattern). Shapes are assumed to inherit from masque.shapes.Shape or provide equivalent functions.
|
(via Ref). Shapes are assumed to inherit from masque.shapes.Shape or provide equivalent functions.
|
||||||
"""
|
"""
|
||||||
__slots__ = ('shapes', 'labels', 'subpatterns', 'ports')
|
__slots__ = ('shapes', 'labels', 'refs', 'ports')
|
||||||
|
|
||||||
shapes: List[Shape]
|
shapes: List[Shape]
|
||||||
""" List of all shapes in this Pattern.
|
""" List of all shapes in this Pattern.
|
||||||
@ -40,8 +40,8 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
labels: List[Label]
|
labels: List[Label]
|
||||||
""" List of all labels in this Pattern. """
|
""" List of all labels in this Pattern. """
|
||||||
|
|
||||||
subpatterns: List[SubPattern]
|
refs: List[Ref]
|
||||||
""" List of all references to other patterns (`SubPattern`s) in this `Pattern`.
|
""" List of all references to other patterns (`Ref`s) in this `Pattern`.
|
||||||
Multiple objects in this list may reference the same Pattern object
|
Multiple objects in this list may reference the same Pattern object
|
||||||
(i.e. multiple instances of the same object).
|
(i.e. multiple instances of the same object).
|
||||||
"""
|
"""
|
||||||
@ -54,18 +54,18 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
*,
|
*,
|
||||||
shapes: Sequence[Shape] = (),
|
shapes: Sequence[Shape] = (),
|
||||||
labels: Sequence[Label] = (),
|
labels: Sequence[Label] = (),
|
||||||
subpatterns: Sequence[SubPattern] = (),
|
refs: Sequence[Ref] = (),
|
||||||
annotations: Optional[annotations_t] = None,
|
annotations: Optional[annotations_t] = None,
|
||||||
ports: Optional[Mapping[str, Port]] = None
|
ports: Optional[Mapping[str, Port]] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Basic init; arguments get assigned to member variables.
|
Basic init; arguments get assigned to member variables.
|
||||||
Non-list inputs for shapes and subpatterns get converted to lists.
|
Non-list inputs for shapes and refs get converted to lists.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
shapes: Initial shapes in the Pattern
|
shapes: Initial shapes in the Pattern
|
||||||
labels: Initial labels in the Pattern
|
labels: Initial labels in the Pattern
|
||||||
subpatterns: Initial subpatterns in the Pattern
|
refs: Initial refs in the Pattern
|
||||||
annotations: Initial annotations for the pattern
|
annotations: Initial annotations for the pattern
|
||||||
ports: Any ports in the pattern
|
ports: Any ports in the pattern
|
||||||
"""
|
"""
|
||||||
@ -79,10 +79,10 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
else:
|
else:
|
||||||
self.labels = list(labels)
|
self.labels = list(labels)
|
||||||
|
|
||||||
if isinstance(subpatterns, list):
|
if isinstance(refs, list):
|
||||||
self.subpatterns = subpatterns
|
self.refs = refs
|
||||||
else:
|
else:
|
||||||
self.subpatterns = list(subpatterns)
|
self.refs = list(refs)
|
||||||
|
|
||||||
if ports is not None:
|
if ports is not None:
|
||||||
ports = dict(copy.deepcopy(ports))
|
ports = dict(copy.deepcopy(ports))
|
||||||
@ -90,7 +90,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
self.annotations = annotations if annotations is not None else {}
|
self.annotations = annotations if annotations is not None else {}
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
s = f'<Pattern: sh{len(self.shapes)} sp{len(self.subpatterns)} la{len(self.labels)} ['
|
s = f'<Pattern: sh{len(self.shapes)} sp{len(self.refs)} la{len(self.labels)} ['
|
||||||
for name, port in self.ports.items():
|
for name, port in self.ports.items():
|
||||||
s += f'\n\t{name}: {port}'
|
s += f'\n\t{name}: {port}'
|
||||||
s += ']>'
|
s += ']>'
|
||||||
@ -100,7 +100,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
return Pattern(
|
return Pattern(
|
||||||
shapes=copy.deepcopy(self.shapes),
|
shapes=copy.deepcopy(self.shapes),
|
||||||
labels=copy.deepcopy(self.labels),
|
labels=copy.deepcopy(self.labels),
|
||||||
subpatterns=[copy.copy(sp) for sp in self.subpatterns],
|
refs=[copy.copy(sp) for sp in self.refs],
|
||||||
annotations=copy.deepcopy(self.annotations),
|
annotations=copy.deepcopy(self.annotations),
|
||||||
ports=copy.deepcopy(self.ports),
|
ports=copy.deepcopy(self.ports),
|
||||||
)
|
)
|
||||||
@ -110,7 +110,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
new = Pattern(
|
new = Pattern(
|
||||||
shapes=copy.deepcopy(self.shapes, memo),
|
shapes=copy.deepcopy(self.shapes, memo),
|
||||||
labels=copy.deepcopy(self.labels, memo),
|
labels=copy.deepcopy(self.labels, memo),
|
||||||
subpatterns=copy.deepcopy(self.subpatterns, memo),
|
refs=copy.deepcopy(self.refs, memo),
|
||||||
annotations=copy.deepcopy(self.annotations, memo),
|
annotations=copy.deepcopy(self.annotations, memo),
|
||||||
ports=copy.deepcopy(self.ports),
|
ports=copy.deepcopy(self.ports),
|
||||||
)
|
)
|
||||||
@ -118,7 +118,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
|
|
||||||
def append(self: P, other_pattern: Pattern) -> P:
|
def append(self: P, other_pattern: Pattern) -> P:
|
||||||
"""
|
"""
|
||||||
Appends all shapes, labels and subpatterns from other_pattern to self's shapes,
|
Appends all shapes, labels and refs from other_pattern to self's shapes,
|
||||||
labels, and supbatterns.
|
labels, and supbatterns.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -127,7 +127,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
self.subpatterns += other_pattern.subpatterns
|
self.refs += other_pattern.refs
|
||||||
self.shapes += other_pattern.shapes
|
self.shapes += other_pattern.shapes
|
||||||
self.labels += other_pattern.labels
|
self.labels += other_pattern.labels
|
||||||
self.annotations += other_pattern.annotations
|
self.annotations += other_pattern.annotations
|
||||||
@ -138,7 +138,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
self,
|
self,
|
||||||
shapes: Optional[Callable[[Shape], bool]] = None,
|
shapes: Optional[Callable[[Shape], bool]] = None,
|
||||||
labels: Optional[Callable[[Label], bool]] = None,
|
labels: Optional[Callable[[Label], bool]] = None,
|
||||||
subpatterns: Optional[Callable[[SubPattern], bool]] = None,
|
refs: Optional[Callable[[Ref], bool]] = None,
|
||||||
annotations: Optional[Callable[[annotation_t], bool]] = None,
|
annotations: Optional[Callable[[annotation_t], bool]] = None,
|
||||||
ports: Optional[Callable[[str], bool]] = None,
|
ports: Optional[Callable[[str], bool]] = None,
|
||||||
default_keep: bool = False
|
default_keep: bool = False
|
||||||
@ -146,19 +146,19 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
"""
|
"""
|
||||||
Returns a Pattern containing only the entities (e.g. shapes) for which the
|
Returns a Pattern containing only the entities (e.g. shapes) for which the
|
||||||
given entity_func returns True.
|
given entity_func returns True.
|
||||||
Self is _not_ altered, but shapes, labels, and subpatterns are _not_ copied, just referenced.
|
Self is _not_ altered, but shapes, labels, and refs are _not_ copied, just referenced.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
shapes: Given a shape, returns a boolean denoting whether the shape is a member of the subset.
|
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.
|
labels: Given a label, returns a boolean denoting whether the label is a member of the subset.
|
||||||
subpatterns: Given a subpattern, returns a boolean denoting if it is a member of the subset.
|
refs: Given a ref, returns a boolean denoting if it is a member of the subset.
|
||||||
annotations: Given an annotation, returns a boolean denoting if it is a member of the subset.
|
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.
|
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_keep: If `True`, keeps all elements of a given type if no function is supplied.
|
||||||
Default `False` (discards all elements).
|
Default `False` (discards all elements).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A Pattern containing all the shapes and subpatterns for which the parameter
|
A Pattern containing all the shapes and refs for which the parameter
|
||||||
functions return True
|
functions return True
|
||||||
"""
|
"""
|
||||||
pat = Pattern()
|
pat = Pattern()
|
||||||
@ -173,10 +173,10 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
elif default_keep:
|
elif default_keep:
|
||||||
pat.labels = copy.copy(self.labels)
|
pat.labels = copy.copy(self.labels)
|
||||||
|
|
||||||
if subpatterns is not None:
|
if refs is not None:
|
||||||
pat.subpatterns = [s for s in self.subpatterns if subpatterns(s)]
|
pat.refs = [s for s in self.refs if refs(s)]
|
||||||
elif default_keep:
|
elif default_keep:
|
||||||
pat.subpatterns = copy.copy(self.subpatterns)
|
pat.refs = copy.copy(self.refs)
|
||||||
|
|
||||||
if annotations is not None:
|
if annotations is not None:
|
||||||
pat.annotations = [s for s in self.annotations if annotations(s)]
|
pat.annotations = [s for s in self.annotations if annotations(s)]
|
||||||
@ -259,7 +259,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
Returns:
|
Returns:
|
||||||
A set of all pattern names referenced by this pattern.
|
A set of all pattern names referenced by this pattern.
|
||||||
"""
|
"""
|
||||||
return set(sp.target for sp in self.subpatterns)
|
return set(sp.target for sp in self.refs)
|
||||||
|
|
||||||
def get_bounds(
|
def get_bounds(
|
||||||
self,
|
self,
|
||||||
@ -286,10 +286,10 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
min_bounds = numpy.minimum(min_bounds, bounds[0, :])
|
min_bounds = numpy.minimum(min_bounds, bounds[0, :])
|
||||||
max_bounds = numpy.maximum(max_bounds, bounds[1, :])
|
max_bounds = numpy.maximum(max_bounds, bounds[1, :])
|
||||||
|
|
||||||
if self.subpatterns and (library is None):
|
if self.refs and (library is None):
|
||||||
raise PatternError('Must provide a library to get_bounds() to resolve subpatterns')
|
raise PatternError('Must provide a library to get_bounds() to resolve refs')
|
||||||
|
|
||||||
for entry in self.subpatterns:
|
for entry in self.refs:
|
||||||
bounds = entry.get_bounds(library=library)
|
bounds = entry.get_bounds(library=library)
|
||||||
if bounds is None:
|
if bounds is None:
|
||||||
continue
|
continue
|
||||||
@ -317,7 +317,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
|
|
||||||
def translate_elements(self: P, offset: ArrayLike) -> P:
|
def translate_elements(self: P, offset: ArrayLike) -> P:
|
||||||
"""
|
"""
|
||||||
Translates all shapes, label, and subpatterns by the given offset.
|
Translates all shapes, label, and refs by the given offset.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
offset: (x, y) to translate by
|
offset: (x, y) to translate by
|
||||||
@ -325,13 +325,13 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
for entry in chain(self.shapes, self.subpatterns, self.labels, self.ports):
|
for entry in chain(self.shapes, self.refs, self.labels, self.ports):
|
||||||
cast(Positionable, entry).translate(offset)
|
cast(Positionable, entry).translate(offset)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def scale_elements(self: P, c: float) -> P:
|
def scale_elements(self: P, c: float) -> P:
|
||||||
""""
|
""""
|
||||||
Scales all shapes and subpatterns by the given value.
|
Scales all shapes and refs by the given value.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
c: factor to scale by
|
c: factor to scale by
|
||||||
@ -339,14 +339,14 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
for entry in chain(self.shapes, self.subpatterns):
|
for entry in chain(self.shapes, self.refs):
|
||||||
cast(Scalable, entry).scale_by(c)
|
cast(Scalable, entry).scale_by(c)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def scale_by(self: P, c: float) -> P:
|
def scale_by(self: P, c: float) -> P:
|
||||||
"""
|
"""
|
||||||
Scale this Pattern by the given value
|
Scale this Pattern by the given value
|
||||||
(all shapes and subpatterns and their offsets are scaled)
|
(all shapes and refs and their offsets are scaled)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
c: factor to scale by
|
c: factor to scale by
|
||||||
@ -354,7 +354,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
for entry in chain(self.shapes, self.subpatterns):
|
for entry in chain(self.shapes, self.refs):
|
||||||
cast(Positionable, entry).offset *= c
|
cast(Positionable, entry).offset *= c
|
||||||
cast(Scalable, entry).scale_by(c)
|
cast(Scalable, entry).scale_by(c)
|
||||||
|
|
||||||
@ -393,7 +393,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
|
|
||||||
def rotate_element_centers(self: P, rotation: float) -> P:
|
def rotate_element_centers(self: P, rotation: float) -> P:
|
||||||
"""
|
"""
|
||||||
Rotate the offsets of all shapes, labels, and subpatterns around (0, 0)
|
Rotate the offsets of all shapes, labels, and refs around (0, 0)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
rotation: Angle to rotate by (counter-clockwise, radians)
|
rotation: Angle to rotate by (counter-clockwise, radians)
|
||||||
@ -401,14 +401,14 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
for entry in chain(self.shapes, self.subpatterns, self.labels, self.ports):
|
for entry in chain(self.shapes, self.refs, self.labels, self.ports):
|
||||||
old_offset = cast(Positionable, entry).offset
|
old_offset = cast(Positionable, entry).offset
|
||||||
cast(Positionable, entry).offset = numpy.dot(rotation_matrix_2d(rotation), old_offset)
|
cast(Positionable, entry).offset = numpy.dot(rotation_matrix_2d(rotation), old_offset)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def rotate_elements(self: P, rotation: float) -> P:
|
def rotate_elements(self: P, rotation: float) -> P:
|
||||||
"""
|
"""
|
||||||
Rotate each shape and subpattern around its center (offset)
|
Rotate each shape and refs around its center (offset)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
rotation: Angle to rotate by (counter-clockwise, radians)
|
rotation: Angle to rotate by (counter-clockwise, radians)
|
||||||
@ -416,13 +416,13 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
for entry in chain(self.shapes, self.subpatterns):
|
for entry in chain(self.shapes, self.refs):
|
||||||
cast(Rotatable, entry).rotate(rotation)
|
cast(Rotatable, entry).rotate(rotation)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def mirror_element_centers(self: P, axis: int) -> P:
|
def mirror_element_centers(self: P, axis: int) -> P:
|
||||||
"""
|
"""
|
||||||
Mirror the offsets of all shapes, labels, and subpatterns across an axis
|
Mirror the offsets of all shapes, labels, and refs across an axis
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
axis: Axis to mirror across
|
axis: Axis to mirror across
|
||||||
@ -431,13 +431,13 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
for entry in chain(self.shapes, self.subpatterns, self.labels, self.ports):
|
for entry in chain(self.shapes, self.refs, self.labels, self.ports):
|
||||||
cast(Positionable, entry).offset[axis - 1] *= -1
|
cast(Positionable, entry).offset[axis - 1] *= -1
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def mirror_elements(self: P, axis: int) -> P:
|
def mirror_elements(self: P, axis: int) -> P:
|
||||||
"""
|
"""
|
||||||
Mirror each shape and subpattern across an axis, relative to its
|
Mirror each shape and refs across an axis, relative to its
|
||||||
offset
|
offset
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -447,7 +447,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
for entry in chain(self.shapes, self.subpatterns):
|
for entry in chain(self.shapes, self.refs):
|
||||||
cast(Mirrorable, entry).mirror(axis)
|
cast(Mirrorable, entry).mirror(axis)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@ -468,7 +468,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
|
|
||||||
def copy(self: P) -> P:
|
def copy(self: P) -> P:
|
||||||
"""
|
"""
|
||||||
Return a copy of the Pattern, deep-copying shapes and copying subpattern
|
Return a copy of the Pattern, deep-copying shapes and copying refs
|
||||||
entries, but not deep-copying any referenced patterns.
|
entries, but not deep-copying any referenced patterns.
|
||||||
|
|
||||||
See also: `Pattern.deepcopy()`
|
See also: `Pattern.deepcopy()`
|
||||||
@ -490,25 +490,25 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
def is_empty(self) -> bool:
|
def is_empty(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Returns:
|
Returns:
|
||||||
True if the pattern is contains no shapes, labels, or subpatterns.
|
True if the pattern is contains no shapes, labels, or refs.
|
||||||
"""
|
"""
|
||||||
return (len(self.subpatterns) == 0
|
return (len(self.refs) == 0
|
||||||
and len(self.shapes) == 0
|
and len(self.shapes) == 0
|
||||||
and len(self.labels) == 0)
|
and len(self.labels) == 0)
|
||||||
|
|
||||||
def addsp(self: P, *args: Any, **kwargs: Any) -> P:
|
def ref(self: P, *args: Any, **kwargs: Any) -> P:
|
||||||
"""
|
"""
|
||||||
Convenience function which constructs a subpattern object and adds it
|
Convenience function which constructs a `Ref` object and adds it
|
||||||
to this pattern.
|
to this pattern.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
*args: Passed to `SubPattern()`
|
*args: Passed to `Ref()`
|
||||||
**kwargs: Passed to `SubPattern()`
|
**kwargs: Passed to `Ref()`
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
self.subpatterns.append(SubPattern(*args, **kwargs))
|
self.refs.append(Ref(*args, **kwargs))
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def flatten(
|
def flatten(
|
||||||
@ -516,7 +516,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
library: Mapping[str, P],
|
library: Mapping[str, P],
|
||||||
) -> 'Pattern':
|
) -> 'Pattern':
|
||||||
"""
|
"""
|
||||||
Removes all subpatterns (recursively) and adds equivalent shapes.
|
Removes all refs (recursively) and adds equivalent shapes.
|
||||||
Alters the current pattern in-place
|
Alters the current pattern in-place
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -534,8 +534,8 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
pat = library[name].deepcopy()
|
pat = library[name].deepcopy()
|
||||||
flattened[name] = None
|
flattened[name] = None
|
||||||
|
|
||||||
for subpat in pat.subpatterns:
|
for ref in pat.refs:
|
||||||
target = subpat.target
|
target = ref.target
|
||||||
if target is None:
|
if target is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -544,10 +544,10 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
if flattened[target] is None:
|
if flattened[target] is None:
|
||||||
raise PatternError(f'Circular reference in {name} to {target}')
|
raise PatternError(f'Circular reference in {name} to {target}')
|
||||||
|
|
||||||
p = subpat.as_pattern(pattern=flattened[target])
|
p = ref.as_pattern(pattern=flattened[target])
|
||||||
pat.append(p)
|
pat.append(p)
|
||||||
|
|
||||||
pat.subpatterns.clear()
|
pat.refs.clear()
|
||||||
flattened[name] = pat
|
flattened[name] = pat
|
||||||
|
|
||||||
flatten_single(None)
|
flatten_single(None)
|
||||||
@ -579,8 +579,8 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
from matplotlib import pyplot # type: ignore
|
from matplotlib import pyplot # type: ignore
|
||||||
import matplotlib.collections # type: ignore
|
import matplotlib.collections # type: ignore
|
||||||
|
|
||||||
if self.subpatterns and library is None:
|
if self.refs and library is None:
|
||||||
raise PatternError('Must provide a library when visualizing a pattern with subpatterns')
|
raise PatternError('Must provide a library when visualizing a pattern with refs')
|
||||||
|
|
||||||
offset = numpy.array(offset, dtype=float)
|
offset = numpy.array(offset, dtype=float)
|
||||||
|
|
||||||
@ -604,8 +604,8 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable, metaclass=AutoSlots):
|
|||||||
axes.add_collection(mpl_poly_collection)
|
axes.add_collection(mpl_poly_collection)
|
||||||
pyplot.axis('equal')
|
pyplot.axis('equal')
|
||||||
|
|
||||||
for subpat in self.subpatterns:
|
for ref in self.refs:
|
||||||
subpat.as_pattern(library=library).visualize(
|
ref.as_pattern(library=library).visualize(
|
||||||
library=library,
|
library=library,
|
||||||
offset=offset,
|
offset=offset,
|
||||||
overdraw=True,
|
overdraw=True,
|
||||||
|
413
masque/ports.py
Normal file
413
masque/ports.py
Normal file
@ -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]
|
@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
SubPattern provides basic support for nesting Pattern objects within each other, by adding
|
Ref provides basic support for nesting Pattern objects within each other, by adding
|
||||||
offset, rotation, scaling, and other such properties to the reference.
|
offset, rotation, scaling, and other such properties to the reference.
|
||||||
"""
|
"""
|
||||||
#TODO more top-level documentation
|
#TODO more top-level documentation
|
||||||
@ -24,17 +24,22 @@ if TYPE_CHECKING:
|
|||||||
from . import Pattern
|
from . import Pattern
|
||||||
|
|
||||||
|
|
||||||
S = TypeVar('S', bound='SubPattern')
|
R = TypeVar('R', bound='Ref')
|
||||||
|
|
||||||
|
|
||||||
class SubPattern(PositionableImpl, RotatableImpl, ScalableImpl, Mirrorable,
|
class Ref(
|
||||||
|
PositionableImpl, RotatableImpl, ScalableImpl, Mirrorable,
|
||||||
PivotableImpl, Copyable, RepeatableImpl, AnnotatableImpl,
|
PivotableImpl, Copyable, RepeatableImpl, AnnotatableImpl,
|
||||||
metaclass=AutoSlots):
|
):
|
||||||
"""
|
"""
|
||||||
SubPattern provides basic support for nesting Pattern objects within each other, by adding
|
`Ref` provides basic support for nesting Pattern objects within each other, by adding
|
||||||
offset, rotation, scaling, and associated methods.
|
offset, rotation, scaling, and associated methods.
|
||||||
"""
|
"""
|
||||||
__slots__ = ('_target', '_mirrored')
|
__slots__ = (
|
||||||
|
'_target', '_mirrored',
|
||||||
|
# inherited
|
||||||
|
'_offset', '_rotation', 'scale', '_repetition', '_annotations',
|
||||||
|
)
|
||||||
|
|
||||||
_target: Optional[str]
|
_target: Optional[str]
|
||||||
""" The name of the `Pattern` being instanced """
|
""" The name of the `Pattern` being instanced """
|
||||||
@ -72,8 +77,8 @@ class SubPattern(PositionableImpl, RotatableImpl, ScalableImpl, Mirrorable,
|
|||||||
self.repetition = repetition
|
self.repetition = repetition
|
||||||
self.annotations = annotations if annotations is not None else {}
|
self.annotations = annotations if annotations is not None else {}
|
||||||
|
|
||||||
def __copy__(self) -> 'SubPattern':
|
def __copy__(self) -> 'Ref':
|
||||||
new = SubPattern(
|
new = Ref(
|
||||||
target=self.target,
|
target=self.target,
|
||||||
offset=self.offset.copy(),
|
offset=self.offset.copy(),
|
||||||
rotation=self.rotation,
|
rotation=self.rotation,
|
||||||
@ -84,7 +89,7 @@ class SubPattern(PositionableImpl, RotatableImpl, ScalableImpl, Mirrorable,
|
|||||||
)
|
)
|
||||||
return new
|
return new
|
||||||
|
|
||||||
def __deepcopy__(self, memo: Optional[Dict] = None) -> 'SubPattern':
|
def __deepcopy__(self, memo: Optional[Dict] = None) -> 'Ref':
|
||||||
memo = {} if memo is None else memo
|
memo = {} if memo is None else memo
|
||||||
new = copy.copy(self)
|
new = copy.copy(self)
|
||||||
new.repetition = copy.deepcopy(self.repetition, memo)
|
new.repetition = copy.deepcopy(self.repetition, memo)
|
||||||
@ -127,7 +132,7 @@ class SubPattern(PositionableImpl, RotatableImpl, ScalableImpl, Mirrorable,
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A copy of the referenced Pattern which has been scaled, rotated, etc.
|
A copy of the referenced Pattern which has been scaled, rotated, etc.
|
||||||
according to this `SubPattern`'s properties.
|
according to this `Ref`'s properties.
|
||||||
"""
|
"""
|
||||||
if pattern is None:
|
if pattern is None:
|
||||||
if library is None:
|
if library is None:
|
||||||
@ -178,7 +183,7 @@ class SubPattern(PositionableImpl, RotatableImpl, ScalableImpl, Mirrorable,
|
|||||||
) -> Optional[NDArray[numpy.float64]]:
|
) -> Optional[NDArray[numpy.float64]]:
|
||||||
"""
|
"""
|
||||||
Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the
|
Return a `numpy.ndarray` containing `[[x_min, y_min], [x_max, y_max]]`, corresponding to the
|
||||||
extent of the `SubPattern` in each dimension.
|
extent of the `Ref` in each dimension.
|
||||||
Returns `None` if the contained `Pattern` is empty.
|
Returns `None` if the contained `Pattern` is empty.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -198,4 +203,4 @@ class SubPattern(PositionableImpl, RotatableImpl, ScalableImpl, Mirrorable,
|
|||||||
rotation = f' r{self.rotation*180/pi:g}' if self.rotation != 0 else ''
|
rotation = f' r{self.rotation*180/pi:g}' if self.rotation != 0 else ''
|
||||||
scale = f' d{self.scale:g}' if self.scale != 1 else ''
|
scale = f' d{self.scale:g}' if self.scale != 1 else ''
|
||||||
mirrored = ' m{:d}{:d}'.format(*self.mirrored) if self.mirrored.any() else ''
|
mirrored = ' m{:d}{:d}'.format(*self.mirrored) if self.mirrored.any() else ''
|
||||||
return f'<SubPattern {name} at {self.offset}{rotation}{scale}{mirrored}>'
|
return f'<Ref {name} at {self.offset}{rotation}{scale}{mirrored}>'
|
@ -1,14 +1,15 @@
|
|||||||
from typing import TypeVar
|
from typing import TypeVar, cast
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy import pi
|
from numpy import pi
|
||||||
from numpy.typing import ArrayLike, NDArray
|
from numpy.typing import ArrayLike, NDArray
|
||||||
|
|
||||||
#from .positionable import Positionable
|
from .positionable import Positionable
|
||||||
from ..error import MasqueError
|
from ..error import MasqueError
|
||||||
from ..utils import is_scalar, rotation_matrix_2d
|
from ..utils import is_scalar, rotation_matrix_2d
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar('T', bound='Rotatable')
|
T = TypeVar('T', bound='Rotatable')
|
||||||
I = TypeVar('I', bound='RotatableImpl')
|
I = TypeVar('I', bound='RotatableImpl')
|
||||||
P = TypeVar('P', bound='Pivotable')
|
P = TypeVar('P', bound='Pivotable')
|
||||||
@ -112,9 +113,9 @@ class PivotableImpl(Pivotable, metaclass=ABCMeta):
|
|||||||
|
|
||||||
def rotate_around(self: J, pivot: ArrayLike, rotation: float) -> J:
|
def rotate_around(self: J, pivot: ArrayLike, rotation: float) -> J:
|
||||||
pivot = numpy.array(pivot, dtype=float)
|
pivot = numpy.array(pivot, dtype=float)
|
||||||
self.translate(-pivot)
|
cast(Positionable, self).translate(-pivot)
|
||||||
self.rotate(rotation)
|
cast(Rotatable, self).rotate(rotation)
|
||||||
self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) #type: ignore #TODO: mypy#3004
|
self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) #type: ignore #TODO: mypy#3004
|
||||||
self.translate(+pivot)
|
cast(Positionable, self).translate(+pivot)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ from numpy.typing import NDArray, ArrayLike
|
|||||||
|
|
||||||
from ..error import MasqueError
|
from ..error import MasqueError
|
||||||
from ..pattern import Pattern
|
from ..pattern import Pattern
|
||||||
from ..subpattern import SubPattern
|
from ..ref import Ref
|
||||||
|
|
||||||
|
|
||||||
def pack_patterns(
|
def pack_patterns(
|
||||||
@ -29,7 +29,7 @@ def pack_patterns(
|
|||||||
locations, reject_inds = packer(sizes, regions, presort=presort, allow_rejects=allow_rejects)
|
locations, reject_inds = packer(sizes, regions, presort=presort, allow_rejects=allow_rejects)
|
||||||
|
|
||||||
pat = Pattern()
|
pat = Pattern()
|
||||||
pat.subpatterns = [SubPattern(pp, offset=oo + loc)
|
pat.refs = [Ref(pp, offset=oo + loc)
|
||||||
for pp, oo, loc in zip(patterns, offsets, locations)]
|
for pp, oo, loc in zip(patterns, offsets, locations)]
|
||||||
|
|
||||||
rejects = [patterns[ii] for ii in reject_inds]
|
rejects = [patterns[ii] for ii in reject_inds]
|
||||||
|
Loading…
Reference in New Issue
Block a user