This commit is contained in:
jan 2023-01-21 21:22:11 -08:00
parent d9ae8dd6e3
commit 9efb6f0eeb
18 changed files with 712 additions and 1113 deletions

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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():
@ -273,12 +284,12 @@ class Device(PortList):
map_out[vi] = None map_out[vi] = None
self.place(other, offset=translation, rotation=rotation, pivot=pivot, self.place(other, offset=translation, rotation=rotation, pivot=pivot,
mirrored=mirrored, port_map=map_out, skip_port_check=True) mirrored=mirrored, port_map=map_out, skip_port_check=True)
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()?

View File

@ -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],

View File

@ -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(

View File

@ -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...')

View File

@ -1,2 +0,0 @@
# FOr backwards compatibility
from .gdsii import *

View File

@ -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,

View File

@ -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

View File

@ -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)

View File

@ -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:
""" """

View File

@ -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
View 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]

View File

@ -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(
PivotableImpl, Copyable, RepeatableImpl, AnnotatableImpl, PositionableImpl, RotatableImpl, ScalableImpl, Mirrorable,
metaclass=AutoSlots): PivotableImpl, Copyable, RepeatableImpl, AnnotatableImpl,
):
""" """
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}>'

View File

@ -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

View File

@ -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,8 +29,8 @@ 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]
return pat, rejects return pat, rejects