Compare commits
99 Commits
master
...
polycollec
| Author | SHA1 | Date | |
|---|---|---|---|
| e69ebc8070 | |||
| 13d013aaf3 | |||
| 07fc8b2ad0 | |||
| b12b406df4 | |||
| 6c6b1c16ff | |||
| 24af43ff48 | |||
| 1505844a0a | |||
| c064ee9d8f | |||
| 22b53a930c | |||
| 355365c0dc | |||
| 2b835ec3a4 | |||
| 519e6ad618 | |||
| e75a76e5a8 | |||
| 5e65dfafa1 | |||
| 04905153d3 | |||
| 1eba387b6a | |||
| 5fbbaa0648 | |||
| 4dc81bd9f7 | |||
| d3b83a7543 | |||
| 184168f623 | |||
| 334bcade31 | |||
| 90b3157b00 | |||
| 7c5c1c26c8 | |||
| fcd3d9663d | |||
| 2b7b1cd6e2 | |||
| dfd61b3a39 | |||
| 3a1a4b9126 | |||
| 8a0c985e36 | |||
| 8d91fb4915 | |||
| 146e6808ee | |||
| f831ccd873 | |||
| 982304bd10 | |||
| 049098ade5 | |||
| dbaa6fc1f3 | |||
| 1fe1334f34 | |||
| 7389be9129 | |||
| fe49e1e25b | |||
| 1faf5ccad5 | |||
| 3ba2ffd33f | |||
| 40e55a9067 | |||
| 1b79cd6f45 | |||
| 639850ab29 | |||
| 2bf44f334a | |||
| d37e6b873c | |||
| 2a8879e3d4 | |||
| de534a755f | |||
| 41bbfee80b | |||
| c7a8fac890 | |||
| fe440b0c53 | |||
| a62deb211c | |||
| 01f624cb6a | |||
| 8996d53479 | |||
| 899d05217e | |||
| f374651bc4 | |||
| fd03e09ea1 | |||
| ba7fab6db2 | |||
| ace34aa7a3 | |||
| 69e6b1bff1 | |||
| 701c297152 | |||
| 74f341db77 | |||
| 4ce7525263 | |||
| e3c1c46b10 | |||
| da35019dc8 | |||
| d71ede927c | |||
| 83850c1cbc | |||
| ebd1fbdfbf | |||
| 3e88ed9438 | |||
| 006e7c428c | |||
| dadaf48d35 | |||
| 240007eb7a | |||
| ee4147ef99 | |||
| fe231e558a | |||
| ffc8dccbef | |||
| debb27cdc8 | |||
| 5a4be88672 | |||
| a2fa7648df | |||
| aa175fbb75 | |||
| 00021c00e6 | |||
| 4e69273b5e | |||
| 923c00d72f | |||
| 7bd15ede88 | |||
| 4960c95637 | |||
| adbc86100b | |||
| cb178bb694 | |||
| 18e5a4ac5a | |||
| 215926269e | |||
| 4487c3825b | |||
| 5608a6717e | |||
| b0ec2a51f5 | |||
| ded473c290 | |||
| d2f85c70ee | |||
| 050f1b597c | |||
| b4116a738d | |||
| aae467021b | |||
| 5ae990a83b | |||
| 3046e33742 | |||
| 6d8efe82f2 | |||
| 534002d4b5 | |||
| 4364c809f3 |
12
README.md
12
README.md
@ -133,7 +133,7 @@ tree = make_tree(...)
|
|||||||
|
|
||||||
# To reference this cell in our layout, we have to add all its children to our `library` first:
|
# To reference this cell in our layout, we have to add all its children to our `library` first:
|
||||||
top_name = tree.top() # get the name of the topcell
|
top_name = tree.top() # get the name of the topcell
|
||||||
name_mapping = library.add(tree) # add all patterns from `tree`, renaming elgible conflicting patterns
|
name_mapping = library.add(tree) # add all patterns from `tree`, renaming eligible conflicting patterns
|
||||||
new_name = name_mapping.get(top_name, top_name) # get the new name for the cell (in case it was auto-renamed)
|
new_name = name_mapping.get(top_name, top_name) # get the new name for the cell (in case it was auto-renamed)
|
||||||
my_pattern.ref(new_name, ...) # instantiate the cell
|
my_pattern.ref(new_name, ...) # instantiate the cell
|
||||||
|
|
||||||
@ -176,7 +176,7 @@ my_pattern.place(library << make_tree(...), ...)
|
|||||||
|
|
||||||
|
|
||||||
### Quickly add geometry, labels, or refs:
|
### Quickly add geometry, labels, or refs:
|
||||||
The long form for adding elements can be overly verbose:
|
Adding elements can be overly verbose:
|
||||||
```python3
|
```python3
|
||||||
my_pattern.shapes[layer].append(Polygon(vertices, ...))
|
my_pattern.shapes[layer].append(Polygon(vertices, ...))
|
||||||
my_pattern.labels[layer] += [Label('my text')]
|
my_pattern.labels[layer] += [Label('my text')]
|
||||||
@ -228,9 +228,11 @@ my_pattern.ref(_make_my_subpattern(), offset=..., ...)
|
|||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
|
* Rework naming/args for path-related (Builder, PortPather, path/pathL/pathS/pathU, path_to, mpath)
|
||||||
|
* PolyCollection & arrow-based read/write
|
||||||
|
* pather and renderpather examples, including .at() (PortPather)
|
||||||
|
* Bus-to-bus connections?
|
||||||
|
* Tests tests tests
|
||||||
* Better interface for polygon operations (e.g. with `pyclipper`)
|
* Better interface for polygon operations (e.g. with `pyclipper`)
|
||||||
- de-embedding
|
- de-embedding
|
||||||
- boolean ops
|
- boolean ops
|
||||||
* Tests tests tests
|
|
||||||
* check renderpather
|
|
||||||
* pather and renderpather examples
|
|
||||||
|
|||||||
@ -77,8 +77,10 @@ from .builder import (
|
|||||||
Pather as Pather,
|
Pather as Pather,
|
||||||
RenderPather as RenderPather,
|
RenderPather as RenderPather,
|
||||||
RenderStep as RenderStep,
|
RenderStep as RenderStep,
|
||||||
BasicTool as BasicTool,
|
SimpleTool as SimpleTool,
|
||||||
|
AutoTool as AutoTool,
|
||||||
PathTool as PathTool,
|
PathTool as PathTool,
|
||||||
|
PortPather as PortPather,
|
||||||
)
|
)
|
||||||
from .utils import (
|
from .utils import (
|
||||||
ports2data as ports2data,
|
ports2data as ports2data,
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
from .builder import Builder as Builder
|
from .builder import Builder as Builder
|
||||||
from .pather import Pather as Pather
|
from .pather import Pather as Pather
|
||||||
from .renderpather import RenderPather as RenderPather
|
from .renderpather import RenderPather as RenderPather
|
||||||
|
from .pather_mixin import PortPather as PortPather
|
||||||
from .utils import ell as ell
|
from .utils import ell as ell
|
||||||
from .tools import (
|
from .tools import (
|
||||||
Tool as Tool,
|
Tool as Tool,
|
||||||
RenderStep as RenderStep,
|
RenderStep as RenderStep,
|
||||||
BasicTool as BasicTool,
|
SimpleTool as SimpleTool,
|
||||||
|
AutoTool as AutoTool,
|
||||||
PathTool as PathTool,
|
PathTool as PathTool,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -67,7 +67,7 @@ class Builder(PortList):
|
|||||||
|
|
||||||
- `my_device.plug(wire, {'myport': 'A'})` places port 'A' of `wire` at '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`,
|
of `my_device`. If `wire` has only two ports (e.g. 'A' and 'B'), no `map_out`,
|
||||||
argument is provided, and the `inherit_name` argument is not explicitly
|
argument is provided, and the `thru` argument is not explicitly
|
||||||
set to `False`, the unconnected port of `wire` is automatically renamed to
|
set to `False`, the unconnected port of `wire` is automatically renamed to
|
||||||
'myport'. This allows easy extension of existing ports without changing
|
'myport'. This allows easy extension of existing ports without changing
|
||||||
their names or having to provide `map_out` each time `plug` is called.
|
their names or having to provide `map_out` each time `plug` is called.
|
||||||
@ -223,7 +223,7 @@ class Builder(PortList):
|
|||||||
map_out: dict[str, str | None] | None = None,
|
map_out: dict[str, str | None] | None = None,
|
||||||
*,
|
*,
|
||||||
mirrored: bool = False,
|
mirrored: bool = False,
|
||||||
inherit_name: bool = True,
|
thru: bool | str = True,
|
||||||
set_rotation: bool | None = None,
|
set_rotation: bool | None = None,
|
||||||
append: bool = False,
|
append: bool = False,
|
||||||
ok_connections: Iterable[tuple[str, str]] = (),
|
ok_connections: Iterable[tuple[str, str]] = (),
|
||||||
@ -246,11 +246,15 @@ class Builder(PortList):
|
|||||||
new names for ports in `other`.
|
new names for ports in `other`.
|
||||||
mirrored: Enables mirroring `other` across the x axis prior to
|
mirrored: Enables mirroring `other` across the x axis prior to
|
||||||
connecting any ports.
|
connecting any ports.
|
||||||
inherit_name: If `True`, and `map_in` specifies only a single port,
|
thru: If map_in specifies only a single port, `thru` provides a mechainsm
|
||||||
and `map_out` is `None`, and `other` has only two ports total,
|
to avoid repeating the port name. Eg, for `map_in={'myport': 'A'}`,
|
||||||
then automatically renames the output port of `other` to the
|
- If True (default), and `other` has only two ports total, and map_out
|
||||||
name of the port from `self` that appears in `map_in`. This
|
doesn't specify a name for the other port, its name is set to the key
|
||||||
makes it easy to extend a device with simple 2-port devices
|
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
|
(e.g. wires) without providing `map_out` each time `plug` is
|
||||||
called. See "Examples" above for more info. Default `True`.
|
called. See "Examples" above for more info. Default `True`.
|
||||||
set_rotation: If the necessary rotation cannot be determined from
|
set_rotation: If the necessary rotation cannot be determined from
|
||||||
@ -292,14 +296,14 @@ class Builder(PortList):
|
|||||||
other = self.library[other.name]
|
other = self.library[other.name]
|
||||||
|
|
||||||
self.pattern.plug(
|
self.pattern.plug(
|
||||||
other=other,
|
other = other,
|
||||||
map_in=map_in,
|
map_in = map_in,
|
||||||
map_out=map_out,
|
map_out = map_out,
|
||||||
mirrored=mirrored,
|
mirrored = mirrored,
|
||||||
inherit_name=inherit_name,
|
thru = thru,
|
||||||
set_rotation=set_rotation,
|
set_rotation = set_rotation,
|
||||||
append=append,
|
append = append,
|
||||||
ok_connections=ok_connections,
|
ok_connections = ok_connections,
|
||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@ -365,14 +369,14 @@ class Builder(PortList):
|
|||||||
other = self.library[other.name]
|
other = self.library[other.name]
|
||||||
|
|
||||||
self.pattern.place(
|
self.pattern.place(
|
||||||
other=other,
|
other = other,
|
||||||
offset=offset,
|
offset = offset,
|
||||||
rotation=rotation,
|
rotation = rotation,
|
||||||
pivot=pivot,
|
pivot = pivot,
|
||||||
mirrored=mirrored,
|
mirrored = mirrored,
|
||||||
port_map=port_map,
|
port_map = port_map,
|
||||||
skip_port_check=skip_port_check,
|
skip_port_check = skip_port_check,
|
||||||
append=append,
|
append = append,
|
||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|||||||
@ -2,31 +2,25 @@
|
|||||||
Manual wire/waveguide routing (`Pather`)
|
Manual wire/waveguide routing (`Pather`)
|
||||||
"""
|
"""
|
||||||
from typing import Self
|
from typing import Self
|
||||||
from collections.abc import Sequence, MutableMapping, Mapping, Iterator
|
from collections.abc import Sequence, Mapping, MutableMapping
|
||||||
import copy
|
import copy
|
||||||
import logging
|
import logging
|
||||||
from contextlib import contextmanager
|
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
|
|
||||||
import numpy
|
|
||||||
from numpy import pi
|
|
||||||
from numpy.typing import ArrayLike
|
|
||||||
|
|
||||||
from ..pattern import Pattern
|
from ..pattern import Pattern
|
||||||
from ..library import ILibrary, SINGLE_USE_PREFIX
|
from ..library import ILibrary
|
||||||
from ..error import PortError, BuildError
|
from ..error import BuildError
|
||||||
from ..ports import PortList, Port
|
from ..ports import PortList, Port
|
||||||
from ..abstract import Abstract
|
from ..utils import SupportsBool
|
||||||
from ..utils import SupportsBool, rotation_matrix_2d
|
|
||||||
from .tools import Tool
|
from .tools import Tool
|
||||||
from .utils import ell
|
from .pather_mixin import PatherMixin
|
||||||
from .builder import Builder
|
from .builder import Builder
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Pather(Builder):
|
class Pather(Builder, PatherMixin):
|
||||||
"""
|
"""
|
||||||
An extension of `Builder` which provides functionality for routing and attaching
|
An extension of `Builder` which provides functionality for routing and attaching
|
||||||
single-use patterns (e.g. wires or waveguides) and bundles / buses of such patterns.
|
single-use patterns (e.g. wires or waveguides) and bundles / buses of such patterns.
|
||||||
@ -258,60 +252,6 @@ class Pather(Builder):
|
|||||||
s = f'<Pather {self.pattern} L({len(self.library)}) {pformat(self.tools)}>'
|
s = f'<Pather {self.pattern} L({len(self.library)}) {pformat(self.tools)}>'
|
||||||
return s
|
return s
|
||||||
|
|
||||||
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(
|
def path(
|
||||||
self,
|
self,
|
||||||
@ -319,7 +259,6 @@ class Pather(Builder):
|
|||||||
ccw: SupportsBool | None,
|
ccw: SupportsBool | None,
|
||||||
length: float,
|
length: float,
|
||||||
*,
|
*,
|
||||||
tool_port_names: tuple[str, str] = ('A', 'B'),
|
|
||||||
plug_into: str | None = None,
|
plug_into: str | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
@ -327,7 +266,7 @@ class Pather(Builder):
|
|||||||
Create a "wire"/"waveguide" and `plug` it into the port `portspec`, with the aim
|
Create a "wire"/"waveguide" and `plug` it into the port `portspec`, with the aim
|
||||||
of traveling exactly `length` distance.
|
of traveling exactly `length` distance.
|
||||||
|
|
||||||
The wire will travel `length` distance along the port's axis, an an unspecified
|
The wire will travel `length` distance along the port's axis, and an unspecified
|
||||||
(tool-dependent) distance in the perpendicular direction. The output port will
|
(tool-dependent) distance in the perpendicular direction. The output port will
|
||||||
be rotated (or not) based on the `ccw` parameter.
|
be rotated (or not) based on the `ccw` parameter.
|
||||||
|
|
||||||
@ -338,9 +277,6 @@ class Pather(Builder):
|
|||||||
and clockwise otherwise.
|
and clockwise otherwise.
|
||||||
length: The total distance from input to output, along the input's axis only.
|
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.)
|
(There may be a tool-dependent offset along the other axis.)
|
||||||
tool_port_names: The names of the ports on the generated pattern. It is unlikely
|
|
||||||
that you will need to change these. The first port is the input (to be
|
|
||||||
connected to `portspec`).
|
|
||||||
plug_into: If not None, attempts to plug the wire's output port into the provided
|
plug_into: If not None, attempts to plug the wire's output port into the provided
|
||||||
port on `self`.
|
port on `self`.
|
||||||
|
|
||||||
@ -355,54 +291,44 @@ class Pather(Builder):
|
|||||||
logger.error('Skipping path() since device is dead')
|
logger.error('Skipping path() since device is dead')
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
tool_port_names = ('A', 'B')
|
||||||
|
|
||||||
tool = self.tools.get(portspec, self.tools[None])
|
tool = self.tools.get(portspec, self.tools[None])
|
||||||
in_ptype = self.pattern[portspec].ptype
|
in_ptype = self.pattern[portspec].ptype
|
||||||
tree = tool.path(ccw, length, in_ptype=in_ptype, port_names=tool_port_names, **kwargs)
|
tree = tool.path(ccw, length, in_ptype=in_ptype, port_names=tool_port_names, **kwargs)
|
||||||
abstract = self.library << tree
|
tname = self.library << tree
|
||||||
if plug_into is not None:
|
if plug_into is not None:
|
||||||
output = {plug_into: tool_port_names[1]}
|
output = {plug_into: tool_port_names[1]}
|
||||||
else:
|
else:
|
||||||
output = {}
|
output = {}
|
||||||
return self.plug(abstract, {portspec: tool_port_names[0], **output})
|
self.plug(tname, {portspec: tool_port_names[0], **output})
|
||||||
|
return self
|
||||||
|
|
||||||
def path_to(
|
def pathS(
|
||||||
self,
|
self,
|
||||||
portspec: str,
|
portspec: str,
|
||||||
ccw: SupportsBool | None,
|
length: float,
|
||||||
position: float | None = None,
|
jog: float,
|
||||||
*,
|
*,
|
||||||
x: float | None = None,
|
|
||||||
y: float | None = None,
|
|
||||||
tool_port_names: tuple[str, str] = ('A', 'B'),
|
|
||||||
plug_into: str | None = None,
|
plug_into: str | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
"""
|
"""
|
||||||
Create a "wire"/"waveguide" and `plug` it into the port `portspec`, with the aim
|
Create an S-shaped "wire"/"waveguide" and `plug` it into the port `portspec`, with the aim
|
||||||
of ending exactly at a target position.
|
of traveling exactly `length` distance with an offset `jog` along the other axis (+ve jog is
|
||||||
|
left of direction of travel).
|
||||||
|
|
||||||
The wire will travel so that the output port will be placed at exactly the target
|
The output port will have the same orientation as the source port (`portspec`).
|
||||||
position along the input port's axis. There can be an unspecified (tool-dependent)
|
|
||||||
offset in the perpendicular direction. The output port will be rotated (or not)
|
This function attempts to use `tool.planS()`, but falls back to `tool.planL()` if the former
|
||||||
based on the `ccw` parameter.
|
raises a NotImplementedError.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
portspec: The name of the port into which the wire will be plugged.
|
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.
|
jog: Total manhattan distance perpendicular to the direction of travel.
|
||||||
Otherwise, cast to bool and turn counterclockwise if True
|
Positive values are to the left of the direction of travel.
|
||||||
and clockwise otherwise.
|
length: The total manhattan distance from input to output, along the input's axis only.
|
||||||
position: The final port position, along the input's axis only.
|
|
||||||
(There may be a tool-dependent offset along the other axis.)
|
(There may be a tool-dependent offset along the other axis.)
|
||||||
Only one of `position`, `x`, and `y` may be specified.
|
|
||||||
x: The final port position along the x axis.
|
|
||||||
`portspec` must refer to a horizontal port if `x` is passed, otherwise a
|
|
||||||
BuildError will be raised.
|
|
||||||
y: The final port position along the y axis.
|
|
||||||
`portspec` must refer to a vertical port if `y` is passed, otherwise a
|
|
||||||
BuildError will be raised.
|
|
||||||
tool_port_names: The names of the ports on the generated pattern. It is unlikely
|
|
||||||
that you will need to change these. The first port is the input (to be
|
|
||||||
connected to `portspec`).
|
|
||||||
plug_into: If not None, attempts to plug the wire's output port into the provided
|
plug_into: If not None, attempts to plug the wire's output port into the provided
|
||||||
port on `self`.
|
port on `self`.
|
||||||
|
|
||||||
@ -410,317 +336,40 @@ class Pather(Builder):
|
|||||||
self
|
self
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
BuildError if `position`, `x`, or `y` is too close to fit the bend (if a bend
|
BuildError if `distance` is too small to fit the s-bend (for nonzero jog).
|
||||||
is present).
|
LibraryError if no valid name could be picked for the pattern.
|
||||||
BuildError if `x` or `y` is specified but does not match the axis of `portspec`.
|
|
||||||
BuildError if more than one of `x`, `y`, and `position` is specified.
|
|
||||||
"""
|
"""
|
||||||
if self._dead:
|
if self._dead:
|
||||||
logger.error('Skipping path_to() since device is dead')
|
logger.error('Skipping pathS() since device is dead')
|
||||||
return self
|
return self
|
||||||
|
|
||||||
pos_count = sum(vv is not None for vv in (position, x, y))
|
tool_port_names = ('A', 'B')
|
||||||
if pos_count > 1:
|
|
||||||
raise BuildError('Only one of `position`, `x`, and `y` may be specified at once')
|
|
||||||
if pos_count < 1:
|
|
||||||
raise BuildError('One of `position`, `x`, and `y` must be specified')
|
|
||||||
|
|
||||||
port = self.pattern[portspec]
|
tool = self.tools.get(portspec, self.tools[None])
|
||||||
if port.rotation is None:
|
in_ptype = self.pattern[portspec].ptype
|
||||||
raise PortError(f'Port {portspec} has no rotation and cannot be used for path_to()')
|
try:
|
||||||
|
tree = tool.pathS(length, jog, in_ptype=in_ptype, port_names=tool_port_names, **kwargs)
|
||||||
|
except NotImplementedError:
|
||||||
|
# Fall back to drawing two L-bends
|
||||||
|
ccw0 = jog > 0
|
||||||
|
kwargs_no_out = kwargs | {'out_ptype': None}
|
||||||
|
t_tree0 = tool.path( ccw0, length / 2, port_names=tool_port_names, in_ptype=in_ptype, **kwargs_no_out)
|
||||||
|
t_pat0 = t_tree0.top_pattern()
|
||||||
|
(_, jog0), _ = t_pat0[tool_port_names[0]].measure_travel(t_pat0[tool_port_names[1]])
|
||||||
|
t_tree1 = tool.path(not ccw0, abs(jog - jog0), port_names=tool_port_names, in_ptype=t_pat0[tool_port_names[1]].ptype, **kwargs)
|
||||||
|
t_pat1 = t_tree1.top_pattern()
|
||||||
|
(_, jog1), _ = t_pat1[tool_port_names[0]].measure_travel(t_pat1[tool_port_names[1]])
|
||||||
|
|
||||||
if not numpy.isclose(port.rotation % (pi / 2), 0):
|
kwargs_plug = kwargs | {'plug_into': plug_into}
|
||||||
raise BuildError('path_to was asked to route from non-manhattan port')
|
self.path(portspec, ccw0, length - abs(jog1), **kwargs_no_out)
|
||||||
|
self.path(portspec, not ccw0, abs(jog - jog0), **kwargs_plug)
|
||||||
is_horizontal = numpy.isclose(port.rotation % pi, 0)
|
|
||||||
if is_horizontal:
|
|
||||||
if y is not None:
|
|
||||||
raise BuildError('Asked to path to y-coordinate, but port is horizontal')
|
|
||||||
if position is None:
|
|
||||||
position = x
|
|
||||||
else:
|
|
||||||
if x is not None:
|
|
||||||
raise BuildError('Asked to path to x-coordinate, but port is vertical')
|
|
||||||
if position is None:
|
|
||||||
position = y
|
|
||||||
|
|
||||||
x0, y0 = port.offset
|
|
||||||
if is_horizontal:
|
|
||||||
if numpy.sign(numpy.cos(port.rotation)) == numpy.sign(position - x0):
|
|
||||||
raise BuildError(f'path_to routing to behind source port: x0={x0:g} to {position:g}')
|
|
||||||
length = numpy.abs(position - x0)
|
|
||||||
else:
|
|
||||||
if numpy.sign(numpy.sin(port.rotation)) == numpy.sign(position - y0):
|
|
||||||
raise BuildError(f'path_to routing to behind source port: y0={y0:g} to {position:g}')
|
|
||||||
length = numpy.abs(position - y0)
|
|
||||||
|
|
||||||
return self.path(
|
|
||||||
portspec,
|
|
||||||
ccw,
|
|
||||||
length,
|
|
||||||
tool_port_names=tool_port_names,
|
|
||||||
plug_into=plug_into,
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
|
|
||||||
def path_into(
|
|
||||||
self,
|
|
||||||
portspec_src: str,
|
|
||||||
portspec_dst: str,
|
|
||||||
*,
|
|
||||||
tool_port_names: tuple[str, str] = ('A', 'B'),
|
|
||||||
out_ptype: str | None = None,
|
|
||||||
plug_destination: bool = True,
|
|
||||||
**kwargs,
|
|
||||||
) -> Self:
|
|
||||||
"""
|
|
||||||
Create a "wire"/"waveguide" and 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.
|
|
||||||
tool_port_names: The names of the ports on the generated pattern. It is unlikely
|
|
||||||
that you will need to change these. The first port is the input (to be
|
|
||||||
connected to `portspec`).
|
|
||||||
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.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
PortError if either port does not have a specified rotation.
|
|
||||||
BuildError if and invalid port config is encountered:
|
|
||||||
- Non-manhattan ports
|
|
||||||
- U-bend
|
|
||||||
- Destination too close to (or behind) source
|
|
||||||
"""
|
|
||||||
if self._dead:
|
|
||||||
logger.error('Skipping path_into() since device is dead')
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
port_src = self.pattern[portspec_src]
|
tname = self.library << tree
|
||||||
port_dst = self.pattern[portspec_dst]
|
if plug_into is not None:
|
||||||
|
output = {plug_into: tool_port_names[1]}
|
||||||
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 path_into()')
|
|
||||||
if port_dst.rotation is None:
|
|
||||||
raise PortError(f'Port {portspec_dst} has no rotation and cannot be used for path_into()')
|
|
||||||
|
|
||||||
if not numpy.isclose(port_src.rotation % (pi / 2), 0):
|
|
||||||
raise BuildError('path_into was asked to route from non-manhattan port')
|
|
||||||
if not numpy.isclose(port_dst.rotation % (pi / 2), 0):
|
|
||||||
raise BuildError('path_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)
|
|
||||||
|
|
||||||
src_ne = port_src.rotation % (2 * pi) > (3 * pi / 4) # path from src will go north or east
|
|
||||||
|
|
||||||
def get_jog(ccw: SupportsBool, length: float) -> float:
|
|
||||||
tool = self.tools.get(portspec_src, self.tools[None])
|
|
||||||
in_ptype = 'unk' # Could use port_src.ptype, but we're assuming this is after one bend already...
|
|
||||||
tree2 = tool.path(ccw, length, in_ptype=in_ptype, port_names=('A', 'B'), out_ptype=out_ptype, **kwargs)
|
|
||||||
top2 = tree2.top_pattern()
|
|
||||||
jog = rotation_matrix_2d(top2['A'].rotation) @ (top2['B'].offset - top2['A'].offset)
|
|
||||||
return jog[1] * [-1, 1][int(bool(ccw))]
|
|
||||||
|
|
||||||
dst_extra_args = {'out_ptype': out_ptype}
|
|
||||||
if plug_destination:
|
|
||||||
dst_extra_args['plug_into'] = portspec_dst
|
|
||||||
|
|
||||||
src_args = {**kwargs, 'tool_port_names': tool_port_names}
|
|
||||||
dst_args = {**src_args, **dst_extra_args}
|
|
||||||
if src_is_horizontal and not dst_is_horizontal:
|
|
||||||
# single bend should suffice
|
|
||||||
self.path_to(portspec_src, angle > pi, x=xd, **src_args)
|
|
||||||
self.path_to(portspec_src, None, y=yd, **dst_args)
|
|
||||||
elif dst_is_horizontal and not src_is_horizontal:
|
|
||||||
# single bend should suffice
|
|
||||||
self.path_to(portspec_src, angle > pi, y=yd, **src_args)
|
|
||||||
self.path_to(portspec_src, None, x=xd, **dst_args)
|
|
||||||
elif numpy.isclose(angle, pi):
|
|
||||||
if src_is_horizontal and ys == yd:
|
|
||||||
# straight connector
|
|
||||||
self.path_to(portspec_src, None, x=xd, **dst_args)
|
|
||||||
elif not src_is_horizontal and xs == xd:
|
|
||||||
# straight connector
|
|
||||||
self.path_to(portspec_src, None, y=yd, **dst_args)
|
|
||||||
elif src_is_horizontal:
|
|
||||||
# figure out how much x our y-segment (2nd) takes up, then path based on that
|
|
||||||
y_len = numpy.abs(yd - ys)
|
|
||||||
ccw2 = src_ne != (yd > ys)
|
|
||||||
jog = get_jog(ccw2, y_len) * numpy.sign(xd - xs)
|
|
||||||
self.path_to(portspec_src, not ccw2, x=xd - jog, **src_args)
|
|
||||||
self.path_to(portspec_src, ccw2, y=yd, **dst_args)
|
|
||||||
else:
|
|
||||||
# figure out how much y our x-segment (2nd) takes up, then path based on that
|
|
||||||
x_len = numpy.abs(xd - xs)
|
|
||||||
ccw2 = src_ne != (xd < xs)
|
|
||||||
jog = get_jog(ccw2, x_len) * numpy.sign(yd - ys)
|
|
||||||
self.path_to(portspec_src, not ccw2, y=yd - jog, **src_args)
|
|
||||||
self.path_to(portspec_src, ccw2, x=xd, **dst_args)
|
|
||||||
elif numpy.isclose(angle, 0):
|
|
||||||
raise BuildError('Don\'t know how to route a U-bend at this time!')
|
|
||||||
else:
|
else:
|
||||||
raise BuildError(f'Don\'t know how to route ports with relative angle {angle}')
|
output = {}
|
||||||
|
self.plug(tname, {portspec: tool_port_names[0], **output})
|
||||||
return self
|
|
||||||
|
|
||||||
def mpath(
|
|
||||||
self,
|
|
||||||
portspec: str | Sequence[str],
|
|
||||||
ccw: SupportsBool | None,
|
|
||||||
*,
|
|
||||||
spacing: float | ArrayLike | None = None,
|
|
||||||
set_rotation: float | None = None,
|
|
||||||
tool_port_names: tuple[str, str] = ('A', 'B'),
|
|
||||||
force_container: bool = False,
|
|
||||||
base_name: str = SINGLE_USE_PREFIX + 'mpath',
|
|
||||||
**kwargs,
|
|
||||||
) -> Self:
|
|
||||||
"""
|
|
||||||
`mpath` is a superset of `path` and `path_to` which can act on bundles or buses
|
|
||||||
of "wires or "waveguides".
|
|
||||||
|
|
||||||
The wires will travel so that the output ports will be placed at well-defined
|
|
||||||
locations along the axis of their input ports, but may have arbitrary (tool-
|
|
||||||
dependent) offsets in the perpendicular direction.
|
|
||||||
|
|
||||||
If `ccw` is not `None`, the wire bundle will turn 90 degres in either the
|
|
||||||
clockwise (`ccw=False`) or counter-clockwise (`ccw=True`) direction. Within the
|
|
||||||
bundle, the center-to-center wire spacings after the turn are set by `spacing`,
|
|
||||||
which is required when `ccw` is not `None`. The final position of bundle as a
|
|
||||||
whole can be set in a number of ways:
|
|
||||||
|
|
||||||
=A>---------------------------V turn direction: `ccw=False`
|
|
||||||
=B>-------------V |
|
|
||||||
=C>-----------------------V |
|
|
||||||
=D=>----------------V |
|
|
||||||
|
|
|
||||||
|
|
||||||
x---x---x---x `spacing` (can be scalar or array)
|
|
||||||
|
|
||||||
<--------------> `emin=`
|
|
||||||
<------> `bound_type='min_past_furthest', bound=`
|
|
||||||
<--------------------------------> `emax=`
|
|
||||||
x `pmin=`
|
|
||||||
x `pmax=`
|
|
||||||
|
|
||||||
- `emin=`, equivalent to `bound_type='min_extension', bound=`
|
|
||||||
The total extension value for the furthest-out port (B in the diagram).
|
|
||||||
- `emax=`, equivalent to `bound_type='max_extension', bound=`:
|
|
||||||
The total extension value for the closest-in port (C in the diagram).
|
|
||||||
- `pmin=`, equivalent to `xmin=`, `ymin=`, or `bound_type='min_position', bound=`:
|
|
||||||
The coordinate of the innermost bend (D's bend).
|
|
||||||
The x/y versions throw an error if they do not match the port axis (for debug)
|
|
||||||
- `pmax=`, `xmax=`, `ymax=`, or `bound_type='max_position', bound=`:
|
|
||||||
The coordinate of the outermost bend (A's bend).
|
|
||||||
The x/y versions throw an error if they do not match the port axis (for debug)
|
|
||||||
- `bound_type='min_past_furthest', bound=`:
|
|
||||||
The distance between furthest out-port (B) and the innermost bend (D's bend).
|
|
||||||
|
|
||||||
If `ccw=None`, final output positions (along the input axis) of all wires will be
|
|
||||||
identical (i.e. wires will all be cut off evenly). In this case, `spacing=None` is
|
|
||||||
required. In this case, `emin=` and `emax=` are equivalent to each other, and
|
|
||||||
`pmin=`, `pmax=`, `xmin=`, etc. are also equivalent to each other.
|
|
||||||
|
|
||||||
|
|
||||||
Args:
|
|
||||||
portspec: The names of the ports which are to be routed.
|
|
||||||
ccw: If `None`, the outputs should be along the same axis as the inputs.
|
|
||||||
Otherwise, cast to bool and turn 90 degrees counterclockwise if `True`
|
|
||||||
and clockwise otherwise.
|
|
||||||
spacing: Center-to-center distance between output ports along the input port's axis.
|
|
||||||
Must be provided if (and only if) `ccw` is not `None`.
|
|
||||||
set_rotation: If the provided ports have `rotation=None`, this can be used
|
|
||||||
to set a rotation for them.
|
|
||||||
tool_port_names: The names of the ports on the generated pattern. It is unlikely
|
|
||||||
that you will need to change these. The first port is the input (to be
|
|
||||||
connected to `portspec`).
|
|
||||||
force_container: If `False` (default), and only a single port is provided, the
|
|
||||||
generated wire for that port will be referenced directly, rather than being
|
|
||||||
wrapped in an additonal `Pattern`.
|
|
||||||
base_name: Name to use for the generated `Pattern`. This will be passed through
|
|
||||||
`self.library.get_name()` to get a unique name for each new `Pattern`.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
BuildError if the implied length for any wire is too close to fit the bend
|
|
||||||
(if a bend is requested).
|
|
||||||
BuildError if `xmin`/`xmax` or `ymin`/`ymax` is specified but does not
|
|
||||||
match the axis of `portspec`.
|
|
||||||
BuildError if an incorrect bound type or spacing is specified.
|
|
||||||
"""
|
|
||||||
if self._dead:
|
|
||||||
logger.error('Skipping mpath() since device is dead')
|
|
||||||
return self
|
|
||||||
|
|
||||||
bound_types = set()
|
|
||||||
if 'bound_type' in kwargs:
|
|
||||||
bound_types.add(kwargs['bound_type'])
|
|
||||||
bound = kwargs['bound']
|
|
||||||
del kwargs['bound_type']
|
|
||||||
del kwargs['bound']
|
|
||||||
for bt in ('emin', 'emax', 'pmin', 'pmax', 'xmin', 'xmax', 'ymin', 'ymax', 'min_past_furthest'):
|
|
||||||
if bt in kwargs:
|
|
||||||
bound_types.add(bt)
|
|
||||||
bound = kwargs[bt]
|
|
||||||
del kwargs[bt]
|
|
||||||
|
|
||||||
if not bound_types:
|
|
||||||
raise BuildError('No bound type specified for mpath')
|
|
||||||
if len(bound_types) > 1:
|
|
||||||
raise BuildError(f'Too many bound types specified for mpath: {bound_types}')
|
|
||||||
bound_type = tuple(bound_types)[0]
|
|
||||||
|
|
||||||
if isinstance(portspec, str):
|
|
||||||
portspec = [portspec]
|
|
||||||
ports = self.pattern[tuple(portspec)]
|
|
||||||
|
|
||||||
extensions = ell(ports, ccw, spacing=spacing, bound=bound, bound_type=bound_type, set_rotation=set_rotation)
|
|
||||||
|
|
||||||
if len(ports) == 1 and not force_container:
|
|
||||||
# Not a bus, so having a container just adds noise to the layout
|
|
||||||
port_name = tuple(portspec)[0]
|
|
||||||
return self.path(port_name, ccw, extensions[port_name], tool_port_names=tool_port_names, **kwargs)
|
|
||||||
|
|
||||||
bld = Pather.interface(source=ports, library=self.library, tools=self.tools)
|
|
||||||
for port_name, length in extensions.items():
|
|
||||||
bld.path(port_name, ccw, length, tool_port_names=tool_port_names, **kwargs)
|
|
||||||
name = self.library.get_name(base_name)
|
|
||||||
self.library[name] = bld.pattern
|
|
||||||
return self.plug(Abstract(name, bld.pattern.ports), {sp: 'in_' + sp for sp in ports}) # TODO safe to use 'in_'?
|
|
||||||
|
|
||||||
# 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
|
return self
|
||||||
|
|
||||||
|
|||||||
677
masque/builder/pather_mixin.py
Normal file
677
masque/builder/pather_mixin.py
Normal file
@ -0,0 +1,677 @@
|
|||||||
|
from typing import Self, overload
|
||||||
|
from collections.abc import Sequence, Iterator, Iterable
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def path(
|
||||||
|
self,
|
||||||
|
portspec: str,
|
||||||
|
ccw: SupportsBool | None,
|
||||||
|
length: float,
|
||||||
|
*,
|
||||||
|
plug_into: str | None = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> Self:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def pathS(
|
||||||
|
self,
|
||||||
|
portspec: str,
|
||||||
|
length: float,
|
||||||
|
jog: float,
|
||||||
|
*,
|
||||||
|
plug_into: str | None = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> Self:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
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:
|
||||||
|
"""
|
||||||
|
Build a "wire"/"waveguide" extending from the port `portspec`, with the aim
|
||||||
|
of ending exactly at a target position.
|
||||||
|
|
||||||
|
The wire will travel so that the output port will be placed at exactly the target
|
||||||
|
position along the input port's axis. There can be an unspecified (tool-dependent)
|
||||||
|
offset in the perpendicular direction. The output port will be rotated (or not)
|
||||||
|
based on the `ccw` parameter.
|
||||||
|
|
||||||
|
If using `RenderPather`, `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.
|
||||||
|
position: The final port position, along the input's axis only.
|
||||||
|
(There may be a tool-dependent offset along the other axis.)
|
||||||
|
Only one of `position`, `x`, and `y` may be specified.
|
||||||
|
x: The final port position along the x axis.
|
||||||
|
`portspec` must refer to a horizontal port if `x` is passed, otherwise a
|
||||||
|
BuildError will be raised.
|
||||||
|
y: The final port position along the y axis.
|
||||||
|
`portspec` must refer to a vertical port if `y` is passed, otherwise a
|
||||||
|
BuildError will be raised.
|
||||||
|
plug_into: If not None, attempts to plug the wire's output port into the provided
|
||||||
|
port on `self`.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
BuildError if `position`, `x`, or `y` is too close to fit the bend (if a bend
|
||||||
|
is present).
|
||||||
|
BuildError if `x` or `y` is specified but does not match the axis of `portspec`.
|
||||||
|
BuildError if more than one of `x`, `y`, and `position` is specified.
|
||||||
|
"""
|
||||||
|
if self._dead:
|
||||||
|
logger.error('Skipping path_to() since device is dead')
|
||||||
|
return self
|
||||||
|
|
||||||
|
pos_count = sum(vv is not None for vv in (position, x, y))
|
||||||
|
if pos_count > 1:
|
||||||
|
raise BuildError('Only one of `position`, `x`, and `y` may be specified at once')
|
||||||
|
if pos_count < 1:
|
||||||
|
raise BuildError('One of `position`, `x`, and `y` must be specified')
|
||||||
|
|
||||||
|
port = self.pattern[portspec]
|
||||||
|
if port.rotation is None:
|
||||||
|
raise PortError(f'Port {portspec} has no rotation and cannot be used for path_to()')
|
||||||
|
|
||||||
|
if not numpy.isclose(port.rotation % (pi / 2), 0):
|
||||||
|
raise BuildError('path_to was asked to route from non-manhattan port')
|
||||||
|
|
||||||
|
is_horizontal = numpy.isclose(port.rotation % pi, 0)
|
||||||
|
if is_horizontal:
|
||||||
|
if y is not None:
|
||||||
|
raise BuildError('Asked to path to y-coordinate, but port is horizontal')
|
||||||
|
if position is None:
|
||||||
|
position = x
|
||||||
|
else:
|
||||||
|
if x is not None:
|
||||||
|
raise BuildError('Asked to path to x-coordinate, but port is vertical')
|
||||||
|
if position is None:
|
||||||
|
position = y
|
||||||
|
|
||||||
|
x0, y0 = port.offset
|
||||||
|
if is_horizontal:
|
||||||
|
if numpy.sign(numpy.cos(port.rotation)) == numpy.sign(position - x0):
|
||||||
|
raise BuildError(f'path_to routing to behind source port: x0={x0:g} to {position:g}')
|
||||||
|
length = numpy.abs(position - x0)
|
||||||
|
else:
|
||||||
|
if numpy.sign(numpy.sin(port.rotation)) == numpy.sign(position - y0):
|
||||||
|
raise BuildError(f'path_to routing to behind source port: y0={y0:g} to {position:g}')
|
||||||
|
length = numpy.abs(position - y0)
|
||||||
|
|
||||||
|
return self.path(
|
||||||
|
portspec,
|
||||||
|
ccw,
|
||||||
|
length,
|
||||||
|
plug_into = plug_into,
|
||||||
|
**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:
|
||||||
|
"""
|
||||||
|
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).
|
||||||
|
|
||||||
|
If using `RenderPather`, `RenderPather.render` must be called after all paths have been fully planned.
|
||||||
|
|
||||||
|
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 rename 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 and invalid port config is encountered:
|
||||||
|
- Non-manhattan ports
|
||||||
|
- U-bend
|
||||||
|
- Destination too close to (or behind) source
|
||||||
|
"""
|
||||||
|
if self._dead:
|
||||||
|
logger.error('Skipping path_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 path_into()')
|
||||||
|
if port_dst.rotation is None:
|
||||||
|
raise PortError(f'Port {portspec_dst} has no rotation and cannot be used for path_into()')
|
||||||
|
|
||||||
|
if not numpy.isclose(port_src.rotation % (pi / 2), 0):
|
||||||
|
raise BuildError('path_into was asked to route from non-manhattan port')
|
||||||
|
if not numpy.isclose(port_dst.rotation % (pi / 2), 0):
|
||||||
|
raise BuildError('path_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.path_to(portspec_src, angle > pi, x=xd, **src_args)
|
||||||
|
self.path_to(portspec_src, None, y=yd, **dst_args)
|
||||||
|
elif dst_is_horizontal and not src_is_horizontal:
|
||||||
|
# single bend should suffice
|
||||||
|
self.path_to(portspec_src, angle > pi, y=yd, **src_args)
|
||||||
|
self.path_to(portspec_src, None, x=xd, **dst_args)
|
||||||
|
elif numpy.isclose(angle, pi):
|
||||||
|
if src_is_horizontal and ys == yd:
|
||||||
|
# straight connector
|
||||||
|
self.path_to(portspec_src, None, x=xd, **dst_args)
|
||||||
|
elif not src_is_horizontal and xs == xd:
|
||||||
|
# straight connector
|
||||||
|
self.path_to(portspec_src, None, y=yd, **dst_args)
|
||||||
|
else:
|
||||||
|
# S-bend, delegate to implementations
|
||||||
|
(travel, jog), _ = port_src.measure_travel(port_dst)
|
||||||
|
self.pathS(portspec_src, -travel, -jog, **dst_args)
|
||||||
|
elif numpy.isclose(angle, 0):
|
||||||
|
raise BuildError('Don\'t know how to route a U-bend yet (TODO)!')
|
||||||
|
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 mpath(
|
||||||
|
self,
|
||||||
|
portspec: str | Sequence[str],
|
||||||
|
ccw: SupportsBool | None,
|
||||||
|
*,
|
||||||
|
spacing: float | ArrayLike | None = None,
|
||||||
|
set_rotation: float | None = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> Self:
|
||||||
|
"""
|
||||||
|
`mpath` is a superset of `path` and `path_to` which can act on bundles or buses
|
||||||
|
of "wires or "waveguides".
|
||||||
|
|
||||||
|
The wires will travel so that the output ports will be placed at well-defined
|
||||||
|
locations along the axis of their input ports, but may have arbitrary (tool-
|
||||||
|
dependent) offsets in the perpendicular direction.
|
||||||
|
|
||||||
|
If `ccw` is not `None`, the wire bundle will turn 90 degres in either the
|
||||||
|
clockwise (`ccw=False`) or counter-clockwise (`ccw=True`) direction. Within the
|
||||||
|
bundle, the center-to-center wire spacings after the turn are set by `spacing`,
|
||||||
|
which is required when `ccw` is not `None`. The final position of bundle as a
|
||||||
|
whole can be set in a number of ways:
|
||||||
|
|
||||||
|
=A>---------------------------V turn direction: `ccw=False`
|
||||||
|
=B>-------------V |
|
||||||
|
=C>-----------------------V |
|
||||||
|
=D=>----------------V |
|
||||||
|
|
|
||||||
|
|
||||||
|
x---x---x---x `spacing` (can be scalar or array)
|
||||||
|
|
||||||
|
<--------------> `emin=`
|
||||||
|
<------> `bound_type='min_past_furthest', bound=`
|
||||||
|
<--------------------------------> `emax=`
|
||||||
|
x `pmin=`
|
||||||
|
x `pmax=`
|
||||||
|
|
||||||
|
- `emin=`, equivalent to `bound_type='min_extension', bound=`
|
||||||
|
The total extension value for the furthest-out port (B in the diagram).
|
||||||
|
- `emax=`, equivalent to `bound_type='max_extension', bound=`:
|
||||||
|
The total extension value for the closest-in port (C in the diagram).
|
||||||
|
- `pmin=`, equivalent to `xmin=`, `ymin=`, or `bound_type='min_position', bound=`:
|
||||||
|
The coordinate of the innermost bend (D's bend).
|
||||||
|
The x/y versions throw an error if they do not match the port axis (for debug)
|
||||||
|
- `pmax=`, `xmax=`, `ymax=`, or `bound_type='max_position', bound=`:
|
||||||
|
The coordinate of the outermost bend (A's bend).
|
||||||
|
The x/y versions throw an error if they do not match the port axis (for debug)
|
||||||
|
- `bound_type='min_past_furthest', bound=`:
|
||||||
|
The distance between furthest out-port (B) and the innermost bend (D's bend).
|
||||||
|
|
||||||
|
If `ccw=None`, final output positions (along the input axis) of all wires will be
|
||||||
|
identical (i.e. wires will all be cut off evenly). In this case, `spacing=None` is
|
||||||
|
required. In this case, `emin=` and `emax=` are equivalent to each other, and
|
||||||
|
`pmin=`, `pmax=`, `xmin=`, etc. are also equivalent to each other.
|
||||||
|
|
||||||
|
If using `RenderPather`, `RenderPather.render` must be called after all paths have been fully planned.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
portspec: The names of the ports which are to be routed.
|
||||||
|
ccw: If `None`, the outputs should be along the same axis as the inputs.
|
||||||
|
Otherwise, cast to bool and turn 90 degrees counterclockwise if `True`
|
||||||
|
and clockwise otherwise.
|
||||||
|
spacing: Center-to-center distance between output ports along the input port's axis.
|
||||||
|
Must be provided if (and only if) `ccw` is not `None`.
|
||||||
|
set_rotation: If the provided ports have `rotation=None`, this can be used
|
||||||
|
to set a rotation for them.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
BuildError if the implied length for any wire is too close to fit the bend
|
||||||
|
(if a bend is requested).
|
||||||
|
BuildError if `xmin`/`xmax` or `ymin`/`ymax` is specified but does not
|
||||||
|
match the axis of `portspec`.
|
||||||
|
BuildError if an incorrect bound type or spacing is specified.
|
||||||
|
"""
|
||||||
|
if self._dead:
|
||||||
|
logger.error('Skipping mpath() since device is dead')
|
||||||
|
return self
|
||||||
|
|
||||||
|
bound_types = set()
|
||||||
|
if 'bound_type' in kwargs:
|
||||||
|
bound_types.add(kwargs.pop('bound_type'))
|
||||||
|
bound = kwargs.pop('bound')
|
||||||
|
for bt in ('emin', 'emax', 'pmin', 'pmax', 'xmin', 'xmax', 'ymin', 'ymax', 'min_past_furthest'):
|
||||||
|
if bt in kwargs:
|
||||||
|
bound_types.add(bt)
|
||||||
|
bound = kwargs.pop(bt)
|
||||||
|
|
||||||
|
if not bound_types:
|
||||||
|
raise BuildError('No bound type specified for mpath')
|
||||||
|
if len(bound_types) > 1:
|
||||||
|
raise BuildError(f'Too many bound types specified for mpath: {bound_types}')
|
||||||
|
bound_type = tuple(bound_types)[0]
|
||||||
|
|
||||||
|
if isinstance(portspec, str):
|
||||||
|
portspec = [portspec]
|
||||||
|
ports = self.pattern[tuple(portspec)]
|
||||||
|
|
||||||
|
extensions = ell(ports, ccw, spacing=spacing, bound=bound, bound_type=bound_type, set_rotation=set_rotation)
|
||||||
|
|
||||||
|
#if container:
|
||||||
|
# assert not getattr(self, 'render'), 'Containers not implemented for RenderPather'
|
||||||
|
# bld = self.interface(source=ports, library=self.library, tools=self.tools)
|
||||||
|
# for port_name, length in extensions.items():
|
||||||
|
# bld.path(port_name, ccw, length, **kwargs)
|
||||||
|
# self.library[container] = bld.pattern
|
||||||
|
# self.plug(Abstract(container, bld.pattern.ports), {sp: 'in_' + sp for sp in ports}) # TODO safe to use 'in_'?
|
||||||
|
#else:
|
||||||
|
for port_name, length in extensions.items():
|
||||||
|
self.path(port_name, ccw, length, **kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
|
# 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 path(self, *args, **kwargs) -> Self:
|
||||||
|
if len(self.ports) > 1:
|
||||||
|
logger.warning('Use path_each() when pathing multiple ports independently')
|
||||||
|
for port in self.ports:
|
||||||
|
self.pather.path(port, *args, **kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def path_each(self, *args, **kwargs) -> Self:
|
||||||
|
for port in self.ports:
|
||||||
|
self.pather.path(port, *args, **kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def pathS(self, *args, **kwargs) -> Self:
|
||||||
|
if len(self.ports) > 1:
|
||||||
|
logger.warning('Use pathS_each() when pathing multiple ports independently')
|
||||||
|
for port in self.ports:
|
||||||
|
self.pather.pathS(port, *args, **kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def pathS_each(self, *args, **kwargs) -> Self:
|
||||||
|
for port in self.ports:
|
||||||
|
self.pather.pathS(port, *args, **kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def path_to(self, *args, **kwargs) -> Self:
|
||||||
|
if len(self.ports) > 1:
|
||||||
|
logger.warning('Use path_each_to() when pathing multiple ports independently')
|
||||||
|
for port in self.ports:
|
||||||
|
self.pather.path_to(port, *args, **kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def path_each_to(self, *args, **kwargs) -> Self:
|
||||||
|
for port in self.ports:
|
||||||
|
self.pather.path_to(port, *args, **kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def mpath(self, *args, **kwargs) -> Self:
|
||||||
|
self.pather.mpath(self.ports, *args, **kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def path_into(self, *args, **kwargs) -> Self:
|
||||||
|
""" Path_into, using the current port as the source """
|
||||||
|
if len(self.ports) > 1:
|
||||||
|
raise BuildError(f'Unable use implicit path_into() with {len(self.ports)} (>1) ports.')
|
||||||
|
self.pather.path_into(self.ports[0], *args, **kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def path_from(self, *args, **kwargs) -> Self:
|
||||||
|
""" Path_into, using the current port as the destination """
|
||||||
|
if len(self.ports) > 1:
|
||||||
|
raise BuildError(f'Unable use implicit path_from() with {len(self.ports)} (>1) ports.')
|
||||||
|
thru = kwargs.pop('thru', None)
|
||||||
|
self.pather.path_into(args[0], self.ports[0], *args[1:], **kwargs)
|
||||||
|
if thru is not None:
|
||||||
|
self.rename_from(thru)
|
||||||
|
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) -> Self:
|
||||||
|
if len(self.ports) > 1:
|
||||||
|
raise BuildError(f'Unable use implicit plugged() with {len(self.ports)} (>1) ports.')
|
||||||
|
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[port].set_ptype(ptype)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def translate(self, *args, **kwargs) -> Self:
|
||||||
|
for port in self.ports:
|
||||||
|
self.pather[port].translate(*args, **kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def mirror(self, *args, **kwargs) -> Self:
|
||||||
|
for port in self.ports:
|
||||||
|
self.pather[port].mirror(*args, **kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def rotate(self, rotation: float) -> Self:
|
||||||
|
for port in self.ports:
|
||||||
|
self.pather[port].rotate(rotation)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def set_rotation(self, rotation: float | None) -> Self:
|
||||||
|
for port in self.ports:
|
||||||
|
self.pather[port].set_rotation(rotation)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def rename_to(self, new_name: str) -> Self:
|
||||||
|
if len(self.ports) > 1:
|
||||||
|
BuildError('Use rename_ports() for >1 port')
|
||||||
|
self.pather.rename_ports({self.ports[0]: new_name})
|
||||||
|
self.ports[0] = new_name
|
||||||
|
return self
|
||||||
|
|
||||||
|
def rename_from(self, old_name: str) -> Self:
|
||||||
|
if len(self.ports) > 1:
|
||||||
|
BuildError('Use rename_ports() for >1 port')
|
||||||
|
self.pather.rename_ports({old_name: self.ports[0]})
|
||||||
|
return self
|
||||||
|
|
||||||
|
def rename_ports(self, name_map: dict[str, str | None]) -> Self:
|
||||||
|
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 add_ports(self, ports: Iterable[str]) -> Self:
|
||||||
|
ports = list(ports)
|
||||||
|
conflicts = set(ports) & set(self.ports)
|
||||||
|
if conflicts:
|
||||||
|
raise BuildError(f'ports {conflicts} already selected')
|
||||||
|
self.ports += ports
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_port(self, port: str, index: int | None = None) -> Self:
|
||||||
|
if port in self.ports:
|
||||||
|
raise BuildError(f'{port=} already selected')
|
||||||
|
if index is not None:
|
||||||
|
self.ports.insert(index, port)
|
||||||
|
else:
|
||||||
|
self.ports.append(port)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def drop_port(self, port: str) -> Self:
|
||||||
|
if port not in self.ports:
|
||||||
|
raise BuildError(f'{port=} already not selected')
|
||||||
|
self.ports = [pp for pp in self.ports if pp != port]
|
||||||
|
return self
|
||||||
|
|
||||||
|
def into_copy(self, new_name: str, src: str | None = None) -> Self:
|
||||||
|
""" Copy a port and replace it with the copy """
|
||||||
|
if not self.ports:
|
||||||
|
raise BuildError('Have no ports to copy')
|
||||||
|
if len(self.ports) == 1:
|
||||||
|
src = self.ports[0]
|
||||||
|
elif src is None:
|
||||||
|
raise BuildError('Must specify src when >1 port is available')
|
||||||
|
if src not in self.ports:
|
||||||
|
raise BuildError(f'{src=} not available')
|
||||||
|
self.pather.ports[new_name] = self.pather[src].copy()
|
||||||
|
self.ports = [(new_name if pp == src else pp) for pp in self.ports]
|
||||||
|
return self
|
||||||
|
|
||||||
|
def save_copy(self, new_name: str, src: str | None = None) -> Self:
|
||||||
|
""" Copy a port and but keep using the original """
|
||||||
|
if not self.ports:
|
||||||
|
raise BuildError('Have no ports to copy')
|
||||||
|
if len(self.ports) == 1:
|
||||||
|
src = self.ports[0]
|
||||||
|
elif src is None:
|
||||||
|
raise BuildError('Must specify src when >1 port is available')
|
||||||
|
if src not in self.ports:
|
||||||
|
raise BuildError(f'{src=} not available')
|
||||||
|
self.pather.ports[new_name] = self.pather[src].copy()
|
||||||
|
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:
|
||||||
|
for pp in self.ports:
|
||||||
|
del self.pather.ports[pp]
|
||||||
|
return None
|
||||||
|
del self.pather.ports[name]
|
||||||
|
self.ports = [pp for pp in self.ports if pp != name]
|
||||||
|
return self
|
||||||
|
|
||||||
@ -2,30 +2,30 @@
|
|||||||
Pather with batched (multi-step) rendering
|
Pather with batched (multi-step) rendering
|
||||||
"""
|
"""
|
||||||
from typing import Self
|
from typing import Self
|
||||||
from collections.abc import Sequence, Mapping, MutableMapping
|
from collections.abc import Sequence, Mapping, MutableMapping, Iterable
|
||||||
import copy
|
import copy
|
||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from functools import wraps
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
|
|
||||||
import numpy
|
|
||||||
from numpy import pi
|
from numpy import pi
|
||||||
from numpy.typing import ArrayLike
|
from numpy.typing import ArrayLike
|
||||||
|
|
||||||
from ..pattern import Pattern
|
from ..pattern import Pattern
|
||||||
from ..library import ILibrary
|
from ..library import ILibrary, TreeView
|
||||||
from ..error import PortError, BuildError
|
from ..error import BuildError
|
||||||
from ..ports import PortList, Port
|
from ..ports import PortList, Port
|
||||||
from ..abstract import Abstract
|
from ..abstract import Abstract
|
||||||
from ..utils import SupportsBool
|
from ..utils import SupportsBool
|
||||||
from .tools import Tool, RenderStep
|
from .tools import Tool, RenderStep
|
||||||
from .utils import ell
|
from .pather_mixin import PatherMixin
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class RenderPather(PortList):
|
class RenderPather(PatherMixin):
|
||||||
"""
|
"""
|
||||||
`RenderPather` is an alternative to `Pather` which uses the `path`/`path_to`/`mpath`
|
`RenderPather` is an alternative to `Pather` which uses the `path`/`path_to`/`mpath`
|
||||||
functions to plan out wire paths without incrementally generating the layout. Instead,
|
functions to plan out wire paths without incrementally generating the layout. Instead,
|
||||||
@ -108,15 +108,11 @@ class RenderPather(PortList):
|
|||||||
if self.pattern.ports:
|
if self.pattern.ports:
|
||||||
raise BuildError('Ports supplied for pattern with pre-existing ports!')
|
raise BuildError('Ports supplied for pattern with pre-existing ports!')
|
||||||
if isinstance(ports, str):
|
if isinstance(ports, str):
|
||||||
if library is None:
|
|
||||||
raise BuildError('Ports given as a string, but `library` was `None`!')
|
|
||||||
ports = library.abstract(ports).ports
|
ports = library.abstract(ports).ports
|
||||||
|
|
||||||
self.pattern.ports.update(copy.deepcopy(dict(ports)))
|
self.pattern.ports.update(copy.deepcopy(dict(ports)))
|
||||||
|
|
||||||
if name is not None:
|
if name is not None:
|
||||||
if library is None:
|
|
||||||
raise BuildError('Name was supplied, but no library was given!')
|
|
||||||
library[name] = self.pattern
|
library[name] = self.pattern
|
||||||
|
|
||||||
if tools is None:
|
if tools is None:
|
||||||
@ -186,16 +182,21 @@ class RenderPather(PortList):
|
|||||||
new = RenderPather(library=library, pattern=pat, name=name, tools=tools)
|
new = RenderPather(library=library, pattern=pat, name=name, tools=tools)
|
||||||
return new
|
return new
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
s = f'<RenderPather {self.pattern} L({len(self.library)}) {pformat(self.tools)}>'
|
||||||
|
return s
|
||||||
|
|
||||||
def plug(
|
def plug(
|
||||||
self,
|
self,
|
||||||
other: Abstract | str,
|
other: Abstract | str | Pattern | TreeView,
|
||||||
map_in: dict[str, str],
|
map_in: dict[str, str],
|
||||||
map_out: dict[str, str | None] | None = None,
|
map_out: dict[str, str | None] | None = None,
|
||||||
*,
|
*,
|
||||||
mirrored: bool = False,
|
mirrored: bool = False,
|
||||||
inherit_name: bool = True,
|
thru: bool | str = True,
|
||||||
set_rotation: bool | None = None,
|
set_rotation: bool | None = None,
|
||||||
append: bool = False,
|
append: bool = False,
|
||||||
|
ok_connections: Iterable[tuple[str, str]] = (),
|
||||||
) -> Self:
|
) -> Self:
|
||||||
"""
|
"""
|
||||||
Wrapper for `Pattern.plug` which adds a `RenderStep` with opcode 'P'
|
Wrapper for `Pattern.plug` which adds a `RenderStep` with opcode 'P'
|
||||||
@ -210,11 +211,15 @@ class RenderPather(PortList):
|
|||||||
new names for ports in `other`.
|
new names for ports in `other`.
|
||||||
mirrored: Enables mirroring `other` across the x axis prior to
|
mirrored: Enables mirroring `other` across the x axis prior to
|
||||||
connecting any ports.
|
connecting any ports.
|
||||||
inherit_name: If `True`, and `map_in` specifies only a single port,
|
thru: If map_in specifies only a single port, `thru` provides a mechainsm
|
||||||
and `map_out` is `None`, and `other` has only two ports total,
|
to avoid repeating the port name. Eg, for `map_in={'myport': 'A'}`,
|
||||||
then automatically renames the output port of `other` to the
|
- If True (default), and `other` has only two ports total, and map_out
|
||||||
name of the port from `self` that appears in `map_in`. This
|
doesn't specify a name for the other port, its name is set to the key
|
||||||
makes it easy to extend a device with simple 2-port devices
|
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
|
(e.g. wires) without providing `map_out` each time `plug` is
|
||||||
called. See "Examples" above for more info. Default `True`.
|
called. See "Examples" above for more info. Default `True`.
|
||||||
set_rotation: If the necessary rotation cannot be determined from
|
set_rotation: If the necessary rotation cannot be determined from
|
||||||
@ -225,6 +230,12 @@ class RenderPather(PortList):
|
|||||||
append: If `True`, `other` is appended instead of being referenced.
|
append: If `True`, `other` is appended instead of being referenced.
|
||||||
Note that this does not flatten `other`, so its refs will still
|
Note that this does not flatten `other`, so its refs will still
|
||||||
be refs (now inside `self`).
|
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:
|
Returns:
|
||||||
self
|
self
|
||||||
@ -261,13 +272,14 @@ class RenderPather(PortList):
|
|||||||
self.paths[new_name].append(RenderStep('P', None, port.copy(), port.copy(), None))
|
self.paths[new_name].append(RenderStep('P', None, port.copy(), port.copy(), None))
|
||||||
|
|
||||||
self.pattern.plug(
|
self.pattern.plug(
|
||||||
other=other_tgt,
|
other = other_tgt,
|
||||||
map_in=map_in,
|
map_in = map_in,
|
||||||
map_out=map_out,
|
map_out = map_out,
|
||||||
mirrored=mirrored,
|
mirrored = mirrored,
|
||||||
inherit_name=inherit_name,
|
thru = thru,
|
||||||
set_rotation=set_rotation,
|
set_rotation = set_rotation,
|
||||||
append=append,
|
append = append,
|
||||||
|
ok_connections = ok_connections,
|
||||||
)
|
)
|
||||||
|
|
||||||
return self
|
return self
|
||||||
@ -333,40 +345,28 @@ class RenderPather(PortList):
|
|||||||
self.paths[new_name].append(RenderStep('P', None, port.copy(), port.copy(), None))
|
self.paths[new_name].append(RenderStep('P', None, port.copy(), port.copy(), None))
|
||||||
|
|
||||||
self.pattern.place(
|
self.pattern.place(
|
||||||
other=other_tgt,
|
other = other_tgt,
|
||||||
offset=offset,
|
offset = offset,
|
||||||
rotation=rotation,
|
rotation = rotation,
|
||||||
pivot=pivot,
|
pivot = pivot,
|
||||||
mirrored=mirrored,
|
mirrored = mirrored,
|
||||||
port_map=port_map,
|
port_map = port_map,
|
||||||
skip_port_check=skip_port_check,
|
skip_port_check = skip_port_check,
|
||||||
append=append,
|
append = append,
|
||||||
)
|
)
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def retool(
|
def plugged(
|
||||||
self,
|
self,
|
||||||
tool: Tool,
|
connections: dict[str, str],
|
||||||
keys: str | Sequence[str | None] | None = None,
|
|
||||||
) -> Self:
|
) -> Self:
|
||||||
"""
|
for aa, bb in connections.items():
|
||||||
Update the `Tool` which will be used when generating `Pattern`s for the ports
|
porta = self.ports[aa]
|
||||||
given by `keys`.
|
portb = self.ports[bb]
|
||||||
|
self.paths[aa].append(RenderStep('P', None, porta.copy(), porta.copy(), None))
|
||||||
Args:
|
self.paths[bb].append(RenderStep('P', None, portb.copy(), portb.copy(), None))
|
||||||
tool: The new `Tool` to use for the given ports.
|
PortList.plugged(self, connections)
|
||||||
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
|
return self
|
||||||
|
|
||||||
def path(
|
def path(
|
||||||
@ -374,6 +374,8 @@ class RenderPather(PortList):
|
|||||||
portspec: str,
|
portspec: str,
|
||||||
ccw: SupportsBool | None,
|
ccw: SupportsBool | None,
|
||||||
length: float,
|
length: float,
|
||||||
|
*,
|
||||||
|
plug_into: str | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
"""
|
"""
|
||||||
@ -393,6 +395,8 @@ class RenderPather(PortList):
|
|||||||
and clockwise otherwise.
|
and clockwise otherwise.
|
||||||
length: The total distance from input to output, along the input's axis only.
|
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.)
|
(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:
|
Returns:
|
||||||
self
|
self
|
||||||
@ -423,163 +427,87 @@ class RenderPather(PortList):
|
|||||||
|
|
||||||
self.pattern.ports[portspec] = out_port.copy()
|
self.pattern.ports[portspec] = out_port.copy()
|
||||||
|
|
||||||
|
if plug_into is not None:
|
||||||
|
self.plugged({portspec: plug_into})
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def path_to(
|
def pathS(
|
||||||
self,
|
self,
|
||||||
portspec: str,
|
portspec: str,
|
||||||
ccw: SupportsBool | None,
|
length: float,
|
||||||
position: float | None = None,
|
jog: float,
|
||||||
*,
|
*,
|
||||||
x: float | None = None,
|
plug_into: str | None = None,
|
||||||
y: float | None = None,
|
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
"""
|
"""
|
||||||
Plan a "wire"/"waveguide" extending from the port `portspec`, with the aim
|
Create an S-shaped "wire"/"waveguide" and `plug` it into the port `portspec`, with the aim
|
||||||
of ending exactly at a target position.
|
of traveling exactly `length` distance with an offset `jog` along the other axis (+ve jog is
|
||||||
|
left of direction of travel).
|
||||||
|
|
||||||
The wire will travel so that the output port will be placed at exactly the target
|
The output port will have the same orientation as the source port (`portspec`).
|
||||||
position along the input port's axis. There can be an unspecified (tool-dependent)
|
|
||||||
offset 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.
|
`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:
|
Args:
|
||||||
portspec: The name of the port into which the wire will be plugged.
|
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.
|
jog: Total manhattan distance perpendicular to the direction of travel.
|
||||||
Otherwise, cast to bool and turn counterclockwise if True
|
Positive values are to the left of the direction of travel.
|
||||||
and clockwise otherwise.
|
length: The total manhattan distance from input to output, along the input's axis only.
|
||||||
position: The final port position, along the input's axis only.
|
|
||||||
(There may be a tool-dependent offset along the other axis.)
|
(There may be a tool-dependent offset along the other axis.)
|
||||||
Only one of `position`, `x`, and `y` may be specified.
|
plug_into: If not None, attempts to plug the wire's output port into the provided
|
||||||
x: The final port position along the x axis.
|
port on `self`.
|
||||||
`portspec` must refer to a horizontal port if `x` is passed, otherwise a
|
|
||||||
BuildError will be raised.
|
|
||||||
y: The final port position along the y axis.
|
|
||||||
`portspec` must refer to a vertical port if `y` is passed, otherwise a
|
|
||||||
BuildError will be raised.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
BuildError if `position`, `x`, or `y` is too close to fit the bend (if a bend
|
BuildError if `distance` is too small to fit the s-bend (for nonzero jog).
|
||||||
is present).
|
LibraryError if no valid name could be picked for the pattern.
|
||||||
BuildError if `x` or `y` is specified but does not match the axis of `portspec`.
|
|
||||||
BuildError if more than one of `x`, `y`, and `position` is specified.
|
|
||||||
"""
|
"""
|
||||||
if self._dead:
|
if self._dead:
|
||||||
logger.error('Skipping path_to() since device is dead')
|
logger.error('Skipping pathS() since device is dead')
|
||||||
return self
|
return self
|
||||||
|
|
||||||
pos_count = sum(vv is not None for vv in (position, x, y))
|
|
||||||
if pos_count > 1:
|
|
||||||
raise BuildError('Only one of `position`, `x`, and `y` may be specified at once')
|
|
||||||
if pos_count < 1:
|
|
||||||
raise BuildError('One of `position`, `x`, and `y` must be specified')
|
|
||||||
|
|
||||||
port = self.pattern[portspec]
|
port = self.pattern[portspec]
|
||||||
if port.rotation is None:
|
in_ptype = port.ptype
|
||||||
raise PortError(f'Port {portspec} has no rotation and cannot be used for path_to()')
|
port_rot = port.rotation
|
||||||
|
assert port_rot is not None # TODO allow manually setting rotation for RenderPather.path()?
|
||||||
|
|
||||||
if not numpy.isclose(port.rotation % (pi / 2), 0):
|
tool = self.tools.get(portspec, self.tools[None])
|
||||||
raise BuildError('path_to was asked to route from non-manhattan port')
|
|
||||||
|
|
||||||
is_horizontal = numpy.isclose(port.rotation % pi, 0)
|
# check feasibility, get output port and data
|
||||||
if is_horizontal:
|
try:
|
||||||
if y is not None:
|
out_port, data = tool.planS(length, jog, in_ptype=in_ptype, **kwargs)
|
||||||
raise BuildError('Asked to path to y-coordinate, but port is horizontal')
|
except NotImplementedError:
|
||||||
if position is None:
|
# Fall back to drawing two L-bends
|
||||||
position = x
|
ccw0 = jog > 0
|
||||||
else:
|
kwargs_no_out = (kwargs | {'out_ptype': None})
|
||||||
if x is not None:
|
t_port0, _ = tool.planL( ccw0, length / 2, in_ptype=in_ptype, **kwargs_no_out)
|
||||||
raise BuildError('Asked to path to x-coordinate, but port is vertical')
|
jog0 = Port((0, 0), 0).measure_travel(t_port0)[0][1]
|
||||||
if position is None:
|
t_port1, _ = tool.planL(not ccw0, abs(jog - jog0), in_ptype=t_port0.ptype, **kwargs)
|
||||||
position = y
|
jog1 = Port((0, 0), 0).measure_travel(t_port1)[0][1]
|
||||||
|
|
||||||
x0, y0 = port.offset
|
kwargs_plug = kwargs | {'plug_into': plug_into}
|
||||||
if is_horizontal:
|
self.path(portspec, ccw0, length - abs(jog1), **kwargs_no_out)
|
||||||
if numpy.sign(numpy.cos(port.rotation)) == numpy.sign(position - x0):
|
self.path(portspec, not ccw0, abs(jog - jog0), **kwargs_plug)
|
||||||
raise BuildError(f'path_to routing to behind source port: x0={x0:g} to {position:g}')
|
|
||||||
length = numpy.abs(position - x0)
|
|
||||||
else:
|
|
||||||
if numpy.sign(numpy.sin(port.rotation)) == numpy.sign(position - y0):
|
|
||||||
raise BuildError(f'path_to routing to behind source port: y0={y0:g} to {position:g}')
|
|
||||||
length = numpy.abs(position - y0)
|
|
||||||
|
|
||||||
return self.path(portspec, ccw, length, **kwargs)
|
|
||||||
|
|
||||||
def mpath(
|
|
||||||
self,
|
|
||||||
portspec: str | Sequence[str],
|
|
||||||
ccw: SupportsBool | None,
|
|
||||||
*,
|
|
||||||
spacing: float | ArrayLike | None = None,
|
|
||||||
set_rotation: float | None = None,
|
|
||||||
**kwargs,
|
|
||||||
) -> Self:
|
|
||||||
"""
|
|
||||||
`mpath` is a superset of `path` and `path_to` which can act on bundles or buses
|
|
||||||
of "wires or "waveguides".
|
|
||||||
|
|
||||||
See `Pather.mpath` for details.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
portspec: The names of the ports which are to be routed.
|
|
||||||
ccw: If `None`, the outputs should be along the same axis as the inputs.
|
|
||||||
Otherwise, cast to bool and turn 90 degrees counterclockwise if `True`
|
|
||||||
and clockwise otherwise.
|
|
||||||
spacing: Center-to-center distance between output ports along the input port's axis.
|
|
||||||
Must be provided if (and only if) `ccw` is not `None`.
|
|
||||||
set_rotation: If the provided ports have `rotation=None`, this can be used
|
|
||||||
to set a rotation for them.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
BuildError if the implied length for any wire is too close to fit the bend
|
|
||||||
(if a bend is requested).
|
|
||||||
BuildError if `xmin`/`xmax` or `ymin`/`ymax` is specified but does not
|
|
||||||
match the axis of `portspec`.
|
|
||||||
BuildError if an incorrect bound type or spacing is specified.
|
|
||||||
"""
|
|
||||||
if self._dead:
|
|
||||||
logger.error('Skipping mpath() since device is dead')
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
bound_types = set()
|
out_port.rotate_around((0, 0), pi + port_rot)
|
||||||
if 'bound_type' in kwargs:
|
out_port.translate(port.offset)
|
||||||
bound_types.add(kwargs['bound_type'])
|
step = RenderStep('S', tool, port.copy(), out_port.copy(), data)
|
||||||
bound = kwargs['bound']
|
self.paths[portspec].append(step)
|
||||||
for bt in ('emin', 'emax', 'pmin', 'pmax', 'xmin', 'xmax', 'ymin', 'ymax', 'min_past_furthest'):
|
self.pattern.ports[portspec] = out_port.copy()
|
||||||
if bt in kwargs:
|
|
||||||
bound_types.add(bt)
|
|
||||||
bound = kwargs[bt]
|
|
||||||
|
|
||||||
if not bound_types:
|
if plug_into is not None:
|
||||||
raise BuildError('No bound type specified for mpath')
|
self.plugged({portspec: plug_into})
|
||||||
if len(bound_types) > 1:
|
|
||||||
raise BuildError(f'Too many bound types specified for mpath: {bound_types}')
|
|
||||||
bound_type = tuple(bound_types)[0]
|
|
||||||
|
|
||||||
if isinstance(portspec, str):
|
|
||||||
portspec = [portspec]
|
|
||||||
ports = self.pattern[tuple(portspec)]
|
|
||||||
|
|
||||||
extensions = ell(ports, ccw, spacing=spacing, bound=bound, bound_type=bound_type, set_rotation=set_rotation)
|
|
||||||
|
|
||||||
if len(ports) == 1:
|
|
||||||
# Not a bus, so having a container just adds noise to the layout
|
|
||||||
port_name = tuple(portspec)[0]
|
|
||||||
self.path(port_name, ccw, extensions[port_name])
|
|
||||||
else:
|
|
||||||
for port_name, length in extensions.items():
|
|
||||||
self.path(port_name, ccw, length)
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
def render(
|
def render(
|
||||||
self,
|
self,
|
||||||
append: bool = True,
|
append: bool = True,
|
||||||
@ -696,8 +624,23 @@ class RenderPather(PortList):
|
|||||||
self._dead = True
|
self._dead = True
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
@wraps(Pattern.label)
|
||||||
s = f'<Pather {self.pattern} L({len(self.library)}) {pformat(self.tools)}>'
|
def label(self, *args, **kwargs) -> Self:
|
||||||
return s
|
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
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,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
|
from typing import Literal, Any, Self
|
||||||
from collections.abc import Sequence, Callable
|
from collections.abc import Sequence, Callable
|
||||||
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
|
||||||
@ -70,7 +70,7 @@ class Tool:
|
|||||||
Create a wire or waveguide that travels exactly `length` distance along the axis
|
Create a wire or waveguide that travels exactly `length` distance along the axis
|
||||||
of its input port.
|
of its input port.
|
||||||
|
|
||||||
Used by `Pather`.
|
Used by `Pather` and `RenderPather`.
|
||||||
|
|
||||||
The output port must be exactly `length` away along the input port's axis, but
|
The output port must be exactly `length` away along the input port's axis, but
|
||||||
may be placed an additional (unspecified) distance away along the perpendicular
|
may be placed an additional (unspecified) distance away along the perpendicular
|
||||||
@ -101,6 +101,48 @@ class Tool:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError(f'path() not implemented for {type(self)}')
|
raise NotImplementedError(f'path() not implemented for {type(self)}')
|
||||||
|
|
||||||
|
def pathS(
|
||||||
|
self,
|
||||||
|
length: float,
|
||||||
|
jog: float,
|
||||||
|
*,
|
||||||
|
in_ptype: str | None = None,
|
||||||
|
out_ptype: str | None = None,
|
||||||
|
port_names: tuple[str, str] = ('A', 'B'),
|
||||||
|
**kwargs,
|
||||||
|
) -> Library:
|
||||||
|
"""
|
||||||
|
Create a wire or waveguide that travels exactly `length` distance along the axis
|
||||||
|
of its input port, and `jog` distance on the perpendicular axis.
|
||||||
|
`jog` is positive when moving left of the direction of travel (from input to ouput port).
|
||||||
|
|
||||||
|
Used by `Pather` and `RenderPather`.
|
||||||
|
|
||||||
|
The output port should be rotated to face the input port (i.e. plugging the device
|
||||||
|
into a port will move that port but keep its orientation).
|
||||||
|
|
||||||
|
The input and output ports should be compatible with `in_ptype` and
|
||||||
|
`out_ptype`, respectively. They should also be named `port_names[0]` and
|
||||||
|
`port_names[1]`, respectively.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
length: The total distance from input to output, along the input's axis only.
|
||||||
|
jog: The total distance from input to output, along the second axis. Positive indicates
|
||||||
|
a leftward shift when moving from input to output port.
|
||||||
|
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.
|
||||||
|
port_names: The output pattern will have its input port named `port_names[0]` and
|
||||||
|
its output named `port_names[1]`.
|
||||||
|
kwargs: Custom tool-specific parameters.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A pattern tree containing the requested S-shaped (or straight) wire or waveguide
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
BuildError if an impossible or unsupported geometry is requested.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError(f'path() not implemented for {type(self)}')
|
||||||
|
|
||||||
def planL(
|
def planL(
|
||||||
self,
|
self,
|
||||||
ccw: SupportsBool | None,
|
ccw: SupportsBool | None,
|
||||||
@ -135,7 +177,7 @@ class Tool:
|
|||||||
kwargs: Custom tool-specific parameters.
|
kwargs: Custom tool-specific parameters.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The calculated output `Port` for the wire.
|
The calculated output `Port` for the wire, assuming an input port at (0, 0) with rotation 0.
|
||||||
Any tool-specifc data, to be stored in `RenderStep.data`, for use during rendering.
|
Any tool-specifc data, to be stored in `RenderStep.data`, for use during rendering.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
@ -173,7 +215,7 @@ class Tool:
|
|||||||
kwargs: Custom tool-specific parameters.
|
kwargs: Custom tool-specific parameters.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The calculated output `Port` for the wire.
|
The calculated output `Port` for the wire, assuming an input port at (0, 0) with rotation 0.
|
||||||
Any tool-specifc data, to be stored in `RenderStep.data`, for use during rendering.
|
Any tool-specifc data, to be stored in `RenderStep.data`, for use during rendering.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
@ -204,14 +246,14 @@ 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 rightwards shift (i.e. clockwise bend followed
|
A positive number implies a leftwards shift (i.e. counterclockwise bend
|
||||||
by a counterclockwise 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.
|
||||||
kwargs: Custom tool-specific parameters.
|
kwargs: Custom tool-specific parameters.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The calculated output `Port` for the wire.
|
The calculated output `Port` for the wire, assuming an input port at (0, 0) with rotation 0.
|
||||||
Any tool-specifc data, to be stored in `RenderStep.data`, for use during rendering.
|
Any tool-specifc data, to be stored in `RenderStep.data`, for use during rendering.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
@ -223,7 +265,7 @@ class Tool:
|
|||||||
self,
|
self,
|
||||||
batch: Sequence[RenderStep],
|
batch: Sequence[RenderStep],
|
||||||
*,
|
*,
|
||||||
port_names: Sequence[str] = ('A', 'B'), # noqa: ARG002 (unused)
|
port_names: tuple[str, str] = ('A', 'B'), # noqa: ARG002 (unused)
|
||||||
**kwargs, # noqa: ARG002 (unused)
|
**kwargs, # noqa: ARG002 (unused)
|
||||||
) -> ILibrary:
|
) -> ILibrary:
|
||||||
"""
|
"""
|
||||||
@ -245,77 +287,40 @@ abstract_tuple_t = tuple[Abstract, str, str]
|
|||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class BasicTool(Tool, metaclass=ABCMeta):
|
class SimpleTool(Tool, metaclass=ABCMeta):
|
||||||
"""
|
"""
|
||||||
A simple tool which relies on a single pre-rendered `bend` pattern, a function
|
A simple tool which relies on a single pre-rendered `bend` pattern, a function
|
||||||
for generating straight paths, and a table of pre-rendered `transitions` for converting
|
for generating straight paths, and a table of pre-rendered `transitions` for converting
|
||||||
from non-native ptypes.
|
from non-native ptypes.
|
||||||
"""
|
"""
|
||||||
straight: tuple[Callable[[float], Pattern], str, str]
|
straight: tuple[Callable[[float], Pattern] | Callable[[float], Library], str, str]
|
||||||
""" `create_straight(length: float), in_port_name, out_port_name` """
|
""" `create_straight(length: float), in_port_name, out_port_name` """
|
||||||
|
|
||||||
bend: abstract_tuple_t # Assumed to be clockwise
|
bend: abstract_tuple_t # Assumed to be clockwise
|
||||||
""" `clockwise_bend_abstract, in_port_name, out_port_name` """
|
""" `clockwise_bend_abstract, in_port_name, out_port_name` """
|
||||||
|
|
||||||
transitions: dict[str, abstract_tuple_t]
|
|
||||||
""" `{ptype: (transition_abstract`, ptype_port_name, other_port_name), ...}` """
|
|
||||||
|
|
||||||
default_out_ptype: str
|
default_out_ptype: str
|
||||||
""" Default value for out_ptype """
|
""" Default value for out_ptype """
|
||||||
|
|
||||||
|
mirror_bend: bool = True
|
||||||
|
""" Whether a clockwise bend should be mirrored (vs rotated) to get a ccw bend """
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class LData:
|
class LData:
|
||||||
""" Data for planL """
|
""" Data for planL """
|
||||||
straight_length: float
|
straight_length: float
|
||||||
|
straight_kwargs: dict[str, Any]
|
||||||
ccw: SupportsBool | None
|
ccw: SupportsBool | None
|
||||||
in_transition: abstract_tuple_t | None
|
|
||||||
out_transition: abstract_tuple_t | None
|
|
||||||
|
|
||||||
def path(
|
|
||||||
self,
|
|
||||||
ccw: SupportsBool | None,
|
|
||||||
length: float,
|
|
||||||
*,
|
|
||||||
in_ptype: str | None = None,
|
|
||||||
out_ptype: str | None = None,
|
|
||||||
port_names: tuple[str, str] = ('A', 'B'),
|
|
||||||
**kwargs,
|
|
||||||
) -> Library:
|
|
||||||
_out_port, data = self.planL(
|
|
||||||
ccw,
|
|
||||||
length,
|
|
||||||
in_ptype=in_ptype,
|
|
||||||
out_ptype=out_ptype,
|
|
||||||
)
|
|
||||||
|
|
||||||
gen_straight, sport_in, sport_out = self.straight
|
|
||||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path')
|
|
||||||
pat.add_port_pair(names=port_names, ptype=in_ptype)
|
|
||||||
if data.in_transition:
|
|
||||||
ipat, iport_theirs, _iport_ours = data.in_transition
|
|
||||||
pat.plug(ipat, {port_names[1]: iport_theirs})
|
|
||||||
if not numpy.isclose(data.straight_length, 0):
|
|
||||||
straight = tree <= {SINGLE_USE_PREFIX + 'straight': gen_straight(data.straight_length, **kwargs)}
|
|
||||||
pat.plug(straight, {port_names[1]: sport_in})
|
|
||||||
if data.ccw is not None:
|
|
||||||
bend, bport_in, bport_out = self.bend
|
|
||||||
pat.plug(bend, {port_names[1]: bport_in}, mirrored=bool(ccw))
|
|
||||||
if data.out_transition:
|
|
||||||
opat, oport_theirs, oport_ours = data.out_transition
|
|
||||||
pat.plug(opat, {port_names[1]: oport_ours})
|
|
||||||
|
|
||||||
return tree
|
|
||||||
|
|
||||||
def planL(
|
def planL(
|
||||||
self,
|
self,
|
||||||
ccw: SupportsBool | None,
|
ccw: SupportsBool | None,
|
||||||
length: float,
|
length: float,
|
||||||
*,
|
*,
|
||||||
in_ptype: str | None = None,
|
in_ptype: str | None = None, # noqa: ARG002 (unused)
|
||||||
out_ptype: str | None = None,
|
out_ptype: str | None = None, # noqa: ARG002 (unused)
|
||||||
**kwargs, # noqa: ARG002 (unused)
|
**kwargs, # noqa: ARG002 (unused)
|
||||||
) -> tuple[Port, LData]:
|
) -> tuple[Port, LData]:
|
||||||
# TODO check all the math for L-shaped bends
|
|
||||||
if ccw is not None:
|
if ccw is not None:
|
||||||
bend, bport_in, bport_out = self.bend
|
bend, bport_in, bport_out = self.bend
|
||||||
|
|
||||||
@ -336,87 +341,532 @@ class BasicTool(Tool, metaclass=ABCMeta):
|
|||||||
bend_angle *= -1
|
bend_angle *= -1
|
||||||
else:
|
else:
|
||||||
bend_dxy = numpy.zeros(2)
|
bend_dxy = numpy.zeros(2)
|
||||||
bend_angle = 0
|
bend_angle = pi
|
||||||
|
|
||||||
in_transition = self.transitions.get('unk' if in_ptype is None else in_ptype, None)
|
if ccw is not None:
|
||||||
if in_transition is not None:
|
|
||||||
ipat, iport_theirs, iport_ours = in_transition
|
|
||||||
irot = ipat.ports[iport_theirs].rotation
|
|
||||||
assert irot is not None
|
|
||||||
itrans_dxy = rotation_matrix_2d(-irot) @ (
|
|
||||||
ipat.ports[iport_ours].offset
|
|
||||||
- ipat.ports[iport_theirs].offset
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
itrans_dxy = numpy.zeros(2)
|
|
||||||
|
|
||||||
out_transition = self.transitions.get('unk' if out_ptype is None else out_ptype, None)
|
|
||||||
if out_transition is not None:
|
|
||||||
opat, oport_theirs, oport_ours = out_transition
|
|
||||||
orot = opat.ports[oport_ours].rotation
|
|
||||||
assert orot is not None
|
|
||||||
|
|
||||||
otrans_dxy = rotation_matrix_2d(-orot + bend_angle) @ (
|
|
||||||
opat.ports[oport_theirs].offset
|
|
||||||
- opat.ports[oport_ours].offset
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
otrans_dxy = numpy.zeros(2)
|
|
||||||
|
|
||||||
if out_transition is not None:
|
|
||||||
out_ptype_actual = opat.ports[oport_theirs].ptype
|
|
||||||
elif ccw is not None:
|
|
||||||
out_ptype_actual = bend.ports[bport_out].ptype
|
out_ptype_actual = bend.ports[bport_out].ptype
|
||||||
else:
|
else:
|
||||||
out_ptype_actual = self.default_out_ptype
|
out_ptype_actual = self.default_out_ptype
|
||||||
|
|
||||||
straight_length = length - bend_dxy[0] - itrans_dxy[0] - otrans_dxy[0]
|
straight_length = length - bend_dxy[0]
|
||||||
bend_run = bend_dxy[1] + itrans_dxy[1] + otrans_dxy[1]
|
bend_run = bend_dxy[1]
|
||||||
|
|
||||||
if straight_length < 0:
|
if straight_length < 0:
|
||||||
raise BuildError(
|
raise BuildError(
|
||||||
f'Asked to draw path with total length {length:,g}, shorter than required bends and transitions:\n'
|
f'Asked to draw L-path with total length {length:,g}, shorter than required bends ({bend_dxy[0]:,})'
|
||||||
f'bend: {bend_dxy[0]:,g} in_trans: {itrans_dxy[0]:,g} out_trans: {otrans_dxy[0]:,g}'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
data = self.LData(straight_length, ccw, in_transition, out_transition)
|
data = self.LData(straight_length, kwargs, ccw)
|
||||||
out_port = Port((length, bend_run), rotation=bend_angle, ptype=out_ptype_actual)
|
out_port = Port((length, bend_run), rotation=bend_angle, ptype=out_ptype_actual)
|
||||||
return out_port, data
|
return out_port, data
|
||||||
|
|
||||||
|
def _renderL(
|
||||||
|
self,
|
||||||
|
data: LData,
|
||||||
|
tree: ILibrary,
|
||||||
|
port_names: tuple[str, str],
|
||||||
|
straight_kwargs: dict[str, Any],
|
||||||
|
) -> ILibrary:
|
||||||
|
"""
|
||||||
|
Render an L step into a preexisting tree
|
||||||
|
"""
|
||||||
|
pat = tree.top_pattern()
|
||||||
|
gen_straight, sport_in, _sport_out = self.straight
|
||||||
|
if not numpy.isclose(data.straight_length, 0):
|
||||||
|
straight_pat_or_tree = gen_straight(data.straight_length, **(straight_kwargs | data.straight_kwargs))
|
||||||
|
pmap = {port_names[1]: sport_in}
|
||||||
|
if isinstance(straight_pat_or_tree, Pattern):
|
||||||
|
straight_pat = straight_pat_or_tree
|
||||||
|
pat.plug(straight_pat, pmap, append=True)
|
||||||
|
else:
|
||||||
|
straight_tree = straight_pat_or_tree
|
||||||
|
top = straight_tree.top()
|
||||||
|
straight_tree.flatten(top, dangling_ok=True)
|
||||||
|
pat.plug(straight_tree[top], pmap, append=True)
|
||||||
|
if data.ccw is not None:
|
||||||
|
bend, bport_in, bport_out = self.bend
|
||||||
|
mirrored = self.mirror_bend and bool(data.ccw)
|
||||||
|
inport = bport_in if (self.mirror_bend or not data.ccw) else bport_out
|
||||||
|
pat.plug(bend, {port_names[1]: inport}, mirrored=mirrored)
|
||||||
|
return tree
|
||||||
|
|
||||||
|
def path(
|
||||||
|
self,
|
||||||
|
ccw: SupportsBool | None,
|
||||||
|
length: float,
|
||||||
|
*,
|
||||||
|
in_ptype: str | None = None,
|
||||||
|
out_ptype: str | None = None,
|
||||||
|
port_names: tuple[str, str] = ('A', 'B'),
|
||||||
|
**kwargs,
|
||||||
|
) -> Library:
|
||||||
|
_out_port, data = self.planL(
|
||||||
|
ccw,
|
||||||
|
length,
|
||||||
|
in_ptype = in_ptype,
|
||||||
|
out_ptype = out_ptype,
|
||||||
|
)
|
||||||
|
|
||||||
|
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path')
|
||||||
|
pat.add_port_pair(names=port_names, ptype='unk' if in_ptype is None else in_ptype)
|
||||||
|
self._renderL(data=data, tree=tree, port_names=port_names, straight_kwargs=kwargs)
|
||||||
|
return tree
|
||||||
|
|
||||||
def render(
|
def render(
|
||||||
self,
|
self,
|
||||||
batch: Sequence[RenderStep],
|
batch: Sequence[RenderStep],
|
||||||
*,
|
*,
|
||||||
port_names: Sequence[str] = ('A', 'B'),
|
port_names: tuple[str, str] = ('A', 'B'),
|
||||||
append: bool = True,
|
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> ILibrary:
|
) -> ILibrary:
|
||||||
|
|
||||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path')
|
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path')
|
||||||
pat.add_port_pair(names=(port_names[0], port_names[1]))
|
pat.add_port_pair(names=(port_names[0], port_names[1]))
|
||||||
|
|
||||||
gen_straight, sport_in, _sport_out = self.straight
|
|
||||||
for step in batch:
|
for step in batch:
|
||||||
straight_length, ccw, in_transition, out_transition = step.data
|
|
||||||
assert step.tool == self
|
assert step.tool == self
|
||||||
|
|
||||||
if step.opcode == 'L':
|
if step.opcode == 'L':
|
||||||
if in_transition:
|
self._renderL(data=step.data, tree=tree, port_names=port_names, straight_kwargs=kwargs)
|
||||||
ipat, iport_theirs, _iport_ours = in_transition
|
return tree
|
||||||
pat.plug(ipat, {port_names[1]: iport_theirs})
|
|
||||||
if not numpy.isclose(straight_length, 0):
|
|
||||||
straight_pat = gen_straight(straight_length, **kwargs)
|
@dataclass
|
||||||
if append:
|
class AutoTool(Tool, metaclass=ABCMeta):
|
||||||
pat.plug(straight_pat, {port_names[1]: sport_in}, append=True)
|
"""
|
||||||
else:
|
A simple tool which relies on a single pre-rendered `bend` pattern, a function
|
||||||
straight = tree <= {SINGLE_USE_PREFIX + 'straight': straight_pat}
|
for generating straight paths, and a table of pre-rendered `transitions` for converting
|
||||||
pat.plug(straight, {port_names[1]: sport_in}, append=True)
|
from non-native ptypes.
|
||||||
if ccw is not None:
|
"""
|
||||||
bend, bport_in, bport_out = self.bend
|
|
||||||
pat.plug(bend, {port_names[1]: bport_in}, mirrored=bool(ccw))
|
@dataclass(frozen=True, slots=True)
|
||||||
if out_transition:
|
class Straight:
|
||||||
opat, oport_theirs, oport_ours = out_transition
|
""" Description of a straight-path generator """
|
||||||
pat.plug(opat, {port_names[1]: oport_ours})
|
ptype: str
|
||||||
|
fn: Callable[[float], Pattern] | Callable[[float], Library]
|
||||||
|
in_port_name: str
|
||||||
|
out_port_name: str
|
||||||
|
length_range: tuple[float, float] = (0, numpy.inf)
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class SBend:
|
||||||
|
""" Description of an s-bend generator """
|
||||||
|
ptype: str
|
||||||
|
|
||||||
|
fn: Callable[[float], Pattern] | Callable[[float], Library]
|
||||||
|
"""
|
||||||
|
Generator function. `jog` (only argument) is assumed to be left (ccw) relative to travel
|
||||||
|
and may be negative for a jog in the opposite direction. Won't be called if jog=0.
|
||||||
|
"""
|
||||||
|
|
||||||
|
in_port_name: str
|
||||||
|
out_port_name: str
|
||||||
|
jog_range: tuple[float, float] = (0, numpy.inf)
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class Bend:
|
||||||
|
""" Description of a pre-rendered bend """
|
||||||
|
abstract: Abstract
|
||||||
|
in_port_name: str
|
||||||
|
out_port_name: str
|
||||||
|
clockwise: bool = True # Is in-to-out clockwise?
|
||||||
|
mirror: bool = True # Should we mirror to get the other rotation?
|
||||||
|
|
||||||
|
@property
|
||||||
|
def in_port(self) -> Port:
|
||||||
|
return self.abstract.ports[self.in_port_name]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def out_port(self) -> Port:
|
||||||
|
return self.abstract.ports[self.out_port_name]
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class Transition:
|
||||||
|
""" Description of a pre-rendered transition """
|
||||||
|
abstract: Abstract
|
||||||
|
their_port_name: str
|
||||||
|
our_port_name: str
|
||||||
|
|
||||||
|
@property
|
||||||
|
def our_port(self) -> Port:
|
||||||
|
return self.abstract.ports[self.our_port_name]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def their_port(self) -> Port:
|
||||||
|
return self.abstract.ports[self.their_port_name]
|
||||||
|
|
||||||
|
def reversed(self) -> Self:
|
||||||
|
return type(self)(self.abstract, self.our_port_name, self.their_port_name)
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class LData:
|
||||||
|
""" Data for planL """
|
||||||
|
straight_length: float
|
||||||
|
straight: 'AutoTool.Straight'
|
||||||
|
straight_kwargs: dict[str, Any]
|
||||||
|
ccw: SupportsBool | None
|
||||||
|
bend: 'AutoTool.Bend | None'
|
||||||
|
in_transition: 'AutoTool.Transition | None'
|
||||||
|
b_transition: 'AutoTool.Transition | None'
|
||||||
|
out_transition: 'AutoTool.Transition | None'
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class SData:
|
||||||
|
""" Data for planS """
|
||||||
|
straight_length: float
|
||||||
|
straight: 'AutoTool.Straight'
|
||||||
|
gen_kwargs: dict[str, Any]
|
||||||
|
jog_remaining: float
|
||||||
|
sbend: 'AutoTool.SBend'
|
||||||
|
in_transition: 'AutoTool.Transition | None'
|
||||||
|
b_transition: 'AutoTool.Transition | None'
|
||||||
|
out_transition: 'AutoTool.Transition | None'
|
||||||
|
|
||||||
|
straights: list[Straight]
|
||||||
|
""" List of straight-generators to choose from, in order of priority """
|
||||||
|
|
||||||
|
bends: list[Bend]
|
||||||
|
""" List of bends to choose from, in order of priority """
|
||||||
|
|
||||||
|
sbends: list[SBend]
|
||||||
|
""" List of S-bend generators to choose from, in order of priority """
|
||||||
|
|
||||||
|
transitions: dict[tuple[str, str], Transition]
|
||||||
|
""" `{(external_ptype, internal_ptype): Transition, ...}` """
|
||||||
|
|
||||||
|
default_out_ptype: str
|
||||||
|
""" Default value for out_ptype """
|
||||||
|
|
||||||
|
def add_complementary_transitions(self) -> Self:
|
||||||
|
for iioo in list(self.transitions.keys()):
|
||||||
|
ooii = (iioo[1], iioo[0])
|
||||||
|
self.transitions.setdefault(ooii, self.transitions[iioo].reversed())
|
||||||
|
return self
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _bend2dxy(bend: Bend, ccw: SupportsBool | None) -> tuple[NDArray[numpy.float64], float]:
|
||||||
|
if ccw is None:
|
||||||
|
return numpy.zeros(2), pi
|
||||||
|
bend_dxy, bend_angle = bend.in_port.measure_travel(bend.out_port)
|
||||||
|
assert bend_angle is not None
|
||||||
|
if bool(ccw):
|
||||||
|
bend_dxy[1] *= -1
|
||||||
|
bend_angle *= -1
|
||||||
|
return bend_dxy, bend_angle
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _sbend2dxy(sbend: SBend, jog: float) -> NDArray[numpy.float64]:
|
||||||
|
if numpy.isclose(jog, 0):
|
||||||
|
return numpy.zeros(2)
|
||||||
|
|
||||||
|
sbend_pat_or_tree = sbend.fn(abs(jog))
|
||||||
|
sbpat = sbend_pat_or_tree if isinstance(sbend_pat_or_tree, Pattern) else sbend_pat_or_tree.top_pattern()
|
||||||
|
dxy, _ = sbpat[sbend.in_port_name].measure_travel(sbpat[sbend.out_port_name])
|
||||||
|
return dxy
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _itransition2dxy(in_transition: Transition | None) -> NDArray[numpy.float64]:
|
||||||
|
if in_transition is None:
|
||||||
|
return numpy.zeros(2)
|
||||||
|
dxy, _ = in_transition.their_port.measure_travel(in_transition.our_port)
|
||||||
|
return dxy
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _otransition2dxy(out_transition: Transition | None, bend_angle: float) -> NDArray[numpy.float64]:
|
||||||
|
if out_transition is None:
|
||||||
|
return numpy.zeros(2)
|
||||||
|
orot = out_transition.our_port.rotation
|
||||||
|
assert orot is not None
|
||||||
|
otrans_dxy = rotation_matrix_2d(pi - orot - bend_angle) @ (out_transition.their_port.offset - out_transition.our_port.offset)
|
||||||
|
return otrans_dxy
|
||||||
|
|
||||||
|
def planL(
|
||||||
|
self,
|
||||||
|
ccw: SupportsBool | None,
|
||||||
|
length: float,
|
||||||
|
*,
|
||||||
|
in_ptype: str | None = None,
|
||||||
|
out_ptype: str | None = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> tuple[Port, LData]:
|
||||||
|
|
||||||
|
success = False
|
||||||
|
for straight in self.straights:
|
||||||
|
for bend in self.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 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 and 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:
|
||||||
|
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
|
||||||
|
|
||||||
|
def _renderL(
|
||||||
|
self,
|
||||||
|
data: LData,
|
||||||
|
tree: ILibrary,
|
||||||
|
port_names: tuple[str, str],
|
||||||
|
straight_kwargs: dict[str, Any],
|
||||||
|
) -> ILibrary:
|
||||||
|
"""
|
||||||
|
Render an L step into a preexisting tree
|
||||||
|
"""
|
||||||
|
pat = tree.top_pattern()
|
||||||
|
if data.in_transition:
|
||||||
|
pat.plug(data.in_transition.abstract, {port_names[1]: data.in_transition.their_port_name})
|
||||||
|
if not numpy.isclose(data.straight_length, 0):
|
||||||
|
straight_pat_or_tree = data.straight.fn(data.straight_length, **(straight_kwargs | data.straight_kwargs))
|
||||||
|
pmap = {port_names[1]: data.straight.in_port_name}
|
||||||
|
if isinstance(straight_pat_or_tree, Pattern):
|
||||||
|
pat.plug(straight_pat_or_tree, pmap, append=True)
|
||||||
|
else:
|
||||||
|
straight_tree = straight_pat_or_tree
|
||||||
|
top = straight_tree.top()
|
||||||
|
straight_tree.flatten(top, dangling_ok=True)
|
||||||
|
pat.plug(straight_tree[top], pmap, append=True)
|
||||||
|
if data.b_transition:
|
||||||
|
pat.plug(data.b_transition.abstract, {port_names[1]: data.b_transition.our_port_name})
|
||||||
|
if data.ccw is not None:
|
||||||
|
bend = data.bend
|
||||||
|
assert bend is not None
|
||||||
|
mirrored = bend.mirror and (bool(data.ccw) == bend.clockwise)
|
||||||
|
inport = bend.in_port_name if (bend.mirror or bool(data.ccw) != bend.clockwise) else bend.out_port_name
|
||||||
|
pat.plug(bend.abstract, {port_names[1]: inport}, mirrored=mirrored)
|
||||||
|
if data.out_transition:
|
||||||
|
pat.plug(data.out_transition.abstract, {port_names[1]: data.out_transition.our_port_name})
|
||||||
|
return tree
|
||||||
|
|
||||||
|
def path(
|
||||||
|
self,
|
||||||
|
ccw: SupportsBool | None,
|
||||||
|
length: float,
|
||||||
|
*,
|
||||||
|
in_ptype: str | None = None,
|
||||||
|
out_ptype: str | None = None,
|
||||||
|
port_names: tuple[str, str] = ('A', 'B'),
|
||||||
|
**kwargs,
|
||||||
|
) -> Library:
|
||||||
|
_out_port, data = self.planL(
|
||||||
|
ccw,
|
||||||
|
length,
|
||||||
|
in_ptype = in_ptype,
|
||||||
|
out_ptype = out_ptype,
|
||||||
|
)
|
||||||
|
|
||||||
|
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path')
|
||||||
|
pat.add_port_pair(names=port_names, ptype='unk' if in_ptype is None else in_ptype)
|
||||||
|
self._renderL(data=data, tree=tree, port_names=port_names, straight_kwargs=kwargs)
|
||||||
|
return tree
|
||||||
|
|
||||||
|
def planS(
|
||||||
|
self,
|
||||||
|
length: float,
|
||||||
|
jog: float,
|
||||||
|
*,
|
||||||
|
in_ptype: str | None = None,
|
||||||
|
out_ptype: str | None = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> tuple[Port, Any]:
|
||||||
|
|
||||||
|
success = False
|
||||||
|
for straight in self.straights:
|
||||||
|
for sbend in self.sbends:
|
||||||
|
out_ptype_pair = (
|
||||||
|
'unk' if out_ptype is None else out_ptype,
|
||||||
|
straight.ptype if numpy.isclose(jog, 0) else sbend.ptype
|
||||||
|
)
|
||||||
|
out_transition = self.transitions.get(out_ptype_pair, None)
|
||||||
|
otrans_dxy = self._otransition2dxy(out_transition, pi)
|
||||||
|
|
||||||
|
# Assume we'll need a straight segment with transitions, then discard them if they don't fit
|
||||||
|
# We do this before generating the s-bend because the transitions might have some dy component
|
||||||
|
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)
|
||||||
|
|
||||||
|
b_transition = None
|
||||||
|
if not numpy.isclose(jog, 0) and sbend.ptype != straight.ptype:
|
||||||
|
b_transition = self.transitions.get((sbend.ptype, straight.ptype), None)
|
||||||
|
btrans_dxy = self._itransition2dxy(b_transition)
|
||||||
|
|
||||||
|
if length > itrans_dxy[0] + btrans_dxy[0] + otrans_dxy[0]:
|
||||||
|
# `if` guard to avoid unnecessary calls to `_sbend2dxy()`, which calls `sbend.fn()`
|
||||||
|
# note some S-bends may have 0 length, so we can't be more restrictive
|
||||||
|
jog_remaining = jog - itrans_dxy[1] - btrans_dxy[1] - otrans_dxy[1]
|
||||||
|
sbend_dxy = self._sbend2dxy(sbend, jog_remaining)
|
||||||
|
straight_length = length - sbend_dxy[0] - itrans_dxy[0] - btrans_dxy[0] - otrans_dxy[0]
|
||||||
|
success = straight.length_range[0] <= straight_length < straight.length_range[1]
|
||||||
|
if success:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Straight didn't work, see if just the s-bend is enough
|
||||||
|
if sbend.ptype != straight.ptype:
|
||||||
|
# Need to use a different in-transition for sbend (vs straight)
|
||||||
|
in_ptype_pair = ('unk' if in_ptype is None else in_ptype, sbend.ptype)
|
||||||
|
in_transition = self.transitions.get(in_ptype_pair, None)
|
||||||
|
itrans_dxy = self._itransition2dxy(in_transition)
|
||||||
|
|
||||||
|
jog_remaining = jog - itrans_dxy[1] - otrans_dxy[1]
|
||||||
|
if sbend.jog_range[0] <= jog_remaining < sbend.jog_range[1]:
|
||||||
|
sbend_dxy = self._sbend2dxy(sbend, jog_remaining)
|
||||||
|
success = numpy.isclose(length, sbend_dxy[0] + itrans_dxy[1] + otrans_dxy[1])
|
||||||
|
if success:
|
||||||
|
b_transition = None
|
||||||
|
straight_length = 0
|
||||||
|
break
|
||||||
|
if success:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
try:
|
||||||
|
ccw0 = jog > 0
|
||||||
|
p_test0, ldata_test0 = self.planL(length / 2, ccw0, in_ptype=in_ptype)
|
||||||
|
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:
|
||||||
|
out_ptype_actual = out_transition.their_port.ptype
|
||||||
|
elif not numpy.isclose(jog_remaining, 0):
|
||||||
|
out_ptype_actual = sbend.ptype
|
||||||
|
elif not numpy.isclose(straight_length, 0):
|
||||||
|
out_ptype_actual = straight.ptype
|
||||||
|
else:
|
||||||
|
out_ptype_actual = self.default_out_ptype
|
||||||
|
|
||||||
|
data = self.SData(straight_length, straight, kwargs, jog_remaining, sbend, in_transition, b_transition, out_transition)
|
||||||
|
out_port = Port((length, jog), rotation=pi, ptype=out_ptype_actual)
|
||||||
|
return out_port, data
|
||||||
|
|
||||||
|
def _renderS(
|
||||||
|
self,
|
||||||
|
data: SData,
|
||||||
|
tree: ILibrary,
|
||||||
|
port_names: tuple[str, str],
|
||||||
|
gen_kwargs: dict[str, Any],
|
||||||
|
) -> ILibrary:
|
||||||
|
"""
|
||||||
|
Render an L step into a preexisting tree
|
||||||
|
"""
|
||||||
|
pat = tree.top_pattern()
|
||||||
|
if data.in_transition:
|
||||||
|
pat.plug(data.in_transition.abstract, {port_names[1]: data.in_transition.their_port_name})
|
||||||
|
if not numpy.isclose(data.straight_length, 0):
|
||||||
|
straight_pat_or_tree = data.straight.fn(data.straight_length, **(gen_kwargs | data.gen_kwargs))
|
||||||
|
pmap = {port_names[1]: data.straight.in_port_name}
|
||||||
|
if isinstance(straight_pat_or_tree, Pattern):
|
||||||
|
straight_pat = straight_pat_or_tree
|
||||||
|
pat.plug(straight_pat, pmap, append=True)
|
||||||
|
else:
|
||||||
|
straight_tree = straight_pat_or_tree
|
||||||
|
top = straight_tree.top()
|
||||||
|
straight_tree.flatten(top, dangling_ok=True)
|
||||||
|
pat.plug(straight_tree[top], pmap, append=True)
|
||||||
|
if data.b_transition:
|
||||||
|
pat.plug(data.b_transition.abstract, {port_names[1]: data.b_transition.our_port_name})
|
||||||
|
if not numpy.isclose(data.jog_remaining, 0):
|
||||||
|
sbend_pat_or_tree = data.sbend.fn(abs(data.jog_remaining), **(gen_kwargs | data.gen_kwargs))
|
||||||
|
pmap = {port_names[1]: data.sbend.in_port_name}
|
||||||
|
if isinstance(sbend_pat_or_tree, Pattern):
|
||||||
|
pat.plug(sbend_pat_or_tree, pmap, append=True, mirrored=data.jog_remaining < 0)
|
||||||
|
else:
|
||||||
|
sbend_tree = sbend_pat_or_tree
|
||||||
|
top = sbend_tree.top()
|
||||||
|
sbend_tree.flatten(top, dangling_ok=True)
|
||||||
|
pat.plug(sbend_tree[top], pmap, append=True, mirrored=data.jog_remaining < 0)
|
||||||
|
if data.out_transition:
|
||||||
|
pat.plug(data.out_transition.abstract, {port_names[1]: data.out_transition.our_port_name})
|
||||||
|
return tree
|
||||||
|
|
||||||
|
def pathS(
|
||||||
|
self,
|
||||||
|
length: float,
|
||||||
|
jog: float,
|
||||||
|
*,
|
||||||
|
in_ptype: str | None = None,
|
||||||
|
out_ptype: str | None = None,
|
||||||
|
port_names: tuple[str, str] = ('A', 'B'),
|
||||||
|
**kwargs,
|
||||||
|
) -> Library:
|
||||||
|
_out_port, data = self.planS(
|
||||||
|
length,
|
||||||
|
jog,
|
||||||
|
in_ptype = in_ptype,
|
||||||
|
out_ptype = out_ptype,
|
||||||
|
)
|
||||||
|
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'pathS')
|
||||||
|
pat.add_port_pair(names=port_names, ptype='unk' if in_ptype is None else in_ptype)
|
||||||
|
self._renderS(data=data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
|
||||||
|
return tree
|
||||||
|
|
||||||
|
def render(
|
||||||
|
self,
|
||||||
|
batch: Sequence[RenderStep],
|
||||||
|
*,
|
||||||
|
port_names: tuple[str, str] = ('A', 'B'),
|
||||||
|
**kwargs,
|
||||||
|
) -> ILibrary:
|
||||||
|
|
||||||
|
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path')
|
||||||
|
pat.add_port_pair(names=(port_names[0], port_names[1]))
|
||||||
|
|
||||||
|
for step in batch:
|
||||||
|
assert step.tool == self
|
||||||
|
if step.opcode == 'L':
|
||||||
|
self._renderL(data=step.data, tree=tree, port_names=port_names, straight_kwargs=kwargs)
|
||||||
|
elif step.opcode == 'S':
|
||||||
|
self._renderS(data=step.data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
|
||||||
return tree
|
return tree
|
||||||
|
|
||||||
|
|
||||||
@ -511,7 +961,7 @@ class PathTool(Tool, metaclass=ABCMeta):
|
|||||||
|
|
||||||
if straight_length < 0:
|
if straight_length < 0:
|
||||||
raise BuildError(
|
raise BuildError(
|
||||||
f'Asked to draw path with total length {length:,g}, shorter than required bend: {bend_dxy[0]:,g}'
|
f'Asked to draw L-path with total length {length:,g}, shorter than required bend: {bend_dxy[0]:,g}'
|
||||||
)
|
)
|
||||||
data = numpy.array((length, bend_run))
|
data = numpy.array((length, bend_run))
|
||||||
out_port = Port(data, rotation=bend_angle, ptype=self.ptype)
|
out_port = Port(data, rotation=bend_angle, ptype=self.ptype)
|
||||||
@ -521,7 +971,7 @@ class PathTool(Tool, metaclass=ABCMeta):
|
|||||||
self,
|
self,
|
||||||
batch: Sequence[RenderStep],
|
batch: Sequence[RenderStep],
|
||||||
*,
|
*,
|
||||||
port_names: Sequence[str] = ('A', 'B'),
|
port_names: tuple[str, str] = ('A', 'B'),
|
||||||
**kwargs, # noqa: ARG002 (unused)
|
**kwargs, # noqa: ARG002 (unused)
|
||||||
) -> ILibrary:
|
) -> ILibrary:
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,10 @@
|
|||||||
|
import traceback
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
|
||||||
|
MASQUE_DIR = str(pathlib.Path(__file__).parent)
|
||||||
|
|
||||||
|
|
||||||
class MasqueError(Exception):
|
class MasqueError(Exception):
|
||||||
"""
|
"""
|
||||||
Parent exception for all Masque-related Exceptions
|
Parent exception for all Masque-related Exceptions
|
||||||
@ -25,15 +32,64 @@ class BuildError(MasqueError):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class PortError(MasqueError):
|
class PortError(MasqueError):
|
||||||
"""
|
"""
|
||||||
Exception raised by builder-related functions
|
Exception raised by port-related functions
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class OneShotError(MasqueError):
|
class OneShotError(MasqueError):
|
||||||
"""
|
"""
|
||||||
Exception raised when a function decorated with `@oneshot` is called more than once
|
Exception raised when a function decorated with `@oneshot` is called more than once
|
||||||
"""
|
"""
|
||||||
def __init__(self, func_name: str) -> None:
|
def __init__(self, func_name: str) -> None:
|
||||||
Exception.__init__(self, f'Function "{func_name}" with @oneshot was called more than once')
|
Exception.__init__(self, f'Function "{func_name}" with @oneshot was called more than once')
|
||||||
|
|
||||||
|
|
||||||
|
def format_stacktrace(
|
||||||
|
stacklevel: int = 1,
|
||||||
|
*,
|
||||||
|
skip_file_prefixes: tuple[str, ...] = (MASQUE_DIR,),
|
||||||
|
low_file_prefixes: tuple[str, ...] = ('<frozen', '<runpy', '<string>'),
|
||||||
|
low_file_suffixes: tuple[str, ...] = ('IPython/utils/py3compat.py', 'concurrent/futures/process.py'),
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Utility function for making nicer stack traces (e.g. excluding <frozen runpy> and similar)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
stacklevel: Number of frames to remove from near this function (default is to
|
||||||
|
show caller but not ourselves). Similar to `warnings.warn` and `logging.warning`.
|
||||||
|
skip_file_prefixes: Indicates frames to ignore after counting stack levels; similar
|
||||||
|
to `warnings.warn` *TODO check if this is actually the same effect re:stacklevel*.
|
||||||
|
Forces stacklevel to max(2, stacklevel).
|
||||||
|
Default is to exclude anything within `masque`.
|
||||||
|
low_file_prefixes: Indicates frames to ignore on the other (entry-point) end of the stack,
|
||||||
|
based on prefixes on their filenames.
|
||||||
|
low_file_suffixes: Indicates frames to ignore on the other (entry-point) end of the stack,
|
||||||
|
based on suffixes on their filenames.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Formatted trimmed stack trace
|
||||||
|
"""
|
||||||
|
if skip_file_prefixes:
|
||||||
|
stacklevel = max(2, stacklevel)
|
||||||
|
|
||||||
|
stack = traceback.extract_stack()
|
||||||
|
|
||||||
|
bad_inds = [ii + 1 for ii, frame in enumerate(stack)
|
||||||
|
if frame.filename.startswith(low_file_prefixes) or frame.filename.endswith(low_file_suffixes)]
|
||||||
|
first_ok = max([0] + bad_inds)
|
||||||
|
|
||||||
|
last_ok = -stacklevel - 1
|
||||||
|
while last_ok >= -len(stack) and stack[last_ok].filename.startswith(skip_file_prefixes):
|
||||||
|
last_ok -= 1
|
||||||
|
|
||||||
|
if selected := stack[first_ok:last_ok + 1]:
|
||||||
|
pass
|
||||||
|
elif selected := stack[:-stacklevel]:
|
||||||
|
pass # noqa: SIM114 # separate elif for clarity
|
||||||
|
else:
|
||||||
|
selected = stack
|
||||||
|
return ''.join(traceback.format_list(selected))
|
||||||
|
|||||||
@ -351,7 +351,7 @@ def _shapes_to_elements(
|
|||||||
)
|
)
|
||||||
|
|
||||||
for polygon in shape.to_polygons():
|
for polygon in shape.to_polygons():
|
||||||
xy_open = polygon.vertices + polygon.offset
|
xy_open = polygon.vertices
|
||||||
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)
|
||||||
|
|
||||||
@ -376,5 +376,5 @@ def _mlayer2dxf(layer: layer_t) -> str:
|
|||||||
if isinstance(layer, int):
|
if isinstance(layer, int):
|
||||||
return str(layer)
|
return str(layer)
|
||||||
if isinstance(layer, tuple):
|
if isinstance(layer, tuple):
|
||||||
return f'{layer[0]}.{layer[1]}'
|
return f'{layer[0]:d}.{layer[1]:d}'
|
||||||
raise PatternError(f'Unknown layer type: {layer} ({type(layer)})')
|
raise PatternError(f'Unknown layer type: {layer} ({type(layer)})')
|
||||||
|
|||||||
@ -21,6 +21,7 @@ Notes:
|
|||||||
"""
|
"""
|
||||||
from typing import IO, cast, Any
|
from typing import IO, cast, Any
|
||||||
from collections.abc import Iterable, Mapping, Callable
|
from collections.abc import Iterable, Mapping, Callable
|
||||||
|
from types import MappingProxyType
|
||||||
import io
|
import io
|
||||||
import mmap
|
import mmap
|
||||||
import logging
|
import logging
|
||||||
@ -52,6 +53,8 @@ path_cap_map = {
|
|||||||
4: Path.Cap.SquareCustom,
|
4: Path.Cap.SquareCustom,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RO_EMPTY_DICT: Mapping[int, bytes] = MappingProxyType({})
|
||||||
|
|
||||||
|
|
||||||
def rint_cast(val: ArrayLike) -> NDArray[numpy.int32]:
|
def rint_cast(val: ArrayLike) -> NDArray[numpy.int32]:
|
||||||
return numpy.rint(val).astype(numpy.int32)
|
return numpy.rint(val).astype(numpy.int32)
|
||||||
@ -399,11 +402,15 @@ def _mrefs_to_grefs(refs: dict[str | None, list[Ref]]) -> list[klamath.library.R
|
|||||||
return grefs
|
return grefs
|
||||||
|
|
||||||
|
|
||||||
def _properties_to_annotations(properties: dict[int, bytes]) -> annotations_t:
|
def _properties_to_annotations(properties: Mapping[int, bytes]) -> annotations_t:
|
||||||
|
if not properties:
|
||||||
|
return None
|
||||||
return {str(k): [v.decode()] for k, v in properties.items()}
|
return {str(k): [v.decode()] for k, v in properties.items()}
|
||||||
|
|
||||||
|
|
||||||
def _annotations_to_properties(annotations: annotations_t, max_len: int = 126) -> dict[int, bytes]:
|
def _annotations_to_properties(annotations: annotations_t, max_len: int = 126) -> Mapping[int, bytes]:
|
||||||
|
if annotations is None:
|
||||||
|
return RO_EMPTY_DICT
|
||||||
cum_len = 0
|
cum_len = 0
|
||||||
props = {}
|
props = {}
|
||||||
for key, vals in annotations.items():
|
for key, vals in annotations.items():
|
||||||
|
|||||||
453
masque/file/gdsii_arrow.py
Normal file
453
masque/file/gdsii_arrow.py
Normal file
@ -0,0 +1,453 @@
|
|||||||
|
# ruff: noqa: ARG001, F401
|
||||||
|
"""
|
||||||
|
GDSII file format readers and writers using the `TODO` library.
|
||||||
|
|
||||||
|
Note that GDSII references follow the same convention as `masque`,
|
||||||
|
with this order of operations:
|
||||||
|
1. Mirroring
|
||||||
|
2. Rotation
|
||||||
|
3. Scaling
|
||||||
|
4. Offset and array expansion (no mirroring/rotation/scaling applied to offsets)
|
||||||
|
|
||||||
|
Scaling, rotation, and mirroring apply to individual instances, not grid
|
||||||
|
vectors or offsets.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
* absolute positioning is not supported
|
||||||
|
* PLEX is not supported
|
||||||
|
* ELFLAGS are not supported
|
||||||
|
* GDS does not support library- or structure-level annotations
|
||||||
|
* GDS creation/modification/access times are set to 1900-01-01 for reproducibility.
|
||||||
|
* Gzip modification time is set to 0 (start of current epoch, usually 1970-01-01)
|
||||||
|
|
||||||
|
TODO writing
|
||||||
|
TODO warn on boxes, nodes
|
||||||
|
"""
|
||||||
|
from typing import IO, cast, Any
|
||||||
|
from collections.abc import Iterable, Mapping, Callable
|
||||||
|
import io
|
||||||
|
import mmap
|
||||||
|
import logging
|
||||||
|
import pathlib
|
||||||
|
import gzip
|
||||||
|
import string
|
||||||
|
from pprint import pformat
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
from numpy.typing import ArrayLike, NDArray
|
||||||
|
from numpy.testing import assert_equal
|
||||||
|
import pyarrow
|
||||||
|
from pyarrow.cffi import ffi
|
||||||
|
|
||||||
|
from .utils import is_gzipped, tmpfile
|
||||||
|
from .. import Pattern, Ref, PatternError, LibraryError, Label, Shape
|
||||||
|
from ..shapes import Polygon, Path, PolyCollection
|
||||||
|
from ..repetition import Grid
|
||||||
|
from ..utils import layer_t, annotations_t
|
||||||
|
from ..library import LazyLibrary, Library, ILibrary, ILibraryView
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
clib = ffi.dlopen('/home/jan/projects/klamath-rs/target/release/libklamath_rs_ext.so')
|
||||||
|
ffi.cdef('void read_path(char* path, struct ArrowArray* array, struct ArrowSchema* schema);')
|
||||||
|
|
||||||
|
|
||||||
|
path_cap_map = {
|
||||||
|
0: Path.Cap.Flush,
|
||||||
|
1: Path.Cap.Circle,
|
||||||
|
2: Path.Cap.Square,
|
||||||
|
4: Path.Cap.SquareCustom,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def rint_cast(val: ArrayLike) -> NDArray[numpy.int32]:
|
||||||
|
return numpy.rint(val).astype(numpy.int32)
|
||||||
|
|
||||||
|
|
||||||
|
def _read_to_arrow(
|
||||||
|
filename: str | pathlib.Path,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
) -> pyarrow.Array:
|
||||||
|
path = pathlib.Path(filename)
|
||||||
|
path.resolve()
|
||||||
|
ptr_array = ffi.new('struct ArrowArray[]', 1)
|
||||||
|
ptr_schema = ffi.new('struct ArrowSchema[]', 1)
|
||||||
|
clib.read_path(str(path).encode(), ptr_array, ptr_schema)
|
||||||
|
|
||||||
|
iptr_schema = int(ffi.cast('uintptr_t', ptr_schema))
|
||||||
|
iptr_array = int(ffi.cast('uintptr_t', ptr_array))
|
||||||
|
arrow_arr = pyarrow.Array._import_from_c(iptr_array, iptr_schema)
|
||||||
|
|
||||||
|
return arrow_arr
|
||||||
|
|
||||||
|
|
||||||
|
def readfile(
|
||||||
|
filename: str | pathlib.Path,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
) -> tuple[Library, dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Wrapper for `read()` that takes a filename or path instead of a stream.
|
||||||
|
|
||||||
|
Will automatically decompress gzipped files.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename: Filename to save to.
|
||||||
|
*args: passed to `read()`
|
||||||
|
**kwargs: passed to `read()`
|
||||||
|
"""
|
||||||
|
arrow_arr = _read_to_arrow(filename)
|
||||||
|
assert len(arrow_arr) == 1
|
||||||
|
|
||||||
|
results = read_arrow(arrow_arr[0])
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def read_arrow(
|
||||||
|
libarr: pyarrow.Array,
|
||||||
|
raw_mode: bool = True,
|
||||||
|
) -> tuple[Library, dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
# TODO check GDSII file for cycles!
|
||||||
|
Read a gdsii file and translate it into a dict of Pattern objects. GDSII structures are
|
||||||
|
translated into Pattern objects; boundaries are translated into polygons, and srefs and arefs
|
||||||
|
are translated into Ref objects.
|
||||||
|
|
||||||
|
Additional library info is returned in a dict, containing:
|
||||||
|
'name': name of the library
|
||||||
|
'meters_per_unit': number of meters per database unit (all values are in database units)
|
||||||
|
'logical_units_per_unit': number of "logical" units displayed by layout tools (typically microns)
|
||||||
|
per database unit
|
||||||
|
|
||||||
|
Args:
|
||||||
|
stream: Stream to read from.
|
||||||
|
raw_mode: If True, constructs shapes in raw mode, bypassing most data validation, Default True.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- dict of pattern_name:Patterns generated from GDSII structures
|
||||||
|
- dict of GDSII library info
|
||||||
|
"""
|
||||||
|
library_info = _read_header(libarr)
|
||||||
|
|
||||||
|
layer_names_np = libarr['layers'].values.to_numpy().view('i2').reshape((-1, 2))
|
||||||
|
layer_tups = [tuple(pair) for pair in layer_names_np]
|
||||||
|
|
||||||
|
cell_ids = libarr['cells'].values.field('id').to_numpy()
|
||||||
|
cell_names = libarr['cell_names'].as_py()
|
||||||
|
|
||||||
|
def get_geom(libarr: pyarrow.Array, geom_type: str) -> dict[str, Any]:
|
||||||
|
el = libarr['cells'].values.field(geom_type)
|
||||||
|
elem = dict(
|
||||||
|
offsets = el.offsets.to_numpy(),
|
||||||
|
xy_arr = el.values.field('xy').values.to_numpy().reshape((-1, 2)),
|
||||||
|
xy_off = el.values.field('xy').offsets.to_numpy() // 2,
|
||||||
|
layer_inds = el.values.field('layer').to_numpy(),
|
||||||
|
prop_off = el.values.field('properties').offsets.to_numpy(),
|
||||||
|
prop_key = el.values.field('properties').values.field('key').to_numpy(),
|
||||||
|
prop_val = el.values.field('properties').values.field('value').to_pylist(),
|
||||||
|
)
|
||||||
|
return elem
|
||||||
|
|
||||||
|
rf = libarr['cells'].values.field('refs')
|
||||||
|
refs = dict(
|
||||||
|
offsets = rf.offsets.to_numpy(),
|
||||||
|
targets = rf.values.field('target').to_numpy(),
|
||||||
|
xy = rf.values.field('xy').to_numpy().view('i4').reshape((-1, 2)),
|
||||||
|
invert_y = rf.values.field('invert_y').fill_null(False).to_numpy(zero_copy_only=False),
|
||||||
|
angle_rad = numpy.rad2deg(rf.values.field('angle_deg').fill_null(0).to_numpy()),
|
||||||
|
scale = rf.values.field('mag').fill_null(1).to_numpy(),
|
||||||
|
rep_valid = rf.values.field('repetition').is_valid().to_numpy(zero_copy_only=False),
|
||||||
|
rep_xy0 = rf.values.field('repetition').field('xy0').fill_null(0).to_numpy().view('i4').reshape((-1, 2)),
|
||||||
|
rep_xy1 = rf.values.field('repetition').field('xy1').fill_null(0).to_numpy().view('i4').reshape((-1, 2)),
|
||||||
|
rep_counts = rf.values.field('repetition').field('counts').fill_null(0).to_numpy().view('i2').reshape((-1, 2)),
|
||||||
|
prop_off = rf.values.field('properties').offsets.to_numpy(),
|
||||||
|
prop_key = rf.values.field('properties').values.field('key').to_numpy(),
|
||||||
|
prop_val = rf.values.field('properties').values.field('value').to_pylist(),
|
||||||
|
)
|
||||||
|
|
||||||
|
txt = libarr['cells'].values.field('texts')
|
||||||
|
texts = dict(
|
||||||
|
offsets = txt.offsets.to_numpy(),
|
||||||
|
layer_inds = txt.values.field('layer').to_numpy(),
|
||||||
|
xy = txt.values.field('xy').to_numpy().view('i4').reshape((-1, 2)),
|
||||||
|
string = txt.values.field('string').to_pylist(),
|
||||||
|
prop_off = txt.values.field('properties').offsets.to_numpy(),
|
||||||
|
prop_key = txt.values.field('properties').values.field('key').to_numpy(),
|
||||||
|
prop_val = txt.values.field('properties').values.field('value').to_pylist(),
|
||||||
|
)
|
||||||
|
|
||||||
|
elements = dict(
|
||||||
|
boundaries = get_geom(libarr, 'boundaries'),
|
||||||
|
paths = get_geom(libarr, 'paths'),
|
||||||
|
boxes = get_geom(libarr, 'boxes'),
|
||||||
|
nodes = get_geom(libarr, 'nodes'),
|
||||||
|
texts = texts,
|
||||||
|
refs = refs,
|
||||||
|
)
|
||||||
|
|
||||||
|
paths = libarr['cells'].values.field('paths')
|
||||||
|
elements['paths'].update(dict(
|
||||||
|
width = paths.values.field('width').fill_null(0).to_numpy(),
|
||||||
|
path_type = paths.values.field('path_type').fill_null(0).to_numpy(),
|
||||||
|
extensions = numpy.stack((
|
||||||
|
paths.values.field('extension_start').fill_null(0).to_numpy(),
|
||||||
|
paths.values.field('extension_end').fill_null(0).to_numpy(),
|
||||||
|
), axis=-1),
|
||||||
|
))
|
||||||
|
|
||||||
|
global_args = dict(
|
||||||
|
cell_names = cell_names,
|
||||||
|
layer_tups = layer_tups,
|
||||||
|
raw_mode = raw_mode,
|
||||||
|
)
|
||||||
|
|
||||||
|
mlib = Library()
|
||||||
|
for cc in range(len(libarr['cells'])):
|
||||||
|
name = cell_names[cell_ids[cc]]
|
||||||
|
pat = Pattern()
|
||||||
|
_boundaries_to_polygons(pat, global_args, elements['boundaries'], cc)
|
||||||
|
_gpaths_to_mpaths(pat, global_args, elements['paths'], cc)
|
||||||
|
_grefs_to_mrefs(pat, global_args, elements['refs'], cc)
|
||||||
|
_texts_to_labels(pat, global_args, elements['texts'], cc)
|
||||||
|
mlib[name] = pat
|
||||||
|
|
||||||
|
return mlib, library_info
|
||||||
|
|
||||||
|
|
||||||
|
def _read_header(libarr: pyarrow.Array) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Read the file header and create the library_info dict.
|
||||||
|
"""
|
||||||
|
library_info = dict(
|
||||||
|
name = libarr['lib_name'],
|
||||||
|
meters_per_unit = libarr['meters_per_db_unit'],
|
||||||
|
logical_units_per_unit = libarr['user_units_per_db_unit'],
|
||||||
|
)
|
||||||
|
return library_info
|
||||||
|
|
||||||
|
|
||||||
|
def _grefs_to_mrefs(
|
||||||
|
pat: Pattern,
|
||||||
|
global_args: dict[str, Any],
|
||||||
|
elem: dict[str, Any],
|
||||||
|
cc: int,
|
||||||
|
) -> None:
|
||||||
|
cell_names = global_args['cell_names']
|
||||||
|
elem_off = elem['offsets'] # which elements belong to each cell
|
||||||
|
xy = elem['xy']
|
||||||
|
prop_key = elem['prop_key']
|
||||||
|
prop_val = elem['prop_val']
|
||||||
|
targets = elem['targets']
|
||||||
|
|
||||||
|
elem_count = elem_off[cc + 1] - elem_off[cc]
|
||||||
|
elem_slc = slice(elem_off[cc], elem_off[cc] + elem_count + 1) # +1 to capture ending location for last elem
|
||||||
|
prop_offs = elem['prop_off'][elem_slc] # which props belong to each element
|
||||||
|
elem_invert_y = elem['invert_y'][elem_slc][:elem_count]
|
||||||
|
elem_angle_rad = elem['angle_rad'][elem_slc][:elem_count]
|
||||||
|
elem_scale = elem['scale'][elem_slc][:elem_count]
|
||||||
|
elem_rep_xy0 = elem['rep_xy0'][elem_slc][:elem_count]
|
||||||
|
elem_rep_xy1 = elem['rep_xy1'][elem_slc][:elem_count]
|
||||||
|
elem_rep_counts = elem['rep_counts'][elem_slc][:elem_count]
|
||||||
|
rep_valid = elem['rep_valid'][elem_slc][:elem_count]
|
||||||
|
|
||||||
|
|
||||||
|
for ee in range(elem_count):
|
||||||
|
target = cell_names[targets[ee]]
|
||||||
|
offset = xy[ee]
|
||||||
|
mirr = elem_invert_y[ee]
|
||||||
|
rot = elem_angle_rad[ee]
|
||||||
|
mag = elem_scale[ee]
|
||||||
|
|
||||||
|
rep: None | Grid = None
|
||||||
|
if rep_valid[ee]:
|
||||||
|
a_vector = elem_rep_xy0[ee]
|
||||||
|
b_vector = elem_rep_xy1[ee]
|
||||||
|
a_count, b_count = elem_rep_counts[ee]
|
||||||
|
rep = Grid(a_vector=a_vector, b_vector=b_vector, a_count=a_count, b_count=b_count)
|
||||||
|
|
||||||
|
annotations: None | dict[str, list[int | float | str]] = None
|
||||||
|
prop_ii, prop_ff = prop_offs[ee], prop_offs[ee + 1]
|
||||||
|
if prop_ii < prop_ff:
|
||||||
|
annotations = {str(prop_key[off]): [prop_val[off]] for off in range(prop_ii, prop_ff)}
|
||||||
|
|
||||||
|
ref = Ref(offset=offset, mirrored=mirr, rotation=rot, scale=mag, repetition=rep, annotations=annotations)
|
||||||
|
pat.refs[target].append(ref)
|
||||||
|
|
||||||
|
|
||||||
|
def _texts_to_labels(
|
||||||
|
pat: Pattern,
|
||||||
|
global_args: dict[str, Any],
|
||||||
|
elem: dict[str, Any],
|
||||||
|
cc: int,
|
||||||
|
) -> None:
|
||||||
|
elem_off = elem['offsets'] # which elements belong to each cell
|
||||||
|
xy = elem['xy']
|
||||||
|
layer_tups = global_args['layer_tups']
|
||||||
|
layer_inds = elem['layer_inds']
|
||||||
|
prop_key = elem['prop_key']
|
||||||
|
prop_val = elem['prop_val']
|
||||||
|
|
||||||
|
elem_count = elem_off[cc + 1] - elem_off[cc]
|
||||||
|
elem_slc = slice(elem_off[cc], elem_off[cc] + elem_count + 1) # +1 to capture ending location for last elem
|
||||||
|
prop_offs = elem['prop_off'][elem_slc] # which props belong to each element
|
||||||
|
elem_layer_inds = layer_inds[elem_slc][:elem_count]
|
||||||
|
elem_strings = elem['string'][elem_slc][:elem_count]
|
||||||
|
|
||||||
|
for ee in range(elem_count):
|
||||||
|
layer = layer_tups[elem_layer_inds[ee]]
|
||||||
|
offset = xy[ee]
|
||||||
|
string = elem_strings[ee]
|
||||||
|
|
||||||
|
annotations: None | dict[str, list[int | float | str]] = None
|
||||||
|
prop_ii, prop_ff = prop_offs[ee], prop_offs[ee + 1]
|
||||||
|
if prop_ii < prop_ff:
|
||||||
|
annotations = {str(prop_key[off]): [prop_val[off]] for off in range(prop_ii, prop_ff)}
|
||||||
|
|
||||||
|
mlabel = Label(string=string, offset=offset, annotations=annotations)
|
||||||
|
pat.labels[layer].append(mlabel)
|
||||||
|
|
||||||
|
|
||||||
|
def _gpaths_to_mpaths(
|
||||||
|
pat: Pattern,
|
||||||
|
global_args: dict[str, Any],
|
||||||
|
elem: dict[str, Any],
|
||||||
|
cc: int,
|
||||||
|
) -> None:
|
||||||
|
elem_off = elem['offsets'] # which elements belong to each cell
|
||||||
|
xy_val = elem['xy_arr']
|
||||||
|
layer_tups = global_args['layer_tups']
|
||||||
|
layer_inds = elem['layer_inds']
|
||||||
|
prop_key = elem['prop_key']
|
||||||
|
prop_val = elem['prop_val']
|
||||||
|
|
||||||
|
elem_count = elem_off[cc + 1] - elem_off[cc]
|
||||||
|
elem_slc = slice(elem_off[cc], elem_off[cc] + elem_count + 1) # +1 to capture ending location for last elem
|
||||||
|
xy_offs = elem['xy_off'][elem_slc] # which xy coords belong to each element
|
||||||
|
prop_offs = elem['prop_off'][elem_slc] # which props belong to each element
|
||||||
|
elem_layer_inds = layer_inds[elem_slc][:elem_count]
|
||||||
|
elem_widths = elem['width'][elem_slc][:elem_count]
|
||||||
|
elem_path_types = elem['path_type'][elem_slc][:elem_count]
|
||||||
|
elem_extensions = elem['extensions'][elem_slc][:elem_count]
|
||||||
|
|
||||||
|
zeros = numpy.zeros((elem_count, 2))
|
||||||
|
raw_mode = global_args['raw_mode']
|
||||||
|
for ee in range(elem_count):
|
||||||
|
layer = layer_tups[elem_layer_inds[ee]]
|
||||||
|
vertices = xy_val[xy_offs[ee]:xy_offs[ee + 1]]
|
||||||
|
width = elem_widths[ee]
|
||||||
|
cap_int = elem_path_types[ee]
|
||||||
|
cap = path_cap_map[cap_int]
|
||||||
|
if cap_int == 4:
|
||||||
|
cap_extensions = elem_extensions[ee]
|
||||||
|
else:
|
||||||
|
cap_extensions = None
|
||||||
|
|
||||||
|
annotations: None | dict[str, list[int | float | str]] = None
|
||||||
|
prop_ii, prop_ff = prop_offs[ee], prop_offs[ee + 1]
|
||||||
|
if prop_ii < prop_ff:
|
||||||
|
annotations = {str(prop_key[off]): [prop_val[off]] for off in range(prop_ii, prop_ff)}
|
||||||
|
|
||||||
|
path = Path(vertices=vertices, offset=zeros[ee], annotations=annotations, raw=raw_mode,
|
||||||
|
width=width, cap=cap,cap_extensions=cap_extensions)
|
||||||
|
pat.shapes[layer].append(path)
|
||||||
|
|
||||||
|
|
||||||
|
def _boundaries_to_polygons(
|
||||||
|
pat: Pattern,
|
||||||
|
global_args: dict[str, Any],
|
||||||
|
elem: dict[str, Any],
|
||||||
|
cc: int,
|
||||||
|
) -> None:
|
||||||
|
elem_off = elem['offsets'] # which elements belong to each cell
|
||||||
|
xy_val = elem['xy_arr']
|
||||||
|
layer_inds = elem['layer_inds']
|
||||||
|
layer_tups = global_args['layer_tups']
|
||||||
|
prop_key = elem['prop_key']
|
||||||
|
prop_val = elem['prop_val']
|
||||||
|
|
||||||
|
elem_count = elem_off[cc + 1] - elem_off[cc]
|
||||||
|
elem_slc = slice(elem_off[cc], elem_off[cc] + elem_count + 1) # +1 to capture ending location for last elem
|
||||||
|
xy_offs = elem['xy_off'][elem_slc] # which xy coords belong to each element
|
||||||
|
xy_counts = xy_offs[1:] - xy_offs[:-1]
|
||||||
|
prop_offs = elem['prop_off'][elem_slc] # which props belong to each element
|
||||||
|
prop_counts = prop_offs[1:] - prop_offs[:-1]
|
||||||
|
elem_layer_inds = layer_inds[elem_slc][:elem_count]
|
||||||
|
|
||||||
|
order = numpy.argsort(elem_layer_inds, stable=True)
|
||||||
|
unilayer_inds, unilayer_first, unilayer_count = numpy.unique(elem_layer_inds, return_index=True, return_counts=True)
|
||||||
|
|
||||||
|
zeros = numpy.zeros((elem_count, 2))
|
||||||
|
raw_mode = global_args['raw_mode']
|
||||||
|
for layer_ind, ff, nn in zip(unilayer_inds, unilayer_first, unilayer_count, strict=True):
|
||||||
|
ee_inds = order[ff:ff + nn]
|
||||||
|
layer = layer_tups[layer_ind]
|
||||||
|
propless_mask = prop_counts[ee_inds] == 0
|
||||||
|
|
||||||
|
poly_count_on_layer = propless_mask.sum()
|
||||||
|
if poly_count_on_layer == 1:
|
||||||
|
propless_mask[:] = 0 # Never make a 1-element collection
|
||||||
|
elif poly_count_on_layer > 1:
|
||||||
|
propless_vert_counts = xy_counts[ee_inds[propless_mask]] - 1 # -1 to drop closing point
|
||||||
|
vertex_lists = numpy.empty((propless_vert_counts.sum(), 2), dtype=numpy.float64)
|
||||||
|
vertex_offsets = numpy.cumsum(numpy.concatenate([[0], propless_vert_counts]))
|
||||||
|
|
||||||
|
for ii, ee in enumerate(ee_inds[propless_mask]):
|
||||||
|
vo = vertex_offsets[ii]
|
||||||
|
vertex_lists[vo:vo + propless_vert_counts[ii]] = xy_val[xy_offs[ee]:xy_offs[ee + 1] - 1]
|
||||||
|
|
||||||
|
polys = PolyCollection(vertex_lists=vertex_lists, vertex_offsets=vertex_offsets, offset=zeros[ee])
|
||||||
|
pat.shapes[layer].append(polys)
|
||||||
|
|
||||||
|
# Handle single polygons
|
||||||
|
for ee in ee_inds[~propless_mask]:
|
||||||
|
layer = layer_tups[elem_layer_inds[ee]]
|
||||||
|
vertices = xy_val[xy_offs[ee]:xy_offs[ee + 1] - 1] # -1 to drop closing point
|
||||||
|
|
||||||
|
annotations: None | dict[str, list[int | float | str]] = None
|
||||||
|
prop_ii, prop_ff = prop_offs[ee], prop_offs[ee + 1]
|
||||||
|
if prop_ii < prop_ff:
|
||||||
|
annotations = {str(prop_key[off]): prop_val[off] for off in range(prop_ii, prop_ff)}
|
||||||
|
|
||||||
|
poly = Polygon(vertices=vertices, offset=zeros[ee], annotations=annotations, raw=raw_mode)
|
||||||
|
pat.shapes[layer].append(poly)
|
||||||
|
|
||||||
|
|
||||||
|
#def _properties_to_annotations(properties: pyarrow.Array) -> annotations_t:
|
||||||
|
# return {prop['key'].as_py(): prop['value'].as_py() for prop in properties}
|
||||||
|
|
||||||
|
|
||||||
|
def check_valid_names(
|
||||||
|
names: Iterable[str],
|
||||||
|
max_length: int = 32,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Check all provided names to see if they're valid GDSII cell names.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
names: Collection of names to check
|
||||||
|
max_length: Max allowed length
|
||||||
|
|
||||||
|
"""
|
||||||
|
allowed_chars = set(string.ascii_letters + string.digits + '_?$')
|
||||||
|
|
||||||
|
bad_chars = [
|
||||||
|
name for name in names
|
||||||
|
if not set(name).issubset(allowed_chars)
|
||||||
|
]
|
||||||
|
|
||||||
|
bad_lengths = [
|
||||||
|
name for name in names
|
||||||
|
if len(name) > max_length
|
||||||
|
]
|
||||||
|
|
||||||
|
if bad_chars:
|
||||||
|
logger.error('Names contain invalid characters:\n' + pformat(bad_chars))
|
||||||
|
|
||||||
|
if bad_lengths:
|
||||||
|
logger.error(f'Names too long (>{max_length}:\n' + pformat(bad_chars))
|
||||||
|
|
||||||
|
if bad_chars or bad_lengths:
|
||||||
|
raise LibraryError('Library contains invalid names, see log above')
|
||||||
@ -661,7 +661,7 @@ def repetition_masq2fata(
|
|||||||
diffs = numpy.diff(rep.displacements, axis=0)
|
diffs = numpy.diff(rep.displacements, axis=0)
|
||||||
diff_ints = rint_cast(diffs)
|
diff_ints = rint_cast(diffs)
|
||||||
frep = fatamorgana.ArbitraryRepetition(diff_ints[:, 0], diff_ints[:, 1]) # type: ignore
|
frep = fatamorgana.ArbitraryRepetition(diff_ints[:, 0], diff_ints[:, 1]) # type: ignore
|
||||||
offset = rep.displacements[0, :]
|
offset = tuple(rep.displacements[0, :])
|
||||||
else:
|
else:
|
||||||
assert rep is None
|
assert rep is None
|
||||||
frep = None
|
frep = None
|
||||||
@ -671,6 +671,8 @@ def repetition_masq2fata(
|
|||||||
|
|
||||||
def annotations_to_properties(annotations: annotations_t) -> list[fatrec.Property]:
|
def annotations_to_properties(annotations: annotations_t) -> list[fatrec.Property]:
|
||||||
#TODO determine is_standard based on key?
|
#TODO determine is_standard based on key?
|
||||||
|
if annotations is None:
|
||||||
|
return []
|
||||||
properties = []
|
properties = []
|
||||||
for key, values in annotations.items():
|
for key, values in annotations.items():
|
||||||
vals = [AString(v) if isinstance(v, str) else v
|
vals = [AString(v) if isinstance(v, str) else v
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
SVG file format readers and writers
|
SVG file format readers and writers
|
||||||
"""
|
"""
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
import warnings
|
import logging
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from numpy.typing import ArrayLike
|
from numpy.typing import ArrayLike
|
||||||
@ -12,6 +12,9 @@ from .utils import mangle_name
|
|||||||
from .. import Pattern
|
from .. import Pattern
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def writefile(
|
def writefile(
|
||||||
library: Mapping[str, Pattern],
|
library: Mapping[str, Pattern],
|
||||||
top: str,
|
top: str,
|
||||||
@ -50,7 +53,7 @@ def writefile(
|
|||||||
bounds = pattern.get_bounds(library=library)
|
bounds = pattern.get_bounds(library=library)
|
||||||
if bounds is None:
|
if bounds is None:
|
||||||
bounds_min, bounds_max = numpy.array([[-1, -1], [1, 1]])
|
bounds_min, bounds_max = numpy.array([[-1, -1], [1, 1]])
|
||||||
warnings.warn('Pattern had no bounds (empty?); setting arbitrary viewbox', stacklevel=1)
|
logger.warning('Pattern had no bounds (empty?); setting arbitrary viewbox', stacklevel=1)
|
||||||
else:
|
else:
|
||||||
bounds_min, bounds_max = bounds
|
bounds_min, bounds_max = bounds
|
||||||
|
|
||||||
@ -117,7 +120,7 @@ def writefile_inverted(
|
|||||||
bounds = pattern.get_bounds(library=library)
|
bounds = pattern.get_bounds(library=library)
|
||||||
if bounds is None:
|
if bounds is None:
|
||||||
bounds_min, bounds_max = numpy.array([[-1, -1], [1, 1]])
|
bounds_min, bounds_max = numpy.array([[-1, -1], [1, 1]])
|
||||||
warnings.warn('Pattern had no bounds (empty?); setting arbitrary viewbox', stacklevel=1)
|
logger.warning('Pattern had no bounds (empty?); setting arbitrary viewbox', stacklevel=1)
|
||||||
else:
|
else:
|
||||||
bounds_min, bounds_max = bounds
|
bounds_min, bounds_max = bounds
|
||||||
|
|
||||||
|
|||||||
@ -264,6 +264,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||||||
self,
|
self,
|
||||||
tops: str | Sequence[str],
|
tops: str | Sequence[str],
|
||||||
flatten_ports: bool = False,
|
flatten_ports: bool = False,
|
||||||
|
dangling_ok: bool = False,
|
||||||
) -> dict[str, 'Pattern']:
|
) -> dict[str, 'Pattern']:
|
||||||
"""
|
"""
|
||||||
Returns copies of all `tops` patterns with all refs
|
Returns copies of all `tops` patterns with all refs
|
||||||
@ -276,6 +277,9 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||||||
tops: The pattern(s) to flattern.
|
tops: The pattern(s) to flattern.
|
||||||
flatten_ports: If `True`, keep ports from any referenced
|
flatten_ports: If `True`, keep ports from any referenced
|
||||||
patterns; otherwise discard them.
|
patterns; otherwise discard them.
|
||||||
|
dangling_ok: If `True`, no error will be thrown if any
|
||||||
|
ref points to a name which is not present in the library.
|
||||||
|
Default False.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
{name: flat_pattern} mapping for all flattened patterns.
|
{name: flat_pattern} mapping for all flattened patterns.
|
||||||
@ -292,6 +296,8 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||||||
for target in pat.refs:
|
for target in pat.refs:
|
||||||
if target is None:
|
if target is None:
|
||||||
continue
|
continue
|
||||||
|
if dangling_ok and target not in self:
|
||||||
|
continue
|
||||||
if target not in flattened:
|
if target not in flattened:
|
||||||
flatten_single(target)
|
flatten_single(target)
|
||||||
|
|
||||||
@ -307,7 +313,9 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||||||
p.ports.clear()
|
p.ports.clear()
|
||||||
pat.append(p)
|
pat.append(p)
|
||||||
|
|
||||||
pat.refs.clear()
|
for target in set(pat.refs.keys()) & set(self.keys()):
|
||||||
|
del pat.refs[target]
|
||||||
|
|
||||||
flattened[name] = pat
|
flattened[name] = pat
|
||||||
|
|
||||||
for top in tops:
|
for top in tops:
|
||||||
|
|||||||
@ -332,7 +332,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
))
|
))
|
||||||
|
|
||||||
self.ports = dict(sorted(self.ports.items()))
|
self.ports = dict(sorted(self.ports.items()))
|
||||||
self.annotations = dict(sorted(self.annotations.items()))
|
self.annotations = dict(sorted(self.annotations.items())) if self.annotations is not None else None
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@ -354,10 +354,13 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
for layer, lseq in other_pattern.labels.items():
|
for layer, lseq in other_pattern.labels.items():
|
||||||
self.labels[layer].extend(lseq)
|
self.labels[layer].extend(lseq)
|
||||||
|
|
||||||
annotation_conflicts = set(self.annotations.keys()) & set(other_pattern.annotations.keys())
|
if other_pattern.annotations is not None:
|
||||||
if annotation_conflicts:
|
if self.annotations is None:
|
||||||
raise PatternError(f'Annotation keys overlap: {annotation_conflicts}')
|
self.annotations = {}
|
||||||
self.annotations.update(other_pattern.annotations)
|
annotation_conflicts = set(self.annotations.keys()) & set(other_pattern.annotations.keys())
|
||||||
|
if annotation_conflicts:
|
||||||
|
raise PatternError(f'Annotation keys overlap: {annotation_conflicts}')
|
||||||
|
self.annotations.update(other_pattern.annotations)
|
||||||
|
|
||||||
port_conflicts = set(self.ports.keys()) & set(other_pattern.ports.keys())
|
port_conflicts = set(self.ports.keys()) & set(other_pattern.ports.keys())
|
||||||
if port_conflicts:
|
if port_conflicts:
|
||||||
@ -415,7 +418,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
elif default_keep:
|
elif default_keep:
|
||||||
pat.refs = copy.copy(self.refs)
|
pat.refs = copy.copy(self.refs)
|
||||||
|
|
||||||
if annotations is not None:
|
if annotations is not None and self.annotations is not None:
|
||||||
pat.annotations = {k: v for k, v in self.annotations.items() if annotations(k, v)}
|
pat.annotations = {k: v for k, v in self.annotations.items() if annotations(k, v)}
|
||||||
elif default_keep:
|
elif default_keep:
|
||||||
pat.annotations = copy.copy(self.annotations)
|
pat.annotations = copy.copy(self.annotations)
|
||||||
@ -581,7 +584,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
bounds = numpy.vstack((numpy.min(corners, axis=0),
|
bounds = numpy.vstack((numpy.min(corners, axis=0),
|
||||||
numpy.max(corners, axis=0))) * ref.scale + [ref.offset]
|
numpy.max(corners, axis=0))) * ref.scale + [ref.offset]
|
||||||
if ref.repetition is not None:
|
if ref.repetition is not None:
|
||||||
bounds += ref.repetition.get_bounds()
|
bounds += ref.repetition.get_bounds_nonempty()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Non-manhattan rotation, have to figure out bounds by rotating the pattern
|
# Non-manhattan rotation, have to figure out bounds by rotating the pattern
|
||||||
@ -742,7 +745,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
for entry in chain(chain_elements(self.shapes, self.refs, self.labels), self.ports.values()):
|
for entry in chain(chain_elements(self.shapes, self.refs, self.labels), self.ports.values()):
|
||||||
cast('Positionable', entry).offset[across_axis - 1] *= -1
|
cast('Positionable', entry).offset[1 - across_axis] *= -1
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def mirror_elements(self, across_axis: int = 0) -> Self:
|
def mirror_elements(self, across_axis: int = 0) -> Self:
|
||||||
@ -1166,12 +1169,13 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
ports[new_name] = port
|
ports[new_name] = port
|
||||||
|
|
||||||
for name, port in ports.items():
|
for name, port in ports.items():
|
||||||
p = port.deepcopy()
|
pp = port.deepcopy()
|
||||||
if mirrored:
|
if mirrored:
|
||||||
p.mirror()
|
pp.mirror()
|
||||||
p.rotate_around(pivot, rotation)
|
pp.offset[1] *= -1
|
||||||
p.translate(offset)
|
pp.rotate_around(pivot, rotation)
|
||||||
self.ports[name] = p
|
pp.translate(offset)
|
||||||
|
self.ports[name] = pp
|
||||||
|
|
||||||
if append:
|
if append:
|
||||||
if isinstance(other, Abstract):
|
if isinstance(other, Abstract):
|
||||||
@ -1199,7 +1203,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
# map_out: dict[str, str | None] | None,
|
# map_out: dict[str, str | None] | None,
|
||||||
# *,
|
# *,
|
||||||
# mirrored: bool,
|
# mirrored: bool,
|
||||||
# inherit_name: bool,
|
# thru: bool | str,
|
||||||
# set_rotation: bool | None,
|
# set_rotation: bool | None,
|
||||||
# append: Literal[False],
|
# append: Literal[False],
|
||||||
# ) -> Self:
|
# ) -> Self:
|
||||||
@ -1213,7 +1217,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
# map_out: dict[str, str | None] | None,
|
# map_out: dict[str, str | None] | None,
|
||||||
# *,
|
# *,
|
||||||
# mirrored: bool,
|
# mirrored: bool,
|
||||||
# inherit_name: bool,
|
# thru: bool | str,
|
||||||
# set_rotation: bool | None,
|
# set_rotation: bool | None,
|
||||||
# append: bool,
|
# append: bool,
|
||||||
# ) -> Self:
|
# ) -> Self:
|
||||||
@ -1226,7 +1230,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
map_out: dict[str, str | None] | None = None,
|
map_out: dict[str, str | None] | None = None,
|
||||||
*,
|
*,
|
||||||
mirrored: bool = False,
|
mirrored: bool = False,
|
||||||
inherit_name: bool = True,
|
thru: bool | str = True,
|
||||||
set_rotation: bool | None = None,
|
set_rotation: bool | None = None,
|
||||||
append: bool = False,
|
append: bool = False,
|
||||||
ok_connections: Iterable[tuple[str, str]] = (),
|
ok_connections: Iterable[tuple[str, str]] = (),
|
||||||
@ -1247,7 +1251,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
- `my_pat.plug(wire, {'myport': 'A'})` places port 'A' of `wire` at 'myport'
|
- `my_pat.plug(wire, {'myport': 'A'})` places port 'A' of `wire` at 'myport'
|
||||||
of `my_pat`.
|
of `my_pat`.
|
||||||
If `wire` has only two ports (e.g. 'A' and 'B'), no `map_out` argument is
|
If `wire` has only two ports (e.g. 'A' and 'B'), no `map_out` argument is
|
||||||
provided, and the `inherit_name` argument is not explicitly set to `False`,
|
provided, and the `thru` argument is not explicitly set to `False`,
|
||||||
the unconnected port of `wire` is automatically renamed to 'myport'. This
|
the unconnected port of `wire` is automatically renamed to 'myport'. This
|
||||||
allows easy extension of existing ports without changing their names or
|
allows easy extension of existing ports without changing their names or
|
||||||
having to provide `map_out` each time `plug` is called.
|
having to provide `map_out` each time `plug` is called.
|
||||||
@ -1260,11 +1264,15 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
new names for ports in `other`.
|
new names for ports in `other`.
|
||||||
mirrored: Enables mirroring `other` across the x axis prior to connecting
|
mirrored: Enables mirroring `other` across the x axis prior to connecting
|
||||||
any ports.
|
any ports.
|
||||||
inherit_name: If `True`, and `map_in` specifies only a single port,
|
thru: If map_in specifies only a single port, `thru` provides a mechainsm
|
||||||
and `map_out` is `None`, and `other` has only two ports total,
|
to avoid repeating the port name. Eg, for `map_in={'myport': 'A'}`,
|
||||||
then automatically renames the output port of `other` to the
|
- If True (default), and `other` has only two ports total, and map_out
|
||||||
name of the port from `self` that appears in `map_in`. This
|
doesn't specify a name for the other port, its name is set to the key
|
||||||
makes it easy to extend a pattern with simple 2-port devices
|
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
|
(e.g. wires) without providing `map_out` each time `plug` is
|
||||||
called. See "Examples" above for more info. Default `True`.
|
called. See "Examples" above for more info. Default `True`.
|
||||||
set_rotation: If the necessary rotation cannot be determined from
|
set_rotation: If the necessary rotation cannot be determined from
|
||||||
@ -1292,25 +1300,32 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
`PortError` if the specified port mapping is not achieveable (the ports
|
`PortError` if the specified port mapping is not achieveable (the ports
|
||||||
do not line up)
|
do not line up)
|
||||||
"""
|
"""
|
||||||
# If asked to inherit a name, check that all conditions are met
|
|
||||||
if (inherit_name
|
|
||||||
and not map_out
|
|
||||||
and len(map_in) == 1
|
|
||||||
and len(other.ports) == 2):
|
|
||||||
out_port_name = next(iter(set(other.ports.keys()) - set(map_in.values())))
|
|
||||||
map_out = {out_port_name: next(iter(map_in.keys()))}
|
|
||||||
|
|
||||||
if map_out is None:
|
if map_out is None:
|
||||||
map_out = {}
|
map_out = {}
|
||||||
map_out = copy.deepcopy(map_out)
|
map_out = copy.deepcopy(map_out)
|
||||||
|
|
||||||
|
# If asked to inherit a name, check that all conditions are met
|
||||||
|
if isinstance(thru, str):
|
||||||
|
if not len(map_in) == 1:
|
||||||
|
raise PatternError(f'Got {thru=} but have multiple map_in entries; don\'t know which one to use')
|
||||||
|
if thru in map_out:
|
||||||
|
raise PatternError(f'Got {thru=} but tha port already exists in map_out')
|
||||||
|
map_out[thru] = next(iter(map_in.keys()))
|
||||||
|
elif (bool(thru)
|
||||||
|
and len(map_in) == 1
|
||||||
|
and not map_out
|
||||||
|
and len(other.ports) == 2
|
||||||
|
):
|
||||||
|
out_port_name = next(iter(set(other.ports.keys()) - set(map_in.values())))
|
||||||
|
map_out = {out_port_name: next(iter(map_in.keys()))}
|
||||||
|
|
||||||
self.check_ports(other.ports.keys(), map_in, map_out)
|
self.check_ports(other.ports.keys(), map_in, map_out)
|
||||||
translation, rotation, pivot = self.find_transform(
|
translation, rotation, pivot = self.find_transform(
|
||||||
other,
|
other,
|
||||||
map_in,
|
map_in,
|
||||||
mirrored=mirrored,
|
mirrored = mirrored,
|
||||||
set_rotation=set_rotation,
|
set_rotation = set_rotation,
|
||||||
ok_connections=ok_connections,
|
ok_connections = ok_connections,
|
||||||
)
|
)
|
||||||
|
|
||||||
# get rid of plugged ports
|
# get rid of plugged ports
|
||||||
@ -1323,13 +1338,13 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||||||
|
|
||||||
self.place(
|
self.place(
|
||||||
other,
|
other,
|
||||||
offset=translation,
|
offset = translation,
|
||||||
rotation=rotation,
|
rotation = rotation,
|
||||||
pivot=pivot,
|
pivot = pivot,
|
||||||
mirrored=mirrored,
|
mirrored = mirrored,
|
||||||
port_map=map_out,
|
port_map = map_out,
|
||||||
skip_port_check=True,
|
skip_port_check = True,
|
||||||
append=append,
|
append = append,
|
||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
from typing import overload, Self, NoReturn, Any
|
from typing import overload, Self, NoReturn, Any
|
||||||
from collections.abc import Iterable, KeysView, ValuesView, Mapping
|
from collections.abc import Iterable, KeysView, ValuesView, Mapping
|
||||||
import warnings
|
|
||||||
import traceback
|
|
||||||
import logging
|
import logging
|
||||||
import functools
|
import functools
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
@ -13,8 +11,8 @@ from numpy import pi
|
|||||||
from numpy.typing import ArrayLike, NDArray
|
from numpy.typing import ArrayLike, NDArray
|
||||||
|
|
||||||
from .traits import PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable
|
from .traits import PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable
|
||||||
from .utils import rotate_offsets_around
|
from .utils import rotate_offsets_around, rotation_matrix_2d
|
||||||
from .error import PortError
|
from .error import PortError, format_stacktrace
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -64,7 +62,7 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable):
|
|||||||
return self._rotation
|
return self._rotation
|
||||||
|
|
||||||
@rotation.setter
|
@rotation.setter
|
||||||
def rotation(self, val: float) -> None:
|
def rotation(self, val: float | None) -> None:
|
||||||
if val is None:
|
if val is None:
|
||||||
self._rotation = None
|
self._rotation = None
|
||||||
else:
|
else:
|
||||||
@ -102,7 +100,6 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def mirror(self, axis: int = 0) -> Self:
|
def mirror(self, axis: int = 0) -> Self:
|
||||||
self.offset[1 - axis] *= -1
|
|
||||||
if self.rotation is not None:
|
if self.rotation is not None:
|
||||||
self.rotation *= -1
|
self.rotation *= -1
|
||||||
self.rotation += axis * pi
|
self.rotation += axis * pi
|
||||||
@ -145,6 +142,28 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable):
|
|||||||
and self.rotation == other.rotation
|
and self.rotation == other.rotation
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def measure_travel(self, destination: 'Port') -> tuple[NDArray[numpy.float64], float | None]:
|
||||||
|
"""
|
||||||
|
Find the (travel, jog) distances and rotation angle from the current port to the provided
|
||||||
|
`destination` port.
|
||||||
|
|
||||||
|
Travel is along the source port's axis (into the device interior), and jog is perpendicular,
|
||||||
|
with left of the travel direction corresponding to a positive jog.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
(self): Source `Port`
|
||||||
|
destination: Destination `Port`
|
||||||
|
|
||||||
|
Returns
|
||||||
|
[travel, jog], rotation
|
||||||
|
"""
|
||||||
|
angle_in = self.rotation
|
||||||
|
angle_out = destination.rotation
|
||||||
|
assert angle_in is not None
|
||||||
|
dxy = rotation_matrix_2d(-angle_in) @ (destination.offset - self.offset)
|
||||||
|
angle = ((angle_out - angle_in) % (2 * pi)) if angle_out is not None else None
|
||||||
|
return dxy, angle
|
||||||
|
|
||||||
|
|
||||||
class PortList(metaclass=ABCMeta):
|
class PortList(metaclass=ABCMeta):
|
||||||
__slots__ = () # Allow subclasses to use __slots__
|
__slots__ = () # Allow subclasses to use __slots__
|
||||||
@ -305,11 +324,11 @@ class PortList(metaclass=ABCMeta):
|
|||||||
|
|
||||||
if type_conflicts.any():
|
if type_conflicts.any():
|
||||||
msg = 'Ports have conflicting types:\n'
|
msg = 'Ports have conflicting types:\n'
|
||||||
for nn, (k, v) in enumerate(connections.items()):
|
for nn, (kk, vv) in enumerate(connections.items()):
|
||||||
if type_conflicts[nn]:
|
if type_conflicts[nn]:
|
||||||
msg += f'{k} | {a_types[nn]}:{b_types[nn]} | {v}\n'
|
msg += f'{kk} | {a_types[nn]}:{b_types[nn]} | {vv}\n'
|
||||||
msg = ''.join(traceback.format_stack()) + '\n' + msg
|
msg += '\nStack trace:\n' + format_stacktrace()
|
||||||
warnings.warn(msg, stacklevel=2)
|
logger.warning(msg)
|
||||||
|
|
||||||
a_offsets = numpy.array([pp.offset for pp in a_ports])
|
a_offsets = numpy.array([pp.offset for pp in a_ports])
|
||||||
b_offsets = numpy.array([pp.offset for pp in b_ports])
|
b_offsets = numpy.array([pp.offset for pp in b_ports])
|
||||||
@ -326,17 +345,17 @@ class PortList(metaclass=ABCMeta):
|
|||||||
if not numpy.allclose(rotations, 0):
|
if not numpy.allclose(rotations, 0):
|
||||||
rot_deg = numpy.rad2deg(rotations)
|
rot_deg = numpy.rad2deg(rotations)
|
||||||
msg = 'Port orientations do not match:\n'
|
msg = 'Port orientations do not match:\n'
|
||||||
for nn, (k, v) in enumerate(connections.items()):
|
for nn, (kk, vv) in enumerate(connections.items()):
|
||||||
if not numpy.isclose(rot_deg[nn], 0):
|
if not numpy.isclose(rot_deg[nn], 0):
|
||||||
msg += f'{k} | {rot_deg[nn]:g} | {v}\n'
|
msg += f'{kk} | {rot_deg[nn]:g} | {vv}\n'
|
||||||
raise PortError(msg)
|
raise PortError(msg)
|
||||||
|
|
||||||
translations = a_offsets - b_offsets
|
translations = a_offsets - b_offsets
|
||||||
if not numpy.allclose(translations, 0):
|
if not numpy.allclose(translations, 0):
|
||||||
msg = 'Port translations do not match:\n'
|
msg = 'Port translations do not match:\n'
|
||||||
for nn, (k, v) in enumerate(connections.items()):
|
for nn, (kk, vv) in enumerate(connections.items()):
|
||||||
if not numpy.allclose(translations[nn], 0):
|
if not numpy.allclose(translations[nn], 0):
|
||||||
msg += f'{k} | {translations[nn]} | {v}\n'
|
msg += f'{kk} | {translations[nn]} | {vv}\n'
|
||||||
raise PortError(msg)
|
raise PortError(msg)
|
||||||
|
|
||||||
for pp in chain(a_names, b_names):
|
for pp in chain(a_names, b_names):
|
||||||
@ -406,7 +425,7 @@ class PortList(metaclass=ABCMeta):
|
|||||||
|
|
||||||
map_out_counts = Counter(map_out.values())
|
map_out_counts = Counter(map_out.values())
|
||||||
map_out_counts[None] = 0
|
map_out_counts[None] = 0
|
||||||
conflicts_out = {k for k, v in map_out_counts.items() if v > 1}
|
conflicts_out = {kk for kk, vv in map_out_counts.items() if vv > 1}
|
||||||
if conflicts_out:
|
if conflicts_out:
|
||||||
raise PortError(f'Duplicate targets in `map_out`: {conflicts_out}')
|
raise PortError(f'Duplicate targets in `map_out`: {conflicts_out}')
|
||||||
|
|
||||||
@ -438,7 +457,7 @@ class PortList(metaclass=ABCMeta):
|
|||||||
`set_rotation` must remain `None`.
|
`set_rotation` must remain `None`.
|
||||||
ok_connections: Set of "allowed" ptype combinations. Identical
|
ok_connections: Set of "allowed" ptype combinations. Identical
|
||||||
ptypes are always allowed to connect, as is `'unk'` with
|
ptypes are always allowed to connect, as is `'unk'` with
|
||||||
any other ptypte. Non-allowed ptype connections will emit a
|
any other ptypte. Non-allowed ptype connections will log a
|
||||||
warning. Order is ignored, i.e. `(a, b)` is equivalent to
|
warning. Order is ignored, i.e. `(a, b)` is equivalent to
|
||||||
`(b, a)`.
|
`(b, a)`.
|
||||||
|
|
||||||
@ -452,12 +471,12 @@ class PortList(metaclass=ABCMeta):
|
|||||||
s_ports = self[map_in.keys()]
|
s_ports = self[map_in.keys()]
|
||||||
o_ports = other[map_in.values()]
|
o_ports = other[map_in.values()]
|
||||||
return self.find_port_transform(
|
return self.find_port_transform(
|
||||||
s_ports=s_ports,
|
s_ports = s_ports,
|
||||||
o_ports=o_ports,
|
o_ports = o_ports,
|
||||||
map_in=map_in,
|
map_in = map_in,
|
||||||
mirrored=mirrored,
|
mirrored = mirrored,
|
||||||
set_rotation=set_rotation,
|
set_rotation = set_rotation,
|
||||||
ok_connections=ok_connections,
|
ok_connections = ok_connections,
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -489,7 +508,7 @@ class PortList(metaclass=ABCMeta):
|
|||||||
`set_rotation` must remain `None`.
|
`set_rotation` must remain `None`.
|
||||||
ok_connections: Set of "allowed" ptype combinations. Identical
|
ok_connections: Set of "allowed" ptype combinations. Identical
|
||||||
ptypes are always allowed to connect, as is `'unk'` with
|
ptypes are always allowed to connect, as is `'unk'` with
|
||||||
any other ptypte. Non-allowed ptype connections will emit a
|
any other ptypte. Non-allowed ptype connections will log a
|
||||||
warning. Order is ignored, i.e. `(a, b)` is equivalent to
|
warning. Order is ignored, i.e. `(a, b)` is equivalent to
|
||||||
`(b, a)`.
|
`(b, a)`.
|
||||||
|
|
||||||
@ -520,11 +539,11 @@ class PortList(metaclass=ABCMeta):
|
|||||||
for st, ot in zip(s_types, o_types, strict=True)])
|
for st, ot in zip(s_types, o_types, strict=True)])
|
||||||
if type_conflicts.any():
|
if type_conflicts.any():
|
||||||
msg = 'Ports have conflicting types:\n'
|
msg = 'Ports have conflicting types:\n'
|
||||||
for nn, (k, v) in enumerate(map_in.items()):
|
for nn, (kk, vv) in enumerate(map_in.items()):
|
||||||
if type_conflicts[nn]:
|
if type_conflicts[nn]:
|
||||||
msg += f'{k} | {s_types[nn]}:{o_types[nn]} | {v}\n'
|
msg += f'{kk} | {s_types[nn]}:{o_types[nn]} | {vv}\n'
|
||||||
msg = ''.join(traceback.format_stack()) + '\n' + msg
|
msg += '\nStack trace:\n' + format_stacktrace()
|
||||||
warnings.warn(msg, stacklevel=2)
|
logger.warning(msg)
|
||||||
|
|
||||||
rotations = numpy.mod(s_rotations - o_rotations - pi, 2 * pi)
|
rotations = numpy.mod(s_rotations - o_rotations - pi, 2 * pi)
|
||||||
if not has_rot.any():
|
if not has_rot.any():
|
||||||
@ -546,8 +565,12 @@ class PortList(metaclass=ABCMeta):
|
|||||||
translations = s_offsets - o_offsets
|
translations = s_offsets - o_offsets
|
||||||
if not numpy.allclose(translations[:1], translations):
|
if not numpy.allclose(translations[:1], translations):
|
||||||
msg = 'Port translations do not match:\n'
|
msg = 'Port translations do not match:\n'
|
||||||
|
common_translation = numpy.min(translations, axis=0)
|
||||||
|
msg += f'Common: {common_translation} \n'
|
||||||
|
msg += 'Deltas:\n'
|
||||||
for nn, (kk, vv) in enumerate(map_in.items()):
|
for nn, (kk, vv) in enumerate(map_in.items()):
|
||||||
msg += f'{kk} | {translations[nn]} | {vv}\n'
|
msg += f'{kk} | {translations[nn] - common_translation} | {vv}\n'
|
||||||
raise PortError(msg)
|
raise PortError(msg)
|
||||||
|
|
||||||
return translations[0], rotations[0], o_offsets[0]
|
return translations[0], rotations[0], o_offsets[0]
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import numpy
|
|||||||
from numpy import pi
|
from numpy import pi
|
||||||
from numpy.typing import NDArray, ArrayLike
|
from numpy.typing import NDArray, ArrayLike
|
||||||
|
|
||||||
from .utils import annotations_t, rotation_matrix_2d, annotations_eq, annotations_lt, rep2key
|
from .utils import annotations_t, rotation_matrix_2d, annotations_eq, annotations_lt, rep2key, SupportsBool
|
||||||
from .repetition import Repetition
|
from .repetition import Repetition
|
||||||
from .traits import (
|
from .traits import (
|
||||||
PositionableImpl, RotatableImpl, ScalableImpl,
|
PositionableImpl, RotatableImpl, ScalableImpl,
|
||||||
@ -50,11 +50,11 @@ class Ref(
|
|||||||
|
|
||||||
# Mirrored property
|
# Mirrored property
|
||||||
@property
|
@property
|
||||||
def mirrored(self) -> bool: # mypy#3004, setter should be SupportsBool
|
def mirrored(self) -> bool:
|
||||||
return self._mirrored
|
return self._mirrored
|
||||||
|
|
||||||
@mirrored.setter
|
@mirrored.setter
|
||||||
def mirrored(self, val: bool) -> None:
|
def mirrored(self, val: SupportsBool) -> None:
|
||||||
self._mirrored = bool(val)
|
self._mirrored = bool(val)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|||||||
@ -327,7 +327,7 @@ class Arbitrary(Repetition):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def displacements(self) -> Any: # mypy#3004 NDArray[numpy.float64]:
|
def displacements(self) -> NDArray[numpy.float64]:
|
||||||
return self._displacements
|
return self._displacements
|
||||||
|
|
||||||
@displacements.setter
|
@displacements.setter
|
||||||
|
|||||||
@ -10,6 +10,7 @@ from .shape import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from .polygon import Polygon as Polygon
|
from .polygon import Polygon as Polygon
|
||||||
|
from .poly_collection import PolyCollection as PolyCollection
|
||||||
from .circle import Circle as Circle
|
from .circle import Circle as Circle
|
||||||
from .ellipse import Ellipse as Ellipse
|
from .ellipse import Ellipse as Ellipse
|
||||||
from .arc import Arc as Arc
|
from .arc import Arc as Arc
|
||||||
|
|||||||
@ -10,10 +10,11 @@ from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
|
|||||||
from ..error import PatternError
|
from ..error import PatternError
|
||||||
from ..repetition import Repetition
|
from ..repetition import Repetition
|
||||||
from ..utils import is_scalar, annotations_t, annotations_lt, annotations_eq, rep2key
|
from ..utils import is_scalar, annotations_t, annotations_lt, annotations_eq, rep2key
|
||||||
|
from ..traits import PositionableImpl
|
||||||
|
|
||||||
|
|
||||||
@functools.total_ordering
|
@functools.total_ordering
|
||||||
class Arc(Shape):
|
class Arc(PositionableImpl, Shape):
|
||||||
"""
|
"""
|
||||||
An elliptical arc, formed by cutting off an elliptical ring with two rays which exit from its
|
An elliptical arc, formed by cutting off an elliptical ring with two rays which exit from its
|
||||||
center. It has a position, two radii, a start and stop angle, a rotation, and a width.
|
center. It has a position, two radii, a start and stop angle, a rotation, and a width.
|
||||||
@ -42,7 +43,7 @@ class Arc(Shape):
|
|||||||
|
|
||||||
# radius properties
|
# radius properties
|
||||||
@property
|
@property
|
||||||
def radii(self) -> Any: # mypy#3004 NDArray[numpy.float64]:
|
def radii(self) -> NDArray[numpy.float64]:
|
||||||
"""
|
"""
|
||||||
Return the radii `[rx, ry]`
|
Return the radii `[rx, ry]`
|
||||||
"""
|
"""
|
||||||
@ -79,7 +80,7 @@ class Arc(Shape):
|
|||||||
|
|
||||||
# arc start/stop angle properties
|
# arc start/stop angle properties
|
||||||
@property
|
@property
|
||||||
def angles(self) -> Any: # mypy#3004 NDArray[numpy.float64]:
|
def angles(self) -> NDArray[numpy.float64]:
|
||||||
"""
|
"""
|
||||||
Return the start and stop angles `[a_start, a_stop]`.
|
Return the start and stop angles `[a_start, a_stop]`.
|
||||||
Angles are measured from x-axis after rotation
|
Angles are measured from x-axis after rotation
|
||||||
@ -157,7 +158,7 @@ class Arc(Shape):
|
|||||||
offset: ArrayLike = (0.0, 0.0),
|
offset: ArrayLike = (0.0, 0.0),
|
||||||
rotation: float = 0,
|
rotation: float = 0,
|
||||||
repetition: Repetition | None = None,
|
repetition: Repetition | None = None,
|
||||||
annotations: annotations_t | None = None,
|
annotations: annotations_t = None,
|
||||||
raw: bool = False,
|
raw: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
if raw:
|
if raw:
|
||||||
@ -170,7 +171,7 @@ class Arc(Shape):
|
|||||||
self._offset = offset
|
self._offset = offset
|
||||||
self._rotation = rotation
|
self._rotation = rotation
|
||||||
self._repetition = repetition
|
self._repetition = repetition
|
||||||
self._annotations = annotations if annotations is not None else {}
|
self._annotations = annotations
|
||||||
else:
|
else:
|
||||||
self.radii = radii
|
self.radii = radii
|
||||||
self.angles = angles
|
self.angles = angles
|
||||||
@ -178,7 +179,7 @@ class Arc(Shape):
|
|||||||
self.offset = offset
|
self.offset = offset
|
||||||
self.rotation = rotation
|
self.rotation = rotation
|
||||||
self.repetition = repetition
|
self.repetition = repetition
|
||||||
self.annotations = annotations if annotations is not None else {}
|
self.annotations = annotations
|
||||||
|
|
||||||
def __deepcopy__(self, memo: dict | None = None) -> 'Arc':
|
def __deepcopy__(self, memo: dict | None = None) -> 'Arc':
|
||||||
memo = {} if memo is None else memo
|
memo = {} if memo is None else memo
|
||||||
@ -412,15 +413,15 @@ class Arc(Shape):
|
|||||||
start_angle -= pi
|
start_angle -= pi
|
||||||
rotation += pi
|
rotation += pi
|
||||||
|
|
||||||
angles = (start_angle, start_angle + delta_angle)
|
norm_angles = (start_angle, start_angle + delta_angle)
|
||||||
rotation %= 2 * pi
|
rotation %= 2 * pi
|
||||||
width = self.width
|
width = self.width
|
||||||
|
|
||||||
return ((type(self), radii, angles, width / norm_value),
|
return ((type(self), radii, norm_angles, width / norm_value),
|
||||||
(self.offset, scale / norm_value, rotation, False),
|
(self.offset, scale / norm_value, rotation, False),
|
||||||
lambda: Arc(
|
lambda: Arc(
|
||||||
radii=radii * norm_value,
|
radii=radii * norm_value,
|
||||||
angles=angles,
|
angles=norm_angles,
|
||||||
width=width * norm_value,
|
width=width * norm_value,
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|||||||
@ -10,10 +10,11 @@ from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
|
|||||||
from ..error import PatternError
|
from ..error import PatternError
|
||||||
from ..repetition import Repetition
|
from ..repetition import Repetition
|
||||||
from ..utils import is_scalar, annotations_t, annotations_lt, annotations_eq, rep2key
|
from ..utils import is_scalar, annotations_t, annotations_lt, annotations_eq, rep2key
|
||||||
|
from ..traits import PositionableImpl
|
||||||
|
|
||||||
|
|
||||||
@functools.total_ordering
|
@functools.total_ordering
|
||||||
class Circle(Shape):
|
class Circle(PositionableImpl, Shape):
|
||||||
"""
|
"""
|
||||||
A circle, which has a position and radius.
|
A circle, which has a position and radius.
|
||||||
"""
|
"""
|
||||||
@ -48,7 +49,7 @@ class Circle(Shape):
|
|||||||
*,
|
*,
|
||||||
offset: ArrayLike = (0.0, 0.0),
|
offset: ArrayLike = (0.0, 0.0),
|
||||||
repetition: Repetition | None = None,
|
repetition: Repetition | None = None,
|
||||||
annotations: annotations_t | None = None,
|
annotations: annotations_t = None,
|
||||||
raw: bool = False,
|
raw: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
if raw:
|
if raw:
|
||||||
@ -56,12 +57,12 @@ class Circle(Shape):
|
|||||||
self._radius = radius
|
self._radius = radius
|
||||||
self._offset = offset
|
self._offset = offset
|
||||||
self._repetition = repetition
|
self._repetition = repetition
|
||||||
self._annotations = annotations if annotations is not None else {}
|
self._annotations = annotations
|
||||||
else:
|
else:
|
||||||
self.radius = radius
|
self.radius = radius
|
||||||
self.offset = offset
|
self.offset = offset
|
||||||
self.repetition = repetition
|
self.repetition = repetition
|
||||||
self.annotations = annotations if annotations is not None else {}
|
self.annotations = annotations
|
||||||
|
|
||||||
def __deepcopy__(self, memo: dict | None = None) -> 'Circle':
|
def __deepcopy__(self, memo: dict | None = None) -> 'Circle':
|
||||||
memo = {} if memo is None else memo
|
memo = {} if memo is None else memo
|
||||||
|
|||||||
@ -11,10 +11,11 @@ from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
|
|||||||
from ..error import PatternError
|
from ..error import PatternError
|
||||||
from ..repetition import Repetition
|
from ..repetition import Repetition
|
||||||
from ..utils import is_scalar, rotation_matrix_2d, annotations_t, annotations_lt, annotations_eq, rep2key
|
from ..utils import is_scalar, rotation_matrix_2d, annotations_t, annotations_lt, annotations_eq, rep2key
|
||||||
|
from ..traits import PositionableImpl
|
||||||
|
|
||||||
|
|
||||||
@functools.total_ordering
|
@functools.total_ordering
|
||||||
class Ellipse(Shape):
|
class Ellipse(PositionableImpl, Shape):
|
||||||
"""
|
"""
|
||||||
An ellipse, which has a position, two radii, and a rotation.
|
An ellipse, which has a position, two radii, and a rotation.
|
||||||
The rotation gives the angle from x-axis, counterclockwise, to the first (x) radius.
|
The rotation gives the angle from x-axis, counterclockwise, to the first (x) radius.
|
||||||
@ -33,7 +34,7 @@ class Ellipse(Shape):
|
|||||||
|
|
||||||
# radius properties
|
# radius properties
|
||||||
@property
|
@property
|
||||||
def radii(self) -> Any: # mypy#3004 NDArray[numpy.float64]:
|
def radii(self) -> NDArray[numpy.float64]:
|
||||||
"""
|
"""
|
||||||
Return the radii `[rx, ry]`
|
Return the radii `[rx, ry]`
|
||||||
"""
|
"""
|
||||||
@ -93,7 +94,7 @@ class Ellipse(Shape):
|
|||||||
offset: ArrayLike = (0.0, 0.0),
|
offset: ArrayLike = (0.0, 0.0),
|
||||||
rotation: float = 0,
|
rotation: float = 0,
|
||||||
repetition: Repetition | None = None,
|
repetition: Repetition | None = None,
|
||||||
annotations: annotations_t | None = None,
|
annotations: annotations_t = None,
|
||||||
raw: bool = False,
|
raw: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
if raw:
|
if raw:
|
||||||
@ -103,13 +104,13 @@ class Ellipse(Shape):
|
|||||||
self._offset = offset
|
self._offset = offset
|
||||||
self._rotation = rotation
|
self._rotation = rotation
|
||||||
self._repetition = repetition
|
self._repetition = repetition
|
||||||
self._annotations = annotations if annotations is not None else {}
|
self._annotations = annotations
|
||||||
else:
|
else:
|
||||||
self.radii = radii
|
self.radii = radii
|
||||||
self.offset = offset
|
self.offset = offset
|
||||||
self.rotation = rotation
|
self.rotation = rotation
|
||||||
self.repetition = repetition
|
self.repetition = repetition
|
||||||
self.annotations = annotations if annotations is not None else {}
|
self.annotations = annotations
|
||||||
|
|
||||||
def __deepcopy__(self, memo: dict | None = None) -> Self:
|
def __deepcopy__(self, memo: dict | None = None) -> Self:
|
||||||
memo = {} if memo is None else memo
|
memo = {} if memo is None else memo
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from typing import Any, cast
|
from typing import Any, cast, Self
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
import copy
|
import copy
|
||||||
import functools
|
import functools
|
||||||
@ -30,8 +30,7 @@ class PathCap(Enum):
|
|||||||
@functools.total_ordering
|
@functools.total_ordering
|
||||||
class Path(Shape):
|
class Path(Shape):
|
||||||
"""
|
"""
|
||||||
A path, consisting of a bunch of vertices (Nx2 ndarray), a width, an end-cap shape,
|
A path, consisting of a bunch of vertices (Nx2 ndarray), a width, and an end-cap shape.
|
||||||
and an offset.
|
|
||||||
|
|
||||||
Note that the setter for `Path.vertices` will create a copy of the passed vertex coordinates.
|
Note that the setter for `Path.vertices` will create a copy of the passed vertex coordinates.
|
||||||
|
|
||||||
@ -40,7 +39,7 @@ class Path(Shape):
|
|||||||
__slots__ = (
|
__slots__ = (
|
||||||
'_vertices', '_width', '_cap', '_cap_extensions',
|
'_vertices', '_width', '_cap', '_cap_extensions',
|
||||||
# Inherited
|
# Inherited
|
||||||
'_offset', '_repetition', '_annotations',
|
'_repetition', '_annotations',
|
||||||
)
|
)
|
||||||
_vertices: NDArray[numpy.float64]
|
_vertices: NDArray[numpy.float64]
|
||||||
_width: float
|
_width: float
|
||||||
@ -87,7 +86,7 @@ class Path(Shape):
|
|||||||
|
|
||||||
# cap_extensions property
|
# cap_extensions property
|
||||||
@property
|
@property
|
||||||
def cap_extensions(self) -> Any | None: # mypy#3004 NDArray[numpy.float64]]:
|
def cap_extensions(self) -> NDArray[numpy.float64] | None:
|
||||||
"""
|
"""
|
||||||
Path end-cap extension
|
Path end-cap extension
|
||||||
|
|
||||||
@ -113,7 +112,7 @@ class Path(Shape):
|
|||||||
|
|
||||||
# vertices property
|
# vertices property
|
||||||
@property
|
@property
|
||||||
def vertices(self) -> Any: # mypy#3004 NDArray[numpy.float64]]:
|
def vertices(self) -> NDArray[numpy.float64]:
|
||||||
"""
|
"""
|
||||||
Vertices of the path (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`
|
Vertices of the path (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`
|
||||||
|
|
||||||
@ -160,6 +159,28 @@ class Path(Shape):
|
|||||||
raise PatternError('Wrong number of vertices')
|
raise PatternError('Wrong number of vertices')
|
||||||
self.vertices[:, 1] = val
|
self.vertices[:, 1] = val
|
||||||
|
|
||||||
|
# Offset property for `Positionable`
|
||||||
|
@property
|
||||||
|
def offset(self) -> NDArray[numpy.float64]:
|
||||||
|
"""
|
||||||
|
[x, y] offset
|
||||||
|
"""
|
||||||
|
return numpy.zeros(2)
|
||||||
|
|
||||||
|
@offset.setter
|
||||||
|
def offset(self, val: ArrayLike) -> None:
|
||||||
|
if numpy.any(val):
|
||||||
|
raise PatternError('Path offset is forced to (0, 0)')
|
||||||
|
|
||||||
|
def set_offset(self, val: ArrayLike) -> Self:
|
||||||
|
if numpy.any(val):
|
||||||
|
raise PatternError('Path offset is forced to (0, 0)')
|
||||||
|
return self
|
||||||
|
|
||||||
|
def translate(self, offset: ArrayLike) -> Self:
|
||||||
|
self._vertices += numpy.atleast_2d(offset)
|
||||||
|
return self
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
vertices: ArrayLike,
|
vertices: ArrayLike,
|
||||||
@ -170,36 +191,35 @@ class Path(Shape):
|
|||||||
offset: ArrayLike = (0.0, 0.0),
|
offset: ArrayLike = (0.0, 0.0),
|
||||||
rotation: float = 0,
|
rotation: float = 0,
|
||||||
repetition: Repetition | None = None,
|
repetition: Repetition | None = None,
|
||||||
annotations: annotations_t | None = None,
|
annotations: annotations_t = None,
|
||||||
raw: bool = False,
|
raw: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._cap_extensions = None # Since .cap setter might access it
|
self._cap_extensions = None # Since .cap setter might access it
|
||||||
|
|
||||||
if raw:
|
if raw:
|
||||||
assert isinstance(vertices, numpy.ndarray)
|
assert isinstance(vertices, numpy.ndarray)
|
||||||
assert isinstance(offset, numpy.ndarray)
|
|
||||||
assert isinstance(cap_extensions, numpy.ndarray) or cap_extensions is None
|
assert isinstance(cap_extensions, numpy.ndarray) or cap_extensions is None
|
||||||
self._vertices = vertices
|
self._vertices = vertices
|
||||||
self._offset = offset
|
|
||||||
self._repetition = repetition
|
self._repetition = repetition
|
||||||
self._annotations = annotations if annotations is not None else {}
|
self._annotations = annotations
|
||||||
self._width = width
|
self._width = width
|
||||||
self._cap = cap
|
self._cap = cap
|
||||||
self._cap_extensions = cap_extensions
|
self._cap_extensions = cap_extensions
|
||||||
else:
|
else:
|
||||||
self.vertices = vertices
|
self.vertices = vertices
|
||||||
self.offset = offset
|
|
||||||
self.repetition = repetition
|
self.repetition = repetition
|
||||||
self.annotations = annotations if annotations is not None else {}
|
self.annotations = annotations
|
||||||
self.width = width
|
self.width = width
|
||||||
self.cap = cap
|
self.cap = cap
|
||||||
self.cap_extensions = cap_extensions
|
self.cap_extensions = cap_extensions
|
||||||
self.rotate(rotation)
|
if rotation:
|
||||||
|
self.rotate(rotation)
|
||||||
|
if numpy.any(offset):
|
||||||
|
self.translate(offset)
|
||||||
|
|
||||||
def __deepcopy__(self, memo: dict | None = None) -> 'Path':
|
def __deepcopy__(self, memo: dict | None = None) -> 'Path':
|
||||||
memo = {} if memo is None else memo
|
memo = {} if memo is None else memo
|
||||||
new = copy.copy(self)
|
new = copy.copy(self)
|
||||||
new._offset = self._offset.copy()
|
|
||||||
new._vertices = self._vertices.copy()
|
new._vertices = self._vertices.copy()
|
||||||
new._cap = copy.deepcopy(self._cap, memo)
|
new._cap = copy.deepcopy(self._cap, memo)
|
||||||
new._cap_extensions = copy.deepcopy(self._cap_extensions, memo)
|
new._cap_extensions = copy.deepcopy(self._cap_extensions, memo)
|
||||||
@ -209,7 +229,6 @@ class Path(Shape):
|
|||||||
def __eq__(self, other: Any) -> bool:
|
def __eq__(self, other: Any) -> bool:
|
||||||
return (
|
return (
|
||||||
type(self) is type(other)
|
type(self) is type(other)
|
||||||
and numpy.array_equal(self.offset, other.offset)
|
|
||||||
and numpy.array_equal(self.vertices, other.vertices)
|
and numpy.array_equal(self.vertices, other.vertices)
|
||||||
and self.width == other.width
|
and self.width == other.width
|
||||||
and self.cap == other.cap
|
and self.cap == other.cap
|
||||||
@ -234,8 +253,6 @@ class Path(Shape):
|
|||||||
if self.cap_extensions is None:
|
if self.cap_extensions is None:
|
||||||
return True
|
return True
|
||||||
return tuple(self.cap_extensions) < tuple(other.cap_extensions)
|
return tuple(self.cap_extensions) < tuple(other.cap_extensions)
|
||||||
if not numpy.array_equal(self.offset, other.offset):
|
|
||||||
return tuple(self.offset) < tuple(other.offset)
|
|
||||||
if self.repetition != other.repetition:
|
if self.repetition != other.repetition:
|
||||||
return rep2key(self.repetition) < rep2key(other.repetition)
|
return rep2key(self.repetition) < rep2key(other.repetition)
|
||||||
return annotations_lt(self.annotations, other.annotations)
|
return annotations_lt(self.annotations, other.annotations)
|
||||||
@ -292,7 +309,7 @@ class Path(Shape):
|
|||||||
|
|
||||||
if self.width == 0:
|
if self.width == 0:
|
||||||
verts = numpy.vstack((v, v[::-1]))
|
verts = numpy.vstack((v, v[::-1]))
|
||||||
return [Polygon(offset=self.offset, vertices=verts)]
|
return [Polygon(vertices=verts)]
|
||||||
|
|
||||||
perp = dvdir[:, ::-1] * [[1, -1]] * self.width / 2
|
perp = dvdir[:, ::-1] * [[1, -1]] * self.width / 2
|
||||||
|
|
||||||
@ -343,7 +360,7 @@ class Path(Shape):
|
|||||||
o1.append(v[-1] - perp[-1])
|
o1.append(v[-1] - perp[-1])
|
||||||
verts = numpy.vstack((o0, o1[::-1]))
|
verts = numpy.vstack((o0, o1[::-1]))
|
||||||
|
|
||||||
polys = [Polygon(offset=self.offset, vertices=verts)]
|
polys = [Polygon(vertices=verts)]
|
||||||
|
|
||||||
if self.cap == PathCap.Circle:
|
if self.cap == PathCap.Circle:
|
||||||
#for vert in v: # not sure if every vertex, or just ends?
|
#for vert in v: # not sure if every vertex, or just ends?
|
||||||
@ -355,8 +372,8 @@ class Path(Shape):
|
|||||||
|
|
||||||
def get_bounds_single(self) -> NDArray[numpy.float64]:
|
def get_bounds_single(self) -> NDArray[numpy.float64]:
|
||||||
if self.cap == PathCap.Circle:
|
if self.cap == PathCap.Circle:
|
||||||
bounds = self.offset + numpy.vstack((numpy.min(self.vertices, axis=0) - self.width / 2,
|
bounds = numpy.vstack((numpy.min(self.vertices, axis=0) - self.width / 2,
|
||||||
numpy.max(self.vertices, axis=0) + self.width / 2))
|
numpy.max(self.vertices, axis=0) + self.width / 2))
|
||||||
elif self.cap in (
|
elif self.cap in (
|
||||||
PathCap.Flush,
|
PathCap.Flush,
|
||||||
PathCap.Square,
|
PathCap.Square,
|
||||||
@ -390,7 +407,7 @@ class Path(Shape):
|
|||||||
def normalized_form(self, norm_value: float) -> normalized_shape_tuple:
|
def normalized_form(self, norm_value: float) -> normalized_shape_tuple:
|
||||||
# Note: this function is going to be pretty slow for many-vertexed paths, relative to
|
# Note: this function is going to be pretty slow for many-vertexed paths, relative to
|
||||||
# other shapes
|
# other shapes
|
||||||
offset = self.vertices.mean(axis=0) + self.offset
|
offset = self.vertices.mean(axis=0)
|
||||||
zeroed_vertices = self.vertices - offset
|
zeroed_vertices = self.vertices - offset
|
||||||
|
|
||||||
scale = zeroed_vertices.std()
|
scale = zeroed_vertices.std()
|
||||||
@ -460,5 +477,5 @@ class Path(Shape):
|
|||||||
return extensions
|
return extensions
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
centroid = self.offset + self.vertices.mean(axis=0)
|
centroid = self.vertices.mean(axis=0)
|
||||||
return f'<Path centroid {centroid} v{len(self.vertices)} w{self.width} c{self.cap}>'
|
return f'<Path centroid {centroid} v{len(self.vertices)} w{self.width} c{self.cap}>'
|
||||||
|
|||||||
223
masque/shapes/poly_collection.py
Normal file
223
masque/shapes/poly_collection.py
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
from typing import Any, cast, Self
|
||||||
|
from collections.abc import Iterator
|
||||||
|
import copy
|
||||||
|
import functools
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
from numpy import pi
|
||||||
|
from numpy.typing import NDArray, ArrayLike
|
||||||
|
|
||||||
|
from . import Shape, normalized_shape_tuple
|
||||||
|
from .polygon import Polygon
|
||||||
|
from ..error import PatternError
|
||||||
|
from ..repetition import Repetition
|
||||||
|
from ..utils import rotation_matrix_2d, annotations_lt, annotations_eq, rep2key, annotations_t
|
||||||
|
|
||||||
|
|
||||||
|
@functools.total_ordering
|
||||||
|
class PolyCollection(Shape):
|
||||||
|
"""
|
||||||
|
A collection of polygons, consisting of concatenated vertex arrays (N_m x 2 ndarray) which specify
|
||||||
|
implicitly-closed boundaries, and an array of offets specifying the first vertex of each
|
||||||
|
successive polygon.
|
||||||
|
|
||||||
|
A `normalized_form(...)` is available, but is untested and probably fairly slow.
|
||||||
|
"""
|
||||||
|
__slots__ = (
|
||||||
|
'_vertex_lists',
|
||||||
|
'_vertex_offsets',
|
||||||
|
# Inherited
|
||||||
|
'_repetition', '_annotations',
|
||||||
|
)
|
||||||
|
|
||||||
|
_vertex_lists: NDArray[numpy.float64]
|
||||||
|
""" 2D NDArray ((N+M+...) x 2) of vertices `[[xa0, ya0], [xa1, ya1], ..., [xb0, yb0], [xb1, yb1], ... ]` """
|
||||||
|
|
||||||
|
_vertex_offsets: NDArray[numpy.intp]
|
||||||
|
""" 1D NDArray specifying the starting offset for each polygon """
|
||||||
|
|
||||||
|
@property
|
||||||
|
def vertex_lists(self) -> NDArray[numpy.float64]:
|
||||||
|
"""
|
||||||
|
Vertices of the polygons, ((N+M+...) x 2). Use with `vertex_offsets`.
|
||||||
|
"""
|
||||||
|
return self._vertex_lists
|
||||||
|
|
||||||
|
@property
|
||||||
|
def vertex_offsets(self) -> NDArray[numpy.intp]:
|
||||||
|
"""
|
||||||
|
Starting offset (in `vertex_lists`) for each polygon
|
||||||
|
"""
|
||||||
|
return self._vertex_offsets
|
||||||
|
|
||||||
|
@property
|
||||||
|
def vertex_slices(self) -> Iterator[slice]:
|
||||||
|
"""
|
||||||
|
Iterator which provides slices which index vertex_lists
|
||||||
|
"""
|
||||||
|
for ii, ff in zip(
|
||||||
|
self._vertex_offsets,
|
||||||
|
chain(self._vertex_offsets, (self._vertex_lists.shape[0],)),
|
||||||
|
strict=True,
|
||||||
|
):
|
||||||
|
yield slice(ii, ff)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def polygon_vertices(self) -> Iterator[NDArray[numpy.float64]]:
|
||||||
|
for slc in self.vertex_slices:
|
||||||
|
yield self._vertex_lists[slc]
|
||||||
|
|
||||||
|
# Offset property for `Positionable`
|
||||||
|
@property
|
||||||
|
def offset(self) -> NDArray[numpy.float64]:
|
||||||
|
"""
|
||||||
|
[x, y] offset
|
||||||
|
"""
|
||||||
|
return numpy.zeros(2)
|
||||||
|
|
||||||
|
@offset.setter
|
||||||
|
def offset(self, _val: ArrayLike) -> None:
|
||||||
|
raise PatternError('PolyCollection offset is forced to (0, 0)')
|
||||||
|
|
||||||
|
def set_offset(self, val: ArrayLike) -> Self:
|
||||||
|
if numpy.any(val):
|
||||||
|
raise PatternError('Path offset is forced to (0, 0)')
|
||||||
|
return self
|
||||||
|
|
||||||
|
def translate(self, offset: ArrayLike) -> Self:
|
||||||
|
self._vertex_lists += numpy.atleast_2d(offset)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
vertex_lists: ArrayLike,
|
||||||
|
vertex_offsets: ArrayLike,
|
||||||
|
*,
|
||||||
|
offset: ArrayLike = (0.0, 0.0),
|
||||||
|
rotation: float = 0.0,
|
||||||
|
repetition: Repetition | None = None,
|
||||||
|
annotations: annotations_t = None,
|
||||||
|
raw: bool = False,
|
||||||
|
) -> None:
|
||||||
|
if raw:
|
||||||
|
assert isinstance(vertex_lists, numpy.ndarray)
|
||||||
|
assert isinstance(vertex_offsets, numpy.ndarray)
|
||||||
|
self._vertex_lists = vertex_lists
|
||||||
|
self._vertex_offsets = vertex_offsets
|
||||||
|
self._repetition = repetition
|
||||||
|
self._annotations = annotations
|
||||||
|
else:
|
||||||
|
self._vertex_lists = numpy.asarray(vertex_lists, dtype=float)
|
||||||
|
self._vertex_offsets = numpy.asarray(vertex_offsets, dtype=numpy.intp)
|
||||||
|
self.repetition = repetition
|
||||||
|
self.annotations = annotations
|
||||||
|
if rotation:
|
||||||
|
self.rotate(rotation)
|
||||||
|
if numpy.any(offset):
|
||||||
|
self.translate(offset)
|
||||||
|
|
||||||
|
def __deepcopy__(self, memo: dict | None = None) -> Self:
|
||||||
|
memo = {} if memo is None else memo
|
||||||
|
new = copy.copy(self)
|
||||||
|
new._vertex_lists = self._vertex_lists.copy()
|
||||||
|
new._vertex_offsets = self._vertex_offsets.copy()
|
||||||
|
new._annotations = copy.deepcopy(self._annotations)
|
||||||
|
return new
|
||||||
|
|
||||||
|
def __eq__(self, other: Any) -> bool:
|
||||||
|
return (
|
||||||
|
type(self) is type(other)
|
||||||
|
and numpy.array_equal(self._vertex_lists, other._vertex_lists)
|
||||||
|
and numpy.array_equal(self._vertex_offsets, other._vertex_offsets)
|
||||||
|
and self.repetition == other.repetition
|
||||||
|
and annotations_eq(self.annotations, other.annotations)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __lt__(self, other: Shape) -> bool:
|
||||||
|
if type(self) is not type(other):
|
||||||
|
if repr(type(self)) != repr(type(other)):
|
||||||
|
return repr(type(self)) < repr(type(other))
|
||||||
|
return id(type(self)) < id(type(other))
|
||||||
|
|
||||||
|
other = cast('PolyCollection', other)
|
||||||
|
|
||||||
|
for vv, oo in zip(self.polygon_vertices, other.polygon_vertices, strict=False):
|
||||||
|
if not numpy.array_equal(vv, oo):
|
||||||
|
min_len = min(vv.shape[0], oo.shape[0])
|
||||||
|
eq_mask = vv[:min_len] != oo[:min_len]
|
||||||
|
eq_lt = vv[:min_len] < oo[:min_len]
|
||||||
|
eq_lt_masked = eq_lt[eq_mask]
|
||||||
|
if eq_lt_masked.size > 0:
|
||||||
|
return eq_lt_masked.flat[0]
|
||||||
|
return vv.shape[0] < oo.shape[0]
|
||||||
|
if len(self.vertex_lists) != len(other.vertex_lists):
|
||||||
|
return len(self.vertex_lists) < len(other.vertex_lists)
|
||||||
|
if self.repetition != other.repetition:
|
||||||
|
return rep2key(self.repetition) < rep2key(other.repetition)
|
||||||
|
return annotations_lt(self.annotations, other.annotations)
|
||||||
|
|
||||||
|
def to_polygons(
|
||||||
|
self,
|
||||||
|
num_vertices: int | None = None, # unused # noqa: ARG002
|
||||||
|
max_arclen: float | None = None, # unused # noqa: ARG002
|
||||||
|
) -> list['Polygon']:
|
||||||
|
return [Polygon(
|
||||||
|
vertices = vv,
|
||||||
|
repetition = copy.deepcopy(self.repetition),
|
||||||
|
annotations = copy.deepcopy(self.annotations),
|
||||||
|
) for vv in self.polygon_vertices]
|
||||||
|
|
||||||
|
def get_bounds_single(self) -> NDArray[numpy.float64]: # TODO note shape get_bounds doesn't include repetition
|
||||||
|
return numpy.vstack((numpy.min(self._vertex_lists, axis=0),
|
||||||
|
numpy.max(self._vertex_lists, axis=0)))
|
||||||
|
|
||||||
|
def rotate(self, theta: float) -> Self:
|
||||||
|
if theta != 0:
|
||||||
|
rot = rotation_matrix_2d(theta)
|
||||||
|
self._vertex_lists = numpy.einsum('ij,kj->ki', rot, self._vertex_lists)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def mirror(self, axis: int = 0) -> Self:
|
||||||
|
self._vertex_lists[:, axis - 1] *= -1
|
||||||
|
return self
|
||||||
|
|
||||||
|
def scale_by(self, c: float) -> Self:
|
||||||
|
self._vertex_lists *= c
|
||||||
|
return self
|
||||||
|
|
||||||
|
def normalized_form(self, norm_value: float) -> normalized_shape_tuple:
|
||||||
|
# Note: this function is going to be pretty slow for many-vertexed polygons, relative to
|
||||||
|
# other shapes
|
||||||
|
meanv = self._vertex_lists.mean(axis=0)
|
||||||
|
zeroed_vertices = self._vertex_lists - [meanv]
|
||||||
|
offset = meanv
|
||||||
|
|
||||||
|
scale = zeroed_vertices.std()
|
||||||
|
normed_vertices = zeroed_vertices / scale
|
||||||
|
|
||||||
|
_, _, vertex_axis = numpy.linalg.svd(zeroed_vertices)
|
||||||
|
rotation = numpy.arctan2(vertex_axis[0][1], vertex_axis[0][0]) % (2 * pi)
|
||||||
|
rotated_vertices = numpy.einsum('ij,kj->ki', rotation_matrix_2d(-rotation), normed_vertices)
|
||||||
|
|
||||||
|
# TODO consider how to reorder vertices for polycollection
|
||||||
|
## Reorder the vertices so that the one with lowest x, then y, comes first.
|
||||||
|
#x_min = rotated_vertices[:, 0].argmin()
|
||||||
|
#if not is_scalar(x_min):
|
||||||
|
# y_min = rotated_vertices[x_min, 1].argmin()
|
||||||
|
# x_min = cast('Sequence', x_min)[y_min]
|
||||||
|
#reordered_vertices = numpy.roll(rotated_vertices, -x_min, axis=0)
|
||||||
|
|
||||||
|
# TODO: normalize mirroring?
|
||||||
|
|
||||||
|
return ((type(self), rotated_vertices.data.tobytes() + self._vertex_offsets.tobytes()),
|
||||||
|
(offset, scale / norm_value, rotation, False),
|
||||||
|
lambda: PolyCollection(
|
||||||
|
vertex_lists=rotated_vertices * norm_value,
|
||||||
|
vertex_offsets=self._vertex_offsets,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
centroid = self.vertex_lists.mean(axis=0)
|
||||||
|
return f'<PolyCollection centroid {centroid} p{len(self.vertex_offsets)}>'
|
||||||
@ -1,4 +1,4 @@
|
|||||||
from typing import Any, cast, TYPE_CHECKING
|
from typing import Any, cast, TYPE_CHECKING, Self
|
||||||
import copy
|
import copy
|
||||||
import functools
|
import functools
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ if TYPE_CHECKING:
|
|||||||
class Polygon(Shape):
|
class Polygon(Shape):
|
||||||
"""
|
"""
|
||||||
A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an
|
A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an
|
||||||
implicitly-closed boundary, and an offset.
|
implicitly-closed boundary.
|
||||||
|
|
||||||
Note that the setter for `Polygon.vertices` creates a copy of the
|
Note that the setter for `Polygon.vertices` creates a copy of the
|
||||||
passed vertex coordinates.
|
passed vertex coordinates.
|
||||||
@ -30,7 +30,7 @@ class Polygon(Shape):
|
|||||||
__slots__ = (
|
__slots__ = (
|
||||||
'_vertices',
|
'_vertices',
|
||||||
# Inherited
|
# Inherited
|
||||||
'_offset', '_repetition', '_annotations',
|
'_repetition', '_annotations',
|
||||||
)
|
)
|
||||||
|
|
||||||
_vertices: NDArray[numpy.float64]
|
_vertices: NDArray[numpy.float64]
|
||||||
@ -38,7 +38,7 @@ class Polygon(Shape):
|
|||||||
|
|
||||||
# vertices property
|
# vertices property
|
||||||
@property
|
@property
|
||||||
def vertices(self) -> Any: # mypy#3004 NDArray[numpy.float64]:
|
def vertices(self) -> NDArray[numpy.float64]:
|
||||||
"""
|
"""
|
||||||
Vertices of the polygon (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`)
|
Vertices of the polygon (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`)
|
||||||
|
|
||||||
@ -85,6 +85,28 @@ class Polygon(Shape):
|
|||||||
raise PatternError('Wrong number of vertices')
|
raise PatternError('Wrong number of vertices')
|
||||||
self.vertices[:, 1] = val
|
self.vertices[:, 1] = val
|
||||||
|
|
||||||
|
# Offset property for `Positionable`
|
||||||
|
@property
|
||||||
|
def offset(self) -> NDArray[numpy.float64]:
|
||||||
|
"""
|
||||||
|
[x, y] offset
|
||||||
|
"""
|
||||||
|
return numpy.zeros(2)
|
||||||
|
|
||||||
|
@offset.setter
|
||||||
|
def offset(self, val: ArrayLike) -> None:
|
||||||
|
if numpy.any(val):
|
||||||
|
raise PatternError('Path offset is forced to (0, 0)')
|
||||||
|
|
||||||
|
def set_offset(self, val: ArrayLike) -> Self:
|
||||||
|
if numpy.any(val):
|
||||||
|
raise PatternError('Path offset is forced to (0, 0)')
|
||||||
|
return self
|
||||||
|
|
||||||
|
def translate(self, offset: ArrayLike) -> Self:
|
||||||
|
self._vertices += numpy.atleast_2d(offset)
|
||||||
|
return self
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
vertices: ArrayLike,
|
vertices: ArrayLike,
|
||||||
@ -92,28 +114,26 @@ class Polygon(Shape):
|
|||||||
offset: ArrayLike = (0.0, 0.0),
|
offset: ArrayLike = (0.0, 0.0),
|
||||||
rotation: float = 0.0,
|
rotation: float = 0.0,
|
||||||
repetition: Repetition | None = None,
|
repetition: Repetition | None = None,
|
||||||
annotations: annotations_t | None = None,
|
annotations: annotations_t = None,
|
||||||
raw: bool = False,
|
raw: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
if raw:
|
if raw:
|
||||||
assert isinstance(vertices, numpy.ndarray)
|
assert isinstance(vertices, numpy.ndarray)
|
||||||
assert isinstance(offset, numpy.ndarray)
|
|
||||||
self._vertices = vertices
|
self._vertices = vertices
|
||||||
self._offset = offset
|
|
||||||
self._repetition = repetition
|
self._repetition = repetition
|
||||||
self._annotations = annotations if annotations is not None else {}
|
self._annotations = annotations
|
||||||
else:
|
else:
|
||||||
self.vertices = vertices
|
self.vertices = vertices
|
||||||
self.offset = offset
|
|
||||||
self.repetition = repetition
|
self.repetition = repetition
|
||||||
self.annotations = annotations if annotations is not None else {}
|
self.annotations = annotations
|
||||||
if rotation:
|
if rotation:
|
||||||
self.rotate(rotation)
|
self.rotate(rotation)
|
||||||
|
if numpy.any(offset):
|
||||||
|
self.translate(offset)
|
||||||
|
|
||||||
def __deepcopy__(self, memo: dict | None = None) -> 'Polygon':
|
def __deepcopy__(self, memo: dict | None = None) -> 'Polygon':
|
||||||
memo = {} if memo is None else memo
|
memo = {} if memo is None else memo
|
||||||
new = copy.copy(self)
|
new = copy.copy(self)
|
||||||
new._offset = self._offset.copy()
|
|
||||||
new._vertices = self._vertices.copy()
|
new._vertices = self._vertices.copy()
|
||||||
new._annotations = copy.deepcopy(self._annotations)
|
new._annotations = copy.deepcopy(self._annotations)
|
||||||
return new
|
return new
|
||||||
@ -121,7 +141,6 @@ class Polygon(Shape):
|
|||||||
def __eq__(self, other: Any) -> bool:
|
def __eq__(self, other: Any) -> bool:
|
||||||
return (
|
return (
|
||||||
type(self) is type(other)
|
type(self) is type(other)
|
||||||
and numpy.array_equal(self.offset, other.offset)
|
|
||||||
and numpy.array_equal(self.vertices, other.vertices)
|
and numpy.array_equal(self.vertices, other.vertices)
|
||||||
and self.repetition == other.repetition
|
and self.repetition == other.repetition
|
||||||
and annotations_eq(self.annotations, other.annotations)
|
and annotations_eq(self.annotations, other.annotations)
|
||||||
@ -141,8 +160,6 @@ class Polygon(Shape):
|
|||||||
if eq_lt_masked.size > 0:
|
if eq_lt_masked.size > 0:
|
||||||
return eq_lt_masked.flat[0]
|
return eq_lt_masked.flat[0]
|
||||||
return self.vertices.shape[0] < other.vertices.shape[0]
|
return self.vertices.shape[0] < other.vertices.shape[0]
|
||||||
if not numpy.array_equal(self.offset, other.offset):
|
|
||||||
return tuple(self.offset) < tuple(other.offset)
|
|
||||||
if self.repetition != other.repetition:
|
if self.repetition != other.repetition:
|
||||||
return rep2key(self.repetition) < rep2key(other.repetition)
|
return rep2key(self.repetition) < rep2key(other.repetition)
|
||||||
return annotations_lt(self.annotations, other.annotations)
|
return annotations_lt(self.annotations, other.annotations)
|
||||||
@ -248,11 +265,11 @@ class Polygon(Shape):
|
|||||||
elif xmax is None:
|
elif xmax is None:
|
||||||
assert xmin is not None
|
assert xmin is not None
|
||||||
assert xctr is not None
|
assert xctr is not None
|
||||||
lx = 2 * (xctr - xmin)
|
lx = 2.0 * (xctr - xmin)
|
||||||
elif xmin is None:
|
elif xmin is None:
|
||||||
assert xctr is not None
|
assert xctr is not None
|
||||||
assert xmax is not None
|
assert xmax is not None
|
||||||
lx = 2 * (xmax - xctr)
|
lx = 2.0 * (xmax - xctr)
|
||||||
else:
|
else:
|
||||||
raise PatternError('Two of xmin, xctr, xmax, lx must be None!')
|
raise PatternError('Two of xmin, xctr, xmax, lx must be None!')
|
||||||
else: # noqa: PLR5501
|
else: # noqa: PLR5501
|
||||||
@ -278,11 +295,11 @@ class Polygon(Shape):
|
|||||||
elif ymax is None:
|
elif ymax is None:
|
||||||
assert ymin is not None
|
assert ymin is not None
|
||||||
assert yctr is not None
|
assert yctr is not None
|
||||||
ly = 2 * (yctr - ymin)
|
ly = 2.0 * (yctr - ymin)
|
||||||
elif ymin is None:
|
elif ymin is None:
|
||||||
assert yctr is not None
|
assert yctr is not None
|
||||||
assert ymax is not None
|
assert ymax is not None
|
||||||
ly = 2 * (ymax - yctr)
|
ly = 2.0 * (ymax - yctr)
|
||||||
else:
|
else:
|
||||||
raise PatternError('Two of ymin, yctr, ymax, ly must be None!')
|
raise PatternError('Two of ymin, yctr, ymax, ly must be None!')
|
||||||
else: # noqa: PLR5501
|
else: # noqa: PLR5501
|
||||||
@ -363,8 +380,8 @@ class Polygon(Shape):
|
|||||||
return [copy.deepcopy(self)]
|
return [copy.deepcopy(self)]
|
||||||
|
|
||||||
def get_bounds_single(self) -> NDArray[numpy.float64]: # TODO note shape get_bounds doesn't include repetition
|
def get_bounds_single(self) -> NDArray[numpy.float64]: # TODO note shape get_bounds doesn't include repetition
|
||||||
return numpy.vstack((self.offset + numpy.min(self.vertices, axis=0),
|
return numpy.vstack((numpy.min(self.vertices, axis=0),
|
||||||
self.offset + numpy.max(self.vertices, axis=0)))
|
numpy.max(self.vertices, axis=0)))
|
||||||
|
|
||||||
def rotate(self, theta: float) -> 'Polygon':
|
def rotate(self, theta: float) -> 'Polygon':
|
||||||
if theta != 0:
|
if theta != 0:
|
||||||
@ -384,7 +401,7 @@ class Polygon(Shape):
|
|||||||
# other shapes
|
# other shapes
|
||||||
meanv = self.vertices.mean(axis=0)
|
meanv = self.vertices.mean(axis=0)
|
||||||
zeroed_vertices = self.vertices - meanv
|
zeroed_vertices = self.vertices - meanv
|
||||||
offset = meanv + self.offset
|
offset = meanv
|
||||||
|
|
||||||
scale = zeroed_vertices.std()
|
scale = zeroed_vertices.std()
|
||||||
normed_vertices = zeroed_vertices / scale
|
normed_vertices = zeroed_vertices / scale
|
||||||
@ -438,5 +455,5 @@ class Polygon(Shape):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
centroid = self.offset + self.vertices.mean(axis=0)
|
centroid = self.vertices.mean(axis=0)
|
||||||
return f'<Polygon centroid {centroid} v{len(self.vertices)}>'
|
return f'<Polygon centroid {centroid} v{len(self.vertices)}>'
|
||||||
|
|||||||
@ -7,7 +7,7 @@ from numpy.typing import NDArray, ArrayLike
|
|||||||
|
|
||||||
from ..traits import (
|
from ..traits import (
|
||||||
Rotatable, Mirrorable, Copyable, Scalable,
|
Rotatable, Mirrorable, Copyable, Scalable,
|
||||||
PositionableImpl, PivotableImpl, RepeatableImpl, AnnotatableImpl,
|
Positionable, PivotableImpl, RepeatableImpl, AnnotatableImpl,
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -26,7 +26,7 @@ normalized_shape_tuple = tuple[
|
|||||||
DEFAULT_POLY_NUM_VERTICES = 24
|
DEFAULT_POLY_NUM_VERTICES = 24
|
||||||
|
|
||||||
|
|
||||||
class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable,
|
class Shape(Positionable, Rotatable, Mirrorable, Copyable, Scalable,
|
||||||
PivotableImpl, RepeatableImpl, AnnotatableImpl, metaclass=ABCMeta):
|
PivotableImpl, RepeatableImpl, AnnotatableImpl, metaclass=ABCMeta):
|
||||||
"""
|
"""
|
||||||
Class specifying functions common to all shapes.
|
Class specifying functions common to all shapes.
|
||||||
@ -134,7 +134,7 @@ class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable,
|
|||||||
mins, maxs = bounds
|
mins, maxs = bounds
|
||||||
|
|
||||||
vertex_lists = []
|
vertex_lists = []
|
||||||
p_verts = polygon.vertices + polygon.offset
|
p_verts = polygon.vertices
|
||||||
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
|
||||||
|
|
||||||
@ -282,7 +282,7 @@ class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable,
|
|||||||
offset = (numpy.where(keep_x)[0][0],
|
offset = (numpy.where(keep_x)[0][0],
|
||||||
numpy.where(keep_y)[0][0])
|
numpy.where(keep_y)[0][0])
|
||||||
|
|
||||||
rastered = float_raster.raster((polygon.vertices + polygon.offset).T, gx, gy)
|
rastered = float_raster.raster((polygon.vertices).T, gx, gy)
|
||||||
binary_rastered = (numpy.abs(rastered) >= 0.5)
|
binary_rastered = (numpy.abs(rastered) >= 0.5)
|
||||||
supersampled = binary_rastered.repeat(2, axis=0).repeat(2, axis=1)
|
supersampled = binary_rastered.repeat(2, axis=0).repeat(2, axis=1)
|
||||||
|
|
||||||
|
|||||||
@ -9,8 +9,8 @@ from numpy.typing import NDArray, ArrayLike
|
|||||||
from . import Shape, Polygon, normalized_shape_tuple
|
from . import Shape, Polygon, normalized_shape_tuple
|
||||||
from ..error import PatternError
|
from ..error import PatternError
|
||||||
from ..repetition import Repetition
|
from ..repetition import Repetition
|
||||||
from ..traits import RotatableImpl
|
from ..traits import PositionableImpl, RotatableImpl
|
||||||
from ..utils import is_scalar, get_bit, annotations_t, annotations_lt, annotations_eq, rep2key
|
from ..utils import is_scalar, get_bit, annotations_t, annotations_lt, annotations_eq, rep2key, SupportsBool
|
||||||
|
|
||||||
# Loaded on use:
|
# Loaded on use:
|
||||||
# from freetype import Face
|
# from freetype import Face
|
||||||
@ -18,7 +18,7 @@ from ..utils import is_scalar, get_bit, annotations_t, annotations_lt, annotatio
|
|||||||
|
|
||||||
|
|
||||||
@functools.total_ordering
|
@functools.total_ordering
|
||||||
class Text(RotatableImpl, Shape):
|
class Text(PositionableImpl, RotatableImpl, Shape):
|
||||||
"""
|
"""
|
||||||
Text (to be printed e.g. as a set of polygons).
|
Text (to be printed e.g. as a set of polygons).
|
||||||
This is distinct from non-printed Label objects.
|
This is distinct from non-printed Label objects.
|
||||||
@ -55,11 +55,11 @@ class Text(RotatableImpl, Shape):
|
|||||||
self._height = val
|
self._height = val
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mirrored(self) -> bool: # mypy#3004, should be bool
|
def mirrored(self) -> bool:
|
||||||
return self._mirrored
|
return self._mirrored
|
||||||
|
|
||||||
@mirrored.setter
|
@mirrored.setter
|
||||||
def mirrored(self, val: bool) -> None:
|
def mirrored(self, val: SupportsBool) -> None:
|
||||||
self._mirrored = bool(val)
|
self._mirrored = bool(val)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -71,7 +71,7 @@ class Text(RotatableImpl, Shape):
|
|||||||
offset: ArrayLike = (0.0, 0.0),
|
offset: ArrayLike = (0.0, 0.0),
|
||||||
rotation: float = 0.0,
|
rotation: float = 0.0,
|
||||||
repetition: Repetition | None = None,
|
repetition: Repetition | None = None,
|
||||||
annotations: annotations_t | None = None,
|
annotations: annotations_t = None,
|
||||||
raw: bool = False,
|
raw: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
if raw:
|
if raw:
|
||||||
@ -81,14 +81,14 @@ class Text(RotatableImpl, Shape):
|
|||||||
self._height = height
|
self._height = height
|
||||||
self._rotation = rotation
|
self._rotation = rotation
|
||||||
self._repetition = repetition
|
self._repetition = repetition
|
||||||
self._annotations = annotations if annotations is not None else {}
|
self._annotations = annotations
|
||||||
else:
|
else:
|
||||||
self.offset = offset
|
self.offset = offset
|
||||||
self.string = string
|
self.string = string
|
||||||
self.height = height
|
self.height = height
|
||||||
self.rotation = rotation
|
self.rotation = rotation
|
||||||
self.repetition = repetition
|
self.repetition = repetition
|
||||||
self.annotations = annotations if annotations is not None else {}
|
self.annotations = annotations
|
||||||
self.font_path = font_path
|
self.font_path = font_path
|
||||||
|
|
||||||
def __deepcopy__(self, memo: dict | None = None) -> Self:
|
def __deepcopy__(self, memo: dict | None = None) -> Self:
|
||||||
@ -201,7 +201,7 @@ def get_char_as_polygons(
|
|||||||
font_path: str,
|
font_path: str,
|
||||||
char: str,
|
char: str,
|
||||||
resolution: float = 48 * 64,
|
resolution: float = 48 * 64,
|
||||||
) -> tuple[list[list[list[float]]], float]:
|
) -> tuple[list[NDArray[numpy.float64]], float]:
|
||||||
from freetype import Face # type: ignore
|
from freetype import Face # type: ignore
|
||||||
from matplotlib.path import Path # type: ignore
|
from matplotlib.path import Path # type: ignore
|
||||||
|
|
||||||
@ -276,11 +276,12 @@ def get_char_as_polygons(
|
|||||||
|
|
||||||
advance = slot.advance.x / resolution
|
advance = slot.advance.x / resolution
|
||||||
|
|
||||||
|
polygons: list[NDArray[numpy.float64]]
|
||||||
if len(all_verts) == 0:
|
if len(all_verts) == 0:
|
||||||
polygons = []
|
polygons = []
|
||||||
else:
|
else:
|
||||||
path = Path(all_verts, all_codes)
|
path = Path(all_verts, all_codes)
|
||||||
path.should_simplify = False
|
path.should_simplify = False
|
||||||
polygons = path.to_polygons()
|
polygons = [numpy.asarray(poly) for poly in path.to_polygons()]
|
||||||
|
|
||||||
return polygons, advance
|
return polygons, advance
|
||||||
|
|||||||
@ -45,6 +45,6 @@ class AnnotatableImpl(Annotatable, metaclass=ABCMeta):
|
|||||||
|
|
||||||
@annotations.setter
|
@annotations.setter
|
||||||
def annotations(self, annotations: annotations_t) -> None:
|
def annotations(self, annotations: annotations_t) -> None:
|
||||||
if not isinstance(annotations, dict):
|
if not isinstance(annotations, dict) and annotations is not None:
|
||||||
raise MasqueError(f'annotations expected dict, got {type(annotations)}')
|
raise MasqueError(f'annotations expected dict or None, got {type(annotations)}')
|
||||||
self._annotations = annotations
|
self._annotations = annotations
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from typing import Self, Any
|
from typing import Self
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
@ -73,7 +73,7 @@ class PositionableImpl(Positionable, metaclass=ABCMeta):
|
|||||||
#
|
#
|
||||||
# offset property
|
# offset property
|
||||||
@property
|
@property
|
||||||
def offset(self) -> Any: # mypy#3004 NDArray[numpy.float64]:
|
def offset(self) -> NDArray[numpy.float64]:
|
||||||
"""
|
"""
|
||||||
[x, y] offset
|
[x, y] offset
|
||||||
"""
|
"""
|
||||||
@ -95,7 +95,7 @@ class PositionableImpl(Positionable, metaclass=ABCMeta):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def translate(self, offset: ArrayLike) -> Self:
|
def translate(self, offset: ArrayLike) -> Self:
|
||||||
self._offset += offset # type: ignore # NDArray += ArrayLike should be fine??
|
self._offset += numpy.asarray(offset)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -116,7 +116,7 @@ class PivotableImpl(Pivotable, metaclass=ABCMeta):
|
|||||||
pivot = numpy.asarray(pivot, dtype=float)
|
pivot = numpy.asarray(pivot, dtype=float)
|
||||||
cast('Positionable', self).translate(-pivot)
|
cast('Positionable', self).translate(-pivot)
|
||||||
cast('Rotatable', self).rotate(rotation)
|
cast('Rotatable', self).rotate(rotation)
|
||||||
self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) # type: ignore # mypy#3004
|
self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset)
|
||||||
cast('Positionable', self).translate(+pivot)
|
cast('Positionable', self).translate(+pivot)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ from numpy import pi
|
|||||||
try:
|
try:
|
||||||
from numpy import trapezoid
|
from numpy import trapezoid
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from numpy import trapz as trapezoid
|
from numpy import trapz as trapezoid # type:ignore
|
||||||
|
|
||||||
|
|
||||||
def bezier(
|
def bezier(
|
||||||
|
|||||||
@ -5,7 +5,7 @@ from typing import Protocol
|
|||||||
|
|
||||||
|
|
||||||
layer_t = int | tuple[int, int] | str
|
layer_t = int | tuple[int, int] | str
|
||||||
annotations_t = dict[str, list[int | float | str]]
|
annotations_t = dict[str, list[int | float | str]] | None
|
||||||
|
|
||||||
|
|
||||||
class SupportsBool(Protocol):
|
class SupportsBool(Protocol):
|
||||||
|
|||||||
@ -56,6 +56,7 @@ dxf = ["ezdxf~=1.0.2"]
|
|||||||
svg = ["svgwrite"]
|
svg = ["svgwrite"]
|
||||||
visualize = ["matplotlib"]
|
visualize = ["matplotlib"]
|
||||||
text = ["matplotlib", "freetype-py"]
|
text = ["matplotlib", "freetype-py"]
|
||||||
|
manhatanize_slow = ["float_raster"]
|
||||||
|
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user