Compare commits
20 commits
26e6a44559
...
963103b859
| Author | SHA1 | Date | |
|---|---|---|---|
| 963103b859 | |||
| e5a6aab940 | |||
| 042941c838 | |||
| 0f63acbad0 | |||
| a0d7d0ed26 | |||
| d32a5ee762 | |||
| 19dafad157 | |||
| 5cb608734d | |||
| d0b48e6bfc | |||
| ef5c8c715e | |||
| 049864ddc7 | |||
| 3bf7efc404 | |||
| 74fa377450 | |||
| c3581243c8 | |||
| 338c123fb1 | |||
| a89f07c441 | |||
| bb7f4906af | |||
| 2513c7f8fd | |||
| ad4e9af59d | |||
| 46555dbd4d |
26 changed files with 1793 additions and 2636 deletions
|
|
@ -1,7 +1,9 @@
|
||||||
from .builder import Builder as Builder
|
from .pather import (
|
||||||
from .pather import Pather as Pather
|
Pather as Pather,
|
||||||
from .renderpather import RenderPather as RenderPather
|
PortPather as PortPather,
|
||||||
from .pather_mixin import PortPather as PortPather
|
Builder as Builder,
|
||||||
|
RenderPather as RenderPather,
|
||||||
|
)
|
||||||
from .utils import ell as ell
|
from .utils import ell as ell
|
||||||
from .tools import (
|
from .tools import (
|
||||||
Tool as Tool,
|
Tool as Tool,
|
||||||
|
|
@ -10,3 +12,4 @@ from .tools import (
|
||||||
AutoTool as AutoTool,
|
AutoTool as AutoTool,
|
||||||
PathTool as PathTool,
|
PathTool as PathTool,
|
||||||
)
|
)
|
||||||
|
from .logging import logged_op as logged_op
|
||||||
|
|
|
||||||
|
|
@ -1,461 +0,0 @@
|
||||||
"""
|
|
||||||
Simplified Pattern assembly (`Builder`)
|
|
||||||
"""
|
|
||||||
from typing import Self
|
|
||||||
from collections.abc import Iterable, Sequence, Mapping
|
|
||||||
import copy
|
|
||||||
import logging
|
|
||||||
from functools import wraps
|
|
||||||
|
|
||||||
from numpy.typing import ArrayLike
|
|
||||||
|
|
||||||
from ..pattern import Pattern
|
|
||||||
from ..library import ILibrary, TreeView
|
|
||||||
from ..error import BuildError
|
|
||||||
from ..ports import PortList, Port
|
|
||||||
from ..abstract import Abstract
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class Builder(PortList):
|
|
||||||
"""
|
|
||||||
A `Builder` is a helper object used for snapping together multiple
|
|
||||||
lower-level patterns at their `Port`s.
|
|
||||||
|
|
||||||
The `Builder` mostly just holds context, in the form of a `Library`,
|
|
||||||
in addition to its underlying pattern. This simplifies some calls
|
|
||||||
to `plug` and `place`, by making the library implicit.
|
|
||||||
|
|
||||||
`Builder` can also be `set_dead()`, at which point further calls to `plug()`
|
|
||||||
and `place()` are ignored (intended for debugging).
|
|
||||||
|
|
||||||
|
|
||||||
Examples: Creating a Builder
|
|
||||||
===========================
|
|
||||||
- `Builder(library, ports={'A': port_a, 'C': port_c}, name='mypat')` makes
|
|
||||||
an empty pattern, adds the given ports, and places it into `library`
|
|
||||||
under the name `'mypat'`.
|
|
||||||
|
|
||||||
- `Builder(library)` makes an empty pattern with no ports. The pattern
|
|
||||||
is not added into `library` and must later be added with e.g.
|
|
||||||
`library['mypat'] = builder.pattern`
|
|
||||||
|
|
||||||
- `Builder(library, pattern=pattern, name='mypat')` uses an existing
|
|
||||||
pattern (including its ports) and sets `library['mypat'] = pattern`.
|
|
||||||
|
|
||||||
- `Builder.interface(other_pat, port_map=['A', 'B'], library=library)`
|
|
||||||
makes a new (empty) pattern, copies over ports 'A' and 'B' from
|
|
||||||
`other_pat`, and creates additional ports 'in_A' and 'in_B' facing
|
|
||||||
in the opposite directions. This can be used to build a device which
|
|
||||||
can plug into `other_pat` (using the 'in_*' ports) but which does not
|
|
||||||
itself include `other_pat` as a subcomponent.
|
|
||||||
|
|
||||||
- `Builder.interface(other_builder, ...)` does the same thing as
|
|
||||||
`Builder.interface(other_builder.pattern, ...)` but also uses
|
|
||||||
`other_builder.library` as its library by default.
|
|
||||||
|
|
||||||
|
|
||||||
Examples: Adding to a pattern
|
|
||||||
=============================
|
|
||||||
- `my_device.plug(subdevice, {'A': 'C', 'B': 'B'}, map_out={'D': 'myport'})`
|
|
||||||
instantiates `subdevice` into `my_device`, plugging ports 'A' and 'B'
|
|
||||||
of `my_device` into ports 'C' and 'B' of `subdevice`. The connected ports
|
|
||||||
are removed and any unconnected ports from `subdevice` are added to
|
|
||||||
`my_device`. Port 'D' of `subdevice` (unconnected) is renamed to 'myport'.
|
|
||||||
|
|
||||||
- `my_device.plug(wire, {'myport': 'A'})` places port 'A' of `wire` at 'myport'
|
|
||||||
of `my_device`. If `wire` has only two ports (e.g. 'A' and 'B'), no `map_out`,
|
|
||||||
argument is provided, and the `thru` argument is not explicitly
|
|
||||||
set to `False`, the unconnected port of `wire` is automatically renamed to
|
|
||||||
'myport'. This allows easy extension of existing ports without changing
|
|
||||||
their names or having to provide `map_out` each time `plug` is called.
|
|
||||||
|
|
||||||
- `my_device.place(pad, offset=(10, 10), rotation=pi / 2, port_map={'A': 'gnd'})`
|
|
||||||
instantiates `pad` at the specified (x, y) offset and with the specified
|
|
||||||
rotation, adding its ports to those of `my_device`. Port 'A' of `pad` is
|
|
||||||
renamed to 'gnd' so that further routing can use this signal or net name
|
|
||||||
rather than the port name on the original `pad` device.
|
|
||||||
"""
|
|
||||||
__slots__ = ('pattern', 'library', '_dead')
|
|
||||||
|
|
||||||
pattern: Pattern
|
|
||||||
""" Layout of this device """
|
|
||||||
|
|
||||||
library: ILibrary
|
|
||||||
"""
|
|
||||||
Library from which patterns should be referenced
|
|
||||||
"""
|
|
||||||
|
|
||||||
_dead: bool
|
|
||||||
""" If True, plug()/place() are skipped (for debugging)"""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def ports(self) -> dict[str, Port]:
|
|
||||||
return self.pattern.ports
|
|
||||||
|
|
||||||
@ports.setter
|
|
||||||
def ports(self, value: dict[str, Port]) -> None:
|
|
||||||
self.pattern.ports = value
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
library: ILibrary,
|
|
||||||
*,
|
|
||||||
pattern: Pattern | None = None,
|
|
||||||
ports: str | Mapping[str, Port] | None = None,
|
|
||||||
name: str | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Args:
|
|
||||||
library: The library from which referenced patterns will be taken
|
|
||||||
pattern: The pattern which will be modified by subsequent operations.
|
|
||||||
If `None` (default), a new pattern is created.
|
|
||||||
ports: Allows specifying the initial set of ports, if `pattern` does
|
|
||||||
not already have any ports (or is not provided). May be a string,
|
|
||||||
in which case it is interpreted as a name in `library`.
|
|
||||||
Default `None` (no ports).
|
|
||||||
name: If specified, `library[name]` is set to `self.pattern`.
|
|
||||||
"""
|
|
||||||
self._dead = False
|
|
||||||
self.library = library
|
|
||||||
if pattern is not None:
|
|
||||||
self.pattern = pattern
|
|
||||||
else:
|
|
||||||
self.pattern = Pattern()
|
|
||||||
|
|
||||||
if ports is not None:
|
|
||||||
if self.pattern.ports:
|
|
||||||
raise BuildError('Ports supplied for pattern with pre-existing ports!')
|
|
||||||
if isinstance(ports, str):
|
|
||||||
ports = library.abstract(ports).ports
|
|
||||||
|
|
||||||
self.pattern.ports.update(copy.deepcopy(dict(ports)))
|
|
||||||
|
|
||||||
if name is not None:
|
|
||||||
library[name] = self.pattern
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def interface(
|
|
||||||
cls: type['Builder'],
|
|
||||||
source: PortList | Mapping[str, Port] | str,
|
|
||||||
*,
|
|
||||||
library: ILibrary | None = None,
|
|
||||||
in_prefix: str = 'in_',
|
|
||||||
out_prefix: str = '',
|
|
||||||
port_map: dict[str, str] | Sequence[str] | None = None,
|
|
||||||
name: str | None = None,
|
|
||||||
) -> 'Builder':
|
|
||||||
"""
|
|
||||||
Wrapper for `Pattern.interface()`, which returns a Builder instead.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
source: A collection of ports (e.g. Pattern, Builder, or dict)
|
|
||||||
from which to create the interface. May be a pattern name if
|
|
||||||
`library` is provided.
|
|
||||||
library: Library from which existing patterns should be referenced,
|
|
||||||
and to which the new one should be added (if named). If not provided,
|
|
||||||
`source.library` must exist and will be used.
|
|
||||||
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 builder, with an empty pattern and 2x as many ports as
|
|
||||||
listed in port_map.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
`PortError` if `port_map` contains port names not present in the
|
|
||||||
current device.
|
|
||||||
`PortError` if applying the prefixes results in duplicate port
|
|
||||||
names.
|
|
||||||
"""
|
|
||||||
if library is None:
|
|
||||||
if hasattr(source, 'library') and isinstance(source.library, ILibrary):
|
|
||||||
library = source.library
|
|
||||||
else:
|
|
||||||
raise BuildError('No library was given, and `source.library` does not have one either.')
|
|
||||||
|
|
||||||
if isinstance(source, str):
|
|
||||||
source = library.abstract(source).ports
|
|
||||||
|
|
||||||
pat = Pattern.interface(source, in_prefix=in_prefix, out_prefix=out_prefix, port_map=port_map)
|
|
||||||
new = Builder(library=library, pattern=pat, name=name)
|
|
||||||
return new
|
|
||||||
|
|
||||||
@wraps(Pattern.label)
|
|
||||||
def label(self, *args, **kwargs) -> Self:
|
|
||||||
self.pattern.label(*args, **kwargs)
|
|
||||||
return self
|
|
||||||
|
|
||||||
@wraps(Pattern.ref)
|
|
||||||
def ref(self, *args, **kwargs) -> Self:
|
|
||||||
self.pattern.ref(*args, **kwargs)
|
|
||||||
return self
|
|
||||||
|
|
||||||
@wraps(Pattern.polygon)
|
|
||||||
def polygon(self, *args, **kwargs) -> Self:
|
|
||||||
self.pattern.polygon(*args, **kwargs)
|
|
||||||
return self
|
|
||||||
|
|
||||||
@wraps(Pattern.rect)
|
|
||||||
def rect(self, *args, **kwargs) -> Self:
|
|
||||||
self.pattern.rect(*args, **kwargs)
|
|
||||||
return self
|
|
||||||
|
|
||||||
# Note: We're a superclass of `Pather`, where path() means something different,
|
|
||||||
# so we shouldn't wrap Pattern.path()
|
|
||||||
#@wraps(Pattern.path)
|
|
||||||
#def path(self, *args, **kwargs) -> Self:
|
|
||||||
# self.pattern.path(*args, **kwargs)
|
|
||||||
# return self
|
|
||||||
|
|
||||||
def plug(
|
|
||||||
self,
|
|
||||||
other: Abstract | str | Pattern | TreeView,
|
|
||||||
map_in: dict[str, str],
|
|
||||||
map_out: dict[str, str | None] | None = None,
|
|
||||||
*,
|
|
||||||
mirrored: bool = False,
|
|
||||||
thru: bool | str = True,
|
|
||||||
set_rotation: bool | None = None,
|
|
||||||
append: bool = False,
|
|
||||||
ok_connections: Iterable[tuple[str, str]] = (),
|
|
||||||
) -> Self:
|
|
||||||
"""
|
|
||||||
Wrapper around `Pattern.plug` which allows a string for `other`.
|
|
||||||
|
|
||||||
The `Builder`'s library is used to dereference the string (or `Abstract`, if
|
|
||||||
one is passed with `append=True`). If a `TreeView` is passed, it is first
|
|
||||||
added into `self.library`.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
other: An `Abstract`, string, `Pattern`, or `TreeView` describing the
|
|
||||||
device to be instatiated. If it is a `TreeView`, it is first
|
|
||||||
added into `self.library`, after which the topcell is plugged;
|
|
||||||
an equivalent statement is `self.plug(self.library << other, ...)`.
|
|
||||||
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 ports in `other`.
|
|
||||||
mirrored: Enables mirroring `other` across the x axis prior to
|
|
||||||
connecting any ports.
|
|
||||||
thru: If map_in specifies only a single port, `thru` provides a mechainsm
|
|
||||||
to avoid repeating the port name. Eg, for `map_in={'myport': 'A'}`,
|
|
||||||
- If True (default), and `other` has only two ports total, and map_out
|
|
||||||
doesn't specify a name for the other port, its name is set to the key
|
|
||||||
in `map_in`, i.e. 'myport'.
|
|
||||||
- If a string, `map_out[thru]` is set to the key in `map_in` (i.e. 'myport').
|
|
||||||
An error is raised if that entry already exists.
|
|
||||||
|
|
||||||
This makes it easy to extend a pattern with simple 2-port devices
|
|
||||||
(e.g. wires) without providing `map_out` each time `plug` is
|
|
||||||
called. See "Examples" above for more info. Default `True`.
|
|
||||||
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`.
|
|
||||||
append: If `True`, `other` is appended instead of being referenced.
|
|
||||||
Note that this does not flatten `other`, so its refs will still
|
|
||||||
be refs (now inside `self`).
|
|
||||||
ok_connections: Set of "allowed" ptype combinations. Identical
|
|
||||||
ptypes are always allowed to connect, as is `'unk'` with
|
|
||||||
any other ptypte. Non-allowed ptype connections will emit a
|
|
||||||
warning. Order is ignored, i.e. `(a, b)` is equivalent to
|
|
||||||
`(b, a)`.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
|
|
||||||
Note:
|
|
||||||
If the builder is 'dead' (see `set_dead()`), geometry generation is
|
|
||||||
skipped but ports are still updated.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
`PortError` if any ports specified in `map_in` or `map_out` do not
|
|
||||||
exist in `self.ports` or `other_names`.
|
|
||||||
`PortError` if there are any duplicate names after `map_in` and `map_out`
|
|
||||||
are applied.
|
|
||||||
`PortError` if the specified port mapping is not achieveable (the ports
|
|
||||||
do not line up)
|
|
||||||
"""
|
|
||||||
if self._dead:
|
|
||||||
logger.warning('Skipping geometry for plug() since device is dead')
|
|
||||||
|
|
||||||
if not isinstance(other, str | Abstract | Pattern):
|
|
||||||
# We got a Tree; add it into self.library and grab an Abstract for it
|
|
||||||
other = self.library << other
|
|
||||||
|
|
||||||
if isinstance(other, str):
|
|
||||||
other = self.library.abstract(other)
|
|
||||||
if append and isinstance(other, Abstract):
|
|
||||||
other = self.library[other.name]
|
|
||||||
|
|
||||||
self.pattern.plug(
|
|
||||||
other = other,
|
|
||||||
map_in = map_in,
|
|
||||||
map_out = map_out,
|
|
||||||
mirrored = mirrored,
|
|
||||||
thru = thru,
|
|
||||||
set_rotation = set_rotation,
|
|
||||||
append = append,
|
|
||||||
ok_connections = ok_connections,
|
|
||||||
skip_geometry = self._dead,
|
|
||||||
)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def place(
|
|
||||||
self,
|
|
||||||
other: Abstract | str | Pattern | TreeView,
|
|
||||||
*,
|
|
||||||
offset: ArrayLike = (0, 0),
|
|
||||||
rotation: float = 0,
|
|
||||||
pivot: ArrayLike = (0, 0),
|
|
||||||
mirrored: bool = False,
|
|
||||||
port_map: dict[str, str | None] | None = None,
|
|
||||||
skip_port_check: bool = False,
|
|
||||||
append: bool = False,
|
|
||||||
) -> Self:
|
|
||||||
"""
|
|
||||||
Wrapper around `Pattern.place` which allows a string or `TreeView` for `other`.
|
|
||||||
|
|
||||||
The `Builder`'s library is used to dereference the string (or `Abstract`, if
|
|
||||||
one is passed with `append=True`). If a `TreeView` is passed, it is first
|
|
||||||
added into `self.library`.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
other: An `Abstract`, string, `Pattern`, or `TreeView` describing the
|
|
||||||
device to be instatiated. If it is a `TreeView`, it is first
|
|
||||||
added into `self.library`, after which the topcell is plugged;
|
|
||||||
an equivalent statement is `self.plug(self.library << other, ...)`.
|
|
||||||
offset: Offset at which to place the instance. Default (0, 0).
|
|
||||||
rotation: Rotation applied to the instance before placement. Default 0.
|
|
||||||
pivot: Rotation is applied around this pivot point (default (0, 0)).
|
|
||||||
Rotation is applied prior to translation (`offset`).
|
|
||||||
mirrored: Whether theinstance should be mirrored across the x axis.
|
|
||||||
Mirroring is applied before translation and rotation.
|
|
||||||
port_map: dict of `{'old_name': 'new_name'}` mappings, specifying
|
|
||||||
new names for ports in the instantiated device. New names can be
|
|
||||||
`None`, which will delete those ports.
|
|
||||||
skip_port_check: Can be used to skip the internal call to `check_ports`,
|
|
||||||
in case it has already been performed elsewhere.
|
|
||||||
append: If `True`, `other` is appended instead of being referenced.
|
|
||||||
Note that this does not flatten `other`, so its refs will still
|
|
||||||
be refs (now inside `self`).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
|
|
||||||
Note:
|
|
||||||
If the builder is 'dead' (see `set_dead()`), geometry generation is
|
|
||||||
skipped but ports are still updated.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
`PortError` if any ports specified in `map_in` or `map_out` do not
|
|
||||||
exist in `self.ports` or `other.ports`.
|
|
||||||
`PortError` if there are any duplicate names after `map_in` and `map_out`
|
|
||||||
are applied.
|
|
||||||
"""
|
|
||||||
if self._dead:
|
|
||||||
logger.warning('Skipping geometry for place() since device is dead')
|
|
||||||
|
|
||||||
if not isinstance(other, str | Abstract | Pattern):
|
|
||||||
# We got a Tree; add it into self.library and grab an Abstract for it
|
|
||||||
other = self.library << other
|
|
||||||
|
|
||||||
if isinstance(other, str):
|
|
||||||
other = self.library.abstract(other)
|
|
||||||
if append and isinstance(other, Abstract):
|
|
||||||
other = self.library[other.name]
|
|
||||||
|
|
||||||
self.pattern.place(
|
|
||||||
other = other,
|
|
||||||
offset = offset,
|
|
||||||
rotation = rotation,
|
|
||||||
pivot = pivot,
|
|
||||||
mirrored = mirrored,
|
|
||||||
port_map = port_map,
|
|
||||||
skip_port_check = skip_port_check,
|
|
||||||
append = append,
|
|
||||||
skip_geometry = self._dead,
|
|
||||||
)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def translate(self, offset: ArrayLike) -> Self:
|
|
||||||
"""
|
|
||||||
Translate the pattern and all ports.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
offset: (x, y) distance to translate by
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
self.pattern.translate_elements(offset)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def rotate_around(self, pivot: ArrayLike, angle: float) -> Self:
|
|
||||||
"""
|
|
||||||
Rotate the pattern and all ports.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
angle: angle (radians, counterclockwise) to rotate by
|
|
||||||
pivot: location to rotate around
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
self.pattern.rotate_around(pivot, angle)
|
|
||||||
for port in self.ports.values():
|
|
||||||
port.rotate_around(pivot, angle)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def mirror(self, axis: int = 0) -> Self:
|
|
||||||
"""
|
|
||||||
Mirror the pattern and all ports across the specified axis.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
axis: Axis to mirror across (x=0, y=1)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
self.pattern.mirror(axis)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def set_dead(self) -> Self:
|
|
||||||
"""
|
|
||||||
Suppresses geometry generation for subsequent `plug()` and `place()`
|
|
||||||
operations. Unlike a complete skip, the port state is still tracked
|
|
||||||
and updated, using 'best-effort' fallbacks for impossible transforms.
|
|
||||||
This allows a layout script to execute through problematic sections
|
|
||||||
while maintaining valid port references for downstream code.
|
|
||||||
|
|
||||||
This is meant for debugging:
|
|
||||||
```
|
|
||||||
dev.plug(a, ...)
|
|
||||||
dev.set_dead() # added for debug purposes
|
|
||||||
dev.plug(b, ...) # usually raises an error, but now uses fallback port update
|
|
||||||
dev.plug(c, ...) # also updated via fallback
|
|
||||||
dev.pattern.visualize() # shows the device as of the set_dead() call
|
|
||||||
```
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
self._dead = True
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
s = f'<Builder {self.pattern} L({len(self.library)})>'
|
|
||||||
return s
|
|
||||||
|
|
||||||
|
|
||||||
120
masque/builder/logging.py
Normal file
120
masque/builder/logging.py
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
"""
|
||||||
|
Logging and operation decorators for Builder/Pather
|
||||||
|
"""
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
from collections.abc import Iterator, Sequence, Callable
|
||||||
|
import logging
|
||||||
|
from functools import wraps
|
||||||
|
import inspect
|
||||||
|
import numpy
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .pather import Pather
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_log_args(**kwargs) -> str:
|
||||||
|
arg_strs = []
|
||||||
|
for k, v in kwargs.items():
|
||||||
|
if isinstance(v, str | int | float | bool | None):
|
||||||
|
arg_strs.append(f"{k}={v}")
|
||||||
|
elif isinstance(v, numpy.ndarray):
|
||||||
|
arg_strs.append(f"{k}={v.tolist()}")
|
||||||
|
elif isinstance(v, list | tuple) and len(v) <= 10:
|
||||||
|
arg_strs.append(f"{k}={v}")
|
||||||
|
else:
|
||||||
|
arg_strs.append(f"{k}=...")
|
||||||
|
return ", ".join(arg_strs)
|
||||||
|
|
||||||
|
|
||||||
|
class PatherLogger:
|
||||||
|
"""
|
||||||
|
Encapsulates state for Pather/Builder diagnostic logging.
|
||||||
|
"""
|
||||||
|
debug: bool
|
||||||
|
indent: int
|
||||||
|
depth: int
|
||||||
|
|
||||||
|
def __init__(self, debug: bool = False) -> None:
|
||||||
|
self.debug = debug
|
||||||
|
self.indent = 0
|
||||||
|
self.depth = 0
|
||||||
|
|
||||||
|
def _log(self, module_name: str, msg: str) -> None:
|
||||||
|
if self.debug and self.depth <= 1:
|
||||||
|
log_obj = logging.getLogger(module_name)
|
||||||
|
log_obj.info(' ' * self.indent + msg)
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def log_operation(
|
||||||
|
self,
|
||||||
|
pather: 'Pather',
|
||||||
|
op: str,
|
||||||
|
portspec: str | Sequence[str] | None = None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> Iterator[None]:
|
||||||
|
if not self.debug or self.depth > 0:
|
||||||
|
self.depth += 1
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
self.depth -= 1
|
||||||
|
return
|
||||||
|
|
||||||
|
target = f"({portspec})" if portspec else ""
|
||||||
|
module_name = pather.__class__.__module__
|
||||||
|
self._log(module_name, f"Operation: {op}{target} {_format_log_args(**kwargs)}")
|
||||||
|
|
||||||
|
before_ports = {name: port.copy() for name, port in pather.ports.items()}
|
||||||
|
self.depth += 1
|
||||||
|
self.indent += 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
after_ports = pather.ports
|
||||||
|
for name in sorted(after_ports.keys()):
|
||||||
|
if name not in before_ports or after_ports[name] != before_ports[name]:
|
||||||
|
self._log(module_name, f"Port {name}: {pather.ports[name].describe()}")
|
||||||
|
for name in sorted(before_ports.keys()):
|
||||||
|
if name not in after_ports:
|
||||||
|
self._log(module_name, f"Port {name}: removed")
|
||||||
|
|
||||||
|
self.indent -= 1
|
||||||
|
self.depth -= 1
|
||||||
|
|
||||||
|
|
||||||
|
def logged_op(
|
||||||
|
portspec_getter: Callable[[dict[str, Any]], str | Sequence[str] | None] | None = None,
|
||||||
|
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
||||||
|
"""
|
||||||
|
Decorator to wrap Builder methods with logging.
|
||||||
|
"""
|
||||||
|
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
||||||
|
sig = inspect.signature(func)
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(self: 'Pather', *args: Any, **kwargs: Any) -> Any:
|
||||||
|
logger_obj = getattr(self, '_logger', None)
|
||||||
|
if logger_obj is None or not logger_obj.debug:
|
||||||
|
return func(self, *args, **kwargs)
|
||||||
|
|
||||||
|
bound = sig.bind(self, *args, **kwargs)
|
||||||
|
bound.apply_defaults()
|
||||||
|
all_args = bound.arguments
|
||||||
|
# remove 'self' from logged args
|
||||||
|
logged_args = {k: v for k, v in all_args.items() if k != 'self'}
|
||||||
|
|
||||||
|
ps = portspec_getter(all_args) if portspec_getter else None
|
||||||
|
|
||||||
|
# Remove portspec from logged_args if it's there to avoid duplicate arg to log_operation
|
||||||
|
logged_args.pop('portspec', None)
|
||||||
|
|
||||||
|
with logger_obj.log_operation(self, func.__name__, ps, **logged_args):
|
||||||
|
if getattr(self, '_dead', False) and func.__name__ in ('plug', 'place'):
|
||||||
|
logger.warning(f"Skipping geometry for {func.__name__}() since device is dead")
|
||||||
|
return func(self, *args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,764 +0,0 @@
|
||||||
from typing import Self, overload
|
|
||||||
from collections.abc import Sequence, Iterator, Iterable, Mapping
|
|
||||||
import logging
|
|
||||||
from contextlib import contextmanager
|
|
||||||
from abc import abstractmethod, ABCMeta
|
|
||||||
|
|
||||||
import numpy
|
|
||||||
from numpy import pi
|
|
||||||
from numpy.typing import ArrayLike
|
|
||||||
|
|
||||||
from ..pattern import Pattern
|
|
||||||
from ..library import ILibrary, TreeView
|
|
||||||
from ..error import PortError, BuildError
|
|
||||||
from ..utils import SupportsBool
|
|
||||||
from ..abstract import Abstract
|
|
||||||
from .tools import Tool
|
|
||||||
from .utils import ell
|
|
||||||
from ..ports import PortList
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class PatherMixin(PortList, metaclass=ABCMeta):
|
|
||||||
pattern: Pattern
|
|
||||||
""" Layout of this device """
|
|
||||||
|
|
||||||
library: ILibrary
|
|
||||||
""" Library from which patterns should be referenced """
|
|
||||||
|
|
||||||
_dead: bool
|
|
||||||
""" If True, plug()/place() are skipped (for debugging) """
|
|
||||||
|
|
||||||
tools: dict[str | None, Tool]
|
|
||||||
"""
|
|
||||||
Tool objects are used to dynamically generate new single-use Devices
|
|
||||||
(e.g wires or waveguides) to be plugged into this device.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def trace(
|
|
||||||
self,
|
|
||||||
portspec: str | Sequence[str],
|
|
||||||
ccw: SupportsBool | None,
|
|
||||||
length: float | None = None,
|
|
||||||
*,
|
|
||||||
spacing: float | ArrayLike | None = None,
|
|
||||||
**bounds,
|
|
||||||
) -> Self:
|
|
||||||
"""
|
|
||||||
Create a "wire"/"waveguide" extending from the port(s) `portspec`.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
portspec: The name(s) of the port(s) into which the wire(s) will be plugged.
|
|
||||||
ccw: If `None`, the output should be along the same axis as the input.
|
|
||||||
Otherwise, cast to bool and turn counterclockwise if True
|
|
||||||
and clockwise otherwise.
|
|
||||||
length: The total distance from input to output, along the input's axis only.
|
|
||||||
Length is only allowed with a single port.
|
|
||||||
spacing: Center-to-center distance between output ports along the input port's axis.
|
|
||||||
Only used when routing multiple ports with a bend.
|
|
||||||
bounds: Boundary constraints for the trace.
|
|
||||||
- each: results in each port being extended by `each` distance.
|
|
||||||
- emin, emax, pmin, pmax, xmin, xmax, ymin, ymax: bundle routing via `ell()`.
|
|
||||||
- set_rotation: explicit rotation for ports without one.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
if isinstance(portspec, str):
|
|
||||||
portspec = [portspec]
|
|
||||||
|
|
||||||
if length is not None:
|
|
||||||
if len(portspec) > 1:
|
|
||||||
raise BuildError('length is only allowed with a single port in trace()')
|
|
||||||
if bounds:
|
|
||||||
raise BuildError('length and bounds are mutually exclusive in trace()')
|
|
||||||
return self._traceL(portspec[0], ccw, length)
|
|
||||||
|
|
||||||
if 'each' in bounds:
|
|
||||||
each = bounds.pop('each')
|
|
||||||
if bounds:
|
|
||||||
raise BuildError('each and other bounds are mutually exclusive in trace()')
|
|
||||||
for port in portspec:
|
|
||||||
self._traceL(port, ccw, each)
|
|
||||||
return self
|
|
||||||
|
|
||||||
# Bundle routing (formerly mpath logic)
|
|
||||||
bound_types = set()
|
|
||||||
if 'bound_type' in bounds:
|
|
||||||
bound_types.add(bounds.pop('bound_type'))
|
|
||||||
bound = bounds.pop('bound')
|
|
||||||
for bt in ('emin', 'emax', 'pmin', 'pmax', 'xmin', 'xmax', 'ymin', 'ymax', 'min_past_furthest'):
|
|
||||||
if bt in bounds:
|
|
||||||
bound_types.add(bt)
|
|
||||||
bound = bounds.pop(bt)
|
|
||||||
|
|
||||||
if not bound_types:
|
|
||||||
raise BuildError('No bound type specified for trace()')
|
|
||||||
if len(bound_types) > 1:
|
|
||||||
raise BuildError(f'Too many bound types specified: {bound_types}')
|
|
||||||
bound_type = tuple(bound_types)[0]
|
|
||||||
|
|
||||||
ports = self.pattern[tuple(portspec)]
|
|
||||||
set_rotation = bounds.pop('set_rotation', None)
|
|
||||||
|
|
||||||
extensions = ell(ports, ccw, spacing=spacing, bound=bound, bound_type=bound_type, set_rotation=set_rotation)
|
|
||||||
|
|
||||||
for port_name, ext_len in extensions.items():
|
|
||||||
self._traceL(port_name, ccw, ext_len, **bounds)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def trace_to(
|
|
||||||
self,
|
|
||||||
portspec: str | Sequence[str],
|
|
||||||
ccw: SupportsBool | None,
|
|
||||||
*,
|
|
||||||
spacing: float | ArrayLike | None = None,
|
|
||||||
**bounds,
|
|
||||||
) -> Self:
|
|
||||||
"""
|
|
||||||
Create a "wire"/"waveguide" extending from the port(s) `portspec` to a target position.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
portspec: The name(s) of the port(s) into which the wire(s) will be plugged.
|
|
||||||
ccw: If `None`, the output should be along the same axis as the input.
|
|
||||||
Otherwise, cast to bool and turn counterclockwise if True
|
|
||||||
and clockwise otherwise.
|
|
||||||
spacing: Center-to-center distance between output ports along the input port's axis.
|
|
||||||
Only used when routing multiple ports with a bend.
|
|
||||||
bounds: Boundary constraints for the target position.
|
|
||||||
- p, x, y, pos, position: Coordinate of the target position. Error if used with multiple ports.
|
|
||||||
- pmin, pmax, xmin, xmax, ymin, ymax, emin, emax: bundle routing via `ell()`.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
if isinstance(portspec, str):
|
|
||||||
portspec = [portspec]
|
|
||||||
|
|
||||||
pos_bounds = {kk: bounds[kk] for kk in ('p', 'x', 'y', 'pos', 'position') if kk in bounds}
|
|
||||||
if pos_bounds:
|
|
||||||
if len(portspec) > 1:
|
|
||||||
raise BuildError(f'{tuple(pos_bounds.keys())} bounds are only allowed with a single port in trace_to()')
|
|
||||||
if len(pos_bounds) > 1:
|
|
||||||
raise BuildError(f'Too many position bounds: {tuple(pos_bounds.keys())}')
|
|
||||||
|
|
||||||
k, v = next(iter(pos_bounds.items()))
|
|
||||||
k = 'position' if k in ('p', 'pos') else k
|
|
||||||
|
|
||||||
# Logic hoisted from path_to()
|
|
||||||
port_name = portspec[0]
|
|
||||||
port = self.pattern[port_name]
|
|
||||||
if port.rotation is None:
|
|
||||||
raise PortError(f'Port {port_name} has no rotation and cannot be used for trace_to()')
|
|
||||||
|
|
||||||
if not numpy.isclose(port.rotation % (pi / 2), 0):
|
|
||||||
raise BuildError('trace_to was asked to route from non-manhattan port')
|
|
||||||
|
|
||||||
is_horizontal = numpy.isclose(port.rotation % pi, 0)
|
|
||||||
if is_horizontal:
|
|
||||||
if k == 'y':
|
|
||||||
raise BuildError('Asked to trace to y-coordinate, but port is horizontal')
|
|
||||||
target = v
|
|
||||||
else:
|
|
||||||
if k == 'x':
|
|
||||||
raise BuildError('Asked to trace to x-coordinate, but port is vertical')
|
|
||||||
target = v
|
|
||||||
|
|
||||||
x0, y0 = port.offset
|
|
||||||
if is_horizontal:
|
|
||||||
if numpy.sign(numpy.cos(port.rotation)) == numpy.sign(target - x0):
|
|
||||||
raise BuildError(f'trace_to routing to behind source port: x0={x0:g} to {target:g}')
|
|
||||||
length = numpy.abs(target - x0)
|
|
||||||
else:
|
|
||||||
if numpy.sign(numpy.sin(port.rotation)) == numpy.sign(target - y0):
|
|
||||||
raise BuildError(f'trace_to routing to behind source port: y0={y0:g} to {target:g}')
|
|
||||||
length = numpy.abs(target - y0)
|
|
||||||
|
|
||||||
other_bounds = {bk: bv for bk, bv in bounds.items() if bk not in pos_bounds and bk != 'length'}
|
|
||||||
if 'length' in bounds and bounds['length'] is not None:
|
|
||||||
raise BuildError('Cannot specify both relative length and absolute position in trace_to()')
|
|
||||||
|
|
||||||
return self._traceL(port_name, ccw, length, **other_bounds)
|
|
||||||
|
|
||||||
# Bundle routing (delegate to trace which handles ell)
|
|
||||||
return self.trace(portspec, ccw, spacing=spacing, **bounds)
|
|
||||||
|
|
||||||
def straight(self, portspec: str | Sequence[str], length: float | None = None, **bounds) -> Self:
|
|
||||||
""" Straight extension. Replaces `path(ccw=None)` and `path_to(ccw=None)` """
|
|
||||||
return self.trace_to(portspec, None, length=length, **bounds)
|
|
||||||
|
|
||||||
def bend(self, portspec: str | Sequence[str], ccw: SupportsBool, length: float | None = None, **bounds) -> Self:
|
|
||||||
""" Bend extension. Replaces `path(ccw=True/False)` and `path_to(ccw=True/False)` """
|
|
||||||
return self.trace_to(portspec, ccw, length=length, **bounds)
|
|
||||||
|
|
||||||
def ccw(self, portspec: str | Sequence[str], length: float | None = None, **bounds) -> Self:
|
|
||||||
""" Counter-clockwise bend extension. """
|
|
||||||
return self.bend(portspec, True, length, **bounds)
|
|
||||||
|
|
||||||
def cw(self, portspec: str | Sequence[str], length: float | None = None, **bounds) -> Self:
|
|
||||||
""" Clockwise bend extension. """
|
|
||||||
return self.bend(portspec, False, length, **bounds)
|
|
||||||
|
|
||||||
def jog(self, portspec: str | Sequence[str], offset: float, length: float | None = None, **bounds) -> Self:
|
|
||||||
""" Jog extension. Replaces `pathS`. """
|
|
||||||
if isinstance(portspec, str):
|
|
||||||
portspec = [portspec]
|
|
||||||
|
|
||||||
for port in portspec:
|
|
||||||
l_actual = length
|
|
||||||
if l_actual is None:
|
|
||||||
# TODO: use bounds to determine length?
|
|
||||||
raise BuildError('jog() currently requires a length')
|
|
||||||
self._traceS(port, l_actual, offset, **bounds)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def uturn(self, portspec: str | Sequence[str], offset: float, length: float | None = None, **bounds) -> Self:
|
|
||||||
""" 180-degree turn extension. """
|
|
||||||
if isinstance(portspec, str):
|
|
||||||
portspec = [portspec]
|
|
||||||
|
|
||||||
for port in portspec:
|
|
||||||
l_actual = length
|
|
||||||
if l_actual is None:
|
|
||||||
# TODO: use bounds to determine length?
|
|
||||||
l_actual = 0
|
|
||||||
self._traceU(port, offset, length=l_actual, **bounds)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def trace_into(
|
|
||||||
self,
|
|
||||||
portspec_src: str,
|
|
||||||
portspec_dst: str,
|
|
||||||
*,
|
|
||||||
out_ptype: str | None = None,
|
|
||||||
plug_destination: bool = True,
|
|
||||||
thru: str | None = None,
|
|
||||||
**kwargs,
|
|
||||||
) -> Self:
|
|
||||||
"""
|
|
||||||
Create a "wire"/"waveguide" traveling between the ports `portspec_src` and
|
|
||||||
`portspec_dst`, and `plug` it into both (or just the source port).
|
|
||||||
|
|
||||||
Only unambiguous scenarios are allowed:
|
|
||||||
- Straight connector between facing ports
|
|
||||||
- Single 90 degree bend
|
|
||||||
- Jog between facing ports
|
|
||||||
(jog is done as late as possible, i.e. only 2 L-shaped segments are used)
|
|
||||||
|
|
||||||
By default, the destination's `pytpe` will be used as the `out_ptype` for the
|
|
||||||
wire, and the `portspec_dst` will be plugged (i.e. removed).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
portspec_src: The name of the starting port into which the wire will be plugged.
|
|
||||||
portspec_dst: The name of the destination port.
|
|
||||||
out_ptype: Passed to the pathing tool in order to specify the desired port type
|
|
||||||
to be generated at the destination end. If `None` (default), the destination
|
|
||||||
port's `ptype` will be used.
|
|
||||||
thru: If not `None`, the port by this name will be renamed to `portspec_src`.
|
|
||||||
This can be used when routing a signal through a pre-placed 2-port device.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
PortError if either port does not have a specified rotation.
|
|
||||||
BuildError if an invalid port config is encountered:
|
|
||||||
- Non-manhattan ports
|
|
||||||
- U-bend
|
|
||||||
- Destination too close to (or behind) source
|
|
||||||
"""
|
|
||||||
if self._dead:
|
|
||||||
logger.error('Skipping trace_into() since device is dead')
|
|
||||||
return self
|
|
||||||
|
|
||||||
port_src = self.pattern[portspec_src]
|
|
||||||
port_dst = self.pattern[portspec_dst]
|
|
||||||
|
|
||||||
if out_ptype is None:
|
|
||||||
out_ptype = port_dst.ptype
|
|
||||||
|
|
||||||
if port_src.rotation is None:
|
|
||||||
raise PortError(f'Port {portspec_src} has no rotation and cannot be used for trace_into()')
|
|
||||||
if port_dst.rotation is None:
|
|
||||||
raise PortError(f'Port {portspec_dst} has no rotation and cannot be used for trace_into()')
|
|
||||||
|
|
||||||
if not numpy.isclose(port_src.rotation % (pi / 2), 0):
|
|
||||||
raise BuildError('trace_into was asked to route from non-manhattan port')
|
|
||||||
if not numpy.isclose(port_dst.rotation % (pi / 2), 0):
|
|
||||||
raise BuildError('trace_into was asked to route to non-manhattan port')
|
|
||||||
|
|
||||||
src_is_horizontal = numpy.isclose(port_src.rotation % pi, 0)
|
|
||||||
dst_is_horizontal = numpy.isclose(port_dst.rotation % pi, 0)
|
|
||||||
xs, ys = port_src.offset
|
|
||||||
xd, yd = port_dst.offset
|
|
||||||
|
|
||||||
angle = (port_dst.rotation - port_src.rotation) % (2 * pi)
|
|
||||||
|
|
||||||
dst_extra_args = {'out_ptype': out_ptype}
|
|
||||||
if plug_destination:
|
|
||||||
dst_extra_args['plug_into'] = portspec_dst
|
|
||||||
|
|
||||||
src_args = {**kwargs}
|
|
||||||
dst_args = {**src_args, **dst_extra_args}
|
|
||||||
if src_is_horizontal and not dst_is_horizontal:
|
|
||||||
# single bend should suffice
|
|
||||||
self.trace_to(portspec_src, angle > pi, x=xd, **src_args)
|
|
||||||
self.trace_to(portspec_src, None, y=yd, **dst_args)
|
|
||||||
elif dst_is_horizontal and not src_is_horizontal:
|
|
||||||
# single bend should suffice
|
|
||||||
self.trace_to(portspec_src, angle > pi, y=yd, **src_args)
|
|
||||||
self.trace_to(portspec_src, None, x=xd, **dst_args)
|
|
||||||
elif numpy.isclose(angle, pi):
|
|
||||||
if src_is_horizontal and ys == yd:
|
|
||||||
# straight connector
|
|
||||||
self.trace_to(portspec_src, None, x=xd, **dst_args)
|
|
||||||
elif not src_is_horizontal and xs == xd:
|
|
||||||
# straight connector
|
|
||||||
self.trace_to(portspec_src, None, y=yd, **dst_args)
|
|
||||||
else:
|
|
||||||
# S-bend
|
|
||||||
(travel, jog), _ = port_src.measure_travel(port_dst)
|
|
||||||
self.jog(portspec_src, -jog, -travel, **dst_args)
|
|
||||||
elif numpy.isclose(angle, 0):
|
|
||||||
# U-bend
|
|
||||||
(travel, jog), _ = port_src.measure_travel(port_dst)
|
|
||||||
self.uturn(portspec_src, -jog, length=-travel, **dst_args)
|
|
||||||
else:
|
|
||||||
raise BuildError(f"Don't know how to route ports with relative angle {angle}")
|
|
||||||
|
|
||||||
if thru is not None:
|
|
||||||
self.rename_ports({thru: portspec_src})
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
def _uturn_fallback(
|
|
||||||
self,
|
|
||||||
tool: Tool,
|
|
||||||
portspec: str,
|
|
||||||
jog: float,
|
|
||||||
length: float,
|
|
||||||
in_ptype: str | None,
|
|
||||||
plug_into: str | None,
|
|
||||||
**kwargs,
|
|
||||||
) -> bool:
|
|
||||||
"""
|
|
||||||
Attempt to perform a U-turn using two L-bends.
|
|
||||||
Returns True if successful, False if planL failed.
|
|
||||||
"""
|
|
||||||
# Fall back to drawing two L-bends
|
|
||||||
ccw = jog > 0
|
|
||||||
kwargs_no_out = kwargs | {'out_ptype': None}
|
|
||||||
try:
|
|
||||||
# First, find R by planning a minimal L-bend.
|
|
||||||
# Use a large length to ensure we don't hit tool-specific minimum length constraints.
|
|
||||||
dummy_port, _ = tool.planL(ccw, 1e9, in_ptype=in_ptype, **kwargs_no_out)
|
|
||||||
R = abs(dummy_port.y)
|
|
||||||
|
|
||||||
L1 = length + R
|
|
||||||
L2 = abs(jog) - R
|
|
||||||
|
|
||||||
kwargs_plug = kwargs | {'plug_into': plug_into}
|
|
||||||
self._traceL(portspec, ccw, L1, **kwargs_no_out)
|
|
||||||
self._traceL(portspec, ccw, L2, **kwargs_plug)
|
|
||||||
except (BuildError, NotImplementedError):
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def _traceL(
|
|
||||||
self,
|
|
||||||
portspec: str,
|
|
||||||
ccw: SupportsBool | None,
|
|
||||||
length: float,
|
|
||||||
*,
|
|
||||||
plug_into: str | None = None,
|
|
||||||
**kwargs,
|
|
||||||
) -> Self:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def _traceS(
|
|
||||||
self,
|
|
||||||
portspec: str,
|
|
||||||
length: float,
|
|
||||||
jog: float,
|
|
||||||
*,
|
|
||||||
plug_into: str | None = None,
|
|
||||||
**kwargs,
|
|
||||||
) -> Self:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def _traceU(
|
|
||||||
self,
|
|
||||||
portspec: str,
|
|
||||||
jog: float,
|
|
||||||
*,
|
|
||||||
length: float = 0,
|
|
||||||
plug_into: str | None = None,
|
|
||||||
**kwargs,
|
|
||||||
) -> Self:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def path(self, *args, **kwargs) -> Self:
|
|
||||||
import warnings
|
|
||||||
warnings.warn("path() is deprecated; use trace(), straight(), or bend() instead", DeprecationWarning, stacklevel=2)
|
|
||||||
return self._traceL(*args, **kwargs)
|
|
||||||
|
|
||||||
def pathS(self, *args, **kwargs) -> Self:
|
|
||||||
import warnings
|
|
||||||
warnings.warn("pathS() is deprecated; use jog() instead", DeprecationWarning, stacklevel=2)
|
|
||||||
return self._traceS(*args, **kwargs)
|
|
||||||
|
|
||||||
def pathU(self, *args, **kwargs) -> Self:
|
|
||||||
import warnings
|
|
||||||
warnings.warn("pathU() is deprecated; use uturn() instead", DeprecationWarning, stacklevel=2)
|
|
||||||
return self._traceU(*args, **kwargs)
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def plug(
|
|
||||||
self,
|
|
||||||
other: Abstract | str | Pattern | TreeView,
|
|
||||||
map_in: dict[str, str],
|
|
||||||
map_out: dict[str, str | None] | None = None,
|
|
||||||
*,
|
|
||||||
mirrored: bool = False,
|
|
||||||
thru: bool | str = True,
|
|
||||||
set_rotation: bool | None = None,
|
|
||||||
append: bool = False,
|
|
||||||
ok_connections: Iterable[tuple[str, str]] = (),
|
|
||||||
) -> Self:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def plugged(self, connections: dict[str, str]) -> Self:
|
|
||||||
""" Manual connection acknowledgment. """
|
|
||||||
pass
|
|
||||||
|
|
||||||
def retool(
|
|
||||||
self,
|
|
||||||
tool: Tool,
|
|
||||||
keys: str | Sequence[str | None] | None = None,
|
|
||||||
) -> Self:
|
|
||||||
"""
|
|
||||||
Update the `Tool` which will be used when generating `Pattern`s for the ports
|
|
||||||
given by `keys`.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
tool: The new `Tool` to use for the given ports.
|
|
||||||
keys: Which ports the tool should apply to. `None` indicates the default tool,
|
|
||||||
used when there is no matching entry in `self.tools` for the port in question.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
if keys is None or isinstance(keys, str):
|
|
||||||
self.tools[keys] = tool
|
|
||||||
else:
|
|
||||||
for key in keys:
|
|
||||||
self.tools[key] = tool
|
|
||||||
return self
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def toolctx(
|
|
||||||
self,
|
|
||||||
tool: Tool,
|
|
||||||
keys: str | Sequence[str | None] | None = None,
|
|
||||||
) -> Iterator[Self]:
|
|
||||||
"""
|
|
||||||
Context manager for temporarily `retool`-ing and reverting the `retool`
|
|
||||||
upon exiting the context.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
tool: The new `Tool` to use for the given ports.
|
|
||||||
keys: Which ports the tool should apply to. `None` indicates the default tool,
|
|
||||||
used when there is no matching entry in `self.tools` for the port in question.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
if keys is None or isinstance(keys, str):
|
|
||||||
keys = [keys]
|
|
||||||
saved_tools = {kk: self.tools.get(kk, None) for kk in keys} # If not in self.tools, save `None`
|
|
||||||
try:
|
|
||||||
yield self.retool(tool=tool, keys=keys)
|
|
||||||
finally:
|
|
||||||
for kk, tt in saved_tools.items():
|
|
||||||
if tt is None:
|
|
||||||
# delete if present
|
|
||||||
self.tools.pop(kk, None)
|
|
||||||
else:
|
|
||||||
self.tools[kk] = tt
|
|
||||||
|
|
||||||
def path_to(
|
|
||||||
self,
|
|
||||||
portspec: str,
|
|
||||||
ccw: SupportsBool | None,
|
|
||||||
position: float | None = None,
|
|
||||||
*,
|
|
||||||
x: float | None = None,
|
|
||||||
y: float | None = None,
|
|
||||||
plug_into: str | None = None,
|
|
||||||
**kwargs,
|
|
||||||
) -> Self:
|
|
||||||
"""
|
|
||||||
[DEPRECATED] use trace_to() instead.
|
|
||||||
"""
|
|
||||||
import warnings
|
|
||||||
warnings.warn("path_to() is deprecated; use trace_to() instead", DeprecationWarning, stacklevel=2)
|
|
||||||
|
|
||||||
bounds = {kk: vv for kk, vv in (('position', position), ('x', x), ('y', y)) if vv is not None}
|
|
||||||
return self.trace_to(portspec, ccw, plug_into=plug_into, **bounds, **kwargs)
|
|
||||||
|
|
||||||
def path_into(
|
|
||||||
self,
|
|
||||||
portspec_src: str,
|
|
||||||
portspec_dst: str,
|
|
||||||
*,
|
|
||||||
out_ptype: str | None = None,
|
|
||||||
plug_destination: bool = True,
|
|
||||||
thru: str | None = None,
|
|
||||||
**kwargs,
|
|
||||||
) -> Self:
|
|
||||||
"""
|
|
||||||
[DEPRECATED] use trace_into() instead.
|
|
||||||
"""
|
|
||||||
import warnings
|
|
||||||
warnings.warn("path_into() is deprecated; use trace_into() instead", DeprecationWarning, stacklevel=2)
|
|
||||||
|
|
||||||
return self.trace_into(
|
|
||||||
portspec_src,
|
|
||||||
portspec_dst,
|
|
||||||
out_ptype = out_ptype,
|
|
||||||
plug_destination = plug_destination,
|
|
||||||
thru = thru,
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
|
|
||||||
def mpath(
|
|
||||||
self,
|
|
||||||
portspec: str | Sequence[str],
|
|
||||||
ccw: SupportsBool | None,
|
|
||||||
*,
|
|
||||||
spacing: float | ArrayLike | None = None,
|
|
||||||
set_rotation: float | None = None,
|
|
||||||
**kwargs,
|
|
||||||
) -> Self:
|
|
||||||
"""
|
|
||||||
[DEPRECATED] use trace() or trace_to() instead.
|
|
||||||
"""
|
|
||||||
import warnings
|
|
||||||
warnings.warn("mpath() is deprecated; use trace() or trace_to() instead", DeprecationWarning, stacklevel=2)
|
|
||||||
|
|
||||||
return self.trace(portspec, ccw, spacing=spacing, set_rotation=set_rotation, **kwargs)
|
|
||||||
|
|
||||||
# TODO def bus_join()?
|
|
||||||
|
|
||||||
def flatten(self) -> Self:
|
|
||||||
"""
|
|
||||||
Flatten the contained pattern, using the contained library to resolve references.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
self.pattern.flatten(self.library)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def at(self, portspec: str | Iterable[str]) -> 'PortPather':
|
|
||||||
return PortPather(portspec, self)
|
|
||||||
|
|
||||||
|
|
||||||
class PortPather:
|
|
||||||
"""
|
|
||||||
Port state manager
|
|
||||||
|
|
||||||
This class provides a convenient way to perform multiple pathing operations on a
|
|
||||||
set of ports without needing to repeatedly pass their names.
|
|
||||||
"""
|
|
||||||
ports: list[str]
|
|
||||||
pather: PatherMixin
|
|
||||||
|
|
||||||
def __init__(self, ports: str | Iterable[str], pather: PatherMixin) -> None:
|
|
||||||
self.ports = [ports] if isinstance(ports, str) else list(ports)
|
|
||||||
self.pather = pather
|
|
||||||
|
|
||||||
#
|
|
||||||
# Delegate to pather
|
|
||||||
#
|
|
||||||
def retool(self, tool: Tool) -> Self:
|
|
||||||
self.pather.retool(tool, keys=self.ports)
|
|
||||||
return self
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def toolctx(self, tool: Tool) -> Iterator[Self]:
|
|
||||||
with self.pather.toolctx(tool, keys=self.ports):
|
|
||||||
yield self
|
|
||||||
|
|
||||||
def trace(self, ccw: SupportsBool | None, length: float | None = None, **kwargs) -> Self:
|
|
||||||
self.pather.trace(self.ports, ccw, length, **kwargs)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def trace_to(self, ccw: SupportsBool | None, **kwargs) -> Self:
|
|
||||||
self.pather.trace_to(self.ports, ccw, **kwargs)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def straight(self, length: float | None = None, **kwargs) -> Self:
|
|
||||||
self.pather.straight(self.ports, length, **kwargs)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def bend(self, ccw: SupportsBool, length: float | None = None, **kwargs) -> Self:
|
|
||||||
self.pather.bend(self.ports, ccw, length, **kwargs)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def ccw(self, length: float | None = None, **kwargs) -> Self:
|
|
||||||
self.pather.ccw(self.ports, length, **kwargs)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def cw(self, length: float | None = None, **kwargs) -> Self:
|
|
||||||
self.pather.cw(self.ports, length, **kwargs)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def jog(self, offset: float, length: float | None = None, **kwargs) -> Self:
|
|
||||||
self.pather.jog(self.ports, offset, length, **kwargs)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def uturn(self, offset: float, length: float | None = None, **kwargs) -> Self:
|
|
||||||
self.pather.uturn(self.ports, offset, length, **kwargs)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def trace_into(self, target_port: str, **kwargs) -> Self:
|
|
||||||
if len(self.ports) > 1:
|
|
||||||
raise BuildError(f'Unable use implicit trace_into() with {len(self.ports)} (>1) ports.')
|
|
||||||
self.pather.trace_into(self.ports[0], target_port, **kwargs)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def plug(
|
|
||||||
self,
|
|
||||||
other: Abstract | str,
|
|
||||||
other_port: str,
|
|
||||||
*args,
|
|
||||||
**kwargs,
|
|
||||||
) -> Self:
|
|
||||||
if len(self.ports) > 1:
|
|
||||||
raise BuildError(f'Unable use implicit plug() with {len(self.ports)} ports.'
|
|
||||||
'Use the pather or pattern directly to plug multiple ports.')
|
|
||||||
self.pather.plug(other, {self.ports[0]: other_port}, *args, **kwargs)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def plugged(self, other_port: str | Mapping[str, str]) -> Self:
|
|
||||||
if isinstance(other_port, Mapping):
|
|
||||||
self.pather.plugged(dict(other_port))
|
|
||||||
elif len(self.ports) > 1:
|
|
||||||
raise BuildError(f'Unable use implicit plugged() with {len(self.ports)} (>1) ports.')
|
|
||||||
else:
|
|
||||||
self.pather.plugged({self.ports[0]: other_port})
|
|
||||||
return self
|
|
||||||
|
|
||||||
#
|
|
||||||
# Delegate to port
|
|
||||||
#
|
|
||||||
def set_ptype(self, ptype: str) -> Self:
|
|
||||||
for port in self.ports:
|
|
||||||
self.pather.pattern[port].set_ptype(ptype)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def translate(self, *args, **kwargs) -> Self:
|
|
||||||
for port in self.ports:
|
|
||||||
self.pather.pattern[port].translate(*args, **kwargs)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def mirror(self, *args, **kwargs) -> Self:
|
|
||||||
for port in self.ports:
|
|
||||||
self.pather.pattern[port].mirror(*args, **kwargs)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def rotate(self, rotation: float) -> Self:
|
|
||||||
for port in self.ports:
|
|
||||||
self.pather.pattern[port].rotate(rotation)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def set_rotation(self, rotation: float | None) -> Self:
|
|
||||||
for port in self.ports:
|
|
||||||
self.pather.pattern[port].set_rotation(rotation)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def rename(self, name: str | Mapping[str, str | None]) -> Self:
|
|
||||||
""" Rename active ports. Replaces `rename_to`. """
|
|
||||||
name_map: dict[str, str | None]
|
|
||||||
if isinstance(name, str):
|
|
||||||
if len(self.ports) > 1:
|
|
||||||
raise BuildError('Use a mapping to rename >1 port')
|
|
||||||
name_map = {self.ports[0]: name}
|
|
||||||
else:
|
|
||||||
name_map = dict(name)
|
|
||||||
self.pather.rename_ports(name_map)
|
|
||||||
self.ports = [mm for mm in [name_map.get(pp, pp) for pp in self.ports] if mm is not None]
|
|
||||||
return self
|
|
||||||
|
|
||||||
def select(self, ports: str | Iterable[str]) -> Self:
|
|
||||||
""" Add ports to the selection. Replaces `add_ports`. """
|
|
||||||
if isinstance(ports, str):
|
|
||||||
ports = [ports]
|
|
||||||
for port in ports:
|
|
||||||
if port not in self.ports:
|
|
||||||
self.ports.append(port)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def deselect(self, ports: str | Iterable[str]) -> Self:
|
|
||||||
""" Remove ports from the selection. Replaces `drop_port`. """
|
|
||||||
if isinstance(ports, str):
|
|
||||||
ports = [ports]
|
|
||||||
ports_set = set(ports)
|
|
||||||
self.ports = [pp for pp in self.ports if pp not in ports_set]
|
|
||||||
return self
|
|
||||||
|
|
||||||
def mark(self, name: str | Mapping[str, str]) -> Self:
|
|
||||||
""" Bookmark current port(s). Replaces `save_copy`. """
|
|
||||||
name_map: Mapping[str, str]
|
|
||||||
if isinstance(name, str):
|
|
||||||
if len(self.ports) > 1:
|
|
||||||
raise BuildError('Use a mapping to mark >1 port')
|
|
||||||
name_map = {self.ports[0]: name}
|
|
||||||
else:
|
|
||||||
name_map = name
|
|
||||||
for src, dst in name_map.items():
|
|
||||||
self.pather.pattern.ports[dst] = self.pather.pattern[src].copy()
|
|
||||||
return self
|
|
||||||
|
|
||||||
def fork(self, name: str | Mapping[str, str]) -> Self:
|
|
||||||
""" Split and follow new name. Replaces `into_copy`. """
|
|
||||||
name_map: Mapping[str, str]
|
|
||||||
if isinstance(name, str):
|
|
||||||
if len(self.ports) > 1:
|
|
||||||
raise BuildError('Use a mapping to fork >1 port')
|
|
||||||
name_map = {self.ports[0]: name}
|
|
||||||
else:
|
|
||||||
name_map = name
|
|
||||||
for src, dst in name_map.items():
|
|
||||||
self.pather.pattern.ports[dst] = self.pather.pattern[src].copy()
|
|
||||||
self.ports = [(dst if pp == src else pp) for pp in self.ports]
|
|
||||||
return self
|
|
||||||
|
|
||||||
def drop(self) -> Self:
|
|
||||||
""" Remove selected ports from the pattern and the PortPather. Replaces `delete(None)`. """
|
|
||||||
for pp in self.ports:
|
|
||||||
del self.pather.pattern.ports[pp]
|
|
||||||
self.ports = []
|
|
||||||
return self
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def delete(self, name: None) -> None: ...
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def delete(self, name: str) -> Self: ...
|
|
||||||
|
|
||||||
def delete(self, name: str | None = None) -> Self | None:
|
|
||||||
if name is None:
|
|
||||||
self.drop()
|
|
||||||
return None
|
|
||||||
del self.pather.pattern.ports[name]
|
|
||||||
self.ports = [pp for pp in self.ports if pp != name]
|
|
||||||
return self
|
|
||||||
|
|
@ -1,805 +0,0 @@
|
||||||
"""
|
|
||||||
Pather with batched (multi-step) rendering
|
|
||||||
"""
|
|
||||||
from typing import Self
|
|
||||||
from collections.abc import Sequence, Mapping, MutableMapping, Iterable
|
|
||||||
import copy
|
|
||||||
import logging
|
|
||||||
from collections import defaultdict
|
|
||||||
from functools import wraps
|
|
||||||
from pprint import pformat
|
|
||||||
|
|
||||||
import numpy
|
|
||||||
from numpy import pi
|
|
||||||
from numpy.typing import ArrayLike, NDArray
|
|
||||||
|
|
||||||
from ..pattern import Pattern
|
|
||||||
from ..library import ILibrary, TreeView
|
|
||||||
from ..error import BuildError
|
|
||||||
from ..ports import PortList, Port
|
|
||||||
from ..abstract import Abstract
|
|
||||||
from ..utils import SupportsBool
|
|
||||||
from .tools import Tool, RenderStep
|
|
||||||
from .pather_mixin import PatherMixin
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class RenderPather(PatherMixin):
|
|
||||||
"""
|
|
||||||
`RenderPather` is an alternative to `Pather` which uses the `trace`/`trace_to`
|
|
||||||
functions to plan out wire paths without incrementally generating the layout. Instead,
|
|
||||||
it waits until `render` is called, at which point it draws all the planned segments
|
|
||||||
simultaneously. This allows it to e.g. draw each wire using a single `Path` or
|
|
||||||
`Polygon` shape instead of multiple rectangles.
|
|
||||||
|
|
||||||
`RenderPather` calls out to `Tool.planL` and `Tool.render` to provide tool-specific
|
|
||||||
dimensions and build the final geometry for each wire. `Tool.planL` provides the
|
|
||||||
output port data (relative to the input) for each segment. The tool, input and output
|
|
||||||
ports are placed into a `RenderStep`, and a sequence of `RenderStep`s is stored for
|
|
||||||
each port. When `render` is called, it bundles `RenderStep`s into batches which use
|
|
||||||
the same `Tool`, and passes each batch to the relevant tool's `Tool.render` to build
|
|
||||||
the geometry.
|
|
||||||
|
|
||||||
See `Pather` for routing examples. After routing is complete, `render` must be called
|
|
||||||
to generate the final geometry.
|
|
||||||
"""
|
|
||||||
__slots__ = ('pattern', 'library', 'paths', 'tools', '_dead', )
|
|
||||||
|
|
||||||
pattern: Pattern
|
|
||||||
""" Layout of this device """
|
|
||||||
|
|
||||||
library: ILibrary
|
|
||||||
""" Library from which patterns should be referenced """
|
|
||||||
|
|
||||||
_dead: bool
|
|
||||||
""" If True, plug()/place() are skipped (for debugging) """
|
|
||||||
|
|
||||||
paths: defaultdict[str, list[RenderStep]]
|
|
||||||
""" Per-port list of operations, to be used by `render` """
|
|
||||||
|
|
||||||
tools: dict[str | None, Tool]
|
|
||||||
"""
|
|
||||||
Tool objects are used to dynamically generate new single-use Devices
|
|
||||||
(e.g wires or waveguides) to be plugged into this device.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def ports(self) -> dict[str, Port]:
|
|
||||||
return self.pattern.ports
|
|
||||||
|
|
||||||
@ports.setter
|
|
||||||
def ports(self, value: dict[str, Port]) -> None:
|
|
||||||
self.pattern.ports = value
|
|
||||||
|
|
||||||
def __del__(self) -> None:
|
|
||||||
if any(pp for pp in self.paths):
|
|
||||||
logger.warning('RenderPather had unrendered paths', stack_info=True)
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
library: ILibrary,
|
|
||||||
*,
|
|
||||||
pattern: Pattern | None = None,
|
|
||||||
ports: str | Mapping[str, Port] | None = None,
|
|
||||||
tools: Tool | MutableMapping[str | None, Tool] | None = None,
|
|
||||||
name: str | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Args:
|
|
||||||
library: The library from which referenced patterns will be taken,
|
|
||||||
and where new patterns (e.g. generated by the `tools`) will be placed.
|
|
||||||
pattern: The pattern which will be modified by subsequent operations.
|
|
||||||
If `None` (default), a new pattern is created.
|
|
||||||
ports: Allows specifying the initial set of ports, if `pattern` does
|
|
||||||
not already have any ports (or is not provided). May be a string,
|
|
||||||
in which case it is interpreted as a name in `library`.
|
|
||||||
Default `None` (no ports).
|
|
||||||
tools: A mapping of {port: tool} which specifies what `Tool` should be used
|
|
||||||
to generate waveguide or wire segments when `trace`/`trace_to`
|
|
||||||
are called. Relies on `Tool.planL` and `Tool.render` implementations.
|
|
||||||
name: If specified, `library[name]` is set to `self.pattern`.
|
|
||||||
"""
|
|
||||||
self._dead = False
|
|
||||||
self.paths = defaultdict(list)
|
|
||||||
self.library = library
|
|
||||||
if pattern is not None:
|
|
||||||
self.pattern = pattern
|
|
||||||
else:
|
|
||||||
self.pattern = Pattern()
|
|
||||||
|
|
||||||
if ports is not None:
|
|
||||||
if self.pattern.ports:
|
|
||||||
raise BuildError('Ports supplied for pattern with pre-existing ports!')
|
|
||||||
if isinstance(ports, str):
|
|
||||||
ports = library.abstract(ports).ports
|
|
||||||
|
|
||||||
self.pattern.ports.update(copy.deepcopy(dict(ports)))
|
|
||||||
|
|
||||||
if name is not None:
|
|
||||||
library[name] = self.pattern
|
|
||||||
|
|
||||||
if tools is None:
|
|
||||||
self.tools = {}
|
|
||||||
elif isinstance(tools, Tool):
|
|
||||||
self.tools = {None: tools}
|
|
||||||
else:
|
|
||||||
self.tools = dict(tools)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def interface(
|
|
||||||
cls: type['RenderPather'],
|
|
||||||
source: PortList | Mapping[str, Port] | str,
|
|
||||||
*,
|
|
||||||
library: ILibrary | None = None,
|
|
||||||
tools: Tool | MutableMapping[str | None, Tool] | None = None,
|
|
||||||
in_prefix: str = 'in_',
|
|
||||||
out_prefix: str = '',
|
|
||||||
port_map: dict[str, str] | Sequence[str] | None = None,
|
|
||||||
name: str | None = None,
|
|
||||||
) -> 'RenderPather':
|
|
||||||
"""
|
|
||||||
Wrapper for `Pattern.interface()`, which returns a RenderPather instead.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
source: A collection of ports (e.g. Pattern, Builder, or dict)
|
|
||||||
from which to create the interface. May be a pattern name if
|
|
||||||
`library` is provided.
|
|
||||||
library: Library from which existing patterns should be referenced,
|
|
||||||
and to which the new one should be added (if named). If not provided,
|
|
||||||
`source.library` must exist and will be used.
|
|
||||||
tools: `Tool`s which will be used by the pather for generating new wires
|
|
||||||
or waveguides (via `trace`/`trace_to`).
|
|
||||||
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 `RenderPather`, with an empty pattern and 2x as many ports as
|
|
||||||
listed in port_map.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
`PortError` if `port_map` contains port names not present in the
|
|
||||||
current device.
|
|
||||||
`PortError` if applying the prefixes results in duplicate port
|
|
||||||
names.
|
|
||||||
"""
|
|
||||||
if library is None:
|
|
||||||
if hasattr(source, 'library') and isinstance(source.library, ILibrary):
|
|
||||||
library = source.library
|
|
||||||
else:
|
|
||||||
raise BuildError('No library provided (and not present in `source.library`')
|
|
||||||
|
|
||||||
if tools is None and hasattr(source, 'tools') and isinstance(source.tools, dict):
|
|
||||||
tools = source.tools
|
|
||||||
|
|
||||||
if isinstance(source, str):
|
|
||||||
source = library.abstract(source).ports
|
|
||||||
|
|
||||||
pat = Pattern.interface(source, in_prefix=in_prefix, out_prefix=out_prefix, port_map=port_map)
|
|
||||||
new = RenderPather(library=library, pattern=pat, name=name, tools=tools)
|
|
||||||
return new
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
s = f'<RenderPather {self.pattern} L({len(self.library)}) {pformat(self.tools)}>'
|
|
||||||
return s
|
|
||||||
|
|
||||||
def plug(
|
|
||||||
self,
|
|
||||||
other: Abstract | str | Pattern | TreeView,
|
|
||||||
map_in: dict[str, str],
|
|
||||||
map_out: dict[str, str | None] | None = None,
|
|
||||||
*,
|
|
||||||
mirrored: bool = False,
|
|
||||||
thru: bool | str = True,
|
|
||||||
set_rotation: bool | None = None,
|
|
||||||
append: bool = False,
|
|
||||||
ok_connections: Iterable[tuple[str, str]] = (),
|
|
||||||
) -> Self:
|
|
||||||
"""
|
|
||||||
Wrapper for `Pattern.plug` which adds a `RenderStep` with opcode 'P'
|
|
||||||
for any affected ports. This separates any future `RenderStep`s on the
|
|
||||||
same port into a new batch, since the plugged device interferes with drawing.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
other: An `Abstract`, string, or `Pattern` describing the device to be instatiated.
|
|
||||||
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 ports in `other`.
|
|
||||||
mirrored: Enables mirroring `other` across the x axis prior to
|
|
||||||
connecting any ports.
|
|
||||||
thru: If map_in specifies only a single port, `thru` provides a mechainsm
|
|
||||||
to avoid repeating the port name. Eg, for `map_in={'myport': 'A'}`,
|
|
||||||
- If True (default), and `other` has only two ports total, and map_out
|
|
||||||
doesn't specify a name for the other port, its name is set to the key
|
|
||||||
in `map_in`, i.e. 'myport'.
|
|
||||||
- If a string, `map_out[thru]` is set to the key in `map_in` (i.e. 'myport').
|
|
||||||
An error is raised if that entry already exists.
|
|
||||||
|
|
||||||
This makes it easy to extend a pattern with simple 2-port devices
|
|
||||||
(e.g. wires) without providing `map_out` each time `plug` is
|
|
||||||
called. See "Examples" above for more info. Default `True`.
|
|
||||||
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`.
|
|
||||||
append: If `True`, `other` is appended instead of being referenced.
|
|
||||||
Note that this does not flatten `other`, so its refs will still
|
|
||||||
be refs (now inside `self`).
|
|
||||||
ok_connections: Set of "allowed" ptype combinations. Identical
|
|
||||||
ptypes are always allowed to connect, as is `'unk'` with
|
|
||||||
any other ptypte. Non-allowed ptype connections will emit a
|
|
||||||
warning. Order is ignored, i.e. `(a, b)` is equivalent to
|
|
||||||
`(b, a)`.
|
|
||||||
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
`PortError` if any ports specified in `map_in` or `map_out` do not
|
|
||||||
exist in `self.ports` or `other_names`.
|
|
||||||
`PortError` if there are any duplicate names after `map_in` and `map_out`
|
|
||||||
are applied.
|
|
||||||
`PortError` if the specified port mapping is not achieveable (the ports
|
|
||||||
do not line up)
|
|
||||||
"""
|
|
||||||
if self._dead:
|
|
||||||
logger.warning('Skipping geometry for plug() since device is dead')
|
|
||||||
|
|
||||||
other_tgt: Pattern | Abstract
|
|
||||||
if isinstance(other, str):
|
|
||||||
other_tgt = self.library.abstract(other)
|
|
||||||
if append and isinstance(other, Abstract):
|
|
||||||
other_tgt = self.library[other.name]
|
|
||||||
|
|
||||||
if not self._dead:
|
|
||||||
# get rid of plugged ports
|
|
||||||
for kk in map_in:
|
|
||||||
if kk in self.paths:
|
|
||||||
self.paths[kk].append(RenderStep('P', None, self.ports[kk].copy(), self.ports[kk].copy(), None))
|
|
||||||
|
|
||||||
plugged = map_in.values()
|
|
||||||
for name, port in other_tgt.ports.items():
|
|
||||||
if name in plugged:
|
|
||||||
continue
|
|
||||||
new_name = map_out.get(name, name) if map_out is not None else name
|
|
||||||
if new_name is not None and new_name in self.paths:
|
|
||||||
self.paths[new_name].append(RenderStep('P', None, port.copy(), port.copy(), None))
|
|
||||||
|
|
||||||
self.pattern.plug(
|
|
||||||
other = other_tgt,
|
|
||||||
map_in = map_in,
|
|
||||||
map_out = map_out,
|
|
||||||
mirrored = mirrored,
|
|
||||||
thru = thru,
|
|
||||||
set_rotation = set_rotation,
|
|
||||||
append = append,
|
|
||||||
ok_connections = ok_connections,
|
|
||||||
skip_geometry = self._dead,
|
|
||||||
)
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
def place(
|
|
||||||
self,
|
|
||||||
other: Abstract | str,
|
|
||||||
*,
|
|
||||||
offset: ArrayLike = (0, 0),
|
|
||||||
rotation: float = 0,
|
|
||||||
pivot: ArrayLike = (0, 0),
|
|
||||||
mirrored: bool = False,
|
|
||||||
port_map: dict[str, str | None] | None = None,
|
|
||||||
skip_port_check: bool = False,
|
|
||||||
append: bool = False,
|
|
||||||
) -> Self:
|
|
||||||
"""
|
|
||||||
Wrapper for `Pattern.place` which adds a `RenderStep` with opcode 'P'
|
|
||||||
for any affected ports. This separates any future `RenderStep`s on the
|
|
||||||
same port into a new batch, since the placed device interferes with drawing.
|
|
||||||
|
|
||||||
Note that mirroring is applied before rotation; translation (`offset`) is applied last.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
other: An `Abstract` or `Pattern` describing the device to be instatiated.
|
|
||||||
offset: Offset at which to place the instance. Default (0, 0).
|
|
||||||
rotation: Rotation applied to the instance before placement. Default 0.
|
|
||||||
pivot: Rotation is applied around this pivot point (default (0, 0)).
|
|
||||||
Rotation is applied prior to translation (`offset`).
|
|
||||||
mirrored: Whether theinstance should be mirrored across the x axis.
|
|
||||||
Mirroring is applied before translation and rotation.
|
|
||||||
port_map: dict of `{'old_name': 'new_name'}` mappings, specifying
|
|
||||||
new names for ports in the instantiated pattern. New names can be
|
|
||||||
`None`, which will delete those ports.
|
|
||||||
skip_port_check: Can be used to skip the internal call to `check_ports`,
|
|
||||||
in case it has already been performed elsewhere.
|
|
||||||
append: If `True`, `other` is appended instead of being referenced.
|
|
||||||
Note that this does not flatten `other`, so its refs will still
|
|
||||||
be refs (now inside `self`).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
`PortError` if any ports specified in `map_in` or `map_out` do not
|
|
||||||
exist in `self.ports` or `other.ports`.
|
|
||||||
`PortError` if there are any duplicate names after `map_in` and `map_out`
|
|
||||||
are applied.
|
|
||||||
"""
|
|
||||||
if self._dead:
|
|
||||||
logger.warning('Skipping geometry for place() since device is dead')
|
|
||||||
|
|
||||||
other_tgt: Pattern | Abstract
|
|
||||||
if isinstance(other, str):
|
|
||||||
other_tgt = self.library.abstract(other)
|
|
||||||
if append and isinstance(other, Abstract):
|
|
||||||
other_tgt = self.library[other.name]
|
|
||||||
|
|
||||||
if not self._dead:
|
|
||||||
for name, port in other_tgt.ports.items():
|
|
||||||
new_name = port_map.get(name, name) if port_map is not None else name
|
|
||||||
if new_name is not None and new_name in self.paths:
|
|
||||||
self.paths[new_name].append(RenderStep('P', None, port.copy(), port.copy(), None))
|
|
||||||
|
|
||||||
self.pattern.place(
|
|
||||||
other = other_tgt,
|
|
||||||
offset = offset,
|
|
||||||
rotation = rotation,
|
|
||||||
pivot = pivot,
|
|
||||||
mirrored = mirrored,
|
|
||||||
port_map = port_map,
|
|
||||||
skip_port_check = skip_port_check,
|
|
||||||
append = append,
|
|
||||||
skip_geometry = self._dead,
|
|
||||||
)
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
def plugged(
|
|
||||||
self,
|
|
||||||
connections: dict[str, str],
|
|
||||||
) -> Self:
|
|
||||||
if not self._dead:
|
|
||||||
for aa, bb in connections.items():
|
|
||||||
porta = self.ports[aa]
|
|
||||||
portb = self.ports[bb]
|
|
||||||
self.paths[aa].append(RenderStep('P', None, porta.copy(), porta.copy(), None))
|
|
||||||
self.paths[bb].append(RenderStep('P', None, portb.copy(), portb.copy(), None))
|
|
||||||
PortList.plugged(self, connections)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def _traceU(
|
|
||||||
self,
|
|
||||||
portspec: str,
|
|
||||||
jog: float,
|
|
||||||
*,
|
|
||||||
length: float = 0,
|
|
||||||
plug_into: str | None = None,
|
|
||||||
**kwargs,
|
|
||||||
) -> Self:
|
|
||||||
"""
|
|
||||||
Plan a U-shaped "wire"/"waveguide" extending from the port `portspec`, with the aim
|
|
||||||
of traveling exactly `length` distance and returning to the same orientation
|
|
||||||
with an offset `jog`.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
portspec: The name of the port into which the wire will be plugged.
|
|
||||||
jog: Total manhattan distance perpendicular to the direction of travel.
|
|
||||||
Positive values are to the left of the direction of travel.
|
|
||||||
length: Extra distance to travel along the port's axis. Default 0.
|
|
||||||
plug_into: If not None, attempts to plug the wire's output port into the provided
|
|
||||||
port on `self`.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
if self._dead:
|
|
||||||
logger.warning('Skipping geometry for _traceU() since device is dead')
|
|
||||||
|
|
||||||
port = self.pattern[portspec]
|
|
||||||
in_ptype = port.ptype
|
|
||||||
port_rot = port.rotation
|
|
||||||
assert port_rot is not None
|
|
||||||
|
|
||||||
tool = self.tools.get(portspec, self.tools[None])
|
|
||||||
|
|
||||||
try:
|
|
||||||
out_port, data = tool.planU(jog, length=length, in_ptype=in_ptype, **kwargs)
|
|
||||||
except (BuildError, NotImplementedError):
|
|
||||||
if self._uturn_fallback(tool, portspec, jog, length, in_ptype, plug_into, **kwargs):
|
|
||||||
return self
|
|
||||||
|
|
||||||
if not self._dead:
|
|
||||||
raise
|
|
||||||
logger.warning("Tool planning failed for dead pather. Using dummy extension.")
|
|
||||||
out_port = Port((length, jog), rotation=0, ptype=in_ptype)
|
|
||||||
data = None
|
|
||||||
|
|
||||||
if out_port is not None:
|
|
||||||
out_port.rotate_around((0, 0), pi + port_rot)
|
|
||||||
out_port.translate(port.offset)
|
|
||||||
if not self._dead:
|
|
||||||
step = RenderStep('U', tool, port.copy(), out_port.copy(), data)
|
|
||||||
self.paths[portspec].append(step)
|
|
||||||
self.pattern.ports[portspec] = out_port.copy()
|
|
||||||
self._log_port_update(portspec)
|
|
||||||
|
|
||||||
if plug_into is not None:
|
|
||||||
self.plugged({portspec: plug_into})
|
|
||||||
return self
|
|
||||||
|
|
||||||
def _traceL(
|
|
||||||
self,
|
|
||||||
portspec: str,
|
|
||||||
ccw: SupportsBool | None,
|
|
||||||
length: float,
|
|
||||||
*,
|
|
||||||
plug_into: str | None = None,
|
|
||||||
**kwargs,
|
|
||||||
) -> Self:
|
|
||||||
"""
|
|
||||||
Plan a "wire"/"waveguide" extending from the port `portspec`, with the aim
|
|
||||||
of traveling exactly `length` distance.
|
|
||||||
|
|
||||||
The wire will travel `length` distance along the port's axis, an an unspecified
|
|
||||||
(tool-dependent) distance in the perpendicular direction. The output port will
|
|
||||||
be rotated (or not) based on the `ccw` parameter.
|
|
||||||
|
|
||||||
`RenderPather.render` must be called after all paths have been fully planned.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
portspec: The name of the port into which the wire will be plugged.
|
|
||||||
ccw: If `None`, the output should be along the same axis as the input.
|
|
||||||
Otherwise, cast to bool and turn counterclockwise if True
|
|
||||||
and clockwise otherwise.
|
|
||||||
length: The total distance from input to output, along the input's axis only.
|
|
||||||
(There may be a tool-dependent offset along the other axis.)
|
|
||||||
plug_into: If not None, attempts to plug the wire's output port into the provided
|
|
||||||
port on `self`.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
|
|
||||||
Note:
|
|
||||||
If the builder is 'dead', this operation will still attempt to update
|
|
||||||
the target port's location. If the pathing tool fails (e.g. due to an
|
|
||||||
impossible length), a dummy linear extension is used to maintain port
|
|
||||||
consistency for downstream operations.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
BuildError if `distance` is too small to fit the bend (if a bend is present).
|
|
||||||
LibraryError if no valid name could be picked for the pattern.
|
|
||||||
"""
|
|
||||||
if self._dead:
|
|
||||||
logger.warning('Skipping geometry for _traceL() since device is dead')
|
|
||||||
|
|
||||||
port = self.pattern[portspec]
|
|
||||||
in_ptype = port.ptype
|
|
||||||
port_rot = port.rotation
|
|
||||||
assert port_rot is not None # TODO allow manually setting rotation for RenderPather.path()?
|
|
||||||
|
|
||||||
tool = self.tools.get(portspec, self.tools[None])
|
|
||||||
# ask the tool for bend size (fill missing dx or dy), check feasibility, and get out_ptype
|
|
||||||
try:
|
|
||||||
out_port, data = tool.planL(ccw, length, in_ptype=in_ptype, **kwargs)
|
|
||||||
except (BuildError, NotImplementedError):
|
|
||||||
if not self._dead:
|
|
||||||
raise
|
|
||||||
logger.warning("Tool planning failed for dead pather. Using dummy extension.")
|
|
||||||
if ccw is None:
|
|
||||||
out_rot = pi
|
|
||||||
elif bool(ccw):
|
|
||||||
out_rot = -pi / 2
|
|
||||||
else:
|
|
||||||
out_rot = pi / 2
|
|
||||||
out_port = Port((length, 0), rotation=out_rot, ptype=in_ptype)
|
|
||||||
data = None
|
|
||||||
|
|
||||||
# Update port
|
|
||||||
out_port.rotate_around((0, 0), pi + port_rot)
|
|
||||||
out_port.translate(port.offset)
|
|
||||||
|
|
||||||
if not self._dead:
|
|
||||||
step = RenderStep('L', tool, port.copy(), out_port.copy(), data)
|
|
||||||
self.paths[portspec].append(step)
|
|
||||||
|
|
||||||
self.pattern.ports[portspec] = out_port.copy()
|
|
||||||
self._log_port_update(portspec)
|
|
||||||
|
|
||||||
if plug_into is not None:
|
|
||||||
self.plugged({portspec: plug_into})
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
def _traceS(
|
|
||||||
self,
|
|
||||||
portspec: str,
|
|
||||||
length: float,
|
|
||||||
jog: float,
|
|
||||||
*,
|
|
||||||
plug_into: str | None = None,
|
|
||||||
**kwargs,
|
|
||||||
) -> Self:
|
|
||||||
"""
|
|
||||||
Create an S-shaped "wire"/"waveguide" and `plug` it into the port `portspec`, with the aim
|
|
||||||
of traveling exactly `length` distance with an offset `jog` along the other axis (+ve jog is
|
|
||||||
left of direction of travel).
|
|
||||||
|
|
||||||
The output port will have the same orientation as the source port (`portspec`).
|
|
||||||
|
|
||||||
`RenderPather.render` must be called after all paths have been fully planned.
|
|
||||||
|
|
||||||
This function attempts to use `tool.planS()`, but falls back to `tool.planL()` if the former
|
|
||||||
raises a NotImplementedError.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
portspec: The name of the port into which the wire will be plugged.
|
|
||||||
jog: Total manhattan distance perpendicular to the direction of travel.
|
|
||||||
Positive values are to the left of the direction of travel.
|
|
||||||
length: The total manhattan distance from input to output, along the input's axis only.
|
|
||||||
(There may be a tool-dependent offset along the other axis.)
|
|
||||||
plug_into: If not None, attempts to plug the wire's output port into the provided
|
|
||||||
port on `self`.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
|
|
||||||
Note:
|
|
||||||
If the builder is 'dead', this operation will still attempt to update
|
|
||||||
the target port's location. If the pathing tool fails (e.g. due to an
|
|
||||||
impossible length), a dummy linear extension is used to maintain port
|
|
||||||
consistency for downstream operations.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
BuildError if `distance` is too small to fit the s-bend (for nonzero jog).
|
|
||||||
LibraryError if no valid name could be picked for the pattern.
|
|
||||||
"""
|
|
||||||
if self._dead:
|
|
||||||
logger.warning('Skipping geometry for _traceS() since device is dead')
|
|
||||||
|
|
||||||
port = self.pattern[portspec]
|
|
||||||
in_ptype = port.ptype
|
|
||||||
port_rot = port.rotation
|
|
||||||
assert port_rot is not None # TODO allow manually setting rotation for RenderPather.path()?
|
|
||||||
|
|
||||||
tool = self.tools.get(portspec, self.tools[None])
|
|
||||||
|
|
||||||
# check feasibility, get output port and data
|
|
||||||
try:
|
|
||||||
out_port, data = tool.planS(length, jog, in_ptype=in_ptype, **kwargs)
|
|
||||||
except NotImplementedError:
|
|
||||||
# Fall back to drawing two L-bends
|
|
||||||
ccw0 = jog > 0
|
|
||||||
kwargs_no_out = (kwargs | {'out_ptype': None})
|
|
||||||
try:
|
|
||||||
t_port0, _ = tool.planL( ccw0, length / 2, in_ptype=in_ptype, **kwargs_no_out) # TODO length/2 may fail w/asymmetric ptypes
|
|
||||||
jog0 = Port((0, 0), 0).measure_travel(t_port0)[0][1]
|
|
||||||
t_port1, _ = tool.planL(not ccw0, abs(jog - jog0), in_ptype=t_port0.ptype, **kwargs)
|
|
||||||
jog1 = Port((0, 0), 0).measure_travel(t_port1)[0][1]
|
|
||||||
|
|
||||||
kwargs_plug = kwargs | {'plug_into': plug_into}
|
|
||||||
self._traceL(portspec, ccw0, length - abs(jog1), **kwargs_no_out)
|
|
||||||
self._traceL(portspec, not ccw0, abs(jog - jog0), **kwargs_plug)
|
|
||||||
except (BuildError, NotImplementedError):
|
|
||||||
if not self._dead:
|
|
||||||
raise
|
|
||||||
# Fall through to dummy extension below
|
|
||||||
else:
|
|
||||||
return self
|
|
||||||
except BuildError:
|
|
||||||
if not self._dead:
|
|
||||||
raise
|
|
||||||
# Fall through to dummy extension below
|
|
||||||
|
|
||||||
if self._dead:
|
|
||||||
logger.warning("Tool planning failed for dead pather. Using dummy extension.")
|
|
||||||
out_port = Port((length, jog), rotation=pi, ptype=in_ptype)
|
|
||||||
data = None
|
|
||||||
|
|
||||||
if out_port is not None:
|
|
||||||
out_port.rotate_around((0, 0), pi + port_rot)
|
|
||||||
out_port.translate(port.offset)
|
|
||||||
if not self._dead:
|
|
||||||
step = RenderStep('S', tool, port.copy(), out_port.copy(), data)
|
|
||||||
self.paths[portspec].append(step)
|
|
||||||
self.pattern.ports[portspec] = out_port.copy()
|
|
||||||
self._log_port_update(portspec)
|
|
||||||
|
|
||||||
if plug_into is not None:
|
|
||||||
self.plugged({portspec: plug_into})
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
def render(
|
|
||||||
self,
|
|
||||||
append: bool = True,
|
|
||||||
) -> Self:
|
|
||||||
"""
|
|
||||||
Generate the geometry which has been planned out with `trace`/`trace_to`/etc.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
append: If `True`, the rendered geometry will be directly appended to
|
|
||||||
`self.pattern`. Note that it will not be flattened, so if only one
|
|
||||||
layer of hierarchy is eliminated.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
lib = self.library
|
|
||||||
tool_port_names = ('A', 'B')
|
|
||||||
pat = Pattern()
|
|
||||||
|
|
||||||
def render_batch(portspec: str, batch: list[RenderStep], append: bool) -> None:
|
|
||||||
assert batch[0].tool is not None
|
|
||||||
# Tools render in local space (first port at 0,0, rotation 0).
|
|
||||||
tree = batch[0].tool.render(batch, port_names=tool_port_names)
|
|
||||||
|
|
||||||
actual_in, actual_out = tool_port_names
|
|
||||||
name = lib << tree
|
|
||||||
|
|
||||||
# To plug the segment at its intended location, we create a
|
|
||||||
# 'stationary' port in our temporary pattern that matches
|
|
||||||
# the batch's planned start.
|
|
||||||
if portspec in pat.ports:
|
|
||||||
del pat.ports[portspec]
|
|
||||||
|
|
||||||
stationary_port = batch[0].start_port.copy()
|
|
||||||
pat.ports[portspec] = stationary_port
|
|
||||||
|
|
||||||
if append:
|
|
||||||
# pat.plug() translates and rotates the tool's local output to the start port.
|
|
||||||
pat.plug(lib[name], {portspec: actual_in}, append=append)
|
|
||||||
del lib[name]
|
|
||||||
else:
|
|
||||||
pat.plug(lib.abstract(name), {portspec: actual_in}, append=append)
|
|
||||||
|
|
||||||
# Rename output back to portspec for the next batch.
|
|
||||||
if portspec not in pat.ports and actual_out in pat.ports:
|
|
||||||
pat.rename_ports({actual_out: portspec}, overwrite=True)
|
|
||||||
|
|
||||||
for portspec, steps in self.paths.items():
|
|
||||||
if not steps:
|
|
||||||
continue
|
|
||||||
|
|
||||||
batch: list[RenderStep] = []
|
|
||||||
# Initialize continuity check with the start of the entire path.
|
|
||||||
prev_end = steps[0].start_port
|
|
||||||
|
|
||||||
for step in steps:
|
|
||||||
appendable_op = step.opcode in ('L', 'S', 'U')
|
|
||||||
same_tool = batch and step.tool == batch[0].tool
|
|
||||||
|
|
||||||
# Check continuity with tolerance
|
|
||||||
offsets_match = numpy.allclose(step.start_port.offset, prev_end.offset)
|
|
||||||
rotations_match = (step.start_port.rotation is None and prev_end.rotation is None) or (
|
|
||||||
step.start_port.rotation is not None and prev_end.rotation is not None and
|
|
||||||
numpy.isclose(step.start_port.rotation, prev_end.rotation)
|
|
||||||
)
|
|
||||||
continuous = offsets_match and rotations_match
|
|
||||||
|
|
||||||
# If we can't continue a batch, render it
|
|
||||||
if batch and (not appendable_op or not same_tool or not continuous):
|
|
||||||
render_batch(portspec, batch, append)
|
|
||||||
batch = []
|
|
||||||
|
|
||||||
# batch is emptied already if we couldn't continue it
|
|
||||||
if appendable_op:
|
|
||||||
batch.append(step)
|
|
||||||
|
|
||||||
# Opcodes which break the batch go below this line
|
|
||||||
if not appendable_op:
|
|
||||||
if portspec in pat.ports:
|
|
||||||
del pat.ports[portspec]
|
|
||||||
# Plugged ports should be tracked
|
|
||||||
if step.opcode == 'P' and portspec in pat.ports:
|
|
||||||
del pat.ports[portspec]
|
|
||||||
|
|
||||||
prev_end = step.end_port
|
|
||||||
|
|
||||||
#If the last batch didn't end yet
|
|
||||||
if batch:
|
|
||||||
render_batch(portspec, batch, append)
|
|
||||||
|
|
||||||
self.paths.clear()
|
|
||||||
pat.ports.clear()
|
|
||||||
self.pattern.append(pat)
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
def translate(self, offset: ArrayLike) -> Self:
|
|
||||||
"""
|
|
||||||
Translate the pattern and all ports.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
offset: (x, y) distance to translate by
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
offset_arr: NDArray[numpy.float64] = numpy.asarray(offset)
|
|
||||||
self.pattern.translate_elements(offset_arr)
|
|
||||||
for steps in self.paths.values():
|
|
||||||
for i, step in enumerate(steps):
|
|
||||||
steps[i] = step.transformed(offset_arr, 0, numpy.zeros(2))
|
|
||||||
return self
|
|
||||||
|
|
||||||
def rotate_around(self, pivot: ArrayLike, angle: float) -> Self:
|
|
||||||
"""
|
|
||||||
Rotate the pattern and all ports.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
angle: angle (radians, counterclockwise) to rotate by
|
|
||||||
pivot: location to rotate around
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
pivot_arr: NDArray[numpy.float64] = numpy.asarray(pivot)
|
|
||||||
self.pattern.rotate_around(pivot_arr, angle)
|
|
||||||
for steps in self.paths.values():
|
|
||||||
for i, step in enumerate(steps):
|
|
||||||
steps[i] = step.transformed(numpy.zeros(2), angle, pivot_arr)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def mirror(self, axis: int) -> Self:
|
|
||||||
"""
|
|
||||||
Mirror the pattern and all ports across the specified axis.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
axis: Axis to mirror across (x=0, y=1)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
self.pattern.mirror(axis)
|
|
||||||
for steps in self.paths.values():
|
|
||||||
for i, step in enumerate(steps):
|
|
||||||
steps[i] = step.mirrored(axis)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def set_dead(self) -> Self:
|
|
||||||
"""
|
|
||||||
Disallows further changes through `plug()` or `place()`.
|
|
||||||
This is meant for debugging:
|
|
||||||
```
|
|
||||||
dev.plug(a, ...)
|
|
||||||
dev.set_dead() # added for debug purposes
|
|
||||||
dev.plug(b, ...) # usually raises an error, but now skipped
|
|
||||||
dev.plug(c, ...) # also skipped
|
|
||||||
dev.pattern.visualize() # shows the device as of the set_dead() call
|
|
||||||
```
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
"""
|
|
||||||
self._dead = True
|
|
||||||
return self
|
|
||||||
|
|
||||||
@wraps(Pattern.label)
|
|
||||||
def label(self, *args, **kwargs) -> Self:
|
|
||||||
self.pattern.label(*args, **kwargs)
|
|
||||||
return self
|
|
||||||
|
|
||||||
@wraps(Pattern.ref)
|
|
||||||
def ref(self, *args, **kwargs) -> Self:
|
|
||||||
self.pattern.ref(*args, **kwargs)
|
|
||||||
return self
|
|
||||||
|
|
||||||
@wraps(Pattern.polygon)
|
|
||||||
def polygon(self, *args, **kwargs) -> Self:
|
|
||||||
self.pattern.polygon(*args, **kwargs)
|
|
||||||
return self
|
|
||||||
|
|
||||||
@wraps(Pattern.rect)
|
|
||||||
def rect(self, *args, **kwargs) -> Self:
|
|
||||||
self.pattern.rect(*args, **kwargs)
|
|
||||||
return self
|
|
||||||
|
|
@ -4,7 +4,7 @@ Tools are objects which dynamically generate simple single-use devices (e.g. wir
|
||||||
# TODO document all tools
|
# TODO document all tools
|
||||||
"""
|
"""
|
||||||
from typing import Literal, Any, Self, cast
|
from typing import Literal, Any, Self, cast
|
||||||
from collections.abc import Sequence, Callable
|
from collections.abc import Sequence, Callable, Iterator
|
||||||
from abc import ABCMeta # , abstractmethod # TODO any way to make Tool ok with implementing only one method?
|
from abc import ABCMeta # , abstractmethod # TODO any way to make Tool ok with implementing only one method?
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
@ -47,6 +47,18 @@ class RenderStep:
|
||||||
if self.opcode != 'P' and self.tool is None:
|
if self.opcode != 'P' and self.tool is None:
|
||||||
raise BuildError('Got tool=None but the opcode is not "P"')
|
raise BuildError('Got tool=None but the opcode is not "P"')
|
||||||
|
|
||||||
|
def is_continuous_with(self, other: 'RenderStep') -> bool:
|
||||||
|
"""
|
||||||
|
Check if another RenderStep can be appended to this one.
|
||||||
|
"""
|
||||||
|
# Check continuity with tolerance
|
||||||
|
offsets_match = bool(numpy.allclose(other.start_port.offset, self.end_port.offset))
|
||||||
|
rotations_match = (other.start_port.rotation is None and self.end_port.rotation is None) or (
|
||||||
|
other.start_port.rotation is not None and self.end_port.rotation is not None and
|
||||||
|
bool(numpy.isclose(other.start_port.rotation, self.end_port.rotation))
|
||||||
|
)
|
||||||
|
return offsets_match and rotations_match
|
||||||
|
|
||||||
def transformed(self, translation: NDArray[numpy.float64], rotation: float, pivot: NDArray[numpy.float64]) -> 'RenderStep':
|
def transformed(self, translation: NDArray[numpy.float64], rotation: float, pivot: NDArray[numpy.float64]) -> 'RenderStep':
|
||||||
"""
|
"""
|
||||||
Return a new RenderStep with transformed start and end ports.
|
Return a new RenderStep with transformed start and end ports.
|
||||||
|
|
@ -85,13 +97,20 @@ class RenderStep:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def measure_tool_plan(tree: ILibrary, port_names: tuple[str, str]) -> tuple[Port, Any]:
|
||||||
|
"""
|
||||||
|
Extracts a Port and returns the tree (as data) for tool planning fallbacks.
|
||||||
|
"""
|
||||||
|
pat = tree.top_pattern()
|
||||||
|
in_p = pat[port_names[0]]
|
||||||
|
out_p = pat[port_names[1]]
|
||||||
|
(travel, jog), rot = in_p.measure_travel(out_p)
|
||||||
|
return Port((travel, jog), rotation=rot, ptype=out_p.ptype), tree
|
||||||
|
|
||||||
|
|
||||||
class Tool:
|
class Tool:
|
||||||
"""
|
"""
|
||||||
Interface for path (e.g. wire or waveguide) generation.
|
Interface for path (e.g. wire or waveguide) generation.
|
||||||
|
|
||||||
Note that subclasses may implement only a subset of the methods and leave others
|
|
||||||
unimplemented (e.g. in cases where they don't make sense or the required components
|
|
||||||
are impractical or unavailable).
|
|
||||||
"""
|
"""
|
||||||
def traceL(
|
def traceL(
|
||||||
self,
|
self,
|
||||||
|
|
@ -220,7 +239,17 @@ class Tool:
|
||||||
Raises:
|
Raises:
|
||||||
BuildError if an impossible or unsupported geometry is requested.
|
BuildError if an impossible or unsupported geometry is requested.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError(f'planL() not implemented for {type(self)}')
|
# Fallback implementation using traceL
|
||||||
|
port_names = kwargs.get('port_names', ('A', 'B'))
|
||||||
|
tree = self.traceL(
|
||||||
|
ccw,
|
||||||
|
length,
|
||||||
|
in_ptype=in_ptype,
|
||||||
|
out_ptype=out_ptype,
|
||||||
|
port_names=port_names,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
return measure_tool_plan(tree, port_names)
|
||||||
|
|
||||||
def planS(
|
def planS(
|
||||||
self,
|
self,
|
||||||
|
|
@ -258,7 +287,17 @@ class Tool:
|
||||||
Raises:
|
Raises:
|
||||||
BuildError if an impossible or unsupported geometry is requested.
|
BuildError if an impossible or unsupported geometry is requested.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError(f'planS() not implemented for {type(self)}')
|
# Fallback implementation using traceS
|
||||||
|
port_names = kwargs.get('port_names', ('A', 'B'))
|
||||||
|
tree = self.traceS(
|
||||||
|
length,
|
||||||
|
jog,
|
||||||
|
in_ptype=in_ptype,
|
||||||
|
out_ptype=out_ptype,
|
||||||
|
port_names=port_names,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
return measure_tool_plan(tree, port_names)
|
||||||
|
|
||||||
def traceU(
|
def traceU(
|
||||||
self,
|
self,
|
||||||
|
|
@ -323,7 +362,7 @@ class Tool:
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
jog: The total offset from the input to output, along the perpendicular axis.
|
jog: The total offset from the input to output, along the perpendicular axis.
|
||||||
A positive number implies a leftwards shift (i.e. counterclockwise bend
|
A positive number implies a leftwards shift (i.e. counterclockwise_bend
|
||||||
followed by a clockwise bend)
|
followed by a clockwise bend)
|
||||||
in_ptype: The `ptype` of the port into which this wire's input will be `plug`ged.
|
in_ptype: The `ptype` of the port into which this wire's input will be `plug`ged.
|
||||||
out_ptype: The `ptype` of the port into which this wire's output will be `plug`ged.
|
out_ptype: The `ptype` of the port into which this wire's output will be `plug`ged.
|
||||||
|
|
@ -336,14 +375,26 @@ class Tool:
|
||||||
Raises:
|
Raises:
|
||||||
BuildError if an impossible or unsupported geometry is requested.
|
BuildError if an impossible or unsupported geometry is requested.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError(f'planU() not implemented for {type(self)}')
|
# Fallback implementation using traceU
|
||||||
|
kwargs = dict(kwargs)
|
||||||
|
length = kwargs.pop('length', 0)
|
||||||
|
port_names = kwargs.pop('port_names', ('A', 'B'))
|
||||||
|
tree = self.traceU(
|
||||||
|
jog,
|
||||||
|
length=length,
|
||||||
|
in_ptype=in_ptype,
|
||||||
|
out_ptype=out_ptype,
|
||||||
|
port_names=port_names,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
return measure_tool_plan(tree, port_names)
|
||||||
|
|
||||||
def render(
|
def render(
|
||||||
self,
|
self,
|
||||||
batch: Sequence[RenderStep],
|
batch: Sequence[RenderStep],
|
||||||
*,
|
*,
|
||||||
port_names: tuple[str, str] = ('A', 'B'), # noqa: ARG002 (unused)
|
port_names: tuple[str, str] = ('A', 'B'),
|
||||||
**kwargs, # noqa: ARG002 (unused)
|
**kwargs,
|
||||||
) -> ILibrary:
|
) -> ILibrary:
|
||||||
"""
|
"""
|
||||||
Render the provided `batch` of `RenderStep`s into geometry, returning a tree
|
Render the provided `batch` of `RenderStep`s into geometry, returning a tree
|
||||||
|
|
@ -357,7 +408,48 @@ class Tool:
|
||||||
kwargs: Custom tool-specific parameters.
|
kwargs: Custom tool-specific parameters.
|
||||||
"""
|
"""
|
||||||
assert not batch or batch[0].tool == self
|
assert not batch or batch[0].tool == self
|
||||||
raise NotImplementedError(f'render() not implemented for {type(self)}')
|
# Fallback: render each step individually
|
||||||
|
lib, pat = Library.mktree(SINGLE_USE_PREFIX + 'batch')
|
||||||
|
pat.add_port_pair(names=port_names, ptype=batch[0].start_port.ptype if batch else 'unk')
|
||||||
|
|
||||||
|
for step in batch:
|
||||||
|
if step.opcode == 'L':
|
||||||
|
if isinstance(step.data, ILibrary):
|
||||||
|
seg_tree = step.data
|
||||||
|
else:
|
||||||
|
# extract parameters from kwargs or data
|
||||||
|
seg_tree = self.traceL(
|
||||||
|
ccw=step.data.get('ccw') if isinstance(step.data, dict) else None,
|
||||||
|
length=float(step.data.get('length', 0)) if isinstance(step.data, dict) else 0.0,
|
||||||
|
port_names=port_names,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
elif step.opcode == 'S':
|
||||||
|
if isinstance(step.data, ILibrary):
|
||||||
|
seg_tree = step.data
|
||||||
|
else:
|
||||||
|
seg_tree = self.traceS(
|
||||||
|
length=float(step.data.get('length', 0)) if isinstance(step.data, dict) else 0.0,
|
||||||
|
jog=float(step.data.get('jog', 0)) if isinstance(step.data, dict) else 0.0,
|
||||||
|
port_names=port_names,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
elif step.opcode == 'U':
|
||||||
|
if isinstance(step.data, ILibrary):
|
||||||
|
seg_tree = step.data
|
||||||
|
else:
|
||||||
|
seg_tree = self.traceU(
|
||||||
|
jog=float(step.data.get('jog', 0)) if isinstance(step.data, dict) else 0.0,
|
||||||
|
length=float(step.data.get('length', 0)) if isinstance(step.data, dict) else 0.0,
|
||||||
|
port_names=port_names,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
pat.plug(seg_tree.top_pattern(), {port_names[1]: port_names[0]}, append=True)
|
||||||
|
|
||||||
|
return lib
|
||||||
|
|
||||||
|
|
||||||
abstract_tuple_t = tuple[Abstract, str, str]
|
abstract_tuple_t = tuple[Abstract, str, str]
|
||||||
|
|
@ -574,6 +666,19 @@ class AutoTool(Tool, metaclass=ABCMeta):
|
||||||
def reversed(self) -> Self:
|
def reversed(self) -> Self:
|
||||||
return type(self)(self.abstract, self.our_port_name, self.their_port_name)
|
return type(self)(self.abstract, self.our_port_name, self.their_port_name)
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class LPlan:
|
||||||
|
""" Template for an L-path configuration """
|
||||||
|
straight: 'AutoTool.Straight'
|
||||||
|
bend: 'AutoTool.Bend | None'
|
||||||
|
in_trans: 'AutoTool.Transition | None'
|
||||||
|
b_trans: 'AutoTool.Transition | None'
|
||||||
|
out_trans: 'AutoTool.Transition | None'
|
||||||
|
overhead_x: float
|
||||||
|
overhead_y: float
|
||||||
|
bend_angle: float
|
||||||
|
out_ptype: str
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class LData:
|
class LData:
|
||||||
""" Data for planL """
|
""" Data for planL """
|
||||||
|
|
@ -586,6 +691,65 @@ class AutoTool(Tool, metaclass=ABCMeta):
|
||||||
b_transition: 'AutoTool.Transition | None'
|
b_transition: 'AutoTool.Transition | None'
|
||||||
out_transition: 'AutoTool.Transition | None'
|
out_transition: 'AutoTool.Transition | None'
|
||||||
|
|
||||||
|
def _iter_l_plans(
|
||||||
|
self,
|
||||||
|
ccw: SupportsBool | None,
|
||||||
|
in_ptype: str | None,
|
||||||
|
out_ptype: str | None,
|
||||||
|
) -> Iterator[LPlan]:
|
||||||
|
"""
|
||||||
|
Iterate over all possible combinations of straights and bends that
|
||||||
|
could form an L-path.
|
||||||
|
"""
|
||||||
|
bends = cast('list[AutoTool.Bend | None]', self.bends)
|
||||||
|
if ccw is None and not bends:
|
||||||
|
bends = [None]
|
||||||
|
|
||||||
|
for straight in self.straights:
|
||||||
|
for bend in bends:
|
||||||
|
bend_dxy, bend_angle = self._bend2dxy(bend, ccw)
|
||||||
|
|
||||||
|
in_ptype_pair = ('unk' if in_ptype is None else in_ptype, straight.ptype)
|
||||||
|
in_transition = self.transitions.get(in_ptype_pair, None)
|
||||||
|
itrans_dxy = self._itransition2dxy(in_transition)
|
||||||
|
|
||||||
|
out_ptype_pair = (
|
||||||
|
'unk' if out_ptype is None else out_ptype,
|
||||||
|
straight.ptype if ccw is None else cast('AutoTool.Bend', bend).out_port.ptype
|
||||||
|
)
|
||||||
|
out_transition = self.transitions.get(out_ptype_pair, None)
|
||||||
|
otrans_dxy = self._otransition2dxy(out_transition, bend_angle)
|
||||||
|
|
||||||
|
b_transition = None
|
||||||
|
if ccw is not None:
|
||||||
|
assert bend is not None
|
||||||
|
if bend.in_port.ptype != straight.ptype:
|
||||||
|
b_transition = self.transitions.get((bend.in_port.ptype, straight.ptype), None)
|
||||||
|
btrans_dxy = self._itransition2dxy(b_transition)
|
||||||
|
|
||||||
|
overhead_x = bend_dxy[0] + itrans_dxy[0] + btrans_dxy[0] + otrans_dxy[0]
|
||||||
|
overhead_y = bend_dxy[1] + itrans_dxy[1] + btrans_dxy[1] + otrans_dxy[1]
|
||||||
|
|
||||||
|
if out_transition is not None:
|
||||||
|
out_ptype_actual = out_transition.their_port.ptype
|
||||||
|
elif ccw is not None:
|
||||||
|
assert bend is not None
|
||||||
|
out_ptype_actual = bend.out_port.ptype
|
||||||
|
else:
|
||||||
|
out_ptype_actual = straight.ptype
|
||||||
|
|
||||||
|
yield self.LPlan(
|
||||||
|
straight = straight,
|
||||||
|
bend = bend,
|
||||||
|
in_trans = in_transition,
|
||||||
|
b_trans = b_transition,
|
||||||
|
out_trans = out_transition,
|
||||||
|
overhead_x = overhead_x,
|
||||||
|
overhead_y = overhead_y,
|
||||||
|
bend_angle = bend_angle,
|
||||||
|
out_ptype = out_ptype_actual,
|
||||||
|
)
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class SData:
|
class SData:
|
||||||
""" Data for planS """
|
""" Data for planS """
|
||||||
|
|
@ -600,11 +764,77 @@ class AutoTool(Tool, metaclass=ABCMeta):
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class UData:
|
class UData:
|
||||||
""" Data for planU """
|
""" Data for planU or planS (double-L) """
|
||||||
ldata0: 'AutoTool.LData'
|
ldata0: 'AutoTool.LData'
|
||||||
ldata1: 'AutoTool.LData'
|
ldata1: 'AutoTool.LData'
|
||||||
straight2: 'AutoTool.Straight'
|
straight2: 'AutoTool.Straight'
|
||||||
l2_length: float
|
l2_length: float
|
||||||
|
mid_transition: 'AutoTool.Transition | None'
|
||||||
|
|
||||||
|
def _solve_double_l(
|
||||||
|
self,
|
||||||
|
length: float,
|
||||||
|
jog: float,
|
||||||
|
ccw1: SupportsBool,
|
||||||
|
ccw2: SupportsBool,
|
||||||
|
in_ptype: str | None,
|
||||||
|
out_ptype: str | None,
|
||||||
|
**kwargs,
|
||||||
|
) -> tuple[Port, UData]:
|
||||||
|
"""
|
||||||
|
Solve for a path consisting of two L-bends connected by a straight segment.
|
||||||
|
Used for both U-turns (ccw1 == ccw2) and S-bends (ccw1 != ccw2).
|
||||||
|
"""
|
||||||
|
for plan1 in self._iter_l_plans(ccw1, in_ptype, None):
|
||||||
|
for plan2 in self._iter_l_plans(ccw2, plan1.out_ptype, out_ptype):
|
||||||
|
# Solving for:
|
||||||
|
# X = L1_total +/- R2_actual = length
|
||||||
|
# Y = R1_actual + L2_straight + overhead_mid + overhead_b2 + L3_total = jog
|
||||||
|
|
||||||
|
# Sign for overhead_y2 depends on whether it's a U-turn or S-bend
|
||||||
|
is_u = bool(ccw1) == bool(ccw2)
|
||||||
|
# U-turn: X = L1_total - R2 = length => L1_total = length + R2
|
||||||
|
# S-bend: X = L1_total + R2 = length => L1_total = length - R2
|
||||||
|
l1_total = length + (abs(plan2.overhead_y) if is_u else -abs(plan2.overhead_y))
|
||||||
|
l1_straight = l1_total - plan1.overhead_x
|
||||||
|
|
||||||
|
|
||||||
|
if plan1.straight.length_range[0] <= l1_straight < plan1.straight.length_range[1]:
|
||||||
|
for straight_mid in self.straights:
|
||||||
|
# overhead_mid accounts for the transition from bend1 to straight_mid
|
||||||
|
mid_ptype_pair = (plan1.out_ptype, straight_mid.ptype)
|
||||||
|
mid_trans = self.transitions.get(mid_ptype_pair, None)
|
||||||
|
mid_trans_dxy = self._itransition2dxy(mid_trans)
|
||||||
|
|
||||||
|
# b_trans2 accounts for the transition from straight_mid to bend2
|
||||||
|
b2_trans = None
|
||||||
|
if plan2.bend is not None and plan2.bend.in_port.ptype != straight_mid.ptype:
|
||||||
|
b2_trans = self.transitions.get((plan2.bend.in_port.ptype, straight_mid.ptype), None)
|
||||||
|
b2_trans_dxy = self._itransition2dxy(b2_trans)
|
||||||
|
|
||||||
|
l2_straight = abs(jog) - abs(plan1.overhead_y) - plan2.overhead_x - mid_trans_dxy[0] - b2_trans_dxy[0]
|
||||||
|
|
||||||
|
if straight_mid.length_range[0] <= l2_straight < straight_mid.length_range[1]:
|
||||||
|
# Found a solution!
|
||||||
|
# For plan2, we assume l3_straight = 0.
|
||||||
|
# We need to verify if l3=0 is valid for plan2.straight.
|
||||||
|
l3_straight = 0
|
||||||
|
if plan2.straight.length_range[0] <= l3_straight < plan2.straight.length_range[1]:
|
||||||
|
ldata0 = self.LData(
|
||||||
|
l1_straight, plan1.straight, kwargs, ccw1, plan1.bend,
|
||||||
|
plan1.in_trans, plan1.b_trans, plan1.out_trans,
|
||||||
|
)
|
||||||
|
ldata1 = self.LData(
|
||||||
|
l3_straight, plan2.straight, kwargs, ccw2, plan2.bend,
|
||||||
|
b2_trans, None, plan2.out_trans,
|
||||||
|
)
|
||||||
|
|
||||||
|
data = self.UData(ldata0, ldata1, straight_mid, l2_straight, mid_trans)
|
||||||
|
# out_port is at (length, jog) rot pi (for S-bend) or 0 (for U-turn) relative to input
|
||||||
|
out_rot = 0 if is_u else pi
|
||||||
|
out_port = Port((length, jog), rotation=out_rot, ptype=plan2.out_ptype)
|
||||||
|
return out_port, data
|
||||||
|
raise BuildError(f"Failed to find a valid double-L configuration for {length=}, {jog=}")
|
||||||
|
|
||||||
straights: list[Straight]
|
straights: list[Straight]
|
||||||
""" List of straight-generators to choose from, in order of priority """
|
""" List of straight-generators to choose from, in order of priority """
|
||||||
|
|
@ -675,70 +905,24 @@ class AutoTool(Tool, metaclass=ABCMeta):
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> tuple[Port, LData]:
|
) -> tuple[Port, LData]:
|
||||||
|
|
||||||
success = False
|
for plan in self._iter_l_plans(ccw, in_ptype, out_ptype):
|
||||||
# If ccw is None, we don't need a bend, but we still loop to reuse the logic.
|
straight_length = length - plan.overhead_x
|
||||||
# We'll use a dummy loop if bends is empty and ccw is None.
|
if plan.straight.length_range[0] <= straight_length < plan.straight.length_range[1]:
|
||||||
bends = cast('list[AutoTool.Bend | None]', self.bends)
|
data = self.LData(
|
||||||
if ccw is None and not bends:
|
straight_length = straight_length,
|
||||||
bends += [None]
|
straight = plan.straight,
|
||||||
|
straight_kwargs = kwargs,
|
||||||
# Initialize these to avoid UnboundLocalError in the error message
|
ccw = ccw,
|
||||||
bend_dxy, bend_angle = numpy.zeros(2), pi
|
bend = plan.bend,
|
||||||
itrans_dxy = numpy.zeros(2)
|
in_transition = plan.in_trans,
|
||||||
otrans_dxy = numpy.zeros(2)
|
b_transition = plan.b_trans,
|
||||||
btrans_dxy = numpy.zeros(2)
|
out_transition = plan.out_trans,
|
||||||
|
|
||||||
for straight in self.straights:
|
|
||||||
for bend in bends:
|
|
||||||
bend_dxy, bend_angle = self._bend2dxy(bend, ccw)
|
|
||||||
|
|
||||||
in_ptype_pair = ('unk' if in_ptype is None else in_ptype, straight.ptype)
|
|
||||||
in_transition = self.transitions.get(in_ptype_pair, None)
|
|
||||||
itrans_dxy = self._itransition2dxy(in_transition)
|
|
||||||
|
|
||||||
out_ptype_pair = (
|
|
||||||
'unk' if out_ptype is None else out_ptype,
|
|
||||||
straight.ptype if ccw is None else cast('AutoTool.Bend', bend).out_port.ptype
|
|
||||||
)
|
)
|
||||||
out_transition = self.transitions.get(out_ptype_pair, None)
|
out_port = Port((length, plan.overhead_y), rotation=plan.bend_angle, ptype=plan.out_ptype)
|
||||||
otrans_dxy = self._otransition2dxy(out_transition, bend_angle)
|
|
||||||
|
|
||||||
b_transition = None
|
|
||||||
if ccw is not None:
|
|
||||||
assert bend is not None
|
|
||||||
if bend.in_port.ptype != straight.ptype:
|
|
||||||
b_transition = self.transitions.get((bend.in_port.ptype, straight.ptype), None)
|
|
||||||
btrans_dxy = self._itransition2dxy(b_transition)
|
|
||||||
|
|
||||||
straight_length = length - bend_dxy[0] - itrans_dxy[0] - btrans_dxy[0] - otrans_dxy[0]
|
|
||||||
bend_run = bend_dxy[1] + itrans_dxy[1] + btrans_dxy[1] + otrans_dxy[1]
|
|
||||||
success = straight.length_range[0] <= straight_length < straight.length_range[1]
|
|
||||||
if success:
|
|
||||||
break
|
|
||||||
if success:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
# Failed to break
|
|
||||||
raise BuildError(
|
|
||||||
f'Asked to draw L-path with total length {length:,g}, shorter than required bends and transitions:\n'
|
|
||||||
f'bend: {bend_dxy[0]:,g} in_trans: {itrans_dxy[0]:,g}\n'
|
|
||||||
f'out_trans: {otrans_dxy[0]:,g} bend_trans: {btrans_dxy[0]:,g}'
|
|
||||||
)
|
|
||||||
|
|
||||||
if out_transition is not None:
|
|
||||||
out_ptype_actual = out_transition.their_port.ptype
|
|
||||||
elif ccw is not None:
|
|
||||||
assert bend is not None
|
|
||||||
out_ptype_actual = bend.out_port.ptype
|
|
||||||
elif not numpy.isclose(straight_length, 0):
|
|
||||||
out_ptype_actual = straight.ptype
|
|
||||||
else:
|
|
||||||
out_ptype_actual = self.default_out_ptype
|
|
||||||
|
|
||||||
data = self.LData(straight_length, straight, kwargs, ccw, bend, in_transition, b_transition, out_transition)
|
|
||||||
out_port = Port((length, bend_run), rotation=bend_angle, ptype=out_ptype_actual)
|
|
||||||
return out_port, data
|
return out_port, data
|
||||||
|
|
||||||
|
raise BuildError(f'Failed to find a valid L-path configuration for {length=:,g}, {ccw=}, {in_ptype=}, {out_ptype=}')
|
||||||
|
|
||||||
def _renderL(
|
def _renderL(
|
||||||
self,
|
self,
|
||||||
data: LData,
|
data: LData,
|
||||||
|
|
@ -856,26 +1040,8 @@ class AutoTool(Tool, metaclass=ABCMeta):
|
||||||
break
|
break
|
||||||
|
|
||||||
if not success:
|
if not success:
|
||||||
try:
|
|
||||||
ccw0 = jog > 0
|
ccw0 = jog > 0
|
||||||
p_test0, ldata_test0 = self.planL(length / 2, ccw0, in_ptype=in_ptype)
|
return self._solve_double_l(length, jog, ccw0, not ccw0, in_ptype, out_ptype, **kwargs)
|
||||||
p_test1, ldata_test1 = self.planL(jog - p_test0.y, not ccw0, in_ptype=p_test0.ptype, out_ptype=out_ptype)
|
|
||||||
|
|
||||||
dx = p_test1.x - length / 2
|
|
||||||
p0, ldata0 = self.planL(length - dx, ccw0, in_ptype=in_ptype)
|
|
||||||
p1, ldata1 = self.planL(jog - p0.y, not ccw0, in_ptype=p0.ptype, out_ptype=out_ptype)
|
|
||||||
success = True
|
|
||||||
except BuildError as err:
|
|
||||||
l2_err: BuildError | None = err
|
|
||||||
else:
|
|
||||||
l2_err = None
|
|
||||||
raise NotImplementedError('TODO need to handle ldata below')
|
|
||||||
|
|
||||||
if not success:
|
|
||||||
# Failed to break
|
|
||||||
raise BuildError(
|
|
||||||
f'Failed to find a valid s-bend configuration for {length=:,g}, {jog=:,g}, {in_ptype=}, {out_ptype=}'
|
|
||||||
) from l2_err
|
|
||||||
|
|
||||||
if out_transition is not None:
|
if out_transition is not None:
|
||||||
out_ptype_actual = out_transition.their_port.ptype
|
out_ptype_actual = out_transition.their_port.ptype
|
||||||
|
|
@ -948,6 +1114,9 @@ class AutoTool(Tool, metaclass=ABCMeta):
|
||||||
)
|
)
|
||||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceS')
|
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceS')
|
||||||
pat.add_port_pair(names=port_names, ptype='unk' if in_ptype is None else in_ptype)
|
pat.add_port_pair(names=port_names, ptype='unk' if in_ptype is None else in_ptype)
|
||||||
|
if isinstance(data, self.UData):
|
||||||
|
self._renderU(data=data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
|
||||||
|
else:
|
||||||
self._renderS(data=data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
|
self._renderS(data=data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
|
||||||
return tree
|
return tree
|
||||||
|
|
||||||
|
|
@ -961,55 +1130,7 @@ class AutoTool(Tool, metaclass=ABCMeta):
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> tuple[Port, UData]:
|
) -> tuple[Port, UData]:
|
||||||
ccw = jog > 0
|
ccw = jog > 0
|
||||||
kwargs_no_out = kwargs | {'out_ptype': None}
|
return self._solve_double_l(length, jog, ccw, ccw, in_ptype, out_ptype, **kwargs)
|
||||||
|
|
||||||
# Use loops to find a combination of straights and bends that fits
|
|
||||||
success = False
|
|
||||||
for _straight1 in self.straights:
|
|
||||||
for _bend1 in self.bends:
|
|
||||||
for straight2 in self.straights:
|
|
||||||
for _bend2 in self.bends:
|
|
||||||
try:
|
|
||||||
# We need to know R1 and R2 to calculate the lengths.
|
|
||||||
# Use large dummy lengths to probe the bends.
|
|
||||||
p_probe1, _ = self.planL(ccw, 1e9, in_ptype=in_ptype, **kwargs_no_out)
|
|
||||||
R1 = abs(Port((0, 0), 0).measure_travel(p_probe1)[0][1])
|
|
||||||
p_probe2, _ = self.planL(ccw, 1e9, in_ptype=p_probe1.ptype, out_ptype=out_ptype, **kwargs)
|
|
||||||
R2 = abs(Port((0, 0), 0).measure_travel(p_probe2)[0][1])
|
|
||||||
|
|
||||||
# Final x will be: x = l1_straight + R1 - R2
|
|
||||||
# We want final x = length. So: l1_straight = length - R1 + R2
|
|
||||||
# Total length for planL(0) is l1 = l1_straight + R1 = length + R2
|
|
||||||
l1 = length + R2
|
|
||||||
|
|
||||||
# Final y will be: y = R1 + l2_straight + R2 = abs(jog)
|
|
||||||
# So: l2_straight = abs(jog) - R1 - R2
|
|
||||||
l2_length = abs(jog) - R1 - R2
|
|
||||||
|
|
||||||
if l2_length >= straight2.length_range[0] and l2_length < straight2.length_range[1]:
|
|
||||||
p0, ldata0 = self.planL(ccw, l1, in_ptype=in_ptype, **kwargs_no_out)
|
|
||||||
# For the second bend, we want straight length = 0.
|
|
||||||
# Total length for planL(1) is l2 = 0 + R2 = R2.
|
|
||||||
p1, ldata1 = self.planL(ccw, R2, in_ptype=p0.ptype, out_ptype=out_ptype, **kwargs)
|
|
||||||
|
|
||||||
success = True
|
|
||||||
break
|
|
||||||
except BuildError:
|
|
||||||
continue
|
|
||||||
if success:
|
|
||||||
break
|
|
||||||
if success:
|
|
||||||
break
|
|
||||||
if success:
|
|
||||||
break
|
|
||||||
|
|
||||||
if not success:
|
|
||||||
raise BuildError(f"AutoTool failed to plan U-turn with {jog=}, {length=}")
|
|
||||||
|
|
||||||
data = self.UData(ldata0, ldata1, straight2, l2_length)
|
|
||||||
# Final port is at (length, jog) rot pi relative to input
|
|
||||||
out_port = Port((length, jog), rotation=pi, ptype=p1.ptype)
|
|
||||||
return out_port, data
|
|
||||||
|
|
||||||
def _renderU(
|
def _renderU(
|
||||||
self,
|
self,
|
||||||
|
|
@ -1022,6 +1143,8 @@ class AutoTool(Tool, metaclass=ABCMeta):
|
||||||
# 1. First L-bend
|
# 1. First L-bend
|
||||||
self._renderL(data.ldata0, tree, port_names, gen_kwargs)
|
self._renderL(data.ldata0, tree, port_names, gen_kwargs)
|
||||||
# 2. Connecting straight
|
# 2. Connecting straight
|
||||||
|
if data.mid_transition:
|
||||||
|
pat.plug(data.mid_transition.abstract, {port_names[1]: data.mid_transition.their_port_name})
|
||||||
if not numpy.isclose(data.l2_length, 0):
|
if not numpy.isclose(data.l2_length, 0):
|
||||||
s2_pat_or_tree = data.straight2.fn(data.l2_length, **(gen_kwargs | data.ldata0.straight_kwargs))
|
s2_pat_or_tree = data.straight2.fn(data.l2_length, **(gen_kwargs | data.ldata0.straight_kwargs))
|
||||||
pmap = {port_names[1]: data.straight2.in_port_name}
|
pmap = {port_names[1]: data.straight2.in_port_name}
|
||||||
|
|
@ -1053,6 +1176,7 @@ class AutoTool(Tool, metaclass=ABCMeta):
|
||||||
out_ptype = out_ptype,
|
out_ptype = out_ptype,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceU')
|
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceU')
|
||||||
pat.add_port_pair(names=port_names, ptype='unk' if in_ptype is None else in_ptype)
|
pat.add_port_pair(names=port_names, ptype='unk' if in_ptype is None else in_ptype)
|
||||||
self._renderU(data=data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
|
self._renderU(data=data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
|
||||||
|
|
@ -1074,6 +1198,9 @@ class AutoTool(Tool, metaclass=ABCMeta):
|
||||||
if step.opcode == 'L':
|
if step.opcode == 'L':
|
||||||
self._renderL(data=step.data, tree=tree, port_names=port_names, straight_kwargs=kwargs)
|
self._renderL(data=step.data, tree=tree, port_names=port_names, straight_kwargs=kwargs)
|
||||||
elif step.opcode == 'S':
|
elif step.opcode == 'S':
|
||||||
|
if isinstance(step.data, self.UData):
|
||||||
|
self._renderU(data=step.data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
|
||||||
|
else:
|
||||||
self._renderS(data=step.data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
|
self._renderS(data=step.data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
|
||||||
elif step.opcode == 'U':
|
elif step.opcode == 'U':
|
||||||
self._renderU(data=step.data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
|
self._renderU(data=step.data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,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)
|
||||||
|
|
||||||
DXF does not support shape repetition (only block repeptition). Please call
|
Shape repetitions are expanded into individual DXF entities.
|
||||||
library.wrap_repeated_shapes() before writing to file.
|
|
||||||
|
|
||||||
Other functions you may want to call:
|
Other functions you may want to call:
|
||||||
- `masque.file.oasis.check_valid_names(library.keys())` to check for invalid names
|
- `masque.file.oasis.check_valid_names(library.keys())` to check for invalid names
|
||||||
|
|
@ -344,14 +343,13 @@ def _shapes_to_elements(
|
||||||
for layer, sseq in shapes.items():
|
for layer, sseq in shapes.items():
|
||||||
attribs = dict(layer=_mlayer2dxf(layer))
|
attribs = dict(layer=_mlayer2dxf(layer))
|
||||||
for shape in sseq:
|
for shape in sseq:
|
||||||
|
displacements = [numpy.zeros(2)]
|
||||||
if shape.repetition is not None:
|
if shape.repetition is not None:
|
||||||
raise PatternError(
|
displacements = shape.repetition.displacements
|
||||||
'Shape repetitions are not supported by DXF.'
|
|
||||||
' Please call library.wrap_repeated_shapes() before writing to file.'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
for dd in displacements:
|
||||||
for polygon in shape.to_polygons():
|
for polygon in shape.to_polygons():
|
||||||
xy_open = polygon.vertices
|
xy_open = polygon.vertices + dd
|
||||||
xy_closed = numpy.vstack((xy_open, xy_open[0, :]))
|
xy_closed = numpy.vstack((xy_open, xy_open[0, :]))
|
||||||
block.add_lwpolyline(xy_closed, dxfattribs=attribs)
|
block.add_lwpolyline(xy_closed, dxfattribs=attribs)
|
||||||
|
|
||||||
|
|
@ -363,11 +361,17 @@ def _labels_to_texts(
|
||||||
for layer, lseq in labels.items():
|
for layer, lseq in labels.items():
|
||||||
attribs = dict(layer=_mlayer2dxf(layer))
|
attribs = dict(layer=_mlayer2dxf(layer))
|
||||||
for label in lseq:
|
for label in lseq:
|
||||||
xy = label.offset
|
if label.repetition is None:
|
||||||
block.add_text(
|
block.add_text(
|
||||||
label.string,
|
label.string,
|
||||||
dxfattribs=attribs
|
dxfattribs=attribs
|
||||||
).set_placement(xy, align=TextEntityAlignment.BOTTOM_LEFT)
|
).set_placement(label.offset, align=TextEntityAlignment.BOTTOM_LEFT)
|
||||||
|
else:
|
||||||
|
for dd in label.repetition.displacements:
|
||||||
|
block.add_text(
|
||||||
|
label.string,
|
||||||
|
dxfattribs=attribs
|
||||||
|
).set_placement(label.offset + dd, align=TextEntityAlignment.BOTTOM_LEFT)
|
||||||
|
|
||||||
|
|
||||||
def _mlayer2dxf(layer: layer_t) -> str:
|
def _mlayer2dxf(layer: layer_t) -> str:
|
||||||
|
|
|
||||||
|
|
@ -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`
|
||||||
|
|
||||||
GDS does not support shape repetition (only cell repeptition). Please call
|
GDS does not support shape repetition (only cell repetition). Please call
|
||||||
`library.wrap_repeated_shapes()` before writing to file.
|
`library.wrap_repeated_shapes()` before writing to file.
|
||||||
|
|
||||||
Other functions you may want to call:
|
Other functions you may want to call:
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ def writefile(
|
||||||
top: str,
|
top: str,
|
||||||
filename: str,
|
filename: str,
|
||||||
custom_attributes: bool = False,
|
custom_attributes: bool = False,
|
||||||
|
annotate_ports: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
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
|
||||||
|
|
@ -44,6 +45,8 @@ def writefile(
|
||||||
filename: Filename to write to.
|
filename: Filename to write to.
|
||||||
custom_attributes: Whether to write non-standard `pattern_layer` attribute to the
|
custom_attributes: Whether to write non-standard `pattern_layer` attribute to the
|
||||||
SVG elements.
|
SVG elements.
|
||||||
|
annotate_ports: If True, draw an arrow for each port (similar to
|
||||||
|
`Pattern.visualize(..., ports=True)`).
|
||||||
"""
|
"""
|
||||||
pattern = library[top]
|
pattern = library[top]
|
||||||
|
|
||||||
|
|
@ -79,6 +82,27 @@ def writefile(
|
||||||
|
|
||||||
svg_group.add(path)
|
svg_group.add(path)
|
||||||
|
|
||||||
|
if annotate_ports:
|
||||||
|
# Draw arrows for the ports, pointing into the device (per port definition)
|
||||||
|
for port_name, port in pat.ports.items():
|
||||||
|
if port.rotation is not None:
|
||||||
|
p1 = port.offset
|
||||||
|
angle = port.rotation
|
||||||
|
size = 1.0 # arrow size
|
||||||
|
p2 = p1 + size * numpy.array([numpy.cos(angle), numpy.sin(angle)])
|
||||||
|
|
||||||
|
# head
|
||||||
|
head_angle = 0.5
|
||||||
|
h1 = p1 + 0.7 * size * numpy.array([numpy.cos(angle + head_angle), numpy.sin(angle + head_angle)])
|
||||||
|
h2 = p1 + 0.7 * size * numpy.array([numpy.cos(angle - head_angle), numpy.sin(angle - head_angle)])
|
||||||
|
|
||||||
|
line = svg.line(start=p1, end=p2, stroke='green', stroke_width=0.2)
|
||||||
|
head = svg.polyline(points=[h1, p1, h2], fill='none', stroke='green', stroke_width=0.2)
|
||||||
|
|
||||||
|
svg_group.add(line)
|
||||||
|
svg_group.add(head)
|
||||||
|
svg_group.add(svg.text(port_name, insert=p2, font_size=0.5, fill='green'))
|
||||||
|
|
||||||
for target, refs in pat.refs.items():
|
for target, refs in pat.refs.items():
|
||||||
if target is None:
|
if target is None:
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -682,6 +682,33 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||||
def _merge(self, key_self: str, other: Mapping[str, 'Pattern'], key_other: str) -> None:
|
def _merge(self, key_self: str, other: Mapping[str, 'Pattern'], key_other: str) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def resolve(
|
||||||
|
self,
|
||||||
|
other: 'Abstract | str | Pattern | TreeView',
|
||||||
|
append: bool = False,
|
||||||
|
) -> 'Abstract | Pattern':
|
||||||
|
"""
|
||||||
|
Resolve another device (name, Abstract, Pattern, or TreeView) into an Abstract or Pattern.
|
||||||
|
If it is a TreeView, it is first added into this library.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
other: The device to resolve.
|
||||||
|
append: If True and `other` is an `Abstract`, returns the full `Pattern` from the library.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An `Abstract` or `Pattern` object.
|
||||||
|
"""
|
||||||
|
from .pattern import Pattern #noqa: PLC0415
|
||||||
|
if not isinstance(other, (str, Abstract, Pattern)):
|
||||||
|
# We got a TreeView; add it into self and grab its topcell as an Abstract
|
||||||
|
other = self << other
|
||||||
|
|
||||||
|
if isinstance(other, str):
|
||||||
|
other = self.abstract(other)
|
||||||
|
if append and isinstance(other, Abstract):
|
||||||
|
other = self[other.name]
|
||||||
|
return other
|
||||||
|
|
||||||
def rename(
|
def rename(
|
||||||
self,
|
self,
|
||||||
old_name: str,
|
old_name: str,
|
||||||
|
|
@ -1036,6 +1063,25 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def resolve_repeated_refs(self, name: str | None = None) -> Self:
|
||||||
|
"""
|
||||||
|
Expand all repeated references into multiple individual references.
|
||||||
|
Alters the library in-place.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: If specified, only resolve repeated refs in this pattern.
|
||||||
|
Otherwise, resolve in all patterns.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
"""
|
||||||
|
if name is not None:
|
||||||
|
self[name].resolve_repeated_refs()
|
||||||
|
else:
|
||||||
|
for pat in self.values():
|
||||||
|
pat.resolve_repeated_refs()
|
||||||
|
return self
|
||||||
|
|
||||||
def subtree(
|
def subtree(
|
||||||
self,
|
self,
|
||||||
tops: str | Sequence[str],
|
tops: str | Sequence[str],
|
||||||
|
|
|
||||||
|
|
@ -976,6 +976,28 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
del self.labels[layer]
|
del self.labels[layer]
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def resolve_repeated_refs(self) -> Self:
|
||||||
|
"""
|
||||||
|
Expand all repeated references into multiple individual references.
|
||||||
|
Alters the current pattern in-place.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
"""
|
||||||
|
new_refs: defaultdict[str | None, list[Ref]] = defaultdict(list)
|
||||||
|
for target, rseq in self.refs.items():
|
||||||
|
for ref in rseq:
|
||||||
|
if ref.repetition is None:
|
||||||
|
new_refs[target].append(ref)
|
||||||
|
else:
|
||||||
|
for dd in ref.repetition.displacements:
|
||||||
|
new_ref = ref.deepcopy()
|
||||||
|
new_ref.offset = ref.offset + dd
|
||||||
|
new_ref.repetition = None
|
||||||
|
new_refs[target].append(new_ref)
|
||||||
|
self.refs = new_refs
|
||||||
|
return self
|
||||||
|
|
||||||
def prune_refs(self) -> Self:
|
def prune_refs(self) -> Self:
|
||||||
"""
|
"""
|
||||||
Remove empty ref lists in `self.refs`.
|
Remove empty ref lists in `self.refs`.
|
||||||
|
|
@ -1049,6 +1071,8 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
line_color: str = 'k',
|
line_color: str = 'k',
|
||||||
fill_color: str = 'none',
|
fill_color: str = 'none',
|
||||||
overdraw: bool = False,
|
overdraw: bool = False,
|
||||||
|
filename: str | None = None,
|
||||||
|
ports: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Draw a picture of the Pattern and wait for the user to inspect it
|
Draw a picture of the Pattern and wait for the user to inspect it
|
||||||
|
|
@ -1059,10 +1083,13 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
klayout or a different GDS viewer!
|
klayout or a different GDS viewer!
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
offset: Coordinates to offset by before drawing
|
library: Mapping of {name: Pattern} for resolving references. Required if `self.has_refs()`.
|
||||||
line_color: Outlines are drawn with this color (passed to `matplotlib.collections.PolyCollection`)
|
offset: Coordinates to offset by before drawing.
|
||||||
fill_color: Interiors are drawn with this color (passed to `matplotlib.collections.PolyCollection`)
|
line_color: Outlines are drawn with this color.
|
||||||
overdraw: Whether to create a new figure or draw on a pre-existing one
|
fill_color: Interiors are drawn with this color.
|
||||||
|
overdraw: Whether to create a new figure or draw on a pre-existing one.
|
||||||
|
filename: If provided, save the figure to this file instead of showing it.
|
||||||
|
ports: If True, annotate the plot with arrows representing the ports.
|
||||||
"""
|
"""
|
||||||
# TODO: add text labels to visualize()
|
# TODO: add text labels to visualize()
|
||||||
try:
|
try:
|
||||||
|
|
@ -1076,48 +1103,154 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
if self.has_refs() and library is None:
|
if self.has_refs() and library is None:
|
||||||
raise PatternError('Must provide a library when visualizing a pattern with refs')
|
raise PatternError('Must provide a library when visualizing a pattern with refs')
|
||||||
|
|
||||||
offset = numpy.asarray(offset, dtype=float)
|
# Cache for {Pattern object ID: List of local polygon vertex arrays}
|
||||||
|
# Polygons are stored relative to the pattern's origin (offset included)
|
||||||
|
poly_cache: dict[int, list[NDArray[numpy.float64]]] = {}
|
||||||
|
|
||||||
|
def get_local_polys(pat: 'Pattern') -> list[NDArray[numpy.float64]]:
|
||||||
|
pid = id(pat)
|
||||||
|
if pid not in poly_cache:
|
||||||
|
polys = []
|
||||||
|
for shape in chain.from_iterable(pat.shapes.values()):
|
||||||
|
for ss in shape.to_polygons():
|
||||||
|
# Shape.to_polygons() returns Polygons with their own offsets and vertices.
|
||||||
|
# We need to expand any shape-level repetition here.
|
||||||
|
v_base = ss.vertices + ss.offset
|
||||||
|
if ss.repetition is not None:
|
||||||
|
for disp in ss.repetition.displacements:
|
||||||
|
polys.append(v_base + disp)
|
||||||
|
else:
|
||||||
|
polys.append(v_base)
|
||||||
|
poly_cache[pid] = polys
|
||||||
|
return poly_cache[pid]
|
||||||
|
|
||||||
|
all_polygons: list[NDArray[numpy.float64]] = []
|
||||||
|
port_info: list[tuple[str, NDArray[numpy.float64], float]] = []
|
||||||
|
|
||||||
|
def collect_polys_recursive(
|
||||||
|
pat: 'Pattern',
|
||||||
|
c_offset: NDArray[numpy.float64],
|
||||||
|
c_rotation: float,
|
||||||
|
c_mirrored: bool,
|
||||||
|
c_scale: float,
|
||||||
|
) -> None:
|
||||||
|
# Current transform: T(c_offset) * R(c_rotation) * M(c_mirrored) * S(c_scale)
|
||||||
|
|
||||||
|
# 1. Transform and collect local polygons
|
||||||
|
local_polys = get_local_polys(pat)
|
||||||
|
if local_polys:
|
||||||
|
rot_mat = rotation_matrix_2d(c_rotation)
|
||||||
|
for v in local_polys:
|
||||||
|
vt = v * c_scale
|
||||||
|
if c_mirrored:
|
||||||
|
vt = vt.copy()
|
||||||
|
vt[:, 1] *= -1
|
||||||
|
vt = (rot_mat @ vt.T).T + c_offset
|
||||||
|
all_polygons.append(vt)
|
||||||
|
|
||||||
|
# 2. Collect ports if requested
|
||||||
|
if ports:
|
||||||
|
for name, p in pat.ports.items():
|
||||||
|
pt_v = p.offset * c_scale
|
||||||
|
if c_mirrored:
|
||||||
|
pt_v = pt_v.copy()
|
||||||
|
pt_v[1] *= -1
|
||||||
|
pt_v = rotation_matrix_2d(c_rotation) @ pt_v + c_offset
|
||||||
|
|
||||||
|
if p.rotation is not None:
|
||||||
|
pt_rot = p.rotation
|
||||||
|
if c_mirrored:
|
||||||
|
pt_rot = -pt_rot
|
||||||
|
pt_rot += c_rotation
|
||||||
|
port_info.append((name, pt_v, pt_rot))
|
||||||
|
|
||||||
|
# 3. Recurse into refs
|
||||||
|
for target, refs in pat.refs.items():
|
||||||
|
if target is None:
|
||||||
|
continue
|
||||||
|
target_pat = library[target]
|
||||||
|
for ref in refs:
|
||||||
|
# Ref order of operations: mirror, rotate, scale, translate, repeat
|
||||||
|
|
||||||
|
# Combined scale and mirror
|
||||||
|
r_scale = c_scale * ref.scale
|
||||||
|
r_mirrored = c_mirrored ^ ref.mirrored
|
||||||
|
|
||||||
|
# Combined rotation: push c_mirrored and c_rotation through ref.rotation
|
||||||
|
r_rot_relative = -ref.rotation if c_mirrored else ref.rotation
|
||||||
|
r_rotation = c_rotation + r_rot_relative
|
||||||
|
|
||||||
|
# Offset composition helper
|
||||||
|
def get_full_offset(rel_offset: NDArray[numpy.float64]) -> NDArray[numpy.float64]:
|
||||||
|
o = rel_offset * c_scale
|
||||||
|
if c_mirrored:
|
||||||
|
o = o.copy()
|
||||||
|
o[1] *= -1
|
||||||
|
return rotation_matrix_2d(c_rotation) @ o + c_offset
|
||||||
|
|
||||||
|
if ref.repetition is not None:
|
||||||
|
for disp in ref.repetition.displacements:
|
||||||
|
collect_polys_recursive(
|
||||||
|
target_pat,
|
||||||
|
get_full_offset(ref.offset + disp),
|
||||||
|
r_rotation,
|
||||||
|
r_mirrored,
|
||||||
|
r_scale
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
collect_polys_recursive(
|
||||||
|
target_pat,
|
||||||
|
get_full_offset(ref.offset),
|
||||||
|
r_rotation,
|
||||||
|
r_mirrored,
|
||||||
|
r_scale
|
||||||
|
)
|
||||||
|
|
||||||
|
# Start recursive collection
|
||||||
|
collect_polys_recursive(self, numpy.asarray(offset, dtype=float), 0.0, False, 1.0)
|
||||||
|
|
||||||
|
# Plotting
|
||||||
if not overdraw:
|
if not overdraw:
|
||||||
figure = pyplot.figure()
|
figure = pyplot.figure()
|
||||||
pyplot.axis('equal')
|
|
||||||
else:
|
else:
|
||||||
figure = pyplot.gcf()
|
figure = pyplot.gcf()
|
||||||
|
|
||||||
axes = figure.gca()
|
axes = figure.gca()
|
||||||
|
|
||||||
polygons = []
|
if all_polygons:
|
||||||
for shape in chain.from_iterable(self.shapes.values()):
|
|
||||||
polygons += [offset + s.offset + s.vertices for s in shape.to_polygons()]
|
|
||||||
|
|
||||||
mpl_poly_collection = matplotlib.collections.PolyCollection(
|
mpl_poly_collection = matplotlib.collections.PolyCollection(
|
||||||
polygons,
|
all_polygons,
|
||||||
facecolors = fill_color,
|
facecolors = fill_color,
|
||||||
edgecolors = line_color,
|
edgecolors = line_color,
|
||||||
)
|
)
|
||||||
axes.add_collection(mpl_poly_collection)
|
axes.add_collection(mpl_poly_collection)
|
||||||
pyplot.axis('equal')
|
|
||||||
|
|
||||||
for target, refs in self.refs.items():
|
if ports:
|
||||||
if target is None:
|
for port_name, pt_v, pt_rot in port_info:
|
||||||
continue
|
p1 = pt_v
|
||||||
if not refs:
|
angle = pt_rot
|
||||||
continue
|
size = 1.0 # arrow size
|
||||||
assert library is not None
|
p2 = p1 + size * numpy.array([numpy.cos(angle), numpy.sin(angle)])
|
||||||
target_pat = library[target]
|
|
||||||
for ref in refs:
|
axes.annotate(
|
||||||
ref.as_pattern(target_pat).visualize(
|
port_name,
|
||||||
library=library,
|
xy = tuple(p1),
|
||||||
offset=offset,
|
xytext = tuple(p2),
|
||||||
overdraw=True,
|
arrowprops = dict(arrowstyle="->", color='g', linewidth=1),
|
||||||
line_color=line_color,
|
color = 'g',
|
||||||
fill_color=fill_color,
|
fontsize = 8,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
axes.autoscale_view()
|
||||||
|
axes.set_aspect('equal')
|
||||||
|
|
||||||
if not overdraw:
|
if not overdraw:
|
||||||
pyplot.xlabel('x')
|
axes.set_xlabel('x')
|
||||||
pyplot.ylabel('y')
|
axes.set_ylabel('y')
|
||||||
pyplot.show()
|
if filename:
|
||||||
|
figure.savefig(filename)
|
||||||
|
else:
|
||||||
|
figure.show()
|
||||||
|
|
||||||
# @overload
|
# @overload
|
||||||
# def place(
|
# def place(
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,33 @@ class Port(PivotableImpl, PositionableImpl, Mirrorable, Flippable, Copyable):
|
||||||
self.rotation = rotation
|
self.rotation = rotation
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def describe(self) -> str:
|
||||||
|
"""
|
||||||
|
Returns a human-readable description of the port's state including cardinal directions.
|
||||||
|
"""
|
||||||
|
deg = numpy.rad2deg(self.rotation) if self.rotation is not None else "any"
|
||||||
|
|
||||||
|
cardinal = ""
|
||||||
|
travel_dir = ""
|
||||||
|
|
||||||
|
if self.rotation is not None:
|
||||||
|
dirs = {0: "East (+x)", 90: "North (+y)", 180: "West (-x)", 270: "South (-y)"}
|
||||||
|
# normalize to [0, 360)
|
||||||
|
deg_norm = deg % 360
|
||||||
|
|
||||||
|
# Find closest cardinal
|
||||||
|
closest = min(dirs.keys(), key=lambda x: abs((deg_norm - x + 180) % 360 - 180))
|
||||||
|
if numpy.isclose((deg_norm - closest + 180) % 360 - 180, 0, atol=1e-3):
|
||||||
|
cardinal = f" ({dirs[closest]})"
|
||||||
|
|
||||||
|
# Travel direction (rotation + 180)
|
||||||
|
t_deg = (deg_norm + 180) % 360
|
||||||
|
closest_t = min(dirs.keys(), key=lambda x: abs((t_deg - x + 180) % 360 - 180))
|
||||||
|
if numpy.isclose((t_deg - closest_t + 180) % 360 - 180, 0, atol=1e-3):
|
||||||
|
travel_dir = f" (Travel -> {dirs[closest_t]})"
|
||||||
|
|
||||||
|
return f"pos=({self.x:g}, {self.y:g}), rot={deg:g}{cardinal}{travel_dir}"
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
if self.rotation is None:
|
if self.rotation is None:
|
||||||
rot = 'any'
|
rot = 'any'
|
||||||
|
|
@ -210,11 +237,11 @@ class PortList(metaclass=ABCMeta):
|
||||||
|
|
||||||
def _log_port_update(self, name: str) -> None:
|
def _log_port_update(self, name: str) -> None:
|
||||||
""" Log the current state of the named port """
|
""" Log the current state of the named port """
|
||||||
port_logger.info("Port %s: %s", name, self.ports[name])
|
port_logger.debug("Port %s: %s", name, self.ports[name].describe())
|
||||||
|
|
||||||
def _log_port_removal(self, name: str) -> None:
|
def _log_port_removal(self, name: str) -> None:
|
||||||
""" Log that the named port has been removed """
|
""" Log that the named port has been removed """
|
||||||
port_logger.info("Port %s: removed", name)
|
port_logger.debug("Port %s: removed", name)
|
||||||
|
|
||||||
def _log_bulk_update(self, label: str) -> None:
|
def _log_bulk_update(self, label: str) -> None:
|
||||||
""" Log all current ports at DEBUG level """
|
""" Log all current ports at DEBUG level """
|
||||||
|
|
|
||||||
|
|
@ -350,7 +350,7 @@ class Arbitrary(Repetition):
|
||||||
return (f'<Arbitrary {len(self.displacements)}pts >')
|
return (f'<Arbitrary {len(self.displacements)}pts >')
|
||||||
|
|
||||||
def __eq__(self, other: Any) -> bool:
|
def __eq__(self, other: Any) -> bool:
|
||||||
if not type(other) is not type(self):
|
if type(other) is not type(self):
|
||||||
return False
|
return False
|
||||||
return numpy.array_equal(self.displacements, other.displacements)
|
return numpy.array_equal(self.displacements, other.displacements)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ class PolyCollection(Shape):
|
||||||
|
|
||||||
def set_offset(self, val: ArrayLike) -> Self:
|
def set_offset(self, val: ArrayLike) -> Self:
|
||||||
if numpy.any(val):
|
if numpy.any(val):
|
||||||
raise PatternError('Path offset is forced to (0, 0)')
|
raise PatternError('PolyCollection offset is forced to (0, 0)')
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def translate(self, offset: ArrayLike) -> Self:
|
def translate(self, offset: ArrayLike) -> Self:
|
||||||
|
|
|
||||||
|
|
@ -96,11 +96,11 @@ class Polygon(Shape):
|
||||||
@offset.setter
|
@offset.setter
|
||||||
def offset(self, val: ArrayLike) -> None:
|
def offset(self, val: ArrayLike) -> None:
|
||||||
if numpy.any(val):
|
if numpy.any(val):
|
||||||
raise PatternError('Path offset is forced to (0, 0)')
|
raise PatternError('Polygon offset is forced to (0, 0)')
|
||||||
|
|
||||||
def set_offset(self, val: ArrayLike) -> Self:
|
def set_offset(self, val: ArrayLike) -> Self:
|
||||||
if numpy.any(val):
|
if numpy.any(val):
|
||||||
raise PatternError('Path offset is forced to (0, 0)')
|
raise PatternError('Polygon offset is forced to (0, 0)')
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def translate(self, offset: ArrayLike) -> Self:
|
def translate(self, offset: ArrayLike) -> Self:
|
||||||
|
|
|
||||||
|
|
@ -139,22 +139,24 @@ class Shape(FlippableImpl, PivotableImpl, RepeatableImpl, AnnotatableImpl,
|
||||||
for v, v_next in zip(p_verts, numpy.roll(p_verts, -1, axis=0), strict=True):
|
for v, v_next in zip(p_verts, numpy.roll(p_verts, -1, axis=0), strict=True):
|
||||||
dv = v_next - v
|
dv = v_next - v
|
||||||
|
|
||||||
# Find x-index bounds for the line # TODO: fix this and err_xmin/xmax for grids smaller than the line / shape
|
# Find x-index bounds for the line
|
||||||
gxi_range = numpy.digitize([v[0], v_next[0]], gx)
|
gxi_range = numpy.digitize([v[0], v_next[0]], gx)
|
||||||
gxi_min = numpy.min(gxi_range - 1).clip(0, len(gx) - 1)
|
gxi_min = int(numpy.min(gxi_range - 1).clip(0, len(gx) - 1))
|
||||||
gxi_max = numpy.max(gxi_range).clip(0, len(gx))
|
gxi_max = int(numpy.max(gxi_range).clip(0, len(gx)))
|
||||||
|
|
||||||
|
if gxi_min < len(gx) - 1:
|
||||||
err_xmin = (min(v[0], v_next[0]) - gx[gxi_min]) / (gx[gxi_min + 1] - gx[gxi_min])
|
err_xmin = (min(v[0], v_next[0]) - gx[gxi_min]) / (gx[gxi_min + 1] - gx[gxi_min])
|
||||||
err_xmax = (max(v[0], v_next[0]) - gx[gxi_max - 1]) / (gx[gxi_max] - gx[gxi_max - 1])
|
|
||||||
|
|
||||||
if err_xmin >= 0.5:
|
if err_xmin >= 0.5:
|
||||||
gxi_min += 1
|
gxi_min += 1
|
||||||
|
|
||||||
|
if gxi_max > 0 and gxi_max < len(gx):
|
||||||
|
err_xmax = (max(v[0], v_next[0]) - gx[gxi_max - 1]) / (gx[gxi_max] - gx[gxi_max - 1])
|
||||||
if err_xmax >= 0.5:
|
if err_xmax >= 0.5:
|
||||||
gxi_max += 1
|
gxi_max += 1
|
||||||
|
|
||||||
if abs(dv[0]) < 1e-20:
|
if abs(dv[0]) < 1e-20:
|
||||||
# Vertical line, don't calculate slope
|
# Vertical line, don't calculate slope
|
||||||
xi = [gxi_min, gxi_max - 1]
|
xi = [gxi_min, max(gxi_min, gxi_max - 1)]
|
||||||
ys = numpy.array([v[1], v_next[1]])
|
ys = numpy.array([v[1], v_next[1]])
|
||||||
yi = numpy.digitize(ys, gy).clip(1, len(gy) - 1)
|
yi = numpy.digitize(ys, gy).clip(1, len(gy) - 1)
|
||||||
err_y = (ys - gy[yi]) / (gy[yi] - gy[yi - 1])
|
err_y = (ys - gy[yi]) / (gy[yi] - gy[yi - 1])
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ def advanced_pather() -> tuple[Pather, PathTool, Library]:
|
||||||
lib = Library()
|
lib = Library()
|
||||||
# Simple PathTool: 2um width on layer (1,0)
|
# Simple PathTool: 2um width on layer (1,0)
|
||||||
tool = PathTool(layer=(1, 0), width=2, ptype="wire")
|
tool = PathTool(layer=(1, 0), width=2, ptype="wire")
|
||||||
p = Pather(lib, tools=tool)
|
p = Pather(lib, tools=tool, auto_render=True, auto_render_append=False)
|
||||||
return p, tool, lib
|
return p, tool, lib
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
170
masque/test/test_autotool_refactor.py
Normal file
170
masque/test/test_autotool_refactor.py
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
import pytest
|
||||||
|
from numpy.testing import assert_allclose
|
||||||
|
from numpy import pi
|
||||||
|
|
||||||
|
from masque.builder.tools import AutoTool
|
||||||
|
from masque.pattern import Pattern
|
||||||
|
from masque.ports import Port
|
||||||
|
from masque.library import Library
|
||||||
|
from masque.builder.pather import Pather, RenderPather
|
||||||
|
|
||||||
|
def make_straight(length, width=2, ptype="wire"):
|
||||||
|
pat = Pattern()
|
||||||
|
pat.rect((1, 0), xmin=0, xmax=length, yctr=0, ly=width)
|
||||||
|
pat.ports["A"] = Port((0, 0), 0, ptype=ptype)
|
||||||
|
pat.ports["B"] = Port((length, 0), pi, ptype=ptype)
|
||||||
|
return pat
|
||||||
|
|
||||||
|
def make_bend(R, width=2, ptype="wire", clockwise=True):
|
||||||
|
pat = Pattern()
|
||||||
|
# 90 degree arc approximation (just two rects for start and end)
|
||||||
|
if clockwise:
|
||||||
|
# (0,0) rot 0 to (R, -R) rot pi/2
|
||||||
|
pat.rect((1, 0), xmin=0, xmax=R, yctr=0, ly=width)
|
||||||
|
pat.rect((1, 0), xctr=R, lx=width, ymin=-R, ymax=0)
|
||||||
|
pat.ports["A"] = Port((0, 0), 0, ptype=ptype)
|
||||||
|
pat.ports["B"] = Port((R, -R), pi/2, ptype=ptype)
|
||||||
|
else:
|
||||||
|
# (0,0) rot 0 to (R, R) rot -pi/2
|
||||||
|
pat.rect((1, 0), xmin=0, xmax=R, yctr=0, ly=width)
|
||||||
|
pat.rect((1, 0), xctr=R, lx=width, ymin=0, ymax=R)
|
||||||
|
pat.ports["A"] = Port((0, 0), 0, ptype=ptype)
|
||||||
|
pat.ports["B"] = Port((R, R), -pi/2, ptype=ptype)
|
||||||
|
return pat
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def multi_bend_tool():
|
||||||
|
lib = Library()
|
||||||
|
|
||||||
|
# Bend 1: R=2
|
||||||
|
lib["b1"] = make_bend(2, ptype="wire")
|
||||||
|
b1_abs = lib.abstract("b1")
|
||||||
|
# Bend 2: R=5
|
||||||
|
lib["b2"] = make_bend(5, ptype="wire")
|
||||||
|
b2_abs = lib.abstract("b2")
|
||||||
|
|
||||||
|
tool = AutoTool(
|
||||||
|
straights=[
|
||||||
|
# Straight 1: only for length < 10
|
||||||
|
AutoTool.Straight(ptype="wire", fn=make_straight, in_port_name="A", out_port_name="B", length_range=(0, 10)),
|
||||||
|
# Straight 2: for length >= 10
|
||||||
|
AutoTool.Straight(ptype="wire", fn=lambda l: make_straight(l, width=4), in_port_name="A", out_port_name="B", length_range=(10, 1e8))
|
||||||
|
],
|
||||||
|
bends=[
|
||||||
|
AutoTool.Bend(b1_abs, "A", "B", clockwise=True, mirror=True),
|
||||||
|
AutoTool.Bend(b2_abs, "A", "B", clockwise=True, mirror=True)
|
||||||
|
],
|
||||||
|
sbends=[],
|
||||||
|
transitions={},
|
||||||
|
default_out_ptype="wire"
|
||||||
|
)
|
||||||
|
return tool, lib
|
||||||
|
|
||||||
|
def test_autotool_planL_selection(multi_bend_tool) -> None:
|
||||||
|
tool, _ = multi_bend_tool
|
||||||
|
|
||||||
|
# Small length: should pick straight 1 and bend 1 (R=2)
|
||||||
|
# L = straight + R. If L=5, straight=3.
|
||||||
|
p, data = tool.planL(True, 5)
|
||||||
|
assert data.straight.length_range == (0, 10)
|
||||||
|
assert data.straight_length == 3
|
||||||
|
assert data.bend.abstract.name == "b1"
|
||||||
|
assert_allclose(p.offset, [5, 2])
|
||||||
|
|
||||||
|
# Large length: should pick straight 2 and bend 1 (R=2)
|
||||||
|
# If L=15, straight=13.
|
||||||
|
p, data = tool.planL(True, 15)
|
||||||
|
assert data.straight.length_range == (10, 1e8)
|
||||||
|
assert data.straight_length == 13
|
||||||
|
assert_allclose(p.offset, [15, 2])
|
||||||
|
|
||||||
|
def test_autotool_planU_consistency(multi_bend_tool) -> None:
|
||||||
|
tool, lib = multi_bend_tool
|
||||||
|
|
||||||
|
# length=10, jog=20.
|
||||||
|
# U-turn: Straight1 -> Bend1 -> Straight_mid -> Straight3(0) -> Bend2
|
||||||
|
# X = L1_total - R2 = length
|
||||||
|
# Y = R1 + L2_mid + R2 = jog
|
||||||
|
|
||||||
|
p, data = tool.planU(20, length=10)
|
||||||
|
assert data.ldata0.straight_length == 7
|
||||||
|
assert data.ldata0.bend.abstract.name == "b2"
|
||||||
|
assert data.l2_length == 13
|
||||||
|
assert data.ldata1.straight_length == 0
|
||||||
|
assert data.ldata1.bend.abstract.name == "b1"
|
||||||
|
|
||||||
|
def test_autotool_planS_double_L(multi_bend_tool) -> None:
|
||||||
|
tool, lib = multi_bend_tool
|
||||||
|
|
||||||
|
# length=20, jog=10. S-bend (ccw1, cw2)
|
||||||
|
# X = L1_total + R2 = length
|
||||||
|
# Y = R1 + L2_mid + R2 = jog
|
||||||
|
|
||||||
|
p, data = tool.planS(20, 10)
|
||||||
|
assert_allclose(p.offset, [20, 10])
|
||||||
|
assert_allclose(p.rotation, pi)
|
||||||
|
|
||||||
|
assert data.ldata0.straight_length == 16
|
||||||
|
assert data.ldata1.straight_length == 0
|
||||||
|
assert data.l2_length == 6
|
||||||
|
|
||||||
|
def test_renderpather_autotool_double_L(multi_bend_tool) -> None:
|
||||||
|
tool, lib = multi_bend_tool
|
||||||
|
rp = RenderPather(lib, tools=tool)
|
||||||
|
rp.ports["A"] = Port((0,0), 0, ptype="wire")
|
||||||
|
|
||||||
|
# This should trigger double-L fallback in planS
|
||||||
|
rp.jog("A", 10, length=20)
|
||||||
|
|
||||||
|
# port_rot=0 -> forward is -x. jog=10 (left) is -y.
|
||||||
|
assert_allclose(rp.ports["A"].offset, [-20, -10])
|
||||||
|
assert_allclose(rp.ports["A"].rotation, 0) # jog rot is pi relative to input, input rot is pi relative to port.
|
||||||
|
# Wait, planS returns out_port at (length, jog) rot pi relative to input (0,0) rot 0.
|
||||||
|
# Input rot relative to port is pi.
|
||||||
|
# Rotate (length, jog) rot pi by pi: (-length, -jog) rot 0. Correct.
|
||||||
|
|
||||||
|
rp.render()
|
||||||
|
assert len(rp.pattern.refs) > 0
|
||||||
|
|
||||||
|
def test_pather_uturn_fallback_no_heuristic(multi_bend_tool) -> None:
|
||||||
|
tool, lib = multi_bend_tool
|
||||||
|
|
||||||
|
class BasicTool(AutoTool):
|
||||||
|
def planU(self, *args, **kwargs):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
tool_basic = BasicTool(
|
||||||
|
straights=tool.straights,
|
||||||
|
bends=tool.bends,
|
||||||
|
sbends=tool.sbends,
|
||||||
|
transitions=tool.transitions,
|
||||||
|
default_out_ptype=tool.default_out_ptype
|
||||||
|
)
|
||||||
|
|
||||||
|
p = Pather(lib, tools=tool_basic)
|
||||||
|
p.ports["A"] = Port((0,0), 0, ptype="wire") # facing West (Actually East points Inwards, West is Extension)
|
||||||
|
|
||||||
|
# uturn jog=10, length=5.
|
||||||
|
# R=2. L1 = 5+2=7. L2 = 10-2=8.
|
||||||
|
p.uturn("A", 10, length=5)
|
||||||
|
|
||||||
|
# port_rot=0 -> forward is -x. jog=10 (left) is -y.
|
||||||
|
# L1=7 along -x -> (-7, 0). Bend1 (ccw) -> rot -pi/2 (South).
|
||||||
|
# L2=8 along -y -> (-7, -8). Bend2 (ccw) -> rot 0 (East).
|
||||||
|
# wait. CCW turn from facing South (-y): turn towards East (+x).
|
||||||
|
# Wait.
|
||||||
|
# Input facing -x. CCW turn -> face -y.
|
||||||
|
# Input facing -y. CCW turn -> face +x.
|
||||||
|
# So final rotation is 0.
|
||||||
|
# Bend1 (ccw) relative to -x: global offset is (-7, -2)?
|
||||||
|
# Let's re-run my manual calculation.
|
||||||
|
# Port rot 0. Wire input rot pi. Wire output relative to input:
|
||||||
|
# L1=7, R1=2, CCW=True. Output (7, 2) rot pi/2.
|
||||||
|
# Rotate wire by pi: output (-7, -2) rot 3pi/2.
|
||||||
|
# Second turn relative to (-7, -2) rot 3pi/2:
|
||||||
|
# local output (8, 2) rot pi/2.
|
||||||
|
# global: (-7, -2) + 8*rot(3pi/2)*x + 2*rot(3pi/2)*y
|
||||||
|
# = (-7, -2) + 8*(0, -1) + 2*(1, 0) = (-7, -2) + (0, -8) + (2, 0) = (-5, -10).
|
||||||
|
# YES! ACTUAL result was (-5, -10).
|
||||||
|
assert_allclose(p.ports["A"].offset, [-5, -10])
|
||||||
|
assert_allclose(p.ports["A"].rotation, pi)
|
||||||
|
|
@ -97,3 +97,25 @@ def test_renderpather_dead_ports() -> None:
|
||||||
# Verify no geometry
|
# Verify no geometry
|
||||||
rp.render()
|
rp.render()
|
||||||
assert not rp.pattern.has_shapes()
|
assert not rp.pattern.has_shapes()
|
||||||
|
|
||||||
|
|
||||||
|
def test_renderpather_rename_port(rpather_setup: tuple[RenderPather, PathTool, Library]) -> None:
|
||||||
|
rp, tool, lib = rpather_setup
|
||||||
|
rp.at("start").straight(10)
|
||||||
|
# Rename port while path is planned
|
||||||
|
rp.rename_ports({"start": "new_start"})
|
||||||
|
# Continue path on new name
|
||||||
|
rp.at("new_start").straight(10)
|
||||||
|
|
||||||
|
assert "start" not in rp.paths
|
||||||
|
assert len(rp.paths["new_start"]) == 2
|
||||||
|
|
||||||
|
rp.render()
|
||||||
|
assert rp.pattern.has_shapes()
|
||||||
|
assert len(rp.pattern.shapes[(1, 0)]) == 1
|
||||||
|
# Total length 20. start_port rot pi/2 -> 270 deg transform.
|
||||||
|
# Vertices (0,0), (0,-10), (0,-20)
|
||||||
|
path_shape = cast("Path", rp.pattern.shapes[(1, 0)][0])
|
||||||
|
assert_allclose(path_shape.vertices, [[0, 0], [0, -10], [0, -20]], atol=1e-10)
|
||||||
|
assert "new_start" in rp.ports
|
||||||
|
assert_allclose(rp.ports["new_start"].offset, [0, -20], atol=1e-10)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ from ..error import PatternError
|
||||||
|
|
||||||
# 1. Text shape tests
|
# 1. Text shape tests
|
||||||
def test_text_to_polygons() -> None:
|
def test_text_to_polygons() -> None:
|
||||||
|
pytest.importorskip("freetype")
|
||||||
font_path = "/usr/share/fonts/truetype/dejavu/DejaVuMathTeXGyre.ttf"
|
font_path = "/usr/share/fonts/truetype/dejavu/DejaVuMathTeXGyre.ttf"
|
||||||
if not Path(font_path).exists():
|
if not Path(font_path).exists():
|
||||||
pytest.skip("Font file not found")
|
pytest.skip("Font file not found")
|
||||||
|
|
@ -28,6 +29,8 @@ def test_text_to_polygons() -> None:
|
||||||
|
|
||||||
# 2. Manhattanization tests
|
# 2. Manhattanization tests
|
||||||
def test_manhattanize() -> None:
|
def test_manhattanize() -> None:
|
||||||
|
pytest.importorskip("float_raster")
|
||||||
|
pytest.importorskip("skimage.measure")
|
||||||
# Diamond shape
|
# Diamond shape
|
||||||
poly = Polygon([[0, 5], [5, 10], [10, 5], [5, 0]])
|
poly = Polygon([[0, 5], [5, 10], [10, 5], [5, 0]])
|
||||||
grid = numpy.arange(0, 11, 1)
|
grid = numpy.arange(0, 11, 1)
|
||||||
|
|
|
||||||
55
masque/test/test_visualize.py
Normal file
55
masque/test/test_visualize.py
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
from masque.pattern import Pattern
|
||||||
|
from masque.ports import Port
|
||||||
|
from masque.repetition import Grid
|
||||||
|
|
||||||
|
try:
|
||||||
|
import matplotlib
|
||||||
|
HAS_MATPLOTLIB = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_MATPLOTLIB = False
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not HAS_MATPLOTLIB, reason="matplotlib not installed")
|
||||||
|
def test_visualize_noninteractive(tmp_path) -> None:
|
||||||
|
"""
|
||||||
|
Test that visualize() runs and saves a file without error.
|
||||||
|
This covers the recursive transformation and collection logic.
|
||||||
|
"""
|
||||||
|
# Create a hierarchy
|
||||||
|
child = Pattern()
|
||||||
|
child.polygon('L1', [[0, 0], [1, 0], [1, 1], [0, 1]])
|
||||||
|
child.ports['P1'] = Port((0.5, 0.5), 0)
|
||||||
|
|
||||||
|
parent = Pattern()
|
||||||
|
# Add some refs with various transforms
|
||||||
|
parent.ref('child', offset=(10, 0), rotation=np.pi/4, mirrored=True, scale=2.0)
|
||||||
|
|
||||||
|
# Add a repetition
|
||||||
|
rep = Grid(a_vector=(5, 5), a_count=2)
|
||||||
|
parent.ref('child', offset=(0, 10), repetition=rep)
|
||||||
|
|
||||||
|
library = {'child': child}
|
||||||
|
|
||||||
|
output_file = tmp_path / "test_plot.png"
|
||||||
|
|
||||||
|
# Run visualize with filename to avoid showing window
|
||||||
|
parent.visualize(library=library, filename=str(output_file), ports=True)
|
||||||
|
|
||||||
|
assert output_file.exists()
|
||||||
|
assert output_file.stat().st_size > 0
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not HAS_MATPLOTLIB, reason="matplotlib not installed")
|
||||||
|
def test_visualize_empty() -> None:
|
||||||
|
""" Test visualizing an empty pattern. """
|
||||||
|
pat = Pattern()
|
||||||
|
# Should not raise
|
||||||
|
pat.visualize(overdraw=True)
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not HAS_MATPLOTLIB, reason="matplotlib not installed")
|
||||||
|
def test_visualize_no_refs() -> None:
|
||||||
|
""" Test visualizing a pattern with only local shapes (no library needed). """
|
||||||
|
pat = Pattern()
|
||||||
|
pat.polygon('L1', [[0, 0], [1, 0], [0, 1]])
|
||||||
|
# Should not raise even if library is None
|
||||||
|
pat.visualize(overdraw=True)
|
||||||
|
|
@ -17,11 +17,12 @@ class AutoSlots(ABCMeta):
|
||||||
for base in bases:
|
for base in bases:
|
||||||
parents |= set(base.mro())
|
parents |= set(base.mro())
|
||||||
|
|
||||||
slots = tuple(dctn.get('__slots__', ()))
|
slots = list(dctn.get('__slots__', ()))
|
||||||
for parent in parents:
|
for parent in parents:
|
||||||
if not hasattr(parent, '__annotations__'):
|
if not hasattr(parent, '__annotations__'):
|
||||||
continue
|
continue
|
||||||
slots += tuple(parent.__annotations__.keys())
|
slots.extend(parent.__annotations__.keys())
|
||||||
|
|
||||||
dctn['__slots__'] = slots
|
# Deduplicate (dict to preserve order)
|
||||||
|
dctn['__slots__'] = tuple(dict.fromkeys(slots))
|
||||||
return super().__new__(cls, name, bases, dctn)
|
return super().__new__(cls, name, bases, dctn)
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ def annotations_eq(aa: annotations_t, bb: annotations_t) -> bool:
|
||||||
keys_a = tuple(sorted(aa.keys()))
|
keys_a = tuple(sorted(aa.keys()))
|
||||||
keys_b = tuple(sorted(bb.keys()))
|
keys_b = tuple(sorted(bb.keys()))
|
||||||
if keys_a != keys_b:
|
if keys_a != keys_b:
|
||||||
return keys_a < keys_b
|
return False
|
||||||
|
|
||||||
for key in keys_a:
|
for key in keys_a:
|
||||||
va = aa[key]
|
va = aa[key]
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,16 @@ class DeferredDict(dict, Generic[Key, Value]):
|
||||||
"""
|
"""
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
dict.__init__(self)
|
dict.__init__(self)
|
||||||
|
if args or kwargs:
|
||||||
self.update(*args, **kwargs)
|
self.update(*args, **kwargs)
|
||||||
|
|
||||||
def __setitem__(self, key: Key, value: Callable[[], Value]) -> None:
|
def __setitem__(self, key: Key, value: Callable[[], Value]) -> None:
|
||||||
|
"""
|
||||||
|
Set a value, which must be a callable that returns the actual value.
|
||||||
|
The result of the callable is cached after the first access.
|
||||||
|
"""
|
||||||
|
if not callable(value):
|
||||||
|
raise TypeError(f"DeferredDict value must be callable, got {type(value)}")
|
||||||
cached_fn = lru_cache(maxsize=1)(value)
|
cached_fn = lru_cache(maxsize=1)(value)
|
||||||
dict.__setitem__(self, key, cached_fn)
|
dict.__setitem__(self, key, cached_fn)
|
||||||
|
|
||||||
|
|
@ -35,8 +42,15 @@ class DeferredDict(dict, Generic[Key, Value]):
|
||||||
return dict.__getitem__(self, key)()
|
return dict.__getitem__(self, key)()
|
||||||
|
|
||||||
def update(self, *args, **kwargs) -> None:
|
def update(self, *args, **kwargs) -> None:
|
||||||
|
"""
|
||||||
|
Update the DeferredDict. If a value is callable, it is used as a generator.
|
||||||
|
Otherwise, it is wrapped as a constant.
|
||||||
|
"""
|
||||||
for k, v in dict(*args, **kwargs).items():
|
for k, v in dict(*args, **kwargs).items():
|
||||||
|
if callable(v):
|
||||||
self[k] = v
|
self[k] = v
|
||||||
|
else:
|
||||||
|
self.set_const(k, v)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return '<DeferredDict with keys ' + repr(set(self.keys())) + '>'
|
return '<DeferredDict with keys ' + repr(set(self.keys())) + '>'
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,16 @@ def remove_duplicate_vertices(vertices: ArrayLike, closed_path: bool = True) ->
|
||||||
`vertices` with no consecutive duplicates. This may be a view into the original array.
|
`vertices` with no consecutive duplicates. This may be a view into the original array.
|
||||||
"""
|
"""
|
||||||
vertices = numpy.asarray(vertices)
|
vertices = numpy.asarray(vertices)
|
||||||
|
if vertices.shape[0] <= 1:
|
||||||
|
return vertices
|
||||||
duplicates = (vertices == numpy.roll(vertices, -1, axis=0)).all(axis=1)
|
duplicates = (vertices == numpy.roll(vertices, -1, axis=0)).all(axis=1)
|
||||||
if not closed_path:
|
if not closed_path:
|
||||||
duplicates[-1] = False
|
duplicates[-1] = False
|
||||||
return vertices[~duplicates]
|
|
||||||
|
result = vertices[~duplicates]
|
||||||
|
if result.shape[0] == 0 and vertices.shape[0] > 0:
|
||||||
|
return vertices[:1]
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def remove_colinear_vertices(vertices: ArrayLike, closed_path: bool = True) -> NDArray[numpy.float64]:
|
def remove_colinear_vertices(vertices: ArrayLike, closed_path: bool = True) -> NDArray[numpy.float64]:
|
||||||
|
|
@ -62,7 +68,7 @@ def poly_contains_points(
|
||||||
vertices: ArrayLike,
|
vertices: ArrayLike,
|
||||||
points: ArrayLike,
|
points: ArrayLike,
|
||||||
include_boundary: bool = True,
|
include_boundary: bool = True,
|
||||||
) -> NDArray[numpy.int_]:
|
) -> NDArray[numpy.bool_]:
|
||||||
"""
|
"""
|
||||||
Tests whether the provided points are inside the implicitly closed polygon
|
Tests whether the provided points are inside the implicitly closed polygon
|
||||||
described by the provided list of vertices.
|
described by the provided list of vertices.
|
||||||
|
|
@ -81,7 +87,7 @@ def poly_contains_points(
|
||||||
vertices = numpy.asarray(vertices, dtype=float)
|
vertices = numpy.asarray(vertices, dtype=float)
|
||||||
|
|
||||||
if points.size == 0:
|
if points.size == 0:
|
||||||
return numpy.zeros(0, dtype=numpy.int8)
|
return numpy.zeros(0, dtype=bool)
|
||||||
|
|
||||||
min_bounds = numpy.min(vertices, axis=0)[None, :]
|
min_bounds = numpy.min(vertices, axis=0)[None, :]
|
||||||
max_bounds = numpy.max(vertices, axis=0)[None, :]
|
max_bounds = numpy.max(vertices, axis=0)[None, :]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue