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:
|
||||
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)
|
||||
my_pattern.ref(new_name, ...) # instantiate the cell
|
||||
|
||||
@ -176,7 +176,7 @@ my_pattern.place(library << make_tree(...), ...)
|
||||
|
||||
|
||||
### Quickly add geometry, labels, or refs:
|
||||
The long form for adding elements can be overly verbose:
|
||||
Adding elements can be overly verbose:
|
||||
```python3
|
||||
my_pattern.shapes[layer].append(Polygon(vertices, ...))
|
||||
my_pattern.labels[layer] += [Label('my text')]
|
||||
@ -228,9 +228,11 @@ my_pattern.ref(_make_my_subpattern(), offset=..., ...)
|
||||
|
||||
## 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`)
|
||||
- de-embedding
|
||||
- boolean ops
|
||||
* Tests tests tests
|
||||
* check renderpather
|
||||
* pather and renderpather examples
|
||||
|
||||
@ -77,8 +77,10 @@ from .builder import (
|
||||
Pather as Pather,
|
||||
RenderPather as RenderPather,
|
||||
RenderStep as RenderStep,
|
||||
BasicTool as BasicTool,
|
||||
SimpleTool as SimpleTool,
|
||||
AutoTool as AutoTool,
|
||||
PathTool as PathTool,
|
||||
PortPather as PortPather,
|
||||
)
|
||||
from .utils import (
|
||||
ports2data as ports2data,
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
from .builder import Builder as Builder
|
||||
from .pather import Pather as Pather
|
||||
from .renderpather import RenderPather as RenderPather
|
||||
from .pather_mixin import PortPather as PortPather
|
||||
from .utils import ell as ell
|
||||
from .tools import (
|
||||
Tool as Tool,
|
||||
RenderStep as RenderStep,
|
||||
BasicTool as BasicTool,
|
||||
SimpleTool as SimpleTool,
|
||||
AutoTool as AutoTool,
|
||||
PathTool as PathTool,
|
||||
)
|
||||
|
||||
@ -67,7 +67,7 @@ class Builder(PortList):
|
||||
|
||||
- `my_device.plug(wire, {'myport': 'A'})` places port 'A' of `wire` at 'myport'
|
||||
of `my_device`. If `wire` has only two ports (e.g. 'A' and 'B'), no `map_out`,
|
||||
argument is provided, and the `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
|
||||
'myport'. This allows easy extension of existing ports without changing
|
||||
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,
|
||||
*,
|
||||
mirrored: bool = False,
|
||||
inherit_name: bool = True,
|
||||
thru: bool | str = True,
|
||||
set_rotation: bool | None = None,
|
||||
append: bool = False,
|
||||
ok_connections: Iterable[tuple[str, str]] = (),
|
||||
@ -246,11 +246,15 @@ class Builder(PortList):
|
||||
new names for ports in `other`.
|
||||
mirrored: Enables mirroring `other` across the x axis prior to
|
||||
connecting any ports.
|
||||
inherit_name: If `True`, and `map_in` specifies only a single port,
|
||||
and `map_out` is `None`, and `other` has only two ports total,
|
||||
then automatically renames the output port of `other` to the
|
||||
name of the port from `self` that appears in `map_in`. This
|
||||
makes it easy to extend a device with simple 2-port devices
|
||||
thru: If map_in specifies only a single port, `thru` provides a mechainsm
|
||||
to avoid repeating the port name. Eg, for `map_in={'myport': 'A'}`,
|
||||
- If True (default), and `other` has only two ports total, and map_out
|
||||
doesn't specify a name for the other port, its name is set to the key
|
||||
in `map_in`, i.e. 'myport'.
|
||||
- If a string, `map_out[thru]` is set to the key in `map_in` (i.e. 'myport').
|
||||
An error is raised if that entry already exists.
|
||||
|
||||
This makes it easy to extend a pattern with simple 2-port devices
|
||||
(e.g. wires) without providing `map_out` each time `plug` is
|
||||
called. See "Examples" above for more info. Default `True`.
|
||||
set_rotation: If the necessary rotation cannot be determined from
|
||||
@ -292,14 +296,14 @@ class Builder(PortList):
|
||||
other = self.library[other.name]
|
||||
|
||||
self.pattern.plug(
|
||||
other=other,
|
||||
map_in=map_in,
|
||||
map_out=map_out,
|
||||
mirrored=mirrored,
|
||||
inherit_name=inherit_name,
|
||||
set_rotation=set_rotation,
|
||||
append=append,
|
||||
ok_connections=ok_connections,
|
||||
other = other,
|
||||
map_in = map_in,
|
||||
map_out = map_out,
|
||||
mirrored = mirrored,
|
||||
thru = thru,
|
||||
set_rotation = set_rotation,
|
||||
append = append,
|
||||
ok_connections = ok_connections,
|
||||
)
|
||||
return self
|
||||
|
||||
@ -365,14 +369,14 @@ class Builder(PortList):
|
||||
other = self.library[other.name]
|
||||
|
||||
self.pattern.place(
|
||||
other=other,
|
||||
offset=offset,
|
||||
rotation=rotation,
|
||||
pivot=pivot,
|
||||
mirrored=mirrored,
|
||||
port_map=port_map,
|
||||
skip_port_check=skip_port_check,
|
||||
append=append,
|
||||
other = other,
|
||||
offset = offset,
|
||||
rotation = rotation,
|
||||
pivot = pivot,
|
||||
mirrored = mirrored,
|
||||
port_map = port_map,
|
||||
skip_port_check = skip_port_check,
|
||||
append = append,
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
@ -2,31 +2,25 @@
|
||||
Manual wire/waveguide routing (`Pather`)
|
||||
"""
|
||||
from typing import Self
|
||||
from collections.abc import Sequence, MutableMapping, Mapping, Iterator
|
||||
from collections.abc import Sequence, Mapping, MutableMapping
|
||||
import copy
|
||||
import logging
|
||||
from contextlib import contextmanager
|
||||
from pprint import pformat
|
||||
|
||||
import numpy
|
||||
from numpy import pi
|
||||
from numpy.typing import ArrayLike
|
||||
|
||||
from ..pattern import Pattern
|
||||
from ..library import ILibrary, SINGLE_USE_PREFIX
|
||||
from ..error import PortError, BuildError
|
||||
from ..library import ILibrary
|
||||
from ..error import BuildError
|
||||
from ..ports import PortList, Port
|
||||
from ..abstract import Abstract
|
||||
from ..utils import SupportsBool, rotation_matrix_2d
|
||||
from ..utils import SupportsBool
|
||||
from .tools import Tool
|
||||
from .utils import ell
|
||||
from .pather_mixin import PatherMixin
|
||||
from .builder import Builder
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Pather(Builder):
|
||||
class Pather(Builder, PatherMixin):
|
||||
"""
|
||||
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.
|
||||
@ -258,60 +252,6 @@ class Pather(Builder):
|
||||
s = f'<Pather {self.pattern} L({len(self.library)}) {pformat(self.tools)}>'
|
||||
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(
|
||||
self,
|
||||
@ -319,7 +259,6 @@ class Pather(Builder):
|
||||
ccw: SupportsBool | None,
|
||||
length: float,
|
||||
*,
|
||||
tool_port_names: tuple[str, str] = ('A', 'B'),
|
||||
plug_into: str | None = None,
|
||||
**kwargs,
|
||||
) -> Self:
|
||||
@ -327,7 +266,7 @@ class Pather(Builder):
|
||||
Create a "wire"/"waveguide" and `plug` it into the port `portspec`, with the aim
|
||||
of traveling exactly `length` distance.
|
||||
|
||||
The wire will travel `length` distance along the port's axis, an an unspecified
|
||||
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
|
||||
be rotated (or not) based on the `ccw` parameter.
|
||||
|
||||
@ -338,9 +277,6 @@ class Pather(Builder):
|
||||
and clockwise otherwise.
|
||||
length: The total distance from input to output, along the input's axis only.
|
||||
(There may be a tool-dependent offset along the other axis.)
|
||||
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
|
||||
port on `self`.
|
||||
|
||||
@ -355,54 +291,44 @@ class Pather(Builder):
|
||||
logger.error('Skipping path() since device is dead')
|
||||
return self
|
||||
|
||||
tool_port_names = ('A', 'B')
|
||||
|
||||
tool = self.tools.get(portspec, self.tools[None])
|
||||
in_ptype = self.pattern[portspec].ptype
|
||||
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:
|
||||
output = {plug_into: tool_port_names[1]}
|
||||
else:
|
||||
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,
|
||||
portspec: str,
|
||||
ccw: SupportsBool | None,
|
||||
position: float | None = None,
|
||||
length: float,
|
||||
jog: float,
|
||||
*,
|
||||
x: float | None = None,
|
||||
y: float | None = None,
|
||||
tool_port_names: tuple[str, str] = ('A', 'B'),
|
||||
plug_into: str | None = None,
|
||||
**kwargs,
|
||||
) -> Self:
|
||||
"""
|
||||
Create a "wire"/"waveguide" and `plug` it into the port `portspec`, with the aim
|
||||
of ending exactly at a target position.
|
||||
Create an S-shaped "wire"/"waveguide" and `plug` it into the port `portspec`, with the aim
|
||||
of traveling exactly `length` distance with an offset `jog` along the other axis (+ve jog is
|
||||
left of direction of travel).
|
||||
|
||||
The 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.
|
||||
The output port will have the same orientation as the source port (`portspec`).
|
||||
|
||||
This function attempts to use `tool.planS()`, but falls back to `tool.planL()` if the former
|
||||
raises a NotImplementedError.
|
||||
|
||||
Args:
|
||||
portspec: The name of the port into which the wire will be plugged.
|
||||
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.
|
||||
jog: Total manhattan distance perpendicular to the direction of travel.
|
||||
Positive values are to the left of the direction of travel.
|
||||
length: The total manhattan distance from input to output, along the input's axis only.
|
||||
(There may be a tool-dependent offset along the other axis.)
|
||||
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
|
||||
port on `self`.
|
||||
|
||||
@ -410,317 +336,40 @@ class Pather(Builder):
|
||||
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.
|
||||
BuildError if `distance` is too small to fit the s-bend (for nonzero jog).
|
||||
LibraryError if no valid name could be picked for the pattern.
|
||||
"""
|
||||
if self._dead:
|
||||
logger.error('Skipping path_to() since device is dead')
|
||||
logger.error('Skipping pathS() 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')
|
||||
tool_port_names = ('A', 'B')
|
||||
|
||||
port = self.pattern[portspec]
|
||||
if port.rotation is None:
|
||||
raise PortError(f'Port {portspec} has no rotation and cannot be used for path_to()')
|
||||
tool = self.tools.get(portspec, self.tools[None])
|
||||
in_ptype = self.pattern[portspec].ptype
|
||||
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):
|
||||
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,
|
||||
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')
|
||||
kwargs_plug = kwargs | {'plug_into': plug_into}
|
||||
self.path(portspec, ccw0, length - abs(jog1), **kwargs_no_out)
|
||||
self.path(portspec, not ccw0, abs(jog - jog0), **kwargs_plug)
|
||||
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)
|
||||
|
||||
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!')
|
||||
tname = self.library << tree
|
||||
if plug_into is not None:
|
||||
output = {plug_into: tool_port_names[1]}
|
||||
else:
|
||||
raise BuildError(f'Don\'t know how to route ports with relative angle {angle}')
|
||||
|
||||
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)
|
||||
output = {}
|
||||
self.plug(tname, {portspec: tool_port_names[0], **output})
|
||||
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
|
||||
"""
|
||||
from typing import Self
|
||||
from collections.abc import Sequence, Mapping, MutableMapping
|
||||
from collections.abc import Sequence, Mapping, MutableMapping, Iterable
|
||||
import copy
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from functools import wraps
|
||||
from pprint import pformat
|
||||
|
||||
import numpy
|
||||
from numpy import pi
|
||||
from numpy.typing import ArrayLike
|
||||
|
||||
from ..pattern import Pattern
|
||||
from ..library import ILibrary
|
||||
from ..error import PortError, BuildError
|
||||
from ..library import ILibrary, TreeView
|
||||
from ..error import BuildError
|
||||
from ..ports import PortList, Port
|
||||
from ..abstract import Abstract
|
||||
from ..utils import SupportsBool
|
||||
from .tools import Tool, RenderStep
|
||||
from .utils import ell
|
||||
from .pather_mixin import PatherMixin
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RenderPather(PortList):
|
||||
class RenderPather(PatherMixin):
|
||||
"""
|
||||
`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,
|
||||
@ -108,15 +108,11 @@ class RenderPather(PortList):
|
||||
if self.pattern.ports:
|
||||
raise BuildError('Ports supplied for pattern with pre-existing ports!')
|
||||
if isinstance(ports, str):
|
||||
if library is None:
|
||||
raise BuildError('Ports given as a string, but `library` was `None`!')
|
||||
ports = library.abstract(ports).ports
|
||||
|
||||
self.pattern.ports.update(copy.deepcopy(dict(ports)))
|
||||
|
||||
if name is not None:
|
||||
if library is None:
|
||||
raise BuildError('Name was supplied, but no library was given!')
|
||||
library[name] = self.pattern
|
||||
|
||||
if tools is None:
|
||||
@ -186,16 +182,21 @@ class RenderPather(PortList):
|
||||
new = RenderPather(library=library, pattern=pat, name=name, tools=tools)
|
||||
return new
|
||||
|
||||
def __repr__(self) -> str:
|
||||
s = f'<RenderPather {self.pattern} L({len(self.library)}) {pformat(self.tools)}>'
|
||||
return s
|
||||
|
||||
def plug(
|
||||
self,
|
||||
other: Abstract | str,
|
||||
other: Abstract | str | Pattern | TreeView,
|
||||
map_in: dict[str, str],
|
||||
map_out: dict[str, str | None] | None = None,
|
||||
*,
|
||||
mirrored: bool = False,
|
||||
inherit_name: bool = True,
|
||||
thru: bool | str = True,
|
||||
set_rotation: bool | None = None,
|
||||
append: bool = False,
|
||||
ok_connections: Iterable[tuple[str, str]] = (),
|
||||
) -> Self:
|
||||
"""
|
||||
Wrapper for `Pattern.plug` which adds a `RenderStep` with opcode 'P'
|
||||
@ -210,11 +211,15 @@ class RenderPather(PortList):
|
||||
new names for ports in `other`.
|
||||
mirrored: Enables mirroring `other` across the x axis prior to
|
||||
connecting any ports.
|
||||
inherit_name: If `True`, and `map_in` specifies only a single port,
|
||||
and `map_out` is `None`, and `other` has only two ports total,
|
||||
then automatically renames the output port of `other` to the
|
||||
name of the port from `self` that appears in `map_in`. This
|
||||
makes it easy to extend a device with simple 2-port devices
|
||||
thru: If map_in specifies only a single port, `thru` provides a mechainsm
|
||||
to avoid repeating the port name. Eg, for `map_in={'myport': 'A'}`,
|
||||
- If True (default), and `other` has only two ports total, and map_out
|
||||
doesn't specify a name for the other port, its name is set to the key
|
||||
in `map_in`, i.e. 'myport'.
|
||||
- If a string, `map_out[thru]` is set to the key in `map_in` (i.e. 'myport').
|
||||
An error is raised if that entry already exists.
|
||||
|
||||
This makes it easy to extend a pattern with simple 2-port devices
|
||||
(e.g. wires) without providing `map_out` each time `plug` is
|
||||
called. See "Examples" above for more info. Default `True`.
|
||||
set_rotation: If the necessary rotation cannot be determined from
|
||||
@ -225,6 +230,12 @@ class RenderPather(PortList):
|
||||
append: If `True`, `other` is appended instead of being referenced.
|
||||
Note that this does not flatten `other`, so its refs will still
|
||||
be refs (now inside `self`).
|
||||
ok_connections: Set of "allowed" ptype combinations. Identical
|
||||
ptypes are always allowed to connect, as is `'unk'` with
|
||||
any other ptypte. Non-allowed ptype connections will emit a
|
||||
warning. Order is ignored, i.e. `(a, b)` is equivalent to
|
||||
`(b, a)`.
|
||||
|
||||
|
||||
Returns:
|
||||
self
|
||||
@ -261,13 +272,14 @@ class RenderPather(PortList):
|
||||
self.paths[new_name].append(RenderStep('P', None, port.copy(), port.copy(), None))
|
||||
|
||||
self.pattern.plug(
|
||||
other=other_tgt,
|
||||
map_in=map_in,
|
||||
map_out=map_out,
|
||||
mirrored=mirrored,
|
||||
inherit_name=inherit_name,
|
||||
set_rotation=set_rotation,
|
||||
append=append,
|
||||
other = other_tgt,
|
||||
map_in = map_in,
|
||||
map_out = map_out,
|
||||
mirrored = mirrored,
|
||||
thru = thru,
|
||||
set_rotation = set_rotation,
|
||||
append = append,
|
||||
ok_connections = ok_connections,
|
||||
)
|
||||
|
||||
return self
|
||||
@ -333,40 +345,28 @@ class RenderPather(PortList):
|
||||
self.paths[new_name].append(RenderStep('P', None, port.copy(), port.copy(), None))
|
||||
|
||||
self.pattern.place(
|
||||
other=other_tgt,
|
||||
offset=offset,
|
||||
rotation=rotation,
|
||||
pivot=pivot,
|
||||
mirrored=mirrored,
|
||||
port_map=port_map,
|
||||
skip_port_check=skip_port_check,
|
||||
append=append,
|
||||
other = other_tgt,
|
||||
offset = offset,
|
||||
rotation = rotation,
|
||||
pivot = pivot,
|
||||
mirrored = mirrored,
|
||||
port_map = port_map,
|
||||
skip_port_check = skip_port_check,
|
||||
append = append,
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
def retool(
|
||||
def plugged(
|
||||
self,
|
||||
tool: Tool,
|
||||
keys: str | Sequence[str | None] | None = None,
|
||||
connections: dict[str, str],
|
||||
) -> 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
|
||||
for aa, bb in connections.items():
|
||||
porta = self.ports[aa]
|
||||
portb = self.ports[bb]
|
||||
self.paths[aa].append(RenderStep('P', None, porta.copy(), porta.copy(), None))
|
||||
self.paths[bb].append(RenderStep('P', None, portb.copy(), portb.copy(), None))
|
||||
PortList.plugged(self, connections)
|
||||
return self
|
||||
|
||||
def path(
|
||||
@ -374,6 +374,8 @@ class RenderPather(PortList):
|
||||
portspec: str,
|
||||
ccw: SupportsBool | None,
|
||||
length: float,
|
||||
*,
|
||||
plug_into: str | None = None,
|
||||
**kwargs,
|
||||
) -> Self:
|
||||
"""
|
||||
@ -393,6 +395,8 @@ class RenderPather(PortList):
|
||||
and clockwise otherwise.
|
||||
length: The total distance from input to output, along the input's axis only.
|
||||
(There may be a tool-dependent offset along the other axis.)
|
||||
plug_into: If not None, attempts to plug the wire's output port into the provided
|
||||
port on `self`.
|
||||
|
||||
Returns:
|
||||
self
|
||||
@ -423,163 +427,87 @@ class RenderPather(PortList):
|
||||
|
||||
self.pattern.ports[portspec] = out_port.copy()
|
||||
|
||||
if plug_into is not None:
|
||||
self.plugged({portspec: plug_into})
|
||||
|
||||
return self
|
||||
|
||||
def path_to(
|
||||
def pathS(
|
||||
self,
|
||||
portspec: str,
|
||||
ccw: SupportsBool | None,
|
||||
position: float | None = None,
|
||||
length: float,
|
||||
jog: float,
|
||||
*,
|
||||
x: float | None = None,
|
||||
y: float | None = None,
|
||||
plug_into: str | None = None,
|
||||
**kwargs,
|
||||
) -> Self:
|
||||
"""
|
||||
Plan a "wire"/"waveguide" extending from the port `portspec`, with the aim
|
||||
of ending exactly at a target position.
|
||||
Create an S-shaped "wire"/"waveguide" and `plug` it into the port `portspec`, with the aim
|
||||
of traveling exactly `length` distance with an offset `jog` along the other axis (+ve jog is
|
||||
left of direction of travel).
|
||||
|
||||
The 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.
|
||||
The output port will have the same orientation as the source port (`portspec`).
|
||||
|
||||
`RenderPather.render` must be called after all paths have been fully planned.
|
||||
|
||||
This function attempts to use `tool.planS()`, but falls back to `tool.planL()` if the former
|
||||
raises a NotImplementedError.
|
||||
|
||||
Args:
|
||||
portspec: The name of the port into which the wire will be plugged.
|
||||
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.
|
||||
jog: Total manhattan distance perpendicular to the direction of travel.
|
||||
Positive values are to the left of the direction of travel.
|
||||
length: The total manhattan distance from input to output, along the input's axis only.
|
||||
(There may be a tool-dependent offset along the other axis.)
|
||||
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.
|
||||
BuildError if `distance` is too small to fit the s-bend (for nonzero jog).
|
||||
LibraryError if no valid name could be picked for the pattern.
|
||||
"""
|
||||
if self._dead:
|
||||
logger.error('Skipping path_to() since device is dead')
|
||||
logger.error('Skipping pathS() 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()')
|
||||
in_ptype = port.ptype
|
||||
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):
|
||||
raise BuildError('path_to was asked to route from non-manhattan port')
|
||||
tool = self.tools.get(portspec, self.tools[None])
|
||||
|
||||
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
|
||||
# check feasibility, get output port and data
|
||||
try:
|
||||
out_port, data = tool.planS(length, jog, in_ptype=in_ptype, **kwargs)
|
||||
except NotImplementedError:
|
||||
# Fall back to drawing two L-bends
|
||||
ccw0 = jog > 0
|
||||
kwargs_no_out = (kwargs | {'out_ptype': None})
|
||||
t_port0, _ = tool.planL( ccw0, length / 2, in_ptype=in_ptype, **kwargs_no_out)
|
||||
jog0 = Port((0, 0), 0).measure_travel(t_port0)[0][1]
|
||||
t_port1, _ = tool.planL(not ccw0, abs(jog - jog0), in_ptype=t_port0.ptype, **kwargs)
|
||||
jog1 = Port((0, 0), 0).measure_travel(t_port1)[0][1]
|
||||
|
||||
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, **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')
|
||||
kwargs_plug = kwargs | {'plug_into': plug_into}
|
||||
self.path(portspec, ccw0, length - abs(jog1), **kwargs_no_out)
|
||||
self.path(portspec, not ccw0, abs(jog - jog0), **kwargs_plug)
|
||||
return self
|
||||
|
||||
bound_types = set()
|
||||
if 'bound_type' in kwargs:
|
||||
bound_types.add(kwargs['bound_type'])
|
||||
bound = 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]
|
||||
out_port.rotate_around((0, 0), pi + port_rot)
|
||||
out_port.translate(port.offset)
|
||||
step = RenderStep('S', tool, port.copy(), out_port.copy(), data)
|
||||
self.paths[portspec].append(step)
|
||||
self.pattern.ports[portspec] = out_port.copy()
|
||||
|
||||
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:
|
||||
# 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)
|
||||
if plug_into is not None:
|
||||
self.plugged({portspec: plug_into})
|
||||
return self
|
||||
|
||||
|
||||
def render(
|
||||
self,
|
||||
append: bool = True,
|
||||
@ -696,8 +624,23 @@ class RenderPather(PortList):
|
||||
self._dead = True
|
||||
return self
|
||||
|
||||
def __repr__(self) -> str:
|
||||
s = f'<Pather {self.pattern} L({len(self.library)}) {pformat(self.tools)}>'
|
||||
return s
|
||||
@wraps(Pattern.label)
|
||||
def label(self, *args, **kwargs) -> Self:
|
||||
self.pattern.label(*args, **kwargs)
|
||||
return self
|
||||
|
||||
@wraps(Pattern.ref)
|
||||
def ref(self, *args, **kwargs) -> Self:
|
||||
self.pattern.ref(*args, **kwargs)
|
||||
return self
|
||||
|
||||
@wraps(Pattern.polygon)
|
||||
def polygon(self, *args, **kwargs) -> Self:
|
||||
self.pattern.polygon(*args, **kwargs)
|
||||
return self
|
||||
|
||||
@wraps(Pattern.rect)
|
||||
def rect(self, *args, **kwargs) -> Self:
|
||||
self.pattern.rect(*args, **kwargs)
|
||||
return self
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ Tools are objects which dynamically generate simple single-use devices (e.g. wir
|
||||
|
||||
# TODO document all tools
|
||||
"""
|
||||
from typing import Literal, Any
|
||||
from typing import Literal, Any, Self
|
||||
from collections.abc import Sequence, Callable
|
||||
from abc import ABCMeta # , abstractmethod # TODO any way to make Tool ok with implementing only one method?
|
||||
from dataclasses import dataclass
|
||||
@ -70,7 +70,7 @@ class Tool:
|
||||
Create a wire or waveguide that travels exactly `length` distance along the axis
|
||||
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
|
||||
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)}')
|
||||
|
||||
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(
|
||||
self,
|
||||
ccw: SupportsBool | None,
|
||||
@ -135,7 +177,7 @@ class Tool:
|
||||
kwargs: Custom tool-specific parameters.
|
||||
|
||||
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.
|
||||
|
||||
Raises:
|
||||
@ -173,7 +215,7 @@ class Tool:
|
||||
kwargs: Custom tool-specific parameters.
|
||||
|
||||
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.
|
||||
|
||||
Raises:
|
||||
@ -204,14 +246,14 @@ class Tool:
|
||||
|
||||
Args:
|
||||
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
|
||||
by a counterclockwise bend)
|
||||
A positive number implies a leftwards shift (i.e. counterclockwise bend
|
||||
followed by a clockwise bend)
|
||||
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.
|
||||
kwargs: Custom tool-specific parameters.
|
||||
|
||||
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.
|
||||
|
||||
Raises:
|
||||
@ -223,7 +265,7 @@ class Tool:
|
||||
self,
|
||||
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)
|
||||
) -> ILibrary:
|
||||
"""
|
||||
@ -245,77 +287,40 @@ abstract_tuple_t = tuple[Abstract, str, str]
|
||||
|
||||
|
||||
@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
|
||||
for generating straight paths, and a table of pre-rendered `transitions` for converting
|
||||
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` """
|
||||
|
||||
bend: abstract_tuple_t # Assumed to be clockwise
|
||||
""" `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 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)
|
||||
class LData:
|
||||
""" Data for planL """
|
||||
straight_length: float
|
||||
straight_kwargs: dict[str, Any]
|
||||
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(
|
||||
self,
|
||||
ccw: SupportsBool | None,
|
||||
length: float,
|
||||
*,
|
||||
in_ptype: str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
in_ptype: str | None = None, # noqa: ARG002 (unused)
|
||||
out_ptype: str | None = None, # noqa: ARG002 (unused)
|
||||
**kwargs, # noqa: ARG002 (unused)
|
||||
) -> tuple[Port, LData]:
|
||||
# TODO check all the math for L-shaped bends
|
||||
if ccw is not None:
|
||||
bend, bport_in, bport_out = self.bend
|
||||
|
||||
@ -336,87 +341,532 @@ class BasicTool(Tool, metaclass=ABCMeta):
|
||||
bend_angle *= -1
|
||||
else:
|
||||
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 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:
|
||||
if ccw is not None:
|
||||
out_ptype_actual = bend.ports[bport_out].ptype
|
||||
else:
|
||||
out_ptype_actual = self.default_out_ptype
|
||||
|
||||
straight_length = length - bend_dxy[0] - itrans_dxy[0] - otrans_dxy[0]
|
||||
bend_run = bend_dxy[1] + itrans_dxy[1] + otrans_dxy[1]
|
||||
straight_length = length - bend_dxy[0]
|
||||
bend_run = bend_dxy[1]
|
||||
|
||||
if straight_length < 0:
|
||||
raise BuildError(
|
||||
f'Asked to draw 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} out_trans: {otrans_dxy[0]:,g}'
|
||||
f'Asked to draw L-path with total length {length:,g}, shorter than required bends ({bend_dxy[0]:,})'
|
||||
)
|
||||
|
||||
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)
|
||||
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(
|
||||
self,
|
||||
batch: Sequence[RenderStep],
|
||||
*,
|
||||
port_names: Sequence[str] = ('A', 'B'),
|
||||
append: bool = True,
|
||||
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]))
|
||||
|
||||
gen_straight, sport_in, _sport_out = self.straight
|
||||
for step in batch:
|
||||
straight_length, ccw, in_transition, out_transition = step.data
|
||||
assert step.tool == self
|
||||
|
||||
if step.opcode == 'L':
|
||||
if in_transition:
|
||||
ipat, iport_theirs, _iport_ours = in_transition
|
||||
pat.plug(ipat, {port_names[1]: iport_theirs})
|
||||
if not numpy.isclose(straight_length, 0):
|
||||
straight_pat = gen_straight(straight_length, **kwargs)
|
||||
if append:
|
||||
pat.plug(straight_pat, {port_names[1]: sport_in}, append=True)
|
||||
else:
|
||||
straight = tree <= {SINGLE_USE_PREFIX + 'straight': straight_pat}
|
||||
pat.plug(straight, {port_names[1]: sport_in}, append=True)
|
||||
if ccw is not None:
|
||||
bend, bport_in, bport_out = self.bend
|
||||
pat.plug(bend, {port_names[1]: bport_in}, mirrored=bool(ccw))
|
||||
if out_transition:
|
||||
opat, oport_theirs, oport_ours = out_transition
|
||||
pat.plug(opat, {port_names[1]: oport_ours})
|
||||
self._renderL(data=step.data, tree=tree, port_names=port_names, straight_kwargs=kwargs)
|
||||
return tree
|
||||
|
||||
|
||||
@dataclass
|
||||
class AutoTool(Tool, metaclass=ABCMeta):
|
||||
"""
|
||||
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
|
||||
from non-native ptypes.
|
||||
"""
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Straight:
|
||||
""" Description of a straight-path generator """
|
||||
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
|
||||
|
||||
|
||||
@ -511,7 +961,7 @@ class PathTool(Tool, metaclass=ABCMeta):
|
||||
|
||||
if straight_length < 0:
|
||||
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))
|
||||
out_port = Port(data, rotation=bend_angle, ptype=self.ptype)
|
||||
@ -521,7 +971,7 @@ class PathTool(Tool, metaclass=ABCMeta):
|
||||
self,
|
||||
batch: Sequence[RenderStep],
|
||||
*,
|
||||
port_names: Sequence[str] = ('A', 'B'),
|
||||
port_names: tuple[str, str] = ('A', 'B'),
|
||||
**kwargs, # noqa: ARG002 (unused)
|
||||
) -> ILibrary:
|
||||
|
||||
|
||||
@ -1,3 +1,10 @@
|
||||
import traceback
|
||||
import pathlib
|
||||
|
||||
|
||||
MASQUE_DIR = str(pathlib.Path(__file__).parent)
|
||||
|
||||
|
||||
class MasqueError(Exception):
|
||||
"""
|
||||
Parent exception for all Masque-related Exceptions
|
||||
@ -25,15 +32,64 @@ class BuildError(MasqueError):
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class PortError(MasqueError):
|
||||
"""
|
||||
Exception raised by builder-related functions
|
||||
Exception raised by port-related functions
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class OneShotError(MasqueError):
|
||||
"""
|
||||
Exception raised when a function decorated with `@oneshot` is called more than once
|
||||
"""
|
||||
def __init__(self, func_name: str) -> None:
|
||||
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():
|
||||
xy_open = polygon.vertices + polygon.offset
|
||||
xy_open = polygon.vertices
|
||||
xy_closed = numpy.vstack((xy_open, xy_open[0, :]))
|
||||
block.add_lwpolyline(xy_closed, dxfattribs=attribs)
|
||||
|
||||
@ -376,5 +376,5 @@ def _mlayer2dxf(layer: layer_t) -> str:
|
||||
if isinstance(layer, int):
|
||||
return str(layer)
|
||||
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)})')
|
||||
|
||||
@ -21,6 +21,7 @@ Notes:
|
||||
"""
|
||||
from typing import IO, cast, Any
|
||||
from collections.abc import Iterable, Mapping, Callable
|
||||
from types import MappingProxyType
|
||||
import io
|
||||
import mmap
|
||||
import logging
|
||||
@ -52,6 +53,8 @@ path_cap_map = {
|
||||
4: Path.Cap.SquareCustom,
|
||||
}
|
||||
|
||||
RO_EMPTY_DICT: Mapping[int, bytes] = MappingProxyType({})
|
||||
|
||||
|
||||
def rint_cast(val: ArrayLike) -> NDArray[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
|
||||
|
||||
|
||||
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()}
|
||||
|
||||
|
||||
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
|
||||
props = {}
|
||||
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)
|
||||
diff_ints = rint_cast(diffs)
|
||||
frep = fatamorgana.ArbitraryRepetition(diff_ints[:, 0], diff_ints[:, 1]) # type: ignore
|
||||
offset = rep.displacements[0, :]
|
||||
offset = tuple(rep.displacements[0, :])
|
||||
else:
|
||||
assert rep is None
|
||||
frep = None
|
||||
@ -671,6 +671,8 @@ def repetition_masq2fata(
|
||||
|
||||
def annotations_to_properties(annotations: annotations_t) -> list[fatrec.Property]:
|
||||
#TODO determine is_standard based on key?
|
||||
if annotations is None:
|
||||
return []
|
||||
properties = []
|
||||
for key, values in annotations.items():
|
||||
vals = [AString(v) if isinstance(v, str) else v
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
SVG file format readers and writers
|
||||
"""
|
||||
from collections.abc import Mapping
|
||||
import warnings
|
||||
import logging
|
||||
|
||||
import numpy
|
||||
from numpy.typing import ArrayLike
|
||||
@ -12,6 +12,9 @@ from .utils import mangle_name
|
||||
from .. import Pattern
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def writefile(
|
||||
library: Mapping[str, Pattern],
|
||||
top: str,
|
||||
@ -50,7 +53,7 @@ def writefile(
|
||||
bounds = pattern.get_bounds(library=library)
|
||||
if bounds is None:
|
||||
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:
|
||||
bounds_min, bounds_max = bounds
|
||||
|
||||
@ -117,7 +120,7 @@ def writefile_inverted(
|
||||
bounds = pattern.get_bounds(library=library)
|
||||
if bounds is None:
|
||||
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:
|
||||
bounds_min, bounds_max = bounds
|
||||
|
||||
|
||||
@ -264,6 +264,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
self,
|
||||
tops: str | Sequence[str],
|
||||
flatten_ports: bool = False,
|
||||
dangling_ok: bool = False,
|
||||
) -> dict[str, 'Pattern']:
|
||||
"""
|
||||
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.
|
||||
flatten_ports: If `True`, keep ports from any referenced
|
||||
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:
|
||||
{name: flat_pattern} mapping for all flattened patterns.
|
||||
@ -292,6 +296,8 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
for target in pat.refs:
|
||||
if target is None:
|
||||
continue
|
||||
if dangling_ok and target not in self:
|
||||
continue
|
||||
if target not in flattened:
|
||||
flatten_single(target)
|
||||
|
||||
@ -307,7 +313,9 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
p.ports.clear()
|
||||
pat.append(p)
|
||||
|
||||
pat.refs.clear()
|
||||
for target in set(pat.refs.keys()) & set(self.keys()):
|
||||
del pat.refs[target]
|
||||
|
||||
flattened[name] = pat
|
||||
|
||||
for top in tops:
|
||||
|
||||
@ -332,7 +332,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
))
|
||||
|
||||
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
|
||||
|
||||
@ -354,10 +354,13 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
for layer, lseq in other_pattern.labels.items():
|
||||
self.labels[layer].extend(lseq)
|
||||
|
||||
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)
|
||||
if other_pattern.annotations is not None:
|
||||
if self.annotations is None:
|
||||
self.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())
|
||||
if port_conflicts:
|
||||
@ -415,7 +418,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
elif default_keep:
|
||||
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)}
|
||||
elif default_keep:
|
||||
pat.annotations = copy.copy(self.annotations)
|
||||
@ -581,7 +584,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
bounds = numpy.vstack((numpy.min(corners, axis=0),
|
||||
numpy.max(corners, axis=0))) * ref.scale + [ref.offset]
|
||||
if ref.repetition is not None:
|
||||
bounds += ref.repetition.get_bounds()
|
||||
bounds += ref.repetition.get_bounds_nonempty()
|
||||
|
||||
else:
|
||||
# Non-manhattan rotation, have to figure out bounds by rotating the pattern
|
||||
@ -742,7 +745,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
self
|
||||
"""
|
||||
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
|
||||
|
||||
def mirror_elements(self, across_axis: int = 0) -> Self:
|
||||
@ -1166,12 +1169,13 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
ports[new_name] = port
|
||||
|
||||
for name, port in ports.items():
|
||||
p = port.deepcopy()
|
||||
pp = port.deepcopy()
|
||||
if mirrored:
|
||||
p.mirror()
|
||||
p.rotate_around(pivot, rotation)
|
||||
p.translate(offset)
|
||||
self.ports[name] = p
|
||||
pp.mirror()
|
||||
pp.offset[1] *= -1
|
||||
pp.rotate_around(pivot, rotation)
|
||||
pp.translate(offset)
|
||||
self.ports[name] = pp
|
||||
|
||||
if append:
|
||||
if isinstance(other, Abstract):
|
||||
@ -1199,7 +1203,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
# map_out: dict[str, str | None] | None,
|
||||
# *,
|
||||
# mirrored: bool,
|
||||
# inherit_name: bool,
|
||||
# thru: bool | str,
|
||||
# set_rotation: bool | None,
|
||||
# append: Literal[False],
|
||||
# ) -> Self:
|
||||
@ -1213,7 +1217,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
# map_out: dict[str, str | None] | None,
|
||||
# *,
|
||||
# mirrored: bool,
|
||||
# inherit_name: bool,
|
||||
# thru: bool | str,
|
||||
# set_rotation: bool | None,
|
||||
# append: bool,
|
||||
# ) -> Self:
|
||||
@ -1226,7 +1230,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
map_out: dict[str, str | None] | None = None,
|
||||
*,
|
||||
mirrored: bool = False,
|
||||
inherit_name: bool = True,
|
||||
thru: bool | str = True,
|
||||
set_rotation: bool | None = None,
|
||||
append: bool = False,
|
||||
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'
|
||||
of `my_pat`.
|
||||
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
|
||||
allows easy extension of existing ports without changing their names or
|
||||
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`.
|
||||
mirrored: Enables mirroring `other` across the x axis prior to connecting
|
||||
any ports.
|
||||
inherit_name: If `True`, and `map_in` specifies only a single port,
|
||||
and `map_out` is `None`, and `other` has only two ports total,
|
||||
then automatically renames the output port of `other` to the
|
||||
name of the port from `self` that appears in `map_in`. This
|
||||
makes it easy to extend a pattern with simple 2-port devices
|
||||
thru: If map_in specifies only a single port, `thru` provides a mechainsm
|
||||
to avoid repeating the port name. Eg, for `map_in={'myport': 'A'}`,
|
||||
- If True (default), and `other` has only two ports total, and map_out
|
||||
doesn't specify a name for the other port, its name is set to the key
|
||||
in `map_in`, i.e. 'myport'.
|
||||
- If a string, `map_out[thru]` is set to the key in `map_in` (i.e. 'myport').
|
||||
An error is raised if that entry already exists.
|
||||
|
||||
This makes it easy to extend a pattern with simple 2-port devices
|
||||
(e.g. wires) without providing `map_out` each time `plug` is
|
||||
called. See "Examples" above for more info. Default `True`.
|
||||
set_rotation: If the necessary rotation cannot be determined from
|
||||
@ -1292,25 +1300,32 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
`PortError` if the specified port mapping is not achieveable (the ports
|
||||
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:
|
||||
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)
|
||||
translation, rotation, pivot = self.find_transform(
|
||||
other,
|
||||
map_in,
|
||||
mirrored=mirrored,
|
||||
set_rotation=set_rotation,
|
||||
ok_connections=ok_connections,
|
||||
mirrored = mirrored,
|
||||
set_rotation = set_rotation,
|
||||
ok_connections = ok_connections,
|
||||
)
|
||||
|
||||
# get rid of plugged ports
|
||||
@ -1323,13 +1338,13 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||
|
||||
self.place(
|
||||
other,
|
||||
offset=translation,
|
||||
rotation=rotation,
|
||||
pivot=pivot,
|
||||
mirrored=mirrored,
|
||||
port_map=map_out,
|
||||
skip_port_check=True,
|
||||
append=append,
|
||||
offset = translation,
|
||||
rotation = rotation,
|
||||
pivot = pivot,
|
||||
mirrored = mirrored,
|
||||
port_map = map_out,
|
||||
skip_port_check = True,
|
||||
append = append,
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
from typing import overload, Self, NoReturn, Any
|
||||
from collections.abc import Iterable, KeysView, ValuesView, Mapping
|
||||
import warnings
|
||||
import traceback
|
||||
import logging
|
||||
import functools
|
||||
from collections import Counter
|
||||
@ -13,8 +11,8 @@ from numpy import pi
|
||||
from numpy.typing import ArrayLike, NDArray
|
||||
|
||||
from .traits import PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable
|
||||
from .utils import rotate_offsets_around
|
||||
from .error import PortError
|
||||
from .utils import rotate_offsets_around, rotation_matrix_2d
|
||||
from .error import PortError, format_stacktrace
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -64,7 +62,7 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable):
|
||||
return self._rotation
|
||||
|
||||
@rotation.setter
|
||||
def rotation(self, val: float) -> None:
|
||||
def rotation(self, val: float | None) -> None:
|
||||
if val is None:
|
||||
self._rotation = None
|
||||
else:
|
||||
@ -102,7 +100,6 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable):
|
||||
return self
|
||||
|
||||
def mirror(self, axis: int = 0) -> Self:
|
||||
self.offset[1 - axis] *= -1
|
||||
if self.rotation is not None:
|
||||
self.rotation *= -1
|
||||
self.rotation += axis * pi
|
||||
@ -145,6 +142,28 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable):
|
||||
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):
|
||||
__slots__ = () # Allow subclasses to use __slots__
|
||||
@ -305,11 +324,11 @@ class PortList(metaclass=ABCMeta):
|
||||
|
||||
if type_conflicts.any():
|
||||
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]:
|
||||
msg += f'{k} | {a_types[nn]}:{b_types[nn]} | {v}\n'
|
||||
msg = ''.join(traceback.format_stack()) + '\n' + msg
|
||||
warnings.warn(msg, stacklevel=2)
|
||||
msg += f'{kk} | {a_types[nn]}:{b_types[nn]} | {vv}\n'
|
||||
msg += '\nStack trace:\n' + format_stacktrace()
|
||||
logger.warning(msg)
|
||||
|
||||
a_offsets = numpy.array([pp.offset for pp in a_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):
|
||||
rot_deg = numpy.rad2deg(rotations)
|
||||
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):
|
||||
msg += f'{k} | {rot_deg[nn]:g} | {v}\n'
|
||||
msg += f'{kk} | {rot_deg[nn]:g} | {vv}\n'
|
||||
raise PortError(msg)
|
||||
|
||||
translations = a_offsets - b_offsets
|
||||
if not numpy.allclose(translations, 0):
|
||||
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):
|
||||
msg += f'{k} | {translations[nn]} | {v}\n'
|
||||
msg += f'{kk} | {translations[nn]} | {vv}\n'
|
||||
raise PortError(msg)
|
||||
|
||||
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[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:
|
||||
raise PortError(f'Duplicate targets in `map_out`: {conflicts_out}')
|
||||
|
||||
@ -438,7 +457,7 @@ class PortList(metaclass=ABCMeta):
|
||||
`set_rotation` must remain `None`.
|
||||
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
|
||||
any other ptypte. Non-allowed ptype connections will log a
|
||||
warning. Order is ignored, i.e. `(a, b)` is equivalent to
|
||||
`(b, a)`.
|
||||
|
||||
@ -452,12 +471,12 @@ class PortList(metaclass=ABCMeta):
|
||||
s_ports = self[map_in.keys()]
|
||||
o_ports = other[map_in.values()]
|
||||
return self.find_port_transform(
|
||||
s_ports=s_ports,
|
||||
o_ports=o_ports,
|
||||
map_in=map_in,
|
||||
mirrored=mirrored,
|
||||
set_rotation=set_rotation,
|
||||
ok_connections=ok_connections,
|
||||
s_ports = s_ports,
|
||||
o_ports = o_ports,
|
||||
map_in = map_in,
|
||||
mirrored = mirrored,
|
||||
set_rotation = set_rotation,
|
||||
ok_connections = ok_connections,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@ -489,7 +508,7 @@ class PortList(metaclass=ABCMeta):
|
||||
`set_rotation` must remain `None`.
|
||||
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
|
||||
any other ptypte. Non-allowed ptype connections will log a
|
||||
warning. Order is ignored, i.e. `(a, b)` is equivalent to
|
||||
`(b, a)`.
|
||||
|
||||
@ -520,11 +539,11 @@ class PortList(metaclass=ABCMeta):
|
||||
for st, ot in zip(s_types, o_types, strict=True)])
|
||||
if type_conflicts.any():
|
||||
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]:
|
||||
msg += f'{k} | {s_types[nn]}:{o_types[nn]} | {v}\n'
|
||||
msg = ''.join(traceback.format_stack()) + '\n' + msg
|
||||
warnings.warn(msg, stacklevel=2)
|
||||
msg += f'{kk} | {s_types[nn]}:{o_types[nn]} | {vv}\n'
|
||||
msg += '\nStack trace:\n' + format_stacktrace()
|
||||
logger.warning(msg)
|
||||
|
||||
rotations = numpy.mod(s_rotations - o_rotations - pi, 2 * pi)
|
||||
if not has_rot.any():
|
||||
@ -546,8 +565,12 @@ class PortList(metaclass=ABCMeta):
|
||||
translations = s_offsets - o_offsets
|
||||
if not numpy.allclose(translations[:1], translations):
|
||||
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()):
|
||||
msg += f'{kk} | {translations[nn]} | {vv}\n'
|
||||
msg += f'{kk} | {translations[nn] - common_translation} | {vv}\n'
|
||||
raise PortError(msg)
|
||||
|
||||
return translations[0], rotations[0], o_offsets[0]
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ import numpy
|
||||
from numpy import pi
|
||||
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 .traits import (
|
||||
PositionableImpl, RotatableImpl, ScalableImpl,
|
||||
@ -50,11 +50,11 @@ class Ref(
|
||||
|
||||
# Mirrored property
|
||||
@property
|
||||
def mirrored(self) -> bool: # mypy#3004, setter should be SupportsBool
|
||||
def mirrored(self) -> bool:
|
||||
return self._mirrored
|
||||
|
||||
@mirrored.setter
|
||||
def mirrored(self, val: bool) -> None:
|
||||
def mirrored(self, val: SupportsBool) -> None:
|
||||
self._mirrored = bool(val)
|
||||
|
||||
def __init__(
|
||||
|
||||
@ -327,7 +327,7 @@ class Arbitrary(Repetition):
|
||||
"""
|
||||
|
||||
@property
|
||||
def displacements(self) -> Any: # mypy#3004 NDArray[numpy.float64]:
|
||||
def displacements(self) -> NDArray[numpy.float64]:
|
||||
return self._displacements
|
||||
|
||||
@displacements.setter
|
||||
|
||||
@ -10,6 +10,7 @@ from .shape import (
|
||||
)
|
||||
|
||||
from .polygon import Polygon as Polygon
|
||||
from .poly_collection import PolyCollection as PolyCollection
|
||||
from .circle import Circle as Circle
|
||||
from .ellipse import Ellipse as Ellipse
|
||||
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 ..repetition import Repetition
|
||||
from ..utils import is_scalar, annotations_t, annotations_lt, annotations_eq, rep2key
|
||||
from ..traits import PositionableImpl
|
||||
|
||||
|
||||
@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
|
||||
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
|
||||
@property
|
||||
def radii(self) -> Any: # mypy#3004 NDArray[numpy.float64]:
|
||||
def radii(self) -> NDArray[numpy.float64]:
|
||||
"""
|
||||
Return the radii `[rx, ry]`
|
||||
"""
|
||||
@ -79,7 +80,7 @@ class Arc(Shape):
|
||||
|
||||
# arc start/stop angle properties
|
||||
@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]`.
|
||||
Angles are measured from x-axis after rotation
|
||||
@ -157,7 +158,7 @@ class Arc(Shape):
|
||||
offset: ArrayLike = (0.0, 0.0),
|
||||
rotation: float = 0,
|
||||
repetition: Repetition | None = None,
|
||||
annotations: annotations_t | None = None,
|
||||
annotations: annotations_t = None,
|
||||
raw: bool = False,
|
||||
) -> None:
|
||||
if raw:
|
||||
@ -170,7 +171,7 @@ class Arc(Shape):
|
||||
self._offset = offset
|
||||
self._rotation = rotation
|
||||
self._repetition = repetition
|
||||
self._annotations = annotations if annotations is not None else {}
|
||||
self._annotations = annotations
|
||||
else:
|
||||
self.radii = radii
|
||||
self.angles = angles
|
||||
@ -178,7 +179,7 @@ class Arc(Shape):
|
||||
self.offset = offset
|
||||
self.rotation = rotation
|
||||
self.repetition = repetition
|
||||
self.annotations = annotations if annotations is not None else {}
|
||||
self.annotations = annotations
|
||||
|
||||
def __deepcopy__(self, memo: dict | None = None) -> 'Arc':
|
||||
memo = {} if memo is None else memo
|
||||
@ -412,15 +413,15 @@ class Arc(Shape):
|
||||
start_angle -= pi
|
||||
rotation += pi
|
||||
|
||||
angles = (start_angle, start_angle + delta_angle)
|
||||
norm_angles = (start_angle, start_angle + delta_angle)
|
||||
rotation %= 2 * pi
|
||||
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),
|
||||
lambda: Arc(
|
||||
radii=radii * norm_value,
|
||||
angles=angles,
|
||||
angles=norm_angles,
|
||||
width=width * norm_value,
|
||||
))
|
||||
|
||||
|
||||
@ -10,10 +10,11 @@ from . import Shape, Polygon, normalized_shape_tuple, DEFAULT_POLY_NUM_VERTICES
|
||||
from ..error import PatternError
|
||||
from ..repetition import Repetition
|
||||
from ..utils import is_scalar, annotations_t, annotations_lt, annotations_eq, rep2key
|
||||
from ..traits import PositionableImpl
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class Circle(Shape):
|
||||
class Circle(PositionableImpl, Shape):
|
||||
"""
|
||||
A circle, which has a position and radius.
|
||||
"""
|
||||
@ -48,7 +49,7 @@ class Circle(Shape):
|
||||
*,
|
||||
offset: ArrayLike = (0.0, 0.0),
|
||||
repetition: Repetition | None = None,
|
||||
annotations: annotations_t | None = None,
|
||||
annotations: annotations_t = None,
|
||||
raw: bool = False,
|
||||
) -> None:
|
||||
if raw:
|
||||
@ -56,12 +57,12 @@ class Circle(Shape):
|
||||
self._radius = radius
|
||||
self._offset = offset
|
||||
self._repetition = repetition
|
||||
self._annotations = annotations if annotations is not None else {}
|
||||
self._annotations = annotations
|
||||
else:
|
||||
self.radius = radius
|
||||
self.offset = offset
|
||||
self.repetition = repetition
|
||||
self.annotations = annotations if annotations is not None else {}
|
||||
self.annotations = annotations
|
||||
|
||||
def __deepcopy__(self, memo: dict | None = None) -> 'Circle':
|
||||
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 ..repetition import Repetition
|
||||
from ..utils import is_scalar, rotation_matrix_2d, annotations_t, annotations_lt, annotations_eq, rep2key
|
||||
from ..traits import PositionableImpl
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class Ellipse(Shape):
|
||||
class Ellipse(PositionableImpl, Shape):
|
||||
"""
|
||||
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.
|
||||
@ -33,7 +34,7 @@ class Ellipse(Shape):
|
||||
|
||||
# radius properties
|
||||
@property
|
||||
def radii(self) -> Any: # mypy#3004 NDArray[numpy.float64]:
|
||||
def radii(self) -> NDArray[numpy.float64]:
|
||||
"""
|
||||
Return the radii `[rx, ry]`
|
||||
"""
|
||||
@ -93,7 +94,7 @@ class Ellipse(Shape):
|
||||
offset: ArrayLike = (0.0, 0.0),
|
||||
rotation: float = 0,
|
||||
repetition: Repetition | None = None,
|
||||
annotations: annotations_t | None = None,
|
||||
annotations: annotations_t = None,
|
||||
raw: bool = False,
|
||||
) -> None:
|
||||
if raw:
|
||||
@ -103,13 +104,13 @@ class Ellipse(Shape):
|
||||
self._offset = offset
|
||||
self._rotation = rotation
|
||||
self._repetition = repetition
|
||||
self._annotations = annotations if annotations is not None else {}
|
||||
self._annotations = annotations
|
||||
else:
|
||||
self.radii = radii
|
||||
self.offset = offset
|
||||
self.rotation = rotation
|
||||
self.repetition = repetition
|
||||
self.annotations = annotations if annotations is not None else {}
|
||||
self.annotations = annotations
|
||||
|
||||
def __deepcopy__(self, memo: dict | None = None) -> Self:
|
||||
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
|
||||
import copy
|
||||
import functools
|
||||
@ -30,8 +30,7 @@ class PathCap(Enum):
|
||||
@functools.total_ordering
|
||||
class Path(Shape):
|
||||
"""
|
||||
A path, consisting of a bunch of vertices (Nx2 ndarray), a width, an end-cap shape,
|
||||
and an offset.
|
||||
A path, consisting of a bunch of vertices (Nx2 ndarray), a width, and an end-cap shape.
|
||||
|
||||
Note that the setter for `Path.vertices` will create a copy of the passed vertex coordinates.
|
||||
|
||||
@ -40,7 +39,7 @@ class Path(Shape):
|
||||
__slots__ = (
|
||||
'_vertices', '_width', '_cap', '_cap_extensions',
|
||||
# Inherited
|
||||
'_offset', '_repetition', '_annotations',
|
||||
'_repetition', '_annotations',
|
||||
)
|
||||
_vertices: NDArray[numpy.float64]
|
||||
_width: float
|
||||
@ -87,7 +86,7 @@ class Path(Shape):
|
||||
|
||||
# cap_extensions 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
|
||||
|
||||
@ -113,7 +112,7 @@ class Path(Shape):
|
||||
|
||||
# vertices 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], ...]`
|
||||
|
||||
@ -160,6 +159,28 @@ class Path(Shape):
|
||||
raise PatternError('Wrong number of vertices')
|
||||
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__(
|
||||
self,
|
||||
vertices: ArrayLike,
|
||||
@ -170,36 +191,35 @@ class Path(Shape):
|
||||
offset: ArrayLike = (0.0, 0.0),
|
||||
rotation: float = 0,
|
||||
repetition: Repetition | None = None,
|
||||
annotations: annotations_t | None = None,
|
||||
annotations: annotations_t = None,
|
||||
raw: bool = False,
|
||||
) -> None:
|
||||
self._cap_extensions = None # Since .cap setter might access it
|
||||
|
||||
if raw:
|
||||
assert isinstance(vertices, numpy.ndarray)
|
||||
assert isinstance(offset, numpy.ndarray)
|
||||
assert isinstance(cap_extensions, numpy.ndarray) or cap_extensions is None
|
||||
self._vertices = vertices
|
||||
self._offset = offset
|
||||
self._repetition = repetition
|
||||
self._annotations = annotations if annotations is not None else {}
|
||||
self._annotations = annotations
|
||||
self._width = width
|
||||
self._cap = cap
|
||||
self._cap_extensions = cap_extensions
|
||||
else:
|
||||
self.vertices = vertices
|
||||
self.offset = offset
|
||||
self.repetition = repetition
|
||||
self.annotations = annotations if annotations is not None else {}
|
||||
self.annotations = annotations
|
||||
self.width = width
|
||||
self.cap = cap
|
||||
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':
|
||||
memo = {} if memo is None else memo
|
||||
new = copy.copy(self)
|
||||
new._offset = self._offset.copy()
|
||||
new._vertices = self._vertices.copy()
|
||||
new._cap = copy.deepcopy(self._cap, memo)
|
||||
new._cap_extensions = copy.deepcopy(self._cap_extensions, memo)
|
||||
@ -209,7 +229,6 @@ class Path(Shape):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return (
|
||||
type(self) is type(other)
|
||||
and numpy.array_equal(self.offset, other.offset)
|
||||
and numpy.array_equal(self.vertices, other.vertices)
|
||||
and self.width == other.width
|
||||
and self.cap == other.cap
|
||||
@ -234,8 +253,6 @@ class Path(Shape):
|
||||
if self.cap_extensions is None:
|
||||
return True
|
||||
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:
|
||||
return rep2key(self.repetition) < rep2key(other.repetition)
|
||||
return annotations_lt(self.annotations, other.annotations)
|
||||
@ -292,7 +309,7 @@ class Path(Shape):
|
||||
|
||||
if self.width == 0:
|
||||
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
|
||||
|
||||
@ -343,7 +360,7 @@ class Path(Shape):
|
||||
o1.append(v[-1] - perp[-1])
|
||||
verts = numpy.vstack((o0, o1[::-1]))
|
||||
|
||||
polys = [Polygon(offset=self.offset, vertices=verts)]
|
||||
polys = [Polygon(vertices=verts)]
|
||||
|
||||
if self.cap == PathCap.Circle:
|
||||
#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]:
|
||||
if self.cap == PathCap.Circle:
|
||||
bounds = self.offset + numpy.vstack((numpy.min(self.vertices, axis=0) - self.width / 2,
|
||||
numpy.max(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))
|
||||
elif self.cap in (
|
||||
PathCap.Flush,
|
||||
PathCap.Square,
|
||||
@ -390,7 +407,7 @@ class Path(Shape):
|
||||
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
|
||||
# other shapes
|
||||
offset = self.vertices.mean(axis=0) + self.offset
|
||||
offset = self.vertices.mean(axis=0)
|
||||
zeroed_vertices = self.vertices - offset
|
||||
|
||||
scale = zeroed_vertices.std()
|
||||
@ -460,5 +477,5 @@ class Path(Shape):
|
||||
return extensions
|
||||
|
||||
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}>'
|
||||
|
||||
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 functools
|
||||
|
||||
@ -20,7 +20,7 @@ if TYPE_CHECKING:
|
||||
class Polygon(Shape):
|
||||
"""
|
||||
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
|
||||
passed vertex coordinates.
|
||||
@ -30,7 +30,7 @@ class Polygon(Shape):
|
||||
__slots__ = (
|
||||
'_vertices',
|
||||
# Inherited
|
||||
'_offset', '_repetition', '_annotations',
|
||||
'_repetition', '_annotations',
|
||||
)
|
||||
|
||||
_vertices: NDArray[numpy.float64]
|
||||
@ -38,7 +38,7 @@ class Polygon(Shape):
|
||||
|
||||
# vertices 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], ...]`)
|
||||
|
||||
@ -85,6 +85,28 @@ class Polygon(Shape):
|
||||
raise PatternError('Wrong number of vertices')
|
||||
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__(
|
||||
self,
|
||||
vertices: ArrayLike,
|
||||
@ -92,28 +114,26 @@ class Polygon(Shape):
|
||||
offset: ArrayLike = (0.0, 0.0),
|
||||
rotation: float = 0.0,
|
||||
repetition: Repetition | None = None,
|
||||
annotations: annotations_t | None = None,
|
||||
annotations: annotations_t = None,
|
||||
raw: bool = False,
|
||||
) -> None:
|
||||
if raw:
|
||||
assert isinstance(vertices, numpy.ndarray)
|
||||
assert isinstance(offset, numpy.ndarray)
|
||||
self._vertices = vertices
|
||||
self._offset = offset
|
||||
self._repetition = repetition
|
||||
self._annotations = annotations if annotations is not None else {}
|
||||
self._annotations = annotations
|
||||
else:
|
||||
self.vertices = vertices
|
||||
self.offset = offset
|
||||
self.repetition = repetition
|
||||
self.annotations = annotations if annotations is not None else {}
|
||||
self.annotations = annotations
|
||||
if rotation:
|
||||
self.rotate(rotation)
|
||||
if numpy.any(offset):
|
||||
self.translate(offset)
|
||||
|
||||
def __deepcopy__(self, memo: dict | None = None) -> 'Polygon':
|
||||
memo = {} if memo is None else memo
|
||||
new = copy.copy(self)
|
||||
new._offset = self._offset.copy()
|
||||
new._vertices = self._vertices.copy()
|
||||
new._annotations = copy.deepcopy(self._annotations)
|
||||
return new
|
||||
@ -121,7 +141,6 @@ class Polygon(Shape):
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
return (
|
||||
type(self) is type(other)
|
||||
and numpy.array_equal(self.offset, other.offset)
|
||||
and numpy.array_equal(self.vertices, other.vertices)
|
||||
and self.repetition == other.repetition
|
||||
and annotations_eq(self.annotations, other.annotations)
|
||||
@ -141,8 +160,6 @@ class Polygon(Shape):
|
||||
if eq_lt_masked.size > 0:
|
||||
return eq_lt_masked.flat[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:
|
||||
return rep2key(self.repetition) < rep2key(other.repetition)
|
||||
return annotations_lt(self.annotations, other.annotations)
|
||||
@ -248,11 +265,11 @@ class Polygon(Shape):
|
||||
elif xmax is None:
|
||||
assert xmin is not None
|
||||
assert xctr is not None
|
||||
lx = 2 * (xctr - xmin)
|
||||
lx = 2.0 * (xctr - xmin)
|
||||
elif xmin is None:
|
||||
assert xctr is not None
|
||||
assert xmax is not None
|
||||
lx = 2 * (xmax - xctr)
|
||||
lx = 2.0 * (xmax - xctr)
|
||||
else:
|
||||
raise PatternError('Two of xmin, xctr, xmax, lx must be None!')
|
||||
else: # noqa: PLR5501
|
||||
@ -278,11 +295,11 @@ class Polygon(Shape):
|
||||
elif ymax is None:
|
||||
assert ymin is not None
|
||||
assert yctr is not None
|
||||
ly = 2 * (yctr - ymin)
|
||||
ly = 2.0 * (yctr - ymin)
|
||||
elif ymin is None:
|
||||
assert yctr is not None
|
||||
assert ymax is not None
|
||||
ly = 2 * (ymax - yctr)
|
||||
ly = 2.0 * (ymax - yctr)
|
||||
else:
|
||||
raise PatternError('Two of ymin, yctr, ymax, ly must be None!')
|
||||
else: # noqa: PLR5501
|
||||
@ -363,8 +380,8 @@ class Polygon(Shape):
|
||||
return [copy.deepcopy(self)]
|
||||
|
||||
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),
|
||||
self.offset + numpy.max(self.vertices, axis=0)))
|
||||
return numpy.vstack((numpy.min(self.vertices, axis=0),
|
||||
numpy.max(self.vertices, axis=0)))
|
||||
|
||||
def rotate(self, theta: float) -> 'Polygon':
|
||||
if theta != 0:
|
||||
@ -384,7 +401,7 @@ class Polygon(Shape):
|
||||
# other shapes
|
||||
meanv = self.vertices.mean(axis=0)
|
||||
zeroed_vertices = self.vertices - meanv
|
||||
offset = meanv + self.offset
|
||||
offset = meanv
|
||||
|
||||
scale = zeroed_vertices.std()
|
||||
normed_vertices = zeroed_vertices / scale
|
||||
@ -438,5 +455,5 @@ class Polygon(Shape):
|
||||
return self
|
||||
|
||||
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)}>'
|
||||
|
||||
@ -7,7 +7,7 @@ from numpy.typing import NDArray, ArrayLike
|
||||
|
||||
from ..traits import (
|
||||
Rotatable, Mirrorable, Copyable, Scalable,
|
||||
PositionableImpl, PivotableImpl, RepeatableImpl, AnnotatableImpl,
|
||||
Positionable, PivotableImpl, RepeatableImpl, AnnotatableImpl,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -26,7 +26,7 @@ normalized_shape_tuple = tuple[
|
||||
DEFAULT_POLY_NUM_VERTICES = 24
|
||||
|
||||
|
||||
class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable,
|
||||
class Shape(Positionable, Rotatable, Mirrorable, Copyable, Scalable,
|
||||
PivotableImpl, RepeatableImpl, AnnotatableImpl, metaclass=ABCMeta):
|
||||
"""
|
||||
Class specifying functions common to all shapes.
|
||||
@ -134,7 +134,7 @@ class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable,
|
||||
mins, maxs = bounds
|
||||
|
||||
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):
|
||||
dv = v_next - v
|
||||
|
||||
@ -282,7 +282,7 @@ class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable,
|
||||
offset = (numpy.where(keep_x)[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)
|
||||
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 ..error import PatternError
|
||||
from ..repetition import Repetition
|
||||
from ..traits import RotatableImpl
|
||||
from ..utils import is_scalar, get_bit, annotations_t, annotations_lt, annotations_eq, rep2key
|
||||
from ..traits import PositionableImpl, RotatableImpl
|
||||
from ..utils import is_scalar, get_bit, annotations_t, annotations_lt, annotations_eq, rep2key, SupportsBool
|
||||
|
||||
# Loaded on use:
|
||||
# from freetype import Face
|
||||
@ -18,7 +18,7 @@ from ..utils import is_scalar, get_bit, annotations_t, annotations_lt, annotatio
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class Text(RotatableImpl, Shape):
|
||||
class Text(PositionableImpl, RotatableImpl, Shape):
|
||||
"""
|
||||
Text (to be printed e.g. as a set of polygons).
|
||||
This is distinct from non-printed Label objects.
|
||||
@ -55,11 +55,11 @@ class Text(RotatableImpl, Shape):
|
||||
self._height = val
|
||||
|
||||
@property
|
||||
def mirrored(self) -> bool: # mypy#3004, should be bool
|
||||
def mirrored(self) -> bool:
|
||||
return self._mirrored
|
||||
|
||||
@mirrored.setter
|
||||
def mirrored(self, val: bool) -> None:
|
||||
def mirrored(self, val: SupportsBool) -> None:
|
||||
self._mirrored = bool(val)
|
||||
|
||||
def __init__(
|
||||
@ -71,7 +71,7 @@ class Text(RotatableImpl, Shape):
|
||||
offset: ArrayLike = (0.0, 0.0),
|
||||
rotation: float = 0.0,
|
||||
repetition: Repetition | None = None,
|
||||
annotations: annotations_t | None = None,
|
||||
annotations: annotations_t = None,
|
||||
raw: bool = False,
|
||||
) -> None:
|
||||
if raw:
|
||||
@ -81,14 +81,14 @@ class Text(RotatableImpl, Shape):
|
||||
self._height = height
|
||||
self._rotation = rotation
|
||||
self._repetition = repetition
|
||||
self._annotations = annotations if annotations is not None else {}
|
||||
self._annotations = annotations
|
||||
else:
|
||||
self.offset = offset
|
||||
self.string = string
|
||||
self.height = height
|
||||
self.rotation = rotation
|
||||
self.repetition = repetition
|
||||
self.annotations = annotations if annotations is not None else {}
|
||||
self.annotations = annotations
|
||||
self.font_path = font_path
|
||||
|
||||
def __deepcopy__(self, memo: dict | None = None) -> Self:
|
||||
@ -201,7 +201,7 @@ def get_char_as_polygons(
|
||||
font_path: str,
|
||||
char: str,
|
||||
resolution: float = 48 * 64,
|
||||
) -> tuple[list[list[list[float]]], float]:
|
||||
) -> tuple[list[NDArray[numpy.float64]], float]:
|
||||
from freetype import Face # type: ignore
|
||||
from matplotlib.path import Path # type: ignore
|
||||
|
||||
@ -276,11 +276,12 @@ def get_char_as_polygons(
|
||||
|
||||
advance = slot.advance.x / resolution
|
||||
|
||||
polygons: list[NDArray[numpy.float64]]
|
||||
if len(all_verts) == 0:
|
||||
polygons = []
|
||||
else:
|
||||
path = Path(all_verts, all_codes)
|
||||
path.should_simplify = False
|
||||
polygons = path.to_polygons()
|
||||
polygons = [numpy.asarray(poly) for poly in path.to_polygons()]
|
||||
|
||||
return polygons, advance
|
||||
|
||||
@ -45,6 +45,6 @@ class AnnotatableImpl(Annotatable, metaclass=ABCMeta):
|
||||
|
||||
@annotations.setter
|
||||
def annotations(self, annotations: annotations_t) -> None:
|
||||
if not isinstance(annotations, dict):
|
||||
raise MasqueError(f'annotations expected dict, got {type(annotations)}')
|
||||
if not isinstance(annotations, dict) and annotations is not None:
|
||||
raise MasqueError(f'annotations expected dict or None, got {type(annotations)}')
|
||||
self._annotations = annotations
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from typing import Self, Any
|
||||
from typing import Self
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
import numpy
|
||||
@ -73,7 +73,7 @@ class PositionableImpl(Positionable, metaclass=ABCMeta):
|
||||
#
|
||||
# offset property
|
||||
@property
|
||||
def offset(self) -> Any: # mypy#3004 NDArray[numpy.float64]:
|
||||
def offset(self) -> NDArray[numpy.float64]:
|
||||
"""
|
||||
[x, y] offset
|
||||
"""
|
||||
@ -95,7 +95,7 @@ class PositionableImpl(Positionable, metaclass=ABCMeta):
|
||||
return self
|
||||
|
||||
def translate(self, offset: ArrayLike) -> Self:
|
||||
self._offset += offset # type: ignore # NDArray += ArrayLike should be fine??
|
||||
self._offset += numpy.asarray(offset)
|
||||
return self
|
||||
|
||||
|
||||
|
||||
@ -116,7 +116,7 @@ class PivotableImpl(Pivotable, metaclass=ABCMeta):
|
||||
pivot = numpy.asarray(pivot, dtype=float)
|
||||
cast('Positionable', self).translate(-pivot)
|
||||
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)
|
||||
return self
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ from numpy import pi
|
||||
try:
|
||||
from numpy import trapezoid
|
||||
except ImportError:
|
||||
from numpy import trapz as trapezoid
|
||||
from numpy import trapz as trapezoid # type:ignore
|
||||
|
||||
|
||||
def bezier(
|
||||
|
||||
@ -5,7 +5,7 @@ from typing import Protocol
|
||||
|
||||
|
||||
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):
|
||||
|
||||
@ -56,6 +56,7 @@ dxf = ["ezdxf~=1.0.2"]
|
||||
svg = ["svgwrite"]
|
||||
visualize = ["matplotlib"]
|
||||
text = ["matplotlib", "freetype-py"]
|
||||
manhatanize_slow = ["float_raster"]
|
||||
|
||||
|
||||
[tool.ruff]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user