Compare commits
No commits in common. "master" and "v3.2" have entirely different histories.
39 changed files with 991 additions and 2547 deletions
65
README.md
65
README.md
|
|
@ -37,55 +37,6 @@ A layout consists of a hierarchy of `Pattern`s stored in a single `Library`.
|
|||
Each `Pattern` can contain `Ref`s pointing at other patterns, `Shape`s, `Label`s, and `Port`s.
|
||||
|
||||
|
||||
Library / Pattern hierarchy:
|
||||
```
|
||||
+-----------------------------------------------------------------------+
|
||||
| Library |
|
||||
| |
|
||||
| Name: "MyChip" ...> Name: "Transistor" |
|
||||
| +---------------------------+ : +---------------------------+ |
|
||||
| | [Pattern] | : | [Pattern] | |
|
||||
| | | : | | |
|
||||
| | shapes: {...} | : | shapes: { | |
|
||||
| | ports: {...} | : | "Si": [<Polygon>, ...] | |
|
||||
| | | : | "M1": [<Polygon>, ...]}| |
|
||||
| | refs: | : | ports: {G, S, D} | |
|
||||
| | "Transistor": [Ref, Ref]|..: +---------------------------+ |
|
||||
| +---------------------------+ |
|
||||
| |
|
||||
| # (`refs` keys resolve to Patterns within the Library) |
|
||||
+-----------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
|
||||
Pattern internals:
|
||||
```
|
||||
+---------------------------------------------------------------+
|
||||
| [Pattern] |
|
||||
| |
|
||||
| shapes: { |
|
||||
| (1, 0): [Polygon, Circle, ...], # Geometry by layer |
|
||||
| (2, 0): [Path, ...] |
|
||||
| "M1" : [Path, ...] |
|
||||
| "M2" : [Polygon, ...] |
|
||||
| } |
|
||||
| |
|
||||
| refs: { # Key sets target name, Ref sets transform |
|
||||
| "my_cell": [ |
|
||||
| Ref(offset=(0,0), rotation=0), |
|
||||
| Ref(offset=(10,0), rotation=R90, repetition=Grid(...)) |
|
||||
| ] |
|
||||
| } |
|
||||
| |
|
||||
| ports: { |
|
||||
| "in": Port(offset=(0,0), rotation=0, ptype="M1"), |
|
||||
| "out": Port(offset=(10,0), rotation=R180, ptype="wg") |
|
||||
| } |
|
||||
| |
|
||||
+---------------------------------------------------------------+
|
||||
```
|
||||
|
||||
|
||||
`masque` departs from several "classic" GDSII paradigms:
|
||||
- A `Pattern` object does not store its own name. A name is only assigned when the pattern is placed
|
||||
into a `Library`, which is effectively a name->`Pattern` mapping.
|
||||
|
|
@ -182,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 eligible conflicting patterns
|
||||
name_mapping = library.add(tree) # add all patterns from `tree`, renaming elgible 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
|
||||
|
||||
|
|
@ -221,11 +172,10 @@ my_pattern.place(abstract, ...)
|
|||
|
||||
# or
|
||||
my_pattern.place(library << make_tree(...), ...)
|
||||
```
|
||||
|
||||
|
||||
### Quickly add geometry, labels, or refs:
|
||||
Adding elements can be overly verbose:
|
||||
The long form for adding elements can be overly verbose:
|
||||
```python3
|
||||
my_pattern.shapes[layer].append(Polygon(vertices, ...))
|
||||
my_pattern.labels[layer] += [Label('my text')]
|
||||
|
|
@ -277,12 +227,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
|
||||
* tuple / string layer auto-translation
|
||||
* Tests tests tests
|
||||
* check renderpather
|
||||
* pather and renderpather examples
|
||||
* context manager for retool
|
||||
* allow a specific mismatch when connecting ports
|
||||
|
|
|
|||
|
|
@ -265,12 +265,6 @@ def main() -> None:
|
|||
# when using pather.retool().
|
||||
pather.path_to('VCC', None, -50_000, out_ptype='m1wire')
|
||||
|
||||
# Now extend GND out to x=-50_000, using M2 for a portion of the path.
|
||||
# We can use `pather.toolctx()` to temporarily retool, instead of calling `retool()` twice.
|
||||
with pather.toolctx(M2_tool, keys=['GND']):
|
||||
pather.path_to('GND', None, -40_000)
|
||||
pather.path_to('GND', None, -50_000)
|
||||
|
||||
# Save the pather's pattern into our library
|
||||
library['Pather_and_BasicTool'] = pather.pattern
|
||||
|
||||
|
|
|
|||
|
|
@ -77,20 +77,16 @@ from .builder import (
|
|||
Pather as Pather,
|
||||
RenderPather as RenderPather,
|
||||
RenderStep as RenderStep,
|
||||
SimpleTool as SimpleTool,
|
||||
AutoTool as AutoTool,
|
||||
BasicTool as BasicTool,
|
||||
PathTool as PathTool,
|
||||
PortPather as PortPather,
|
||||
)
|
||||
from .utils import (
|
||||
ports2data as ports2data,
|
||||
oneshot as oneshot,
|
||||
R90 as R90,
|
||||
R180 as R180,
|
||||
)
|
||||
|
||||
|
||||
__author__ = 'Jan Petykiewicz'
|
||||
|
||||
__version__ = '3.4'
|
||||
__version__ = '3.2'
|
||||
version = __version__ # legacy
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ class Abstract(PortList):
|
|||
When snapping a sub-component to an existing pattern, only the name (not contained
|
||||
in a `Pattern` object) and port info is needed, and not the geometry itself.
|
||||
"""
|
||||
# Alternate design option: do we want to store a Ref instead of just a name? then we can translate/rotate/mirror...
|
||||
__slots__ = ('name', '_ports')
|
||||
|
||||
name: str
|
||||
|
|
@ -49,6 +48,8 @@ class Abstract(PortList):
|
|||
self.name = name
|
||||
self.ports = copy.deepcopy(ports)
|
||||
|
||||
# TODO do we want to store a Ref instead of just a name? then we can translate/rotate/mirror...
|
||||
|
||||
def __repr__(self) -> str:
|
||||
s = f'<Abstract {self.name} ['
|
||||
for name, port in self.ports.items():
|
||||
|
|
@ -87,7 +88,7 @@ class Abstract(PortList):
|
|||
|
||||
def rotate_around(self, pivot: ArrayLike, rotation: float) -> Self:
|
||||
"""
|
||||
Rotate the Abstract around a pivot point.
|
||||
Rotate the Abstract around the a location.
|
||||
|
||||
Args:
|
||||
pivot: (x, y) location to rotate around
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
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,
|
||||
SimpleTool as SimpleTool,
|
||||
AutoTool as AutoTool,
|
||||
BasicTool as BasicTool,
|
||||
PathTool as PathTool,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
Simplified Pattern assembly (`Builder`)
|
||||
"""
|
||||
from typing import Self
|
||||
from collections.abc import Iterable, Sequence, Mapping
|
||||
from collections.abc import Sequence, Mapping
|
||||
import copy
|
||||
import logging
|
||||
from functools import wraps
|
||||
|
|
@ -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 `thru` argument is not explicitly
|
||||
argument is provided, and the `inherit_name` 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.
|
||||
|
|
@ -210,8 +210,7 @@ class Builder(PortList):
|
|||
self.pattern.rect(*args, **kwargs)
|
||||
return self
|
||||
|
||||
# Note: We're a superclass of `Pather`, where path() means something different,
|
||||
# so we shouldn't wrap Pattern.path()
|
||||
# Note: We're a superclass of `Pather`, where path() means something different...
|
||||
#@wraps(Pattern.path)
|
||||
#def path(self, *args, **kwargs) -> Self:
|
||||
# self.pattern.path(*args, **kwargs)
|
||||
|
|
@ -224,10 +223,9 @@ class Builder(PortList):
|
|||
map_out: dict[str, str | None] | None = None,
|
||||
*,
|
||||
mirrored: bool = False,
|
||||
thru: bool | str = True,
|
||||
inherit_name: bool = True,
|
||||
set_rotation: bool | None = None,
|
||||
append: bool = False,
|
||||
ok_connections: Iterable[tuple[str, str]] = (),
|
||||
) -> Self:
|
||||
"""
|
||||
Wrapper around `Pattern.plug` which allows a string for `other`.
|
||||
|
|
@ -247,15 +245,11 @@ class Builder(PortList):
|
|||
new names for ports in `other`.
|
||||
mirrored: Enables mirroring `other` across the x axis prior to
|
||||
connecting any ports.
|
||||
thru: If map_in specifies only a single port, `thru` provides a mechainsm
|
||||
to avoid repeating the port name. Eg, for `map_in={'myport': 'A'}`,
|
||||
- If True (default), and `other` has only two ports total, and map_out
|
||||
doesn't specify a name for the other port, its name is set to the key
|
||||
in `map_in`, i.e. 'myport'.
|
||||
- If a string, `map_out[thru]` is set to the key in `map_in` (i.e. 'myport').
|
||||
An error is raised if that entry already exists.
|
||||
|
||||
This makes it easy to extend a pattern with simple 2-port devices
|
||||
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
|
||||
(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
|
||||
|
|
@ -266,11 +260,6 @@ class Builder(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
|
||||
|
|
@ -301,10 +290,9 @@ class Builder(PortList):
|
|||
map_in=map_in,
|
||||
map_out=map_out,
|
||||
mirrored=mirrored,
|
||||
thru = thru,
|
||||
inherit_name=inherit_name,
|
||||
set_rotation=set_rotation,
|
||||
append=append,
|
||||
ok_connections = ok_connections,
|
||||
)
|
||||
return self
|
||||
|
||||
|
|
|
|||
|
|
@ -2,25 +2,30 @@
|
|||
Manual wire/waveguide routing (`Pather`)
|
||||
"""
|
||||
from typing import Self
|
||||
from collections.abc import Sequence, Mapping, MutableMapping
|
||||
from collections.abc import Sequence, MutableMapping, Mapping
|
||||
import copy
|
||||
import logging
|
||||
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 BuildError
|
||||
from ..library import ILibrary, SINGLE_USE_PREFIX
|
||||
from ..error import PortError, BuildError
|
||||
from ..ports import PortList, Port
|
||||
from ..utils import SupportsBool
|
||||
from ..abstract import Abstract
|
||||
from ..utils import SupportsBool, rotation_matrix_2d
|
||||
from .tools import Tool
|
||||
from .pather_mixin import PatherMixin
|
||||
from .utils import ell
|
||||
from .builder import Builder
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Pather(Builder, PatherMixin):
|
||||
class Pather(Builder):
|
||||
"""
|
||||
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.
|
||||
|
|
@ -252,6 +257,29 @@ class Pather(Builder, PatherMixin):
|
|||
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
|
||||
|
||||
def path(
|
||||
self,
|
||||
|
|
@ -259,6 +287,7 @@ class Pather(Builder, PatherMixin):
|
|||
ccw: SupportsBool | None,
|
||||
length: float,
|
||||
*,
|
||||
tool_port_names: tuple[str, str] = ('A', 'B'),
|
||||
plug_into: str | None = None,
|
||||
**kwargs,
|
||||
) -> Self:
|
||||
|
|
@ -266,7 +295,7 @@ class Pather(Builder, PatherMixin):
|
|||
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, and an unspecified
|
||||
The wire will travel `length` distance along the port's axis, an an unspecified
|
||||
(tool-dependent) distance in the perpendicular direction. The output port will
|
||||
be rotated (or not) based on the `ccw` parameter.
|
||||
|
||||
|
|
@ -277,6 +306,9 @@ class Pather(Builder, PatherMixin):
|
|||
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`.
|
||||
|
||||
|
|
@ -291,44 +323,54 @@ class Pather(Builder, PatherMixin):
|
|||
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)
|
||||
tname = self.library << tree
|
||||
abstract = self.library << tree
|
||||
if plug_into is not None:
|
||||
output = {plug_into: tool_port_names[1]}
|
||||
else:
|
||||
output = {}
|
||||
self.plug(tname, {portspec: tool_port_names[0], **output})
|
||||
return self
|
||||
return self.plug(abstract, {portspec: tool_port_names[0], **output})
|
||||
|
||||
def pathS(
|
||||
def path_to(
|
||||
self,
|
||||
portspec: str,
|
||||
length: float,
|
||||
jog: float,
|
||||
ccw: SupportsBool | None,
|
||||
position: float | None = None,
|
||||
*,
|
||||
x: float | None = None,
|
||||
y: float | None = None,
|
||||
tool_port_names: tuple[str, str] = ('A', 'B'),
|
||||
plug_into: str | None = None,
|
||||
**kwargs,
|
||||
) -> Self:
|
||||
"""
|
||||
Create an S-shaped "wire"/"waveguide" and `plug` it into the port `portspec`, with the aim
|
||||
of traveling exactly `length` distance with an offset `jog` along the other axis (+ve jog is
|
||||
left of direction of travel).
|
||||
Create a "wire"/"waveguide" and `plug` it into the port `portspec`, with the aim
|
||||
of ending exactly at a target position.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
Args:
|
||||
portspec: The name of the port into which the wire will be plugged.
|
||||
jog: Total manhattan distance perpendicular to the direction of travel.
|
||||
Positive values are to the left of the direction of travel.
|
||||
length: The total manhattan distance from input to output, along the input's axis only.
|
||||
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.
|
||||
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`.
|
||||
|
||||
|
|
@ -336,40 +378,317 @@ class Pather(Builder, PatherMixin):
|
|||
self
|
||||
|
||||
Raises:
|
||||
BuildError if `distance` is too small to fit the s-bend (for nonzero jog).
|
||||
LibraryError if no valid name could be picked for the pattern.
|
||||
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 pathS() since device is dead')
|
||||
logger.error('Skipping path_to() since device is dead')
|
||||
return self
|
||||
|
||||
tool_port_names = ('A', 'B')
|
||||
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 = 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]])
|
||||
port = self.pattern[portspec]
|
||||
if port.rotation is None:
|
||||
raise PortError(f'Port {portspec} has no rotation and cannot be used for path_to()')
|
||||
|
||||
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
|
||||
if not numpy.isclose(port.rotation % (pi / 2), 0):
|
||||
raise BuildError('path_to was asked to route from non-manhattan port')
|
||||
|
||||
tname = self.library << tree
|
||||
if plug_into is not None:
|
||||
output = {plug_into: tool_port_names[1]}
|
||||
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:
|
||||
output = {}
|
||||
self.plug(tname, {portspec: tool_port_names[0], **output})
|
||||
if x is not None:
|
||||
raise BuildError('Asked to path to x-coordinate, but port is vertical')
|
||||
if position is None:
|
||||
position = y
|
||||
|
||||
x0, y0 = port.offset
|
||||
if is_horizontal:
|
||||
if numpy.sign(numpy.cos(port.rotation)) == numpy.sign(position - x0):
|
||||
raise BuildError(f'path_to routing to behind source port: x0={x0:g} to {position:g}')
|
||||
length = numpy.abs(position - x0)
|
||||
else:
|
||||
if numpy.sign(numpy.sin(port.rotation)) == numpy.sign(position - y0):
|
||||
raise BuildError(f'path_to routing to behind source port: y0={y0:g} to {position:g}')
|
||||
length = numpy.abs(position - y0)
|
||||
|
||||
return self.path(
|
||||
portspec,
|
||||
ccw,
|
||||
length,
|
||||
tool_port_names=tool_port_names,
|
||||
plug_into=plug_into,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def path_into(
|
||||
self,
|
||||
portspec_src: str,
|
||||
portspec_dst: str,
|
||||
*,
|
||||
tool_port_names: tuple[str, str] = ('A', 'B'),
|
||||
out_ptype: str | None = None,
|
||||
plug_destination: bool = True,
|
||||
**kwargs,
|
||||
) -> Self:
|
||||
"""
|
||||
Create a "wire"/"waveguide" and traveling between the ports `portspec_src` and
|
||||
`portspec_dst`, and `plug` it into both (or just the source port).
|
||||
|
||||
Only unambiguous scenarios are allowed:
|
||||
- Straight connector between facing ports
|
||||
- Single 90 degree bend
|
||||
- Jog between facing ports
|
||||
(jog is done as late as possible, i.e. only 2 L-shaped segments are used)
|
||||
|
||||
By default, the destination's `pytpe` will be used as the `out_ptype` for the
|
||||
wire, and the `portspec_dst` will be plugged (i.e. removed).
|
||||
|
||||
Args:
|
||||
portspec_src: The name of the starting port into which the wire will be plugged.
|
||||
portspec_dst: The name of the destination port.
|
||||
tool_port_names: The names of the ports on the generated pattern. It is unlikely
|
||||
that you will need to change these. The first port is the input (to be
|
||||
connected to `portspec`).
|
||||
out_ptype: Passed to the pathing tool in order to specify the desired port type
|
||||
to be generated at the destination end. If `None` (default), the destination
|
||||
port's `ptype` will be used.
|
||||
|
||||
Returns:
|
||||
self
|
||||
|
||||
Raises:
|
||||
PortError if either port does not have a specified rotation.
|
||||
BuildError if and invalid port config is encountered:
|
||||
- Non-manhattan ports
|
||||
- U-bend
|
||||
- Destination too close to (or behind) source
|
||||
"""
|
||||
if self._dead:
|
||||
logger.error('Skipping path_into() since device is dead')
|
||||
return self
|
||||
|
||||
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]
|
||||
|
||||
dst_extra_args = {'out_ptype': out_ptype}
|
||||
if plug_destination:
|
||||
dst_extra_args['plug_into'] = portspec_dst
|
||||
|
||||
src_args = {**kwargs, 'tool_port_names': tool_port_names}
|
||||
dst_args = {**src_args, **dst_extra_args}
|
||||
if src_is_horizontal and not dst_is_horizontal:
|
||||
# single bend should suffice
|
||||
self.path_to(portspec_src, angle > pi, x=xd, **src_args)
|
||||
self.path_to(portspec_src, None, y=yd, **dst_args)
|
||||
elif dst_is_horizontal and not src_is_horizontal:
|
||||
# single bend should suffice
|
||||
self.path_to(portspec_src, angle > pi, y=yd, **src_args)
|
||||
self.path_to(portspec_src, None, x=xd, **dst_args)
|
||||
elif numpy.isclose(angle, pi):
|
||||
if src_is_horizontal and ys == yd:
|
||||
# straight connector
|
||||
self.path_to(portspec_src, None, x=xd, **dst_args)
|
||||
elif not src_is_horizontal and xs == xd:
|
||||
# straight connector
|
||||
self.path_to(portspec_src, None, y=yd, **dst_args)
|
||||
elif src_is_horizontal:
|
||||
# figure out how much x our y-segment (2nd) takes up, then path based on that
|
||||
y_len = numpy.abs(yd - ys)
|
||||
ccw2 = src_ne != (yd > ys)
|
||||
jog = get_jog(ccw2, y_len) * numpy.sign(xd - xs)
|
||||
self.path_to(portspec_src, not ccw2, x=xd - jog, **src_args)
|
||||
self.path_to(portspec_src, ccw2, y=yd, **dst_args)
|
||||
else:
|
||||
# figure out how much y our x-segment (2nd) takes up, then path based on that
|
||||
x_len = numpy.abs(xd - xs)
|
||||
ccw2 = src_ne != (xd < xs)
|
||||
jog = get_jog(ccw2, x_len) * numpy.sign(yd - ys)
|
||||
self.path_to(portspec_src, not ccw2, y=yd - jog, **src_args)
|
||||
self.path_to(portspec_src, ccw2, x=xd, **dst_args)
|
||||
elif numpy.isclose(angle, 0):
|
||||
raise BuildError('Don\'t know how to route a U-bend at this time!')
|
||||
else:
|
||||
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)
|
||||
return self
|
||||
|
||||
|
|
|
|||
|
|
@ -1,677 +0,0 @@
|
|||
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, Iterable
|
||||
from collections.abc import Sequence, Mapping, MutableMapping
|
||||
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, TreeView
|
||||
from ..error import BuildError
|
||||
from ..library import ILibrary
|
||||
from ..error import PortError, BuildError
|
||||
from ..ports import PortList, Port
|
||||
from ..abstract import Abstract
|
||||
from ..utils import SupportsBool
|
||||
from .tools import Tool, RenderStep
|
||||
from .pather_mixin import PatherMixin
|
||||
from .utils import ell
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RenderPather(PatherMixin):
|
||||
class RenderPather(PortList):
|
||||
"""
|
||||
`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,11 +108,15 @@ class RenderPather(PatherMixin):
|
|||
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:
|
||||
|
|
@ -182,21 +186,16 @@ class RenderPather(PatherMixin):
|
|||
new = RenderPather(library=library, pattern=pat, name=name, tools=tools)
|
||||
return new
|
||||
|
||||
def __repr__(self) -> str:
|
||||
s = f'<RenderPather {self.pattern} L({len(self.library)}) {pformat(self.tools)}>'
|
||||
return s
|
||||
|
||||
def plug(
|
||||
self,
|
||||
other: Abstract | str | Pattern | TreeView,
|
||||
other: Abstract | str,
|
||||
map_in: dict[str, str],
|
||||
map_out: dict[str, str | None] | None = None,
|
||||
*,
|
||||
mirrored: bool = False,
|
||||
thru: bool | str = True,
|
||||
inherit_name: bool = 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'
|
||||
|
|
@ -211,15 +210,11 @@ class RenderPather(PatherMixin):
|
|||
new names for ports in `other`.
|
||||
mirrored: Enables mirroring `other` across the x axis prior to
|
||||
connecting any ports.
|
||||
thru: If map_in specifies only a single port, `thru` provides a mechainsm
|
||||
to avoid repeating the port name. Eg, for `map_in={'myport': 'A'}`,
|
||||
- If True (default), and `other` has only two ports total, and map_out
|
||||
doesn't specify a name for the other port, its name is set to the key
|
||||
in `map_in`, i.e. 'myport'.
|
||||
- If a string, `map_out[thru]` is set to the key in `map_in` (i.e. 'myport').
|
||||
An error is raised if that entry already exists.
|
||||
|
||||
This makes it easy to extend a pattern with simple 2-port devices
|
||||
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
|
||||
(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
|
||||
|
|
@ -230,12 +225,6 @@ class RenderPather(PatherMixin):
|
|||
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
|
||||
|
|
@ -276,10 +265,9 @@ class RenderPather(PatherMixin):
|
|||
map_in=map_in,
|
||||
map_out=map_out,
|
||||
mirrored=mirrored,
|
||||
thru = thru,
|
||||
inherit_name=inherit_name,
|
||||
set_rotation=set_rotation,
|
||||
append=append,
|
||||
ok_connections = ok_connections,
|
||||
)
|
||||
|
||||
return self
|
||||
|
|
@ -357,16 +345,28 @@ class RenderPather(PatherMixin):
|
|||
|
||||
return self
|
||||
|
||||
def plugged(
|
||||
def retool(
|
||||
self,
|
||||
connections: dict[str, str],
|
||||
tool: Tool,
|
||||
keys: str | Sequence[str | None] | None = None,
|
||||
) -> Self:
|
||||
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)
|
||||
"""
|
||||
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
|
||||
|
||||
def path(
|
||||
|
|
@ -374,8 +374,6 @@ class RenderPather(PatherMixin):
|
|||
portspec: str,
|
||||
ccw: SupportsBool | None,
|
||||
length: float,
|
||||
*,
|
||||
plug_into: str | None = None,
|
||||
**kwargs,
|
||||
) -> Self:
|
||||
"""
|
||||
|
|
@ -395,8 +393,6 @@ class RenderPather(PatherMixin):
|
|||
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
|
||||
|
|
@ -427,87 +423,163 @@ class RenderPather(PatherMixin):
|
|||
|
||||
self.pattern.ports[portspec] = out_port.copy()
|
||||
|
||||
if plug_into is not None:
|
||||
self.plugged({portspec: plug_into})
|
||||
|
||||
return self
|
||||
|
||||
def pathS(
|
||||
def path_to(
|
||||
self,
|
||||
portspec: str,
|
||||
length: float,
|
||||
jog: float,
|
||||
ccw: SupportsBool | None,
|
||||
position: float | None = None,
|
||||
*,
|
||||
plug_into: str | None = None,
|
||||
x: float | None = None,
|
||||
y: float | None = None,
|
||||
**kwargs,
|
||||
) -> Self:
|
||||
"""
|
||||
Create an S-shaped "wire"/"waveguide" and `plug` it into the port `portspec`, with the aim
|
||||
of traveling exactly `length` distance with an offset `jog` along the other axis (+ve jog is
|
||||
left of direction of travel).
|
||||
Plan a "wire"/"waveguide" extending from the port `portspec`, with the aim
|
||||
of ending exactly at a target position.
|
||||
|
||||
The output port will have the same orientation as the source port (`portspec`).
|
||||
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.
|
||||
|
||||
`RenderPather.render` must be called after all paths have been fully planned.
|
||||
|
||||
This function attempts to use `tool.planS()`, but falls back to `tool.planL()` if the former
|
||||
raises a NotImplementedError.
|
||||
|
||||
Args:
|
||||
portspec: The name of the port into which the wire will be plugged.
|
||||
jog: Total manhattan distance perpendicular to the direction of travel.
|
||||
Positive values are to the left of the direction of travel.
|
||||
length: The total manhattan distance from input to output, along the input's axis only.
|
||||
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.)
|
||||
plug_into: If not None, attempts to plug the wire's output port into the provided
|
||||
port on `self`.
|
||||
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.
|
||||
|
||||
Returns:
|
||||
self
|
||||
|
||||
Raises:
|
||||
BuildError if `distance` is too small to fit the s-bend (for nonzero jog).
|
||||
LibraryError if no valid name could be picked for the pattern.
|
||||
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 pathS() since device is 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]
|
||||
in_ptype = port.ptype
|
||||
port_rot = port.rotation
|
||||
assert port_rot is not None # TODO allow manually setting rotation for RenderPather.path()?
|
||||
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])
|
||||
if not numpy.isclose(port.rotation % (pi / 2), 0):
|
||||
raise BuildError('path_to was asked to route from non-manhattan port')
|
||||
|
||||
# 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) # TODO length/2 may fail with asymmetric ptypes
|
||||
jog0 = Port((0, 0), 0).measure_travel(t_port0)[0][1]
|
||||
t_port1, _ = tool.planL(not ccw0, abs(jog - jog0), in_ptype=t_port0.ptype, **kwargs)
|
||||
jog1 = Port((0, 0), 0).measure_travel(t_port1)[0][1]
|
||||
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
|
||||
|
||||
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)
|
||||
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')
|
||||
return self
|
||||
|
||||
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()
|
||||
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]
|
||||
|
||||
if plug_into is not None:
|
||||
self.plugged({portspec: plug_into})
|
||||
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)
|
||||
return self
|
||||
|
||||
|
||||
def render(
|
||||
self,
|
||||
append: bool = True,
|
||||
|
|
@ -624,23 +696,8 @@ class RenderPather(PatherMixin):
|
|||
self._dead = True
|
||||
return self
|
||||
|
||||
@wraps(Pattern.label)
|
||||
def label(self, *args, **kwargs) -> Self:
|
||||
self.pattern.label(*args, **kwargs)
|
||||
return self
|
||||
def __repr__(self) -> str:
|
||||
s = f'<Pather {self.pattern} L({len(self.library)}) {pformat(self.tools)}>'
|
||||
return s
|
||||
|
||||
@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, Self
|
||||
from typing import Literal, Any
|
||||
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` and `RenderPather`.
|
||||
Used by `Pather`.
|
||||
|
||||
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,48 +101,6 @@ 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,
|
||||
|
|
@ -177,7 +135,7 @@ class Tool:
|
|||
kwargs: Custom tool-specific parameters.
|
||||
|
||||
Returns:
|
||||
The calculated output `Port` for the wire, assuming an input port at (0, 0) with rotation 0.
|
||||
The calculated output `Port` for the wire.
|
||||
Any tool-specifc data, to be stored in `RenderStep.data`, for use during rendering.
|
||||
|
||||
Raises:
|
||||
|
|
@ -215,7 +173,7 @@ class Tool:
|
|||
kwargs: Custom tool-specific parameters.
|
||||
|
||||
Returns:
|
||||
The calculated output `Port` for the wire, assuming an input port at (0, 0) with rotation 0.
|
||||
The calculated output `Port` for the wire.
|
||||
Any tool-specifc data, to be stored in `RenderStep.data`, for use during rendering.
|
||||
|
||||
Raises:
|
||||
|
|
@ -246,14 +204,14 @@ class Tool:
|
|||
|
||||
Args:
|
||||
jog: The total offset from the input to output, along the perpendicular axis.
|
||||
A positive number implies a leftwards shift (i.e. counterclockwise bend
|
||||
followed by a clockwise bend)
|
||||
A positive number implies a rightwards shift (i.e. clockwise bend followed
|
||||
by a counterclockwise 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, assuming an input port at (0, 0) with rotation 0.
|
||||
The calculated output `Port` for the wire.
|
||||
Any tool-specifc data, to be stored in `RenderStep.data`, for use during rendering.
|
||||
|
||||
Raises:
|
||||
|
|
@ -265,7 +223,7 @@ class Tool:
|
|||
self,
|
||||
batch: Sequence[RenderStep],
|
||||
*,
|
||||
port_names: tuple[str, str] = ('A', 'B'), # noqa: ARG002 (unused)
|
||||
port_names: Sequence[str] = ('A', 'B'), # noqa: ARG002 (unused)
|
||||
**kwargs, # noqa: ARG002 (unused)
|
||||
) -> ILibrary:
|
||||
"""
|
||||
|
|
@ -287,40 +245,77 @@ abstract_tuple_t = tuple[Abstract, str, str]
|
|||
|
||||
|
||||
@dataclass
|
||||
class SimpleTool(Tool, metaclass=ABCMeta):
|
||||
class BasicTool(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] | Callable[[float], Library], str, str]
|
||||
straight: tuple[Callable[[float], Pattern], 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)
|
||||
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, # noqa: ARG002 (unused)
|
||||
out_ptype: str | None = None, # noqa: ARG002 (unused)
|
||||
in_ptype: str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
**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
|
||||
|
||||
|
|
@ -341,532 +336,87 @@ class SimpleTool(Tool, metaclass=ABCMeta):
|
|||
bend_angle *= -1
|
||||
else:
|
||||
bend_dxy = numpy.zeros(2)
|
||||
bend_angle = pi
|
||||
bend_angle = 0
|
||||
|
||||
if ccw is not None:
|
||||
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:
|
||||
out_ptype_actual = bend.ports[bport_out].ptype
|
||||
else:
|
||||
out_ptype_actual = self.default_out_ptype
|
||||
|
||||
straight_length = length - bend_dxy[0]
|
||||
bend_run = bend_dxy[1]
|
||||
straight_length = length - bend_dxy[0] - itrans_dxy[0] - otrans_dxy[0]
|
||||
bend_run = bend_dxy[1] + itrans_dxy[1] + otrans_dxy[1]
|
||||
|
||||
if straight_length < 0:
|
||||
raise BuildError(
|
||||
f'Asked to draw L-path with total length {length:,g}, shorter than required bends ({bend_dxy[0]:,})'
|
||||
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}'
|
||||
)
|
||||
|
||||
data = self.LData(straight_length, kwargs, ccw)
|
||||
data = self.LData(straight_length, ccw, in_transition, out_transition)
|
||||
out_port = Port((length, bend_run), rotation=bend_angle, ptype=out_ptype_actual)
|
||||
return out_port, data
|
||||
|
||||
def _renderL(
|
||||
def render(
|
||||
self,
|
||||
data: LData,
|
||||
tree: ILibrary,
|
||||
port_names: tuple[str, str],
|
||||
straight_kwargs: dict[str, Any],
|
||||
batch: Sequence[RenderStep],
|
||||
*,
|
||||
port_names: Sequence[str] = ('A', 'B'),
|
||||
append: bool = True,
|
||||
**kwargs,
|
||||
) -> ILibrary:
|
||||
"""
|
||||
Render an L step into a preexisting tree
|
||||
"""
|
||||
pat = tree.top_pattern()
|
||||
|
||||
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
|
||||
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)
|
||||
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 = 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:
|
||||
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
|
||||
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: 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)
|
||||
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)
|
||||
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})
|
||||
return tree
|
||||
|
||||
|
||||
|
|
@ -961,7 +511,7 @@ class PathTool(Tool, metaclass=ABCMeta):
|
|||
|
||||
if straight_length < 0:
|
||||
raise BuildError(
|
||||
f'Asked to draw L-path with total length {length:,g}, shorter than required bend: {bend_dxy[0]:,g}'
|
||||
f'Asked to draw 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)
|
||||
|
|
@ -971,7 +521,7 @@ class PathTool(Tool, metaclass=ABCMeta):
|
|||
self,
|
||||
batch: Sequence[RenderStep],
|
||||
*,
|
||||
port_names: tuple[str, str] = ('A', 'B'),
|
||||
port_names: Sequence[str] = ('A', 'B'),
|
||||
**kwargs, # noqa: ARG002 (unused)
|
||||
) -> ILibrary:
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ def ell(
|
|||
*,
|
||||
spacing: float | ArrayLike | None = None,
|
||||
set_rotation: float | None = None,
|
||||
) -> dict[str, numpy.float64]:
|
||||
) -> dict[str, float]:
|
||||
"""
|
||||
Calculate extension for each port in order to build a 90-degree bend with the provided
|
||||
channel spacing:
|
||||
|
|
@ -169,11 +169,11 @@ def ell(
|
|||
'emax', 'max_extension',
|
||||
'min_past_furthest',):
|
||||
if numpy.size(bound) == 2:
|
||||
bound = cast('Sequence[float]', bound)
|
||||
bound = cast(Sequence[float], bound)
|
||||
rot_bound = (rot_matrix @ ((bound[0], 0),
|
||||
(0, bound[1])))[0, :]
|
||||
else:
|
||||
bound = cast('float', bound)
|
||||
bound = cast(float, bound)
|
||||
rot_bound = numpy.array(bound)
|
||||
|
||||
if rot_bound < 0:
|
||||
|
|
@ -185,10 +185,10 @@ def ell(
|
|||
offsets += rot_bound.min() - offsets.max()
|
||||
else:
|
||||
if numpy.size(bound) == 2:
|
||||
bound = cast('Sequence[float]', bound)
|
||||
bound = cast(Sequence[float], bound)
|
||||
rot_bound = (rot_matrix @ bound)[0]
|
||||
else:
|
||||
bound = cast('float', bound)
|
||||
bound = cast(float, bound)
|
||||
neg = (direction + pi / 4) % (2 * pi) > pi
|
||||
rot_bound = -bound if neg else bound
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
import traceback
|
||||
import pathlib
|
||||
|
||||
|
||||
MASQUE_DIR = str(pathlib.Path(__file__).parent)
|
||||
|
||||
|
||||
class MasqueError(Exception):
|
||||
"""
|
||||
Parent exception for all Masque-related Exceptions
|
||||
|
|
@ -32,64 +25,15 @@ class BuildError(MasqueError):
|
|||
"""
|
||||
pass
|
||||
|
||||
|
||||
class PortError(MasqueError):
|
||||
"""
|
||||
Exception raised by port-related functions
|
||||
Exception raised by builder-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))
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ def writefile(
|
|||
with tmpfile(path) as base_stream:
|
||||
streams: tuple[Any, ...] = (base_stream,)
|
||||
if path.suffix == '.gz':
|
||||
gz_stream = cast('IO[bytes]', gzip.GzipFile(filename='', mtime=0, fileobj=base_stream, mode='wb'))
|
||||
gz_stream = cast(IO[bytes], gzip.GzipFile(filename='', mtime=0, fileobj=base_stream, mode='wb'))
|
||||
streams = (gz_stream,) + streams
|
||||
else:
|
||||
gz_stream = base_stream
|
||||
|
|
@ -214,7 +214,7 @@ def _read_block(block: ezdxf.layouts.BlockLayout | ezdxf.layouts.Modelspace) ->
|
|||
if isinstance(element, LWPolyline):
|
||||
points = numpy.asarray(element.get_points())
|
||||
elif isinstance(element, Polyline):
|
||||
points = numpy.asarray([pp.xyz for pp in element.points()])
|
||||
points = numpy.asarray(element.points())[:, :2]
|
||||
attr = element.dxfattribs()
|
||||
layer = attr.get('layer', DEFAULT_LAYER)
|
||||
|
||||
|
|
@ -351,7 +351,7 @@ def _shapes_to_elements(
|
|||
)
|
||||
|
||||
for polygon in shape.to_polygons():
|
||||
xy_open = polygon.vertices
|
||||
xy_open = polygon.vertices + polygon.offset
|
||||
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]:d}.{layer[1]:d}'
|
||||
return f'{layer[0]}.{layer[1]}'
|
||||
raise PatternError(f'Unknown layer type: {layer} ({type(layer)})')
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ Notes:
|
|||
"""
|
||||
from typing import IO, cast, Any
|
||||
from collections.abc import Iterable, Mapping, Callable
|
||||
from types import MappingProxyType
|
||||
import io
|
||||
import mmap
|
||||
import logging
|
||||
|
|
@ -53,8 +52,6 @@ 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)
|
||||
|
|
@ -148,7 +145,7 @@ def writefile(
|
|||
with tmpfile(path) as base_stream:
|
||||
streams: tuple[Any, ...] = (base_stream,)
|
||||
if path.suffix == '.gz':
|
||||
stream = cast('IO[bytes]', gzip.GzipFile(filename='', mtime=0, fileobj=base_stream, mode='wb', compresslevel=6))
|
||||
stream = cast(IO[bytes], gzip.GzipFile(filename='', mtime=0, fileobj=base_stream, mode='wb', compresslevel=6))
|
||||
streams = (stream,) + streams
|
||||
else:
|
||||
stream = base_stream
|
||||
|
|
@ -402,15 +399,11 @@ def _mrefs_to_grefs(refs: dict[str | None, list[Ref]]) -> list[klamath.library.R
|
|||
return grefs
|
||||
|
||||
|
||||
def _properties_to_annotations(properties: Mapping[int, bytes]) -> annotations_t:
|
||||
if not properties:
|
||||
return None
|
||||
def _properties_to_annotations(properties: dict[int, bytes]) -> annotations_t:
|
||||
return {str(k): [v.decode()] for k, v in properties.items()}
|
||||
|
||||
|
||||
def _annotations_to_properties(annotations: annotations_t, max_len: int = 126) -> Mapping[int, bytes]:
|
||||
if annotations is None:
|
||||
return RO_EMPTY_DICT
|
||||
def _annotations_to_properties(annotations: annotations_t, max_len: int = 126) -> dict[int, bytes]:
|
||||
cum_len = 0
|
||||
props = {}
|
||||
for key, vals in annotations.items():
|
||||
|
|
@ -418,8 +411,8 @@ def _annotations_to_properties(annotations: annotations_t, max_len: int = 126) -
|
|||
i = int(key)
|
||||
except ValueError as err:
|
||||
raise PatternError(f'Annotation key {key} is not convertable to an integer') from err
|
||||
if not (0 < i <= 126):
|
||||
raise PatternError(f'Annotation key {key} converts to {i} (must be in the range [1,126])')
|
||||
if not (0 < i < 126):
|
||||
raise PatternError(f'Annotation key {key} converts to {i} (must be in the range [1,125])')
|
||||
|
||||
val_strings = ' '.join(str(val) for val in vals)
|
||||
b = val_strings.encode()
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ def writefile(
|
|||
with tmpfile(path) as base_stream:
|
||||
streams: tuple[Any, ...] = (base_stream,)
|
||||
if path.suffix == '.gz':
|
||||
stream = cast('IO[bytes]', gzip.GzipFile(filename='', mtime=0, fileobj=base_stream, mode='wb'))
|
||||
stream = cast(IO[bytes], gzip.GzipFile(filename='', mtime=0, fileobj=base_stream, mode='wb'))
|
||||
streams += (stream,)
|
||||
else:
|
||||
stream = base_stream
|
||||
|
|
@ -551,7 +551,7 @@ def _shapes_to_elements(
|
|||
circle = fatrec.Circle(
|
||||
layer=layer,
|
||||
datatype=datatype,
|
||||
radius=cast('int', radius),
|
||||
radius=cast(int, radius),
|
||||
x=offset[0],
|
||||
y=offset[1],
|
||||
properties=properties,
|
||||
|
|
@ -568,8 +568,8 @@ def _shapes_to_elements(
|
|||
path = fatrec.Path(
|
||||
layer=layer,
|
||||
datatype=datatype,
|
||||
point_list=cast('Sequence[Sequence[int]]', deltas),
|
||||
half_width=cast('int', half_width),
|
||||
point_list=cast(Sequence[Sequence[int]], deltas),
|
||||
half_width=cast(int, half_width),
|
||||
x=xy[0],
|
||||
y=xy[1],
|
||||
extension_start=extension_start, # TODO implement multiple cap types?
|
||||
|
|
@ -587,7 +587,7 @@ def _shapes_to_elements(
|
|||
datatype=datatype,
|
||||
x=xy[0],
|
||||
y=xy[1],
|
||||
point_list=cast('list[list[int]]', points),
|
||||
point_list=cast(list[list[int]], points),
|
||||
properties=properties,
|
||||
repetition=repetition,
|
||||
))
|
||||
|
|
@ -651,17 +651,17 @@ def repetition_masq2fata(
|
|||
a_count = rint_cast(rep.a_count)
|
||||
b_count = rint_cast(rep.b_count) if rep.b_count is not None else None
|
||||
frep = fatamorgana.GridRepetition(
|
||||
a_vector=cast('list[int]', a_vector),
|
||||
b_vector=cast('list[int] | None', b_vector),
|
||||
a_count=cast('int', a_count),
|
||||
b_count=cast('int | None', b_count),
|
||||
a_vector=cast(list[int], a_vector),
|
||||
b_vector=cast(list[int] | None, b_vector),
|
||||
a_count=cast(int, a_count),
|
||||
b_count=cast(int | None, b_count),
|
||||
)
|
||||
offset = (0, 0)
|
||||
elif isinstance(rep, Arbitrary):
|
||||
diffs = numpy.diff(rep.displacements, axis=0)
|
||||
diff_ints = rint_cast(diffs)
|
||||
frep = fatamorgana.ArbitraryRepetition(diff_ints[:, 0], diff_ints[:, 1]) # type: ignore
|
||||
offset = tuple(rep.displacements[0, :])
|
||||
offset = rep.displacements[0, :]
|
||||
else:
|
||||
assert rep is None
|
||||
frep = None
|
||||
|
|
@ -671,8 +671,6 @@ 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 logging
|
||||
import warnings
|
||||
|
||||
import numpy
|
||||
from numpy.typing import ArrayLike
|
||||
|
|
@ -12,9 +12,6 @@ from .utils import mangle_name
|
|||
from .. import Pattern
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def writefile(
|
||||
library: Mapping[str, Pattern],
|
||||
top: str,
|
||||
|
|
@ -53,7 +50,7 @@ def writefile(
|
|||
bounds = pattern.get_bounds(library=library)
|
||||
if bounds is None:
|
||||
bounds_min, bounds_max = numpy.array([[-1, -1], [1, 1]])
|
||||
logger.warning('Pattern had no bounds (empty?); setting arbitrary viewbox', stacklevel=1)
|
||||
warnings.warn('Pattern had no bounds (empty?); setting arbitrary viewbox', stacklevel=1)
|
||||
else:
|
||||
bounds_min, bounds_max = bounds
|
||||
|
||||
|
|
@ -120,7 +117,7 @@ def writefile_inverted(
|
|||
bounds = pattern.get_bounds(library=library)
|
||||
if bounds is None:
|
||||
bounds_min, bounds_max = numpy.array([[-1, -1], [1, 1]])
|
||||
logger.warning('Pattern had no bounds (empty?); setting arbitrary viewbox', stacklevel=1)
|
||||
warnings.warn('Pattern had no bounds (empty?); setting arbitrary viewbox', stacklevel=1)
|
||||
else:
|
||||
bounds_min, bounds_max = bounds
|
||||
|
||||
|
|
|
|||
|
|
@ -22,13 +22,12 @@ import copy
|
|||
from pprint import pformat
|
||||
from collections import defaultdict
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from graphlib import TopologicalSorter
|
||||
|
||||
import numpy
|
||||
from numpy.typing import ArrayLike, NDArray
|
||||
|
||||
from .error import LibraryError, PatternError
|
||||
from .utils import layer_t, apply_transforms
|
||||
from .utils import rotation_matrix_2d, layer_t
|
||||
from .shapes import Shape, Polygon
|
||||
from .label import Label
|
||||
from .abstract import Abstract
|
||||
|
|
@ -141,6 +140,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
Args:
|
||||
tops: Name(s) of the pattern(s) to check.
|
||||
Default is all patterns in the library.
|
||||
skip: Memo, set patterns which have already been traversed.
|
||||
|
||||
Returns:
|
||||
Set of all referenced pattern names
|
||||
|
|
@ -210,7 +210,7 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
if isinstance(tops, str):
|
||||
tops = (tops,)
|
||||
|
||||
keep = cast('set[str]', self.referenced_patterns(tops) - {None})
|
||||
keep = cast(set[str], self.referenced_patterns(tops) - {None})
|
||||
keep |= set(tops)
|
||||
|
||||
filtered = {kk: vv for kk, vv in self.items() if kk in keep}
|
||||
|
|
@ -263,7 +263,6 @@ 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
|
||||
|
|
@ -273,12 +272,9 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
For an in-place variant, see `Pattern.flatten`.
|
||||
|
||||
Args:
|
||||
tops: The pattern(s) to flatten.
|
||||
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.
|
||||
|
|
@ -295,8 +291,6 @@ 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)
|
||||
|
||||
|
|
@ -312,16 +306,14 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
p.ports.clear()
|
||||
pat.append(p)
|
||||
|
||||
for target in set(pat.refs.keys()) & set(self.keys()):
|
||||
del pat.refs[target]
|
||||
|
||||
pat.refs.clear()
|
||||
flattened[name] = pat
|
||||
|
||||
for top in tops:
|
||||
flatten_single(top)
|
||||
|
||||
assert None not in flattened.values()
|
||||
return cast('dict[str, Pattern]', flattened)
|
||||
return cast(dict[str, 'Pattern'], flattened)
|
||||
|
||||
def get_name(
|
||||
self,
|
||||
|
|
@ -482,13 +474,16 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
raise LibraryError(f'.dfs() called on pattern with circular reference to "{target}"')
|
||||
|
||||
for ref in pattern.refs[target]:
|
||||
ref_transforms: list[bool] | NDArray[numpy.float64]
|
||||
if transform is not False:
|
||||
ref_transforms = apply_transforms(transform, ref.as_transforms())
|
||||
sign = numpy.ones(2)
|
||||
if transform[3]:
|
||||
sign[1] = -1
|
||||
xy = numpy.dot(rotation_matrix_2d(transform[2]), ref.offset * sign)
|
||||
ref_transform = transform + (xy[0], xy[1], ref.rotation, ref.mirrored)
|
||||
ref_transform[3] %= 2
|
||||
else:
|
||||
ref_transforms = [False]
|
||||
ref_transform = False
|
||||
|
||||
for ref_transform in ref_transforms:
|
||||
self.dfs(
|
||||
pattern=self[target],
|
||||
visit_before=visit_before,
|
||||
|
|
@ -511,147 +506,10 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
raise LibraryError('visit_* functions returned a new `Pattern` object'
|
||||
' but no top-level name was provided in `hierarchy`')
|
||||
|
||||
cast('ILibrary', self)[name] = pattern
|
||||
cast(ILibrary, self)[name] = pattern
|
||||
|
||||
return self
|
||||
|
||||
def child_graph(self) -> dict[str, set[str | None]]:
|
||||
"""
|
||||
Return a mapping from pattern name to a set of all child patterns
|
||||
(patterns it references).
|
||||
|
||||
Returns:
|
||||
Mapping from pattern name to a set of all pattern names it references.
|
||||
"""
|
||||
graph = {name: set(pat.refs.keys()) for name, pat in self.items()}
|
||||
return graph
|
||||
|
||||
def parent_graph(self) -> dict[str, set[str]]:
|
||||
"""
|
||||
Return a mapping from pattern name to a set of all parent patterns
|
||||
(patterns which reference it).
|
||||
|
||||
Returns:
|
||||
Mapping from pattern name to a set of all patterns which reference it.
|
||||
"""
|
||||
igraph: dict[str, set[str]] = {name: set() for name in self}
|
||||
for name, pat in self.items():
|
||||
for child, reflist in pat.refs.items():
|
||||
if reflist and child is not None:
|
||||
igraph[child].add(name)
|
||||
return igraph
|
||||
|
||||
def child_order(self) -> list[str]:
|
||||
"""
|
||||
Return a topologically sorted list of all contained pattern names.
|
||||
Child (referenced) patterns will appear before their parents.
|
||||
|
||||
Return:
|
||||
Topologically sorted list of pattern names.
|
||||
"""
|
||||
return cast('list[str]', list(TopologicalSorter(self.child_graph()).static_order()))
|
||||
|
||||
def find_refs_local(
|
||||
self,
|
||||
name: str,
|
||||
parent_graph: dict[str, set[str]] | None = None,
|
||||
) -> dict[str, list[NDArray[numpy.float64]]]:
|
||||
"""
|
||||
Find the location and orientation of all refs pointing to `name`.
|
||||
Refs with a `repetition` are resolved into multiple instances (locations).
|
||||
|
||||
Args:
|
||||
name: Name of the referenced pattern.
|
||||
parent_graph: Mapping from pattern name to the set of patterns which
|
||||
reference it. Default (`None`) calls `self.parent_graph()`.
|
||||
The provided graph may be for a superset of `self` (i.e. it may
|
||||
contain additional patterns which are not present in self; they
|
||||
will be ignored).
|
||||
|
||||
Returns:
|
||||
Mapping of {parent_name: transform_list}, where transform_list
|
||||
is an Nx4 ndarray with rows
|
||||
`(x_offset, y_offset, rotation_ccw_rad, mirror_across_x)`.
|
||||
"""
|
||||
instances = defaultdict(list)
|
||||
if parent_graph is None:
|
||||
parent_graph = self.parent_graph()
|
||||
for parent in parent_graph[name]:
|
||||
if parent not in self: # parent_graph may be a for a superset of self
|
||||
continue
|
||||
for ref in self[parent].refs[name]:
|
||||
instances[parent].append(ref.as_transforms())
|
||||
|
||||
return instances
|
||||
|
||||
def find_refs_global(
|
||||
self,
|
||||
name: str,
|
||||
order: list[str] | None = None,
|
||||
parent_graph: dict[str, set[str]] | None = None,
|
||||
) -> dict[tuple[str, ...], NDArray[numpy.float64]]:
|
||||
"""
|
||||
Find the absolute (top-level) location and orientation of all refs (including
|
||||
repetitions) pointing to `name`.
|
||||
|
||||
Args:
|
||||
name: Name of the referenced pattern.
|
||||
order: List of pattern names in which children are guaranteed
|
||||
to appear before their parents (i.e. topologically sorted).
|
||||
Default (`None`) calls `self.child_order()`.
|
||||
parent_graph: Passed to `find_refs_local`.
|
||||
Mapping from pattern name to the set of patterns which
|
||||
reference it. Default (`None`) calls `self.parent_graph()`.
|
||||
The provided graph may be for a superset of `self` (i.e. it may
|
||||
contain additional patterns which are not present in self; they
|
||||
will be ignored).
|
||||
|
||||
Returns:
|
||||
Mapping of `{hierarchy: transform_list}`, where `hierarchy` is a tuple of the form
|
||||
`(toplevel_pattern, lvl1_pattern, ..., name)` and `transform_list` is an Nx4 ndarray
|
||||
with rows `(x_offset, y_offset, rotation_ccw_rad, mirror_across_x)`.
|
||||
"""
|
||||
if name not in self:
|
||||
return {}
|
||||
if order is None:
|
||||
order = self.child_order()
|
||||
if parent_graph is None:
|
||||
parent_graph = self.parent_graph()
|
||||
|
||||
self_keys = set(self.keys())
|
||||
|
||||
transforms: dict[str, list[tuple[
|
||||
tuple[str, ...],
|
||||
NDArray[numpy.float64]
|
||||
]]]
|
||||
transforms = defaultdict(list)
|
||||
for parent, vals in self.find_refs_local(name, parent_graph=parent_graph).items():
|
||||
transforms[parent] = [((name,), numpy.concatenate(vals))]
|
||||
|
||||
for next_name in order:
|
||||
if next_name not in transforms:
|
||||
continue
|
||||
if not parent_graph[next_name] & self_keys:
|
||||
continue
|
||||
|
||||
outers = self.find_refs_local(next_name, parent_graph=parent_graph)
|
||||
inners = transforms.pop(next_name)
|
||||
for parent, outer in outers.items():
|
||||
for path, inner in inners:
|
||||
combined = apply_transforms(numpy.concatenate(outer), inner)
|
||||
transforms[parent].append((
|
||||
(next_name,) + path,
|
||||
combined,
|
||||
))
|
||||
result = {}
|
||||
for parent, targets in transforms.items():
|
||||
for path, instances in targets:
|
||||
full_path = (parent,) + path
|
||||
assert full_path not in result
|
||||
result[full_path] = instances
|
||||
return result
|
||||
|
||||
|
||||
|
||||
class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||
"""
|
||||
|
|
@ -834,7 +692,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
for old_name in temp:
|
||||
new_name = rename_map.get(old_name, old_name)
|
||||
pat = self[new_name]
|
||||
pat.refs = map_targets(pat.refs, lambda tt: cast('dict[str | None, str | None]', rename_map).get(tt, tt))
|
||||
pat.refs = map_targets(pat.refs, lambda tt: cast(dict[str | None, str | None], rename_map).get(tt, tt))
|
||||
|
||||
return rename_map
|
||||
|
||||
|
|
@ -951,8 +809,8 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
|
||||
shape_table: dict[tuple, list] = defaultdict(list)
|
||||
for layer, sseq in pat.shapes.items():
|
||||
for ii, shape in enumerate(sseq):
|
||||
if any(isinstance(shape, tt) for tt in exclude_types):
|
||||
for i, shape in enumerate(sseq):
|
||||
if any(isinstance(shape, t) for t in exclude_types):
|
||||
continue
|
||||
|
||||
base_label, values, _func = shape.normalized_form(norm_value)
|
||||
|
|
@ -961,16 +819,16 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
if label not in shape_pats:
|
||||
continue
|
||||
|
||||
shape_table[label].append((ii, values))
|
||||
shape_table[label].append((i, values))
|
||||
|
||||
# For repeated shapes, create a `Pattern` holding a normalized shape object,
|
||||
# and add `pat.refs` entries for each occurrence in pat. Also, note down that
|
||||
# we should delete the `pat.shapes` entries for which we made `Ref`s.
|
||||
shapes_to_remove = []
|
||||
for label, shape_entries in shape_table.items():
|
||||
for label in shape_table:
|
||||
layer = label[-1]
|
||||
target = label2name(label)
|
||||
for ii, values in shape_entries:
|
||||
for ii, values in shape_table[label]:
|
||||
offset, scale, rotation, mirror_x = values
|
||||
pat.ref(target=target, offset=offset, scale=scale,
|
||||
rotation=rotation, mirrored=(mirror_x, False))
|
||||
|
|
@ -1054,7 +912,7 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
if isinstance(tops, str):
|
||||
tops = (tops,)
|
||||
|
||||
keep = cast('set[str]', self.referenced_patterns(tops) - {None})
|
||||
keep = cast(set[str], self.referenced_patterns(tops) - {None})
|
||||
keep |= set(tops)
|
||||
|
||||
new = type(self)()
|
||||
|
|
@ -1075,22 +933,20 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
|||
Returns:
|
||||
A set containing the names of all deleted patterns
|
||||
"""
|
||||
parent_graph = self.parent_graph()
|
||||
empty = {name for name, pat in self.items() if pat.is_empty()}
|
||||
trimmed = set()
|
||||
while empty:
|
||||
parents = set()
|
||||
while empty := {name for name, pat in self.items() if pat.is_empty()}:
|
||||
for name in empty:
|
||||
del self[name]
|
||||
for parent in parent_graph[name]:
|
||||
del self[parent].refs[name]
|
||||
parents |= parent_graph[name]
|
||||
|
||||
for pat in self.values():
|
||||
for name in empty:
|
||||
# Second pass to skip looking at refs in empty patterns
|
||||
if name in pat.refs:
|
||||
del pat.refs[name]
|
||||
|
||||
trimmed |= empty
|
||||
if not repeat:
|
||||
break
|
||||
|
||||
empty = {parent for parent in parents if self[parent].is_empty()}
|
||||
return trimmed
|
||||
|
||||
def delete(
|
||||
|
|
|
|||
|
|
@ -332,7 +332,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||
))
|
||||
|
||||
self.ports = dict(sorted(self.ports.items()))
|
||||
self.annotations = dict(sorted(self.annotations.items())) if self.annotations is not None else None
|
||||
self.annotations = dict(sorted(self.annotations.items()))
|
||||
|
||||
return self
|
||||
|
||||
|
|
@ -354,9 +354,6 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||
for layer, lseq in other_pattern.labels.items():
|
||||
self.labels[layer].extend(lseq)
|
||||
|
||||
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}')
|
||||
|
|
@ -418,7 +415,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||
elif default_keep:
|
||||
pat.refs = copy.copy(self.refs)
|
||||
|
||||
if annotations is not None and self.annotations is not None:
|
||||
if 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)
|
||||
|
|
@ -494,7 +491,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||
"""
|
||||
pat = self.deepcopy().polygonize().flatten(library=library)
|
||||
polys = [
|
||||
cast('Polygon', shape).vertices + cast('Polygon', shape).offset
|
||||
cast(Polygon, shape).vertices + cast(Polygon, shape).offset
|
||||
for shape in chain_elements(pat.shapes)
|
||||
]
|
||||
return polys
|
||||
|
|
@ -536,7 +533,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||
n_elems = sum(1 for _ in chain_elements(self.shapes, self.labels))
|
||||
ebounds = numpy.full((n_elems, 2, 2), nan)
|
||||
for ee, entry in enumerate(chain_elements(self.shapes, self.labels)):
|
||||
maybe_ebounds = cast('Bounded', entry).get_bounds()
|
||||
maybe_ebounds = cast(Bounded, entry).get_bounds()
|
||||
if maybe_ebounds is not None:
|
||||
ebounds[ee] = maybe_ebounds
|
||||
mask = ~numpy.isnan(ebounds[:, 0, 0])
|
||||
|
|
@ -584,7 +581,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_nonempty()
|
||||
bounds += ref.repetition.get_bounds()
|
||||
|
||||
else:
|
||||
# Non-manhattan rotation, have to figure out bounds by rotating the pattern
|
||||
|
|
@ -634,7 +631,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||
self
|
||||
"""
|
||||
for entry in chain(chain_elements(self.shapes, self.labels, self.refs), self.ports.values()):
|
||||
cast('Positionable', entry).translate(offset)
|
||||
cast(Positionable, entry).translate(offset)
|
||||
return self
|
||||
|
||||
def scale_elements(self, c: float) -> Self:
|
||||
|
|
@ -648,37 +645,33 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||
self
|
||||
"""
|
||||
for entry in chain_elements(self.shapes, self.refs):
|
||||
cast('Scalable', entry).scale_by(c)
|
||||
cast(Scalable, entry).scale_by(c)
|
||||
return self
|
||||
|
||||
def scale_by(self, c: float, scale_refs: bool = True) -> Self:
|
||||
def scale_by(self, c: float) -> Self:
|
||||
"""
|
||||
Scale this Pattern by the given value
|
||||
All shapes and (optionally) refs and their offsets are scaled,
|
||||
as are all label and port offsets.
|
||||
(all shapes and refs and their offsets are scaled,
|
||||
as are all label and port offsets)
|
||||
|
||||
Args:
|
||||
c: factor to scale by
|
||||
scale_refs: Whether to scale refs. Ref offsets are always scaled,
|
||||
but it may be desirable to not scale the ref itself (e.g. if
|
||||
the target cell was also scaled).
|
||||
|
||||
Returns:
|
||||
self
|
||||
"""
|
||||
for entry in chain_elements(self.shapes, self.refs):
|
||||
cast('Positionable', entry).offset *= c
|
||||
if scale_refs or not isinstance(entry, Ref):
|
||||
cast('Scalable', entry).scale_by(c)
|
||||
cast(Positionable, entry).offset *= c
|
||||
cast(Scalable, entry).scale_by(c)
|
||||
|
||||
rep = cast('Repeatable', entry).repetition
|
||||
rep = cast(Repeatable, entry).repetition
|
||||
if rep:
|
||||
rep.scale_by(c)
|
||||
|
||||
for label in chain_elements(self.labels):
|
||||
cast('Positionable', label).offset *= c
|
||||
cast(Positionable, label).offset *= c
|
||||
|
||||
rep = cast('Repeatable', label).repetition
|
||||
rep = cast(Repeatable, label).repetition
|
||||
if rep:
|
||||
rep.scale_by(c)
|
||||
|
||||
|
|
@ -715,8 +708,8 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||
self
|
||||
"""
|
||||
for entry in chain(chain_elements(self.shapes, self.refs, self.labels), self.ports.values()):
|
||||
old_offset = cast('Positionable', entry).offset
|
||||
cast('Positionable', entry).offset = numpy.dot(rotation_matrix_2d(rotation), old_offset)
|
||||
old_offset = cast(Positionable, entry).offset
|
||||
cast(Positionable, entry).offset = numpy.dot(rotation_matrix_2d(rotation), old_offset)
|
||||
return self
|
||||
|
||||
def rotate_elements(self, rotation: float) -> Self:
|
||||
|
|
@ -730,7 +723,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||
self
|
||||
"""
|
||||
for entry in chain(chain_elements(self.shapes, self.refs), self.ports.values()):
|
||||
cast('Rotatable', entry).rotate(rotation)
|
||||
cast(Rotatable, entry).rotate(rotation)
|
||||
return self
|
||||
|
||||
def mirror_element_centers(self, across_axis: int = 0) -> Self:
|
||||
|
|
@ -745,7 +738,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[1 - across_axis] *= -1
|
||||
cast(Positionable, entry).offset[across_axis - 1] *= -1
|
||||
return self
|
||||
|
||||
def mirror_elements(self, across_axis: int = 0) -> Self:
|
||||
|
|
@ -761,7 +754,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||
self
|
||||
"""
|
||||
for entry in chain(chain_elements(self.shapes, self.refs), self.ports.values()):
|
||||
cast('Mirrorable', entry).mirror(across_axis)
|
||||
cast(Mirrorable, entry).mirror(across_axis)
|
||||
return self
|
||||
|
||||
def mirror(self, across_axis: int = 0) -> Self:
|
||||
|
|
@ -1169,13 +1162,12 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||
ports[new_name] = port
|
||||
|
||||
for name, port in ports.items():
|
||||
pp = port.deepcopy()
|
||||
p = port.deepcopy()
|
||||
if mirrored:
|
||||
pp.mirror()
|
||||
pp.offset[1] *= -1
|
||||
pp.rotate_around(pivot, rotation)
|
||||
pp.translate(offset)
|
||||
self.ports[name] = pp
|
||||
p.mirror()
|
||||
p.rotate_around(pivot, rotation)
|
||||
p.translate(offset)
|
||||
self.ports[name] = p
|
||||
|
||||
if append:
|
||||
if isinstance(other, Abstract):
|
||||
|
|
@ -1203,7 +1195,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||
# map_out: dict[str, str | None] | None,
|
||||
# *,
|
||||
# mirrored: bool,
|
||||
# thru: bool | str,
|
||||
# inherit_name: bool,
|
||||
# set_rotation: bool | None,
|
||||
# append: Literal[False],
|
||||
# ) -> Self:
|
||||
|
|
@ -1217,7 +1209,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||
# map_out: dict[str, str | None] | None,
|
||||
# *,
|
||||
# mirrored: bool,
|
||||
# thru: bool | str,
|
||||
# inherit_name: bool,
|
||||
# set_rotation: bool | None,
|
||||
# append: bool,
|
||||
# ) -> Self:
|
||||
|
|
@ -1230,10 +1222,9 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||
map_out: dict[str, str | None] | None = None,
|
||||
*,
|
||||
mirrored: bool = False,
|
||||
thru: bool | str = True,
|
||||
inherit_name: bool = True,
|
||||
set_rotation: bool | None = None,
|
||||
append: bool = False,
|
||||
ok_connections: Iterable[tuple[str, str]] = (),
|
||||
) -> Self:
|
||||
"""
|
||||
Instantiate or append a pattern into the current pattern, connecting
|
||||
|
|
@ -1241,7 +1232,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||
ports specified by `map_out`.
|
||||
|
||||
Examples:
|
||||
=========
|
||||
======list, ===
|
||||
- `my_pat.plug(subdevice, {'A': 'C', 'B': 'B'}, map_out={'D': 'myport'})`
|
||||
instantiates `subdevice` into `my_pat`, plugging ports 'A' and 'B'
|
||||
of `my_pat` into ports 'C' and 'B' of `subdevice`. The connected ports
|
||||
|
|
@ -1251,7 +1242,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 `thru` argument is not explicitly set to `False`,
|
||||
provided, and the `inherit_name` 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.
|
||||
|
|
@ -1264,15 +1255,11 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||
new names for ports in `other`.
|
||||
mirrored: Enables mirroring `other` across the x axis prior to connecting
|
||||
any ports.
|
||||
thru: If map_in specifies only a single port, `thru` provides a mechainsm
|
||||
to avoid repeating the port name. Eg, for `map_in={'myport': 'A'}`,
|
||||
- If True (default), and `other` has only two ports total, and map_out
|
||||
doesn't specify a name for the other port, its name is set to the key
|
||||
in `map_in`, i.e. 'myport'.
|
||||
- If a string, `map_out[thru]` is set to the key in `map_in` (i.e. 'myport').
|
||||
An error is raised if that entry already exists.
|
||||
|
||||
This makes it easy to extend a pattern with simple 2-port devices
|
||||
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
|
||||
(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
|
||||
|
|
@ -1283,11 +1270,6 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
|||
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
|
||||
|
|
@ -1300,32 +1282,24 @@ 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,
|
||||
)
|
||||
|
||||
# get rid of plugged ports
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
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
|
||||
|
|
@ -11,8 +13,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, rotation_matrix_2d
|
||||
from .error import PortError, format_stacktrace
|
||||
from .utils import rotate_offsets_around
|
||||
from .error import PortError
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -62,7 +64,7 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable):
|
|||
return self._rotation
|
||||
|
||||
@rotation.setter
|
||||
def rotation(self, val: float | None) -> None:
|
||||
def rotation(self, val: float) -> None:
|
||||
if val is None:
|
||||
self._rotation = None
|
||||
else:
|
||||
|
|
@ -100,6 +102,7 @@ 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
|
||||
|
|
@ -142,28 +145,6 @@ 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__
|
||||
|
|
@ -324,11 +305,11 @@ class PortList(metaclass=ABCMeta):
|
|||
|
||||
if type_conflicts.any():
|
||||
msg = 'Ports have conflicting types:\n'
|
||||
for nn, (kk, vv) in enumerate(connections.items()):
|
||||
for nn, (k, v) in enumerate(connections.items()):
|
||||
if type_conflicts[nn]:
|
||||
msg += f'{kk} | {a_types[nn]}:{b_types[nn]} | {vv}\n'
|
||||
msg += '\nStack trace:\n' + format_stacktrace()
|
||||
logger.warning(msg)
|
||||
msg += f'{k} | {a_types[nn]}:{b_types[nn]} | {v}\n'
|
||||
msg = ''.join(traceback.format_stack()) + '\n' + msg
|
||||
warnings.warn(msg, stacklevel=2)
|
||||
|
||||
a_offsets = numpy.array([pp.offset for pp in a_ports])
|
||||
b_offsets = numpy.array([pp.offset for pp in b_ports])
|
||||
|
|
@ -345,17 +326,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, (kk, vv) in enumerate(connections.items()):
|
||||
for nn, (k, v) in enumerate(connections.items()):
|
||||
if not numpy.isclose(rot_deg[nn], 0):
|
||||
msg += f'{kk} | {rot_deg[nn]:g} | {vv}\n'
|
||||
msg += f'{k} | {rot_deg[nn]:g} | {v}\n'
|
||||
raise PortError(msg)
|
||||
|
||||
translations = a_offsets - b_offsets
|
||||
if not numpy.allclose(translations, 0):
|
||||
msg = 'Port translations do not match:\n'
|
||||
for nn, (kk, vv) in enumerate(connections.items()):
|
||||
for nn, (k, v) in enumerate(connections.items()):
|
||||
if not numpy.allclose(translations[nn], 0):
|
||||
msg += f'{kk} | {translations[nn]} | {vv}\n'
|
||||
msg += f'{k} | {translations[nn]} | {v}\n'
|
||||
raise PortError(msg)
|
||||
|
||||
for pp in chain(a_names, b_names):
|
||||
|
|
@ -425,7 +406,7 @@ class PortList(metaclass=ABCMeta):
|
|||
|
||||
map_out_counts = Counter(map_out.values())
|
||||
map_out_counts[None] = 0
|
||||
conflicts_out = {kk for kk, vv in map_out_counts.items() if vv > 1}
|
||||
conflicts_out = {k for k, v in map_out_counts.items() if v > 1}
|
||||
if conflicts_out:
|
||||
raise PortError(f'Duplicate targets in `map_out`: {conflicts_out}')
|
||||
|
||||
|
|
@ -438,7 +419,6 @@ class PortList(metaclass=ABCMeta):
|
|||
*,
|
||||
mirrored: bool = False,
|
||||
set_rotation: bool | None = None,
|
||||
ok_connections: Iterable[tuple[str, str]] = (),
|
||||
) -> tuple[NDArray[numpy.float64], float, NDArray[numpy.float64]]:
|
||||
"""
|
||||
Given a device `other` and a mapping `map_in` specifying port connections,
|
||||
|
|
@ -455,11 +435,6 @@ class PortList(metaclass=ABCMeta):
|
|||
port with `rotation=None`), `set_rotation` must be provided
|
||||
to indicate how much `other` should be rotated. Otherwise,
|
||||
`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 log a
|
||||
warning. Order is ignored, i.e. `(a, b)` is equivalent to
|
||||
`(b, a)`.
|
||||
|
||||
Returns:
|
||||
- The (x, y) translation (performed last)
|
||||
|
|
@ -476,7 +451,6 @@ class PortList(metaclass=ABCMeta):
|
|||
map_in=map_in,
|
||||
mirrored=mirrored,
|
||||
set_rotation=set_rotation,
|
||||
ok_connections = ok_connections,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -487,14 +461,13 @@ class PortList(metaclass=ABCMeta):
|
|||
*,
|
||||
mirrored: bool = False,
|
||||
set_rotation: bool | None = None,
|
||||
ok_connections: Iterable[tuple[str, str]] = (),
|
||||
) -> tuple[NDArray[numpy.float64], float, NDArray[numpy.float64]]:
|
||||
"""
|
||||
Given two sets of ports (s_ports and o_ports) and a mapping `map_in`
|
||||
specifying port connections, find the transform which will correctly
|
||||
align the specified o_ports onto their respective s_ports.
|
||||
|
||||
Args:
|
||||
Args:t
|
||||
s_ports: A list of stationary ports
|
||||
o_ports: A list of ports which are to be moved/mirrored.
|
||||
map_in: dict of `{'s_port': 'o_port'}` mappings, specifying
|
||||
|
|
@ -506,11 +479,6 @@ class PortList(metaclass=ABCMeta):
|
|||
port with `rotation=None`), `set_rotation` must be provided
|
||||
to indicate how much `o_ports` should be rotated. Otherwise,
|
||||
`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 log a
|
||||
warning. Order is ignored, i.e. `(a, b)` is equivalent to
|
||||
`(b, a)`.
|
||||
|
||||
Returns:
|
||||
- The (x, y) translation (performed last)
|
||||
|
|
@ -534,16 +502,15 @@ class PortList(metaclass=ABCMeta):
|
|||
o_offsets[:, 1] *= -1
|
||||
o_rotations *= -1
|
||||
|
||||
ok_pairs = {tuple(sorted(pair)) for pair in ok_connections if pair[0] != pair[1]}
|
||||
type_conflicts = numpy.array([(st != ot) and ('unk' not in (st, ot)) and (tuple(sorted((st, ot))) not in ok_pairs)
|
||||
type_conflicts = numpy.array([st != ot and 'unk' not in (st, ot)
|
||||
for st, ot in zip(s_types, o_types, strict=True)])
|
||||
if type_conflicts.any():
|
||||
msg = 'Ports have conflicting types:\n'
|
||||
for nn, (kk, vv) in enumerate(map_in.items()):
|
||||
for nn, (k, v) in enumerate(map_in.items()):
|
||||
if type_conflicts[nn]:
|
||||
msg += f'{kk} | {s_types[nn]}:{o_types[nn]} | {vv}\n'
|
||||
msg += '\nStack trace:\n' + format_stacktrace()
|
||||
logger.warning(msg)
|
||||
msg += f'{k} | {s_types[nn]}:{o_types[nn]} | {v}\n'
|
||||
msg = ''.join(traceback.format_stack()) + '\n' + msg
|
||||
warnings.warn(msg, stacklevel=2)
|
||||
|
||||
rotations = numpy.mod(s_rotations - o_rotations - pi, 2 * pi)
|
||||
if not has_rot.any():
|
||||
|
|
@ -556,8 +523,8 @@ class PortList(metaclass=ABCMeta):
|
|||
if not numpy.allclose(rotations[:1], rotations):
|
||||
rot_deg = numpy.rad2deg(rotations)
|
||||
msg = 'Port orientations do not match:\n'
|
||||
for nn, (kk, vv) in enumerate(map_in.items()):
|
||||
msg += f'{kk} | {rot_deg[nn]:g} | {vv}\n'
|
||||
for nn, (k, v) in enumerate(map_in.items()):
|
||||
msg += f'{k} | {rot_deg[nn]:g} | {v}\n'
|
||||
raise PortError(msg)
|
||||
|
||||
pivot = o_offsets[0].copy()
|
||||
|
|
@ -565,12 +532,8 @@ 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] - common_translation} | {vv}\n'
|
||||
for nn, (k, v) in enumerate(map_in.items()):
|
||||
msg += f'{k} | {translations[nn]} | {v}\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, SupportsBool
|
||||
from .utils import annotations_t, rotation_matrix_2d, annotations_eq, annotations_lt, rep2key
|
||||
from .repetition import Repetition
|
||||
from .traits import (
|
||||
PositionableImpl, RotatableImpl, ScalableImpl,
|
||||
|
|
@ -50,11 +50,11 @@ class Ref(
|
|||
|
||||
# Mirrored property
|
||||
@property
|
||||
def mirrored(self) -> bool:
|
||||
def mirrored(self) -> bool: # mypy#3004, setter should be SupportsBool
|
||||
return self._mirrored
|
||||
|
||||
@mirrored.setter
|
||||
def mirrored(self, val: SupportsBool) -> None:
|
||||
def mirrored(self, val: bool) -> None:
|
||||
self._mirrored = bool(val)
|
||||
|
||||
def __init__(
|
||||
|
|
@ -183,16 +183,6 @@ class Ref(
|
|||
self.rotation += pi
|
||||
return self
|
||||
|
||||
def as_transforms(self) -> NDArray[numpy.float64]:
|
||||
xys = self.offset[None, :]
|
||||
if self.repetition is not None:
|
||||
xys = xys + self.repetition.displacements
|
||||
transforms = numpy.empty((xys.shape[0], 4))
|
||||
transforms[:, :2] = xys
|
||||
transforms[:, 2] = self.rotation
|
||||
transforms[:, 3] = self.mirrored
|
||||
return transforms
|
||||
|
||||
def get_bounds_single(
|
||||
self,
|
||||
pattern: 'Pattern',
|
||||
|
|
|
|||
|
|
@ -294,7 +294,7 @@ class Grid(Repetition):
|
|||
def __le__(self, other: Repetition) -> bool:
|
||||
if type(self) is not type(other):
|
||||
return repr(type(self)) < repr(type(other))
|
||||
other = cast('Grid', other)
|
||||
other = cast(Grid, other)
|
||||
if self.a_count != other.a_count:
|
||||
return self.a_count < other.a_count
|
||||
if self.b_count != other.b_count:
|
||||
|
|
@ -327,7 +327,7 @@ class Arbitrary(Repetition):
|
|||
"""
|
||||
|
||||
@property
|
||||
def displacements(self) -> NDArray[numpy.float64]:
|
||||
def displacements(self) -> Any: # mypy#3004 NDArray[numpy.float64]:
|
||||
return self._displacements
|
||||
|
||||
@displacements.setter
|
||||
|
|
@ -357,7 +357,7 @@ class Arbitrary(Repetition):
|
|||
def __le__(self, other: Repetition) -> bool:
|
||||
if type(self) is not type(other):
|
||||
return repr(type(self)) < repr(type(other))
|
||||
other = cast('Arbitrary', other)
|
||||
other = cast(Arbitrary, other)
|
||||
if self.displacements.size != other.displacements.size:
|
||||
return self.displacements.size < other.displacements.size
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ 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,11 +10,10 @@ 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(PositionableImpl, Shape):
|
||||
class Arc(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.
|
||||
|
|
@ -43,7 +42,7 @@ class Arc(PositionableImpl, Shape):
|
|||
|
||||
# radius properties
|
||||
@property
|
||||
def radii(self) -> NDArray[numpy.float64]:
|
||||
def radii(self) -> Any: # mypy#3004 NDArray[numpy.float64]:
|
||||
"""
|
||||
Return the radii `[rx, ry]`
|
||||
"""
|
||||
|
|
@ -80,7 +79,7 @@ class Arc(PositionableImpl, Shape):
|
|||
|
||||
# arc start/stop angle properties
|
||||
@property
|
||||
def angles(self) -> NDArray[numpy.float64]:
|
||||
def angles(self) -> Any: # mypy#3004 NDArray[numpy.float64]:
|
||||
"""
|
||||
Return the start and stop angles `[a_start, a_stop]`.
|
||||
Angles are measured from x-axis after rotation
|
||||
|
|
@ -158,7 +157,7 @@ class Arc(PositionableImpl, Shape):
|
|||
offset: ArrayLike = (0.0, 0.0),
|
||||
rotation: float = 0,
|
||||
repetition: Repetition | None = None,
|
||||
annotations: annotations_t = None,
|
||||
annotations: annotations_t | None = None,
|
||||
raw: bool = False,
|
||||
) -> None:
|
||||
if raw:
|
||||
|
|
@ -171,7 +170,7 @@ class Arc(PositionableImpl, Shape):
|
|||
self._offset = offset
|
||||
self._rotation = rotation
|
||||
self._repetition = repetition
|
||||
self._annotations = annotations
|
||||
self._annotations = annotations if annotations is not None else {}
|
||||
else:
|
||||
self.radii = radii
|
||||
self.angles = angles
|
||||
|
|
@ -179,7 +178,7 @@ class Arc(PositionableImpl, Shape):
|
|||
self.offset = offset
|
||||
self.rotation = rotation
|
||||
self.repetition = repetition
|
||||
self.annotations = annotations
|
||||
self.annotations = annotations if annotations is not None else {}
|
||||
|
||||
def __deepcopy__(self, memo: dict | None = None) -> 'Arc':
|
||||
memo = {} if memo is None else memo
|
||||
|
|
@ -207,7 +206,7 @@ class Arc(PositionableImpl, Shape):
|
|||
if repr(type(self)) != repr(type(other)):
|
||||
return repr(type(self)) < repr(type(other))
|
||||
return id(type(self)) < id(type(other))
|
||||
other = cast('Arc', other)
|
||||
other = cast(Arc, other)
|
||||
if self.width != other.width:
|
||||
return self.width < other.width
|
||||
if not numpy.array_equal(self.radii, other.radii):
|
||||
|
|
@ -234,7 +233,7 @@ class Arc(PositionableImpl, Shape):
|
|||
r0, r1 = self.radii
|
||||
|
||||
# Convert from polar angle to ellipse parameter (for [rx*cos(t), ry*sin(t)] representation)
|
||||
a_ranges = cast('_array2x2_t', self._angles_to_parameters())
|
||||
a_ranges = self._angles_to_parameters()
|
||||
|
||||
# Approximate perimeter via numerical integration
|
||||
|
||||
|
|
@ -245,31 +244,30 @@ class Arc(PositionableImpl, Shape):
|
|||
#t0 = ellipeinc(a0 - pi / 2, m)
|
||||
#perimeter2 = r0 * (t1 - t0)
|
||||
|
||||
def get_arclens(n_pts: int, a0: float, a1: float, dr: float) -> tuple[NDArray[numpy.float64], NDArray[numpy.float64]]:
|
||||
def get_arclens(n_pts: int, a0: float, a1: float) -> tuple[NDArray[numpy.float64], NDArray[numpy.float64]]:
|
||||
""" Get `n_pts` arclengths """
|
||||
tt, dt = numpy.linspace(a0, a1, n_pts, retstep=True) # NOTE: could probably use an adaptive number of points
|
||||
r0sin = (r0 + dr) * numpy.sin(tt)
|
||||
r1cos = (r1 + dr) * numpy.cos(tt)
|
||||
t, dt = numpy.linspace(a0, a1, n_pts, retstep=True) # NOTE: could probably use an adaptive number of points
|
||||
r0sin = r0 * numpy.sin(t)
|
||||
r1cos = r1 * numpy.cos(t)
|
||||
arc_dl = numpy.sqrt(r0sin * r0sin + r1cos * r1cos)
|
||||
#arc_lengths = numpy.diff(tt) * (arc_dl[1:] + arc_dl[:-1]) / 2
|
||||
#arc_lengths = numpy.diff(t) * (arc_dl[1:] + arc_dl[:-1]) / 2
|
||||
arc_lengths = (arc_dl[1:] + arc_dl[:-1]) * numpy.abs(dt) / 2
|
||||
return arc_lengths, tt
|
||||
return arc_lengths, t
|
||||
|
||||
wh = self.width / 2.0
|
||||
if num_vertices is not None:
|
||||
n_pts = numpy.ceil(max(self.radii + wh) / min(self.radii) * num_vertices * 100).astype(int)
|
||||
perimeter_inner = get_arclens(n_pts, *a_ranges[0], dr=-wh)[0].sum()
|
||||
perimeter_outer = get_arclens(n_pts, *a_ranges[1], dr= wh)[0].sum()
|
||||
n_pts = numpy.ceil(max(self.radii) / min(self.radii) * num_vertices * 100).astype(int)
|
||||
perimeter_inner = get_arclens(n_pts, *a_ranges[0])[0].sum()
|
||||
perimeter_outer = get_arclens(n_pts, *a_ranges[1])[0].sum()
|
||||
implied_arclen = (perimeter_outer + perimeter_inner + self.width * 2) / num_vertices
|
||||
max_arclen = min(implied_arclen, max_arclen if max_arclen is not None else numpy.inf)
|
||||
assert max_arclen is not None
|
||||
|
||||
def get_thetas(inner: bool) -> NDArray[numpy.float64]:
|
||||
""" Figure out the parameter values at which we should place vertices to meet the arclength constraint"""
|
||||
dr = -wh if inner else wh
|
||||
#dr = -self.width / 2.0 * (-1 if inner else 1)
|
||||
|
||||
n_pts = numpy.ceil(2 * pi * max(self.radii + dr) / max_arclen).astype(int)
|
||||
arc_lengths, thetas = get_arclens(n_pts, *a_ranges[0 if inner else 1], dr=dr)
|
||||
n_pts = numpy.ceil(2 * pi * max(self.radii) / max_arclen).astype(int)
|
||||
arc_lengths, thetas = get_arclens(n_pts, *a_ranges[0 if inner else 1])
|
||||
|
||||
keep = [0]
|
||||
removable = (numpy.cumsum(arc_lengths) <= max_arclen)
|
||||
|
|
@ -287,7 +285,7 @@ class Arc(PositionableImpl, Shape):
|
|||
thetas = thetas[::-1]
|
||||
return thetas
|
||||
|
||||
thetas_inner: NDArray[numpy.float64]
|
||||
wh = self.width / 2.0
|
||||
if wh in (r0, r1):
|
||||
thetas_inner = numpy.zeros(1) # Don't generate multiple vertices if we're at the origin
|
||||
else:
|
||||
|
|
@ -322,11 +320,11 @@ class Arc(PositionableImpl, Shape):
|
|||
|
||||
If the extrema are innaccessible due to arc constraints, check the arc endpoints instead.
|
||||
"""
|
||||
a_ranges = cast('_array2x2_t', self._angles_to_parameters())
|
||||
a_ranges = self._angles_to_parameters()
|
||||
|
||||
mins = []
|
||||
maxs = []
|
||||
for aa, sgn in zip(a_ranges, (-1, +1), strict=True):
|
||||
for a, sgn in zip(a_ranges, (-1, +1), strict=True):
|
||||
wh = sgn * self.width / 2
|
||||
rx = self.radius_x + wh
|
||||
ry = self.radius_y + wh
|
||||
|
|
@ -337,13 +335,13 @@ class Arc(PositionableImpl, Shape):
|
|||
maxs.append([0, 0])
|
||||
continue
|
||||
|
||||
a0, a1 = aa
|
||||
a0, a1 = a
|
||||
a0_offset = a0 - (a0 % (2 * pi))
|
||||
|
||||
sin_r = numpy.sin(self.rotation)
|
||||
cos_r = numpy.cos(self.rotation)
|
||||
sin_a = numpy.sin(aa)
|
||||
cos_a = numpy.cos(aa)
|
||||
sin_a = numpy.sin(a)
|
||||
cos_a = numpy.cos(a)
|
||||
|
||||
# Cutoff angles
|
||||
xpt = (-self.rotation) % (2 * pi) + a0_offset
|
||||
|
|
@ -413,15 +411,15 @@ class Arc(PositionableImpl, Shape):
|
|||
start_angle -= pi
|
||||
rotation += pi
|
||||
|
||||
norm_angles = (start_angle, start_angle + delta_angle)
|
||||
angles = (start_angle, start_angle + delta_angle)
|
||||
rotation %= 2 * pi
|
||||
width = self.width
|
||||
|
||||
return ((type(self), radii, norm_angles, width / norm_value),
|
||||
return ((type(self), radii, angles, width / norm_value),
|
||||
(self.offset, scale / norm_value, rotation, False),
|
||||
lambda: Arc(
|
||||
radii=radii * norm_value,
|
||||
angles=norm_angles,
|
||||
angles=angles,
|
||||
width=width * norm_value,
|
||||
))
|
||||
|
||||
|
|
@ -433,19 +431,19 @@ class Arc(PositionableImpl, Shape):
|
|||
[[x2, y2], [x3, y3]]], would create this arc from its corresponding ellipse.
|
||||
```
|
||||
"""
|
||||
a_ranges = cast('_array2x2_t', self._angles_to_parameters())
|
||||
a_ranges = self._angles_to_parameters()
|
||||
|
||||
mins = []
|
||||
maxs = []
|
||||
for aa, sgn in zip(a_ranges, (-1, +1), strict=True):
|
||||
for a, sgn in zip(a_ranges, (-1, +1), strict=True):
|
||||
wh = sgn * self.width / 2
|
||||
rx = self.radius_x + wh
|
||||
ry = self.radius_y + wh
|
||||
|
||||
sin_r = numpy.sin(self.rotation)
|
||||
cos_r = numpy.cos(self.rotation)
|
||||
sin_a = numpy.sin(aa)
|
||||
cos_a = numpy.cos(aa)
|
||||
sin_a = numpy.sin(a)
|
||||
cos_a = numpy.cos(a)
|
||||
|
||||
# arc endpoints
|
||||
xn, xp = sorted(rx * cos_r * cos_a - ry * sin_r * sin_a)
|
||||
|
|
@ -457,29 +455,26 @@ class Arc(PositionableImpl, Shape):
|
|||
|
||||
def _angles_to_parameters(self) -> NDArray[numpy.float64]:
|
||||
"""
|
||||
Convert from polar angle to ellipse parameter (for [rx*cos(t), ry*sin(t)] representation)
|
||||
|
||||
Returns:
|
||||
"Eccentric anomaly" parameter ranges for the inner and outer edges, in the form
|
||||
`[[a_min_inner, a_max_inner], [a_min_outer, a_max_outer]]`
|
||||
"""
|
||||
aa = []
|
||||
a = []
|
||||
for sgn in (-1, +1):
|
||||
wh = sgn * self.width / 2.0
|
||||
wh = sgn * self.width / 2
|
||||
rx = self.radius_x + wh
|
||||
ry = self.radius_y + wh
|
||||
|
||||
a0, a1 = (numpy.arctan2(rx * numpy.sin(ai), ry * numpy.cos(ai)) for ai in self.angles)
|
||||
# create paremeter 'a' for parametrized ellipse
|
||||
a0, a1 = (numpy.arctan2(rx * numpy.sin(a), ry * numpy.cos(a)) for a in self.angles)
|
||||
sign = numpy.sign(self.angles[1] - self.angles[0])
|
||||
if sign != numpy.sign(a1 - a0):
|
||||
a1 += sign * 2 * pi
|
||||
|
||||
aa.append((a0, a1))
|
||||
return numpy.array(aa, dtype=float)
|
||||
a.append((a0, a1))
|
||||
return numpy.array(a, dtype=float)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
angles = f' a°{numpy.rad2deg(self.angles)}'
|
||||
rotation = f' r°{numpy.rad2deg(self.rotation):g}' if self.rotation != 0 else ''
|
||||
return f'<Arc o{self.offset} r{self.radii}{angles} w{self.width:g}{rotation}>'
|
||||
|
||||
_array2x2_t = tuple[tuple[float, float], tuple[float, float]]
|
||||
|
|
|
|||
|
|
@ -10,11 +10,10 @@ 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(PositionableImpl, Shape):
|
||||
class Circle(Shape):
|
||||
"""
|
||||
A circle, which has a position and radius.
|
||||
"""
|
||||
|
|
@ -49,7 +48,7 @@ class Circle(PositionableImpl, Shape):
|
|||
*,
|
||||
offset: ArrayLike = (0.0, 0.0),
|
||||
repetition: Repetition | None = None,
|
||||
annotations: annotations_t = None,
|
||||
annotations: annotations_t | None = None,
|
||||
raw: bool = False,
|
||||
) -> None:
|
||||
if raw:
|
||||
|
|
@ -57,12 +56,12 @@ class Circle(PositionableImpl, Shape):
|
|||
self._radius = radius
|
||||
self._offset = offset
|
||||
self._repetition = repetition
|
||||
self._annotations = annotations
|
||||
self._annotations = annotations if annotations is not None else {}
|
||||
else:
|
||||
self.radius = radius
|
||||
self.offset = offset
|
||||
self.repetition = repetition
|
||||
self.annotations = annotations
|
||||
self.annotations = annotations if annotations is not None else {}
|
||||
|
||||
def __deepcopy__(self, memo: dict | None = None) -> 'Circle':
|
||||
memo = {} if memo is None else memo
|
||||
|
|
@ -85,7 +84,7 @@ class Circle(PositionableImpl, Shape):
|
|||
if repr(type(self)) != repr(type(other)):
|
||||
return repr(type(self)) < repr(type(other))
|
||||
return id(type(self)) < id(type(other))
|
||||
other = cast('Circle', other)
|
||||
other = cast(Circle, other)
|
||||
if not self.radius == other.radius:
|
||||
return self.radius < other.radius
|
||||
if not numpy.array_equal(self.offset, other.offset):
|
||||
|
|
@ -124,7 +123,7 @@ class Circle(PositionableImpl, Shape):
|
|||
return self
|
||||
|
||||
def mirror(self, axis: int = 0) -> 'Circle': # noqa: ARG002 (axis unused)
|
||||
self.offset[axis - 1] *= -1
|
||||
self.offset *= -1
|
||||
return self
|
||||
|
||||
def scale_by(self, c: float) -> 'Circle':
|
||||
|
|
|
|||
|
|
@ -11,11 +11,10 @@ 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(PositionableImpl, Shape):
|
||||
class Ellipse(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.
|
||||
|
|
@ -34,7 +33,7 @@ class Ellipse(PositionableImpl, Shape):
|
|||
|
||||
# radius properties
|
||||
@property
|
||||
def radii(self) -> NDArray[numpy.float64]:
|
||||
def radii(self) -> Any: # mypy#3004 NDArray[numpy.float64]:
|
||||
"""
|
||||
Return the radii `[rx, ry]`
|
||||
"""
|
||||
|
|
@ -94,7 +93,7 @@ class Ellipse(PositionableImpl, Shape):
|
|||
offset: ArrayLike = (0.0, 0.0),
|
||||
rotation: float = 0,
|
||||
repetition: Repetition | None = None,
|
||||
annotations: annotations_t = None,
|
||||
annotations: annotations_t | None = None,
|
||||
raw: bool = False,
|
||||
) -> None:
|
||||
if raw:
|
||||
|
|
@ -104,13 +103,13 @@ class Ellipse(PositionableImpl, Shape):
|
|||
self._offset = offset
|
||||
self._rotation = rotation
|
||||
self._repetition = repetition
|
||||
self._annotations = annotations
|
||||
self._annotations = annotations if annotations is not None else {}
|
||||
else:
|
||||
self.radii = radii
|
||||
self.offset = offset
|
||||
self.rotation = rotation
|
||||
self.repetition = repetition
|
||||
self.annotations = annotations
|
||||
self.annotations = annotations if annotations is not None else {}
|
||||
|
||||
def __deepcopy__(self, memo: dict | None = None) -> Self:
|
||||
memo = {} if memo is None else memo
|
||||
|
|
@ -135,7 +134,7 @@ class Ellipse(PositionableImpl, Shape):
|
|||
if repr(type(self)) != repr(type(other)):
|
||||
return repr(type(self)) < repr(type(other))
|
||||
return id(type(self)) < id(type(other))
|
||||
other = cast('Ellipse', other)
|
||||
other = cast(Ellipse, other)
|
||||
if not numpy.array_equal(self.radii, other.radii):
|
||||
return tuple(self.radii) < tuple(other.radii)
|
||||
if not numpy.array_equal(self.offset, other.offset):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Any, cast, Self
|
||||
from typing import Any, cast
|
||||
from collections.abc import Sequence
|
||||
import copy
|
||||
import functools
|
||||
|
|
@ -30,7 +30,8 @@ class PathCap(Enum):
|
|||
@functools.total_ordering
|
||||
class Path(Shape):
|
||||
"""
|
||||
A path, consisting of a bunch of vertices (Nx2 ndarray), a width, and an end-cap shape.
|
||||
A path, consisting of a bunch of vertices (Nx2 ndarray), a width, an end-cap shape,
|
||||
and an offset.
|
||||
|
||||
Note that the setter for `Path.vertices` will create a copy of the passed vertex coordinates.
|
||||
|
||||
|
|
@ -39,7 +40,7 @@ class Path(Shape):
|
|||
__slots__ = (
|
||||
'_vertices', '_width', '_cap', '_cap_extensions',
|
||||
# Inherited
|
||||
'_repetition', '_annotations',
|
||||
'_offset', '_repetition', '_annotations',
|
||||
)
|
||||
_vertices: NDArray[numpy.float64]
|
||||
_width: float
|
||||
|
|
@ -86,7 +87,7 @@ class Path(Shape):
|
|||
|
||||
# cap_extensions property
|
||||
@property
|
||||
def cap_extensions(self) -> NDArray[numpy.float64] | None:
|
||||
def cap_extensions(self) -> Any | None: # mypy#3004 NDArray[numpy.float64]]:
|
||||
"""
|
||||
Path end-cap extension
|
||||
|
||||
|
|
@ -112,7 +113,7 @@ class Path(Shape):
|
|||
|
||||
# vertices property
|
||||
@property
|
||||
def vertices(self) -> NDArray[numpy.float64]:
|
||||
def vertices(self) -> Any: # mypy#3004 NDArray[numpy.float64]]:
|
||||
"""
|
||||
Vertices of the path (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`
|
||||
|
||||
|
|
@ -159,28 +160,6 @@ 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,
|
||||
|
|
@ -191,35 +170,36 @@ class Path(Shape):
|
|||
offset: ArrayLike = (0.0, 0.0),
|
||||
rotation: float = 0,
|
||||
repetition: Repetition | None = None,
|
||||
annotations: annotations_t = None,
|
||||
annotations: annotations_t | None = 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
|
||||
self._annotations = annotations if annotations is not None else {}
|
||||
self._width = width
|
||||
self._cap = cap
|
||||
self._cap_extensions = cap_extensions
|
||||
else:
|
||||
self.vertices = vertices
|
||||
self.offset = offset
|
||||
self.repetition = repetition
|
||||
self.annotations = annotations
|
||||
self.annotations = annotations if annotations is not None else {}
|
||||
self.width = width
|
||||
self.cap = cap
|
||||
self.cap_extensions = cap_extensions
|
||||
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)
|
||||
|
|
@ -229,6 +209,7 @@ 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
|
||||
|
|
@ -242,7 +223,7 @@ class Path(Shape):
|
|||
if repr(type(self)) != repr(type(other)):
|
||||
return repr(type(self)) < repr(type(other))
|
||||
return id(type(self)) < id(type(other))
|
||||
other = cast('Path', other)
|
||||
other = cast(Path, other)
|
||||
if self.width != other.width:
|
||||
return self.width < other.width
|
||||
if self.cap != other.cap:
|
||||
|
|
@ -253,6 +234,8 @@ 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)
|
||||
|
|
@ -288,7 +271,7 @@ class Path(Shape):
|
|||
# TODO: Path.travel() needs testing
|
||||
direction = numpy.array([1, 0])
|
||||
|
||||
verts: list[NDArray[numpy.float64]] = [numpy.zeros(2)]
|
||||
verts = [numpy.zeros(2)]
|
||||
for angle, distance in travel_pairs:
|
||||
direction = numpy.dot(rotation_matrix_2d(angle), direction.T).T
|
||||
verts.append(verts[-1] + direction * distance)
|
||||
|
|
@ -309,7 +292,7 @@ class Path(Shape):
|
|||
|
||||
if self.width == 0:
|
||||
verts = numpy.vstack((v, v[::-1]))
|
||||
return [Polygon(vertices=verts)]
|
||||
return [Polygon(offset=self.offset, vertices=verts)]
|
||||
|
||||
perp = dvdir[:, ::-1] * [[1, -1]] * self.width / 2
|
||||
|
||||
|
|
@ -324,8 +307,8 @@ class Path(Shape):
|
|||
bs = v[1:-1] - v[:-2] + perp[1:] - perp[:-1]
|
||||
ds = v[1:-1] - v[:-2] - perp[1:] + perp[:-1]
|
||||
|
||||
rp = numpy.linalg.solve(As, bs[:, :, None])[:, 0]
|
||||
rn = numpy.linalg.solve(As, ds[:, :, None])[:, 0]
|
||||
rp = numpy.linalg.solve(As, bs)[:, 0, None]
|
||||
rn = numpy.linalg.solve(As, ds)[:, 0, None]
|
||||
|
||||
intersection_p = v[:-2] + rp * dv[:-1] + perp[:-1]
|
||||
intersection_n = v[:-2] + rn * dv[:-1] - perp[:-1]
|
||||
|
|
@ -360,7 +343,7 @@ class Path(Shape):
|
|||
o1.append(v[-1] - perp[-1])
|
||||
verts = numpy.vstack((o0, o1[::-1]))
|
||||
|
||||
polys = [Polygon(vertices=verts)]
|
||||
polys = [Polygon(offset=self.offset, vertices=verts)]
|
||||
|
||||
if self.cap == PathCap.Circle:
|
||||
#for vert in v: # not sure if every vertex, or just ends?
|
||||
|
|
@ -372,7 +355,7 @@ class Path(Shape):
|
|||
|
||||
def get_bounds_single(self) -> NDArray[numpy.float64]:
|
||||
if self.cap == PathCap.Circle:
|
||||
bounds = numpy.vstack((numpy.min(self.vertices, axis=0) - self.width / 2,
|
||||
bounds = self.offset + 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,
|
||||
|
|
@ -407,7 +390,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)
|
||||
offset = self.vertices.mean(axis=0) + self.offset
|
||||
zeroed_vertices = self.vertices - offset
|
||||
|
||||
scale = zeroed_vertices.std()
|
||||
|
|
@ -422,7 +405,7 @@ class Path(Shape):
|
|||
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]
|
||||
x_min = cast(Sequence, x_min)[y_min]
|
||||
reordered_vertices = numpy.roll(rotated_vertices, -x_min, axis=0)
|
||||
|
||||
width0 = self.width / norm_value
|
||||
|
|
@ -477,5 +460,5 @@ class Path(Shape):
|
|||
return extensions
|
||||
|
||||
def __repr__(self) -> str:
|
||||
centroid = self.vertices.mean(axis=0)
|
||||
centroid = self.offset + self.vertices.mean(axis=0)
|
||||
return f'<Path centroid {centroid} v{len(self.vertices)} w{self.width} c{self.cap}>'
|
||||
|
|
|
|||
|
|
@ -1,223 +0,0 @@
|
|||
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,5 @@
|
|||
from typing import Any, cast, TYPE_CHECKING, Self
|
||||
from typing import Any, cast
|
||||
from collections.abc import Sequence
|
||||
import copy
|
||||
import functools
|
||||
|
||||
|
|
@ -12,17 +13,14 @@ from ..repetition import Repetition
|
|||
from ..utils import is_scalar, rotation_matrix_2d, annotations_lt, annotations_eq, rep2key
|
||||
from ..utils import remove_colinear_vertices, remove_duplicate_vertices, annotations_t
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class Polygon(Shape):
|
||||
"""
|
||||
A polygon, consisting of a bunch of vertices (Nx2 ndarray) which specify an
|
||||
implicitly-closed boundary.
|
||||
implicitly-closed boundary, and an offset.
|
||||
|
||||
Note that the setter for `Polygon.vertices` creates a copy of the
|
||||
Note that the setter for `Polygon.vertices` may creates a copy of the
|
||||
passed vertex coordinates.
|
||||
|
||||
A `normalized_form(...)` is available, but can be quite slow with lots of vertices.
|
||||
|
|
@ -30,7 +28,7 @@ class Polygon(Shape):
|
|||
__slots__ = (
|
||||
'_vertices',
|
||||
# Inherited
|
||||
'_repetition', '_annotations',
|
||||
'_offset', '_repetition', '_annotations',
|
||||
)
|
||||
|
||||
_vertices: NDArray[numpy.float64]
|
||||
|
|
@ -38,7 +36,7 @@ class Polygon(Shape):
|
|||
|
||||
# vertices property
|
||||
@property
|
||||
def vertices(self) -> NDArray[numpy.float64]:
|
||||
def vertices(self) -> Any: # mypy#3004 NDArray[numpy.float64]:
|
||||
"""
|
||||
Vertices of the polygon (Nx2 ndarray: `[[x0, y0], [x1, y1], ...]`)
|
||||
|
||||
|
|
@ -85,28 +83,6 @@ 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,
|
||||
|
|
@ -114,26 +90,27 @@ class Polygon(Shape):
|
|||
offset: ArrayLike = (0.0, 0.0),
|
||||
rotation: float = 0.0,
|
||||
repetition: Repetition | None = None,
|
||||
annotations: annotations_t = None,
|
||||
annotations: annotations_t | None = 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
|
||||
self._annotations = annotations if annotations is not None else {}
|
||||
else:
|
||||
self.vertices = vertices
|
||||
self.offset = offset
|
||||
self.repetition = repetition
|
||||
self.annotations = annotations
|
||||
if rotation:
|
||||
self.annotations = annotations if annotations is not None else {}
|
||||
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
|
||||
|
|
@ -141,6 +118,7 @@ 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)
|
||||
|
|
@ -151,7 +129,7 @@ class Polygon(Shape):
|
|||
if repr(type(self)) != repr(type(other)):
|
||||
return repr(type(self)) < repr(type(other))
|
||||
return id(type(self)) < id(type(other))
|
||||
other = cast('Polygon', other)
|
||||
other = cast(Polygon, other)
|
||||
if not numpy.array_equal(self.vertices, other.vertices):
|
||||
min_len = min(self.vertices.shape[0], other.vertices.shape[0])
|
||||
eq_mask = self.vertices[:min_len] != other.vertices[:min_len]
|
||||
|
|
@ -160,6 +138,8 @@ 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)
|
||||
|
|
@ -256,11 +236,6 @@ class Polygon(Shape):
|
|||
Returns:
|
||||
A Polygon object containing the requested rectangle
|
||||
"""
|
||||
if sum(int(pp is None) for pp in (xmin, xmax, xctr, lx)) != 2:
|
||||
raise PatternError('Exactly two of xmin, xctr, xmax, lx must be provided!')
|
||||
if sum(int(pp is None) for pp in (ymin, ymax, yctr, ly)) != 2:
|
||||
raise PatternError('Exactly two of ymin, yctr, ymax, ly must be provided!')
|
||||
|
||||
if lx is None:
|
||||
if xctr is None:
|
||||
assert xmin is not None
|
||||
|
|
@ -270,11 +245,11 @@ class Polygon(Shape):
|
|||
elif xmax is None:
|
||||
assert xmin is not None
|
||||
assert xctr is not None
|
||||
lx = 2.0 * (xctr - xmin)
|
||||
lx = 2 * (xctr - xmin)
|
||||
elif xmin is None:
|
||||
assert xctr is not None
|
||||
assert xmax is not None
|
||||
lx = 2.0 * (xmax - xctr)
|
||||
lx = 2 * (xmax - xctr)
|
||||
else:
|
||||
raise PatternError('Two of xmin, xctr, xmax, lx must be None!')
|
||||
else: # noqa: PLR5501
|
||||
|
|
@ -300,11 +275,11 @@ class Polygon(Shape):
|
|||
elif ymax is None:
|
||||
assert ymin is not None
|
||||
assert yctr is not None
|
||||
ly = 2.0 * (yctr - ymin)
|
||||
ly = 2 * (yctr - ymin)
|
||||
elif ymin is None:
|
||||
assert yctr is not None
|
||||
assert ymax is not None
|
||||
ly = 2.0 * (ymax - yctr)
|
||||
ly = 2 * (ymax - yctr)
|
||||
else:
|
||||
raise PatternError('Two of ymin, yctr, ymax, ly must be None!')
|
||||
else: # noqa: PLR5501
|
||||
|
|
@ -385,8 +360,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((numpy.min(self.vertices, axis=0),
|
||||
numpy.max(self.vertices, axis=0)))
|
||||
return numpy.vstack((self.offset + numpy.min(self.vertices, axis=0),
|
||||
self.offset + numpy.max(self.vertices, axis=0)))
|
||||
|
||||
def rotate(self, theta: float) -> 'Polygon':
|
||||
if theta != 0:
|
||||
|
|
@ -404,9 +379,8 @@ class Polygon(Shape):
|
|||
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.vertices.mean(axis=0)
|
||||
zeroed_vertices = self.vertices - meanv
|
||||
offset = meanv
|
||||
offset = self.vertices.mean(axis=0) + self.offset
|
||||
zeroed_vertices = self.vertices - offset
|
||||
|
||||
scale = zeroed_vertices.std()
|
||||
normed_vertices = zeroed_vertices / scale
|
||||
|
|
@ -420,7 +394,7 @@ class Polygon(Shape):
|
|||
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]
|
||||
x_min = cast(Sequence, x_min)[y_min]
|
||||
reordered_vertices = numpy.roll(rotated_vertices, -x_min, axis=0)
|
||||
|
||||
# TODO: normalize mirroring?
|
||||
|
|
@ -460,5 +434,5 @@ class Polygon(Shape):
|
|||
return self
|
||||
|
||||
def __repr__(self) -> str:
|
||||
centroid = self.vertices.mean(axis=0)
|
||||
centroid = self.offset + 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,
|
||||
Positionable, PivotableImpl, RepeatableImpl, AnnotatableImpl,
|
||||
PositionableImpl, PivotableImpl, RepeatableImpl, AnnotatableImpl,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -26,7 +26,7 @@ normalized_shape_tuple = tuple[
|
|||
DEFAULT_POLY_NUM_VERTICES = 24
|
||||
|
||||
|
||||
class Shape(Positionable, Rotatable, Mirrorable, Copyable, Scalable,
|
||||
class Shape(PositionableImpl, Rotatable, Mirrorable, Copyable, Scalable,
|
||||
PivotableImpl, RepeatableImpl, AnnotatableImpl, metaclass=ABCMeta):
|
||||
"""
|
||||
Class specifying functions common to all shapes.
|
||||
|
|
@ -134,7 +134,7 @@ class Shape(Positionable, Rotatable, Mirrorable, Copyable, Scalable,
|
|||
mins, maxs = bounds
|
||||
|
||||
vertex_lists = []
|
||||
p_verts = polygon.vertices
|
||||
p_verts = polygon.vertices + polygon.offset
|
||||
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(Positionable, Rotatable, Mirrorable, Copyable, Scalable,
|
|||
offset = (numpy.where(keep_x)[0][0],
|
||||
numpy.where(keep_y)[0][0])
|
||||
|
||||
rastered = float_raster.raster((polygon.vertices).T, gx, gy)
|
||||
rastered = float_raster.raster((polygon.vertices + polygon.offset).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 PositionableImpl, RotatableImpl
|
||||
from ..utils import is_scalar, get_bit, annotations_t, annotations_lt, annotations_eq, rep2key, SupportsBool
|
||||
from ..traits import RotatableImpl
|
||||
from ..utils import is_scalar, get_bit, annotations_t, annotations_lt, annotations_eq, rep2key
|
||||
|
||||
# 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(PositionableImpl, RotatableImpl, Shape):
|
||||
class Text(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(PositionableImpl, RotatableImpl, Shape):
|
|||
self._height = val
|
||||
|
||||
@property
|
||||
def mirrored(self) -> bool:
|
||||
def mirrored(self) -> bool: # mypy#3004, should be bool
|
||||
return self._mirrored
|
||||
|
||||
@mirrored.setter
|
||||
def mirrored(self, val: SupportsBool) -> None:
|
||||
def mirrored(self, val: bool) -> None:
|
||||
self._mirrored = bool(val)
|
||||
|
||||
def __init__(
|
||||
|
|
@ -71,7 +71,7 @@ class Text(PositionableImpl, RotatableImpl, Shape):
|
|||
offset: ArrayLike = (0.0, 0.0),
|
||||
rotation: float = 0.0,
|
||||
repetition: Repetition | None = None,
|
||||
annotations: annotations_t = None,
|
||||
annotations: annotations_t | None = None,
|
||||
raw: bool = False,
|
||||
) -> None:
|
||||
if raw:
|
||||
|
|
@ -81,14 +81,14 @@ class Text(PositionableImpl, RotatableImpl, Shape):
|
|||
self._height = height
|
||||
self._rotation = rotation
|
||||
self._repetition = repetition
|
||||
self._annotations = annotations
|
||||
self._annotations = annotations if annotations is not None else {}
|
||||
else:
|
||||
self.offset = offset
|
||||
self.string = string
|
||||
self.height = height
|
||||
self.rotation = rotation
|
||||
self.repetition = repetition
|
||||
self.annotations = annotations
|
||||
self.annotations = annotations if annotations is not None else {}
|
||||
self.font_path = font_path
|
||||
|
||||
def __deepcopy__(self, memo: dict | None = None) -> Self:
|
||||
|
|
@ -115,7 +115,7 @@ class Text(PositionableImpl, RotatableImpl, Shape):
|
|||
if repr(type(self)) != repr(type(other)):
|
||||
return repr(type(self)) < repr(type(other))
|
||||
return id(type(self)) < id(type(other))
|
||||
other = cast('Text', other)
|
||||
other = cast(Text, other)
|
||||
if not self.height == other.height:
|
||||
return self.height < other.height
|
||||
if not self.string == other.string:
|
||||
|
|
@ -201,7 +201,7 @@ def get_char_as_polygons(
|
|||
font_path: str,
|
||||
char: str,
|
||||
resolution: float = 48 * 64,
|
||||
) -> tuple[list[NDArray[numpy.float64]], float]:
|
||||
) -> tuple[list[list[list[float]]], float]:
|
||||
from freetype import Face # type: ignore
|
||||
from matplotlib.path import Path # type: ignore
|
||||
|
||||
|
|
@ -276,12 +276,11 @@ 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 = [numpy.asarray(poly) for poly in path.to_polygons()]
|
||||
polygons = 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) and annotations is not None:
|
||||
raise MasqueError(f'annotations expected dict or None, got {type(annotations)}')
|
||||
if not isinstance(annotations, dict):
|
||||
raise MasqueError(f'annotations expected dict, got {type(annotations)}')
|
||||
self._annotations = annotations
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Self
|
||||
from typing import Self, Any
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
import numpy
|
||||
|
|
@ -73,7 +73,7 @@ class PositionableImpl(Positionable, metaclass=ABCMeta):
|
|||
#
|
||||
# offset property
|
||||
@property
|
||||
def offset(self) -> NDArray[numpy.float64]:
|
||||
def offset(self) -> Any: # mypy#3004 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 += numpy.asarray(offset)
|
||||
self._offset += offset # type: ignore # NDArray += ArrayLike should be fine??
|
||||
return self
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
from typing import Self, cast, Any, TYPE_CHECKING
|
||||
from typing import Self, cast, Any
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
import numpy
|
||||
from numpy import pi
|
||||
from numpy.typing import ArrayLike
|
||||
|
||||
from .positionable import Positionable
|
||||
from ..error import MasqueError
|
||||
from ..utils import rotation_matrix_2d
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .positionable import Positionable
|
||||
|
||||
_empty_slots = () # Workaround to get mypy to ignore intentionally empty slots for superclass
|
||||
|
||||
|
|
@ -114,9 +113,9 @@ class PivotableImpl(Pivotable, metaclass=ABCMeta):
|
|||
|
||||
def rotate_around(self, pivot: ArrayLike, rotation: float) -> Self:
|
||||
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)
|
||||
cast('Positionable', self).translate(+pivot)
|
||||
cast(Positionable, self).translate(-pivot)
|
||||
cast(Rotatable, self).rotate(rotation)
|
||||
self.offset = numpy.dot(rotation_matrix_2d(rotation), self.offset) # type: ignore # mypy#3004
|
||||
cast(Positionable, self).translate(+pivot)
|
||||
return self
|
||||
|
||||
|
|
|
|||
|
|
@ -24,9 +24,6 @@ from .transform import (
|
|||
rotation_matrix_2d as rotation_matrix_2d,
|
||||
normalize_mirror as normalize_mirror,
|
||||
rotate_offsets_around as rotate_offsets_around,
|
||||
apply_transforms as apply_transforms,
|
||||
R90 as R90,
|
||||
R180 as R180,
|
||||
)
|
||||
from .comparisons import (
|
||||
annotation2key as annotation2key,
|
||||
|
|
|
|||
|
|
@ -1,104 +0,0 @@
|
|||
import numpy
|
||||
from numpy.typing import ArrayLike, NDArray
|
||||
from numpy import pi
|
||||
|
||||
try:
|
||||
from numpy import trapezoid
|
||||
except ImportError:
|
||||
from numpy import trapz as trapezoid # type:ignore
|
||||
|
||||
|
||||
def bezier(
|
||||
nodes: ArrayLike,
|
||||
tt: ArrayLike,
|
||||
weights: ArrayLike | None = None,
|
||||
) -> NDArray[numpy.float64]:
|
||||
"""
|
||||
Sample a Bezier curve with the provided control points at the parametrized positions `tt`.
|
||||
|
||||
Using the calculation method from arXiv:1803.06843, Chudy and Woźny.
|
||||
|
||||
Args:
|
||||
nodes: `[[x0, y0], ...]` control points for the Bezier curve
|
||||
tt: Parametrized positions at which to sample the curve (1D array with values in the interval [0, 1])
|
||||
weights: Control point weights; if provided, length should be the same as number of control points.
|
||||
Default 1 for all control points.
|
||||
|
||||
Returns:
|
||||
`[[x0, y0], [x1, y1], ...]` corresponding to `[tt0, tt1, ...]`
|
||||
"""
|
||||
nodes = numpy.asarray(nodes)
|
||||
tt = numpy.asarray(tt)
|
||||
nn = nodes.shape[0]
|
||||
weights = numpy.ones(nn) if weights is None else numpy.asarray(weights)
|
||||
|
||||
with numpy.errstate(divide='ignore'):
|
||||
umul = (tt / (1 - tt)).clip(max=1)
|
||||
udiv = ((1 - tt) / tt).clip(max=1)
|
||||
|
||||
hh = numpy.ones((tt.size,))
|
||||
qq = nodes[None, 0, :] * hh[:, None]
|
||||
for kk in range(1, nn):
|
||||
hh *= umul * (nn - kk) * weights[kk]
|
||||
hh /= kk * udiv * weights[kk - 1] + hh
|
||||
qq *= 1.0 - hh[:, None]
|
||||
qq += hh[:, None] * nodes[None, kk, :]
|
||||
return qq
|
||||
|
||||
|
||||
|
||||
def euler_bend(
|
||||
switchover_angle: float,
|
||||
num_points: int = 200,
|
||||
) -> NDArray[numpy.float64]:
|
||||
"""
|
||||
Generate a 90 degree Euler bend (AKA Clothoid bend or Cornu spiral).
|
||||
|
||||
Args:
|
||||
switchover_angle: After this angle, the bend will transition into a circular arc
|
||||
(and transition back to an Euler spiral on the far side). If this is set to
|
||||
`>= pi / 4`, no circular arc will be added.
|
||||
num_points: Number of points in the curve
|
||||
|
||||
Returns:
|
||||
`[[x0, y0], ...]` for the curve
|
||||
"""
|
||||
ll_max = numpy.sqrt(2 * switchover_angle) # total length of (one) spiral portion
|
||||
ll_tot = 2 * ll_max + (pi / 2 - 2 * switchover_angle)
|
||||
num_points_spiral = numpy.floor(ll_max / ll_tot * num_points).astype(int)
|
||||
num_points_arc = num_points - 2 * num_points_spiral
|
||||
|
||||
def gen_spiral(ll_max: float) -> NDArray[numpy.float64]:
|
||||
xx = []
|
||||
yy = []
|
||||
for ll in numpy.linspace(0, ll_max, num_points_spiral):
|
||||
qq = numpy.linspace(0, ll, 1000) # integrate to current arclength
|
||||
xx.append(trapezoid( numpy.cos(qq * qq / 2), qq))
|
||||
yy.append(trapezoid(-numpy.sin(qq * qq / 2), qq))
|
||||
xy_part = numpy.stack((xx, yy), axis=1)
|
||||
return xy_part
|
||||
|
||||
xy_spiral = gen_spiral(ll_max)
|
||||
xy_parts = [xy_spiral]
|
||||
|
||||
if switchover_angle < pi / 4:
|
||||
# Build a circular segment to join the two euler portions
|
||||
rmin = 1.0 / ll_max
|
||||
half_angle = pi / 4 - switchover_angle
|
||||
qq = numpy.linspace(half_angle * 2, 0, num_points_arc + 1) + switchover_angle
|
||||
xc = rmin * numpy.cos(qq)
|
||||
yc = rmin * numpy.sin(qq) + xy_spiral[-1, 1]
|
||||
xc += xy_spiral[-1, 0] - xc[0]
|
||||
yc += xy_spiral[-1, 1] - yc[0]
|
||||
xy_parts.append(numpy.stack((xc[1:], yc[1:]), axis=1))
|
||||
|
||||
endpoint_xy = xy_parts[-1][-1, :]
|
||||
second_spiral = xy_spiral[::-1, ::-1] + endpoint_xy - xy_spiral[-1, ::-1]
|
||||
|
||||
xy_parts.append(second_spiral)
|
||||
xy = numpy.concatenate(xy_parts)
|
||||
|
||||
# Remove any 2x-duplicate points
|
||||
xy = xy[(numpy.roll(xy, 1, axis=0) != xy).any(axis=1)]
|
||||
|
||||
return xy
|
||||
|
|
@ -5,15 +5,10 @@ from collections.abc import Sequence
|
|||
from functools import lru_cache
|
||||
|
||||
import numpy
|
||||
from numpy.typing import NDArray, ArrayLike
|
||||
from numpy.typing import NDArray
|
||||
from numpy import pi
|
||||
|
||||
|
||||
# Constants for shorthand rotations
|
||||
R90 = pi / 2
|
||||
R180 = pi
|
||||
|
||||
|
||||
@lru_cache
|
||||
def rotation_matrix_2d(theta: float) -> NDArray[numpy.float64]:
|
||||
"""
|
||||
|
|
@ -62,62 +57,8 @@ def rotate_offsets_around(
|
|||
) -> NDArray[numpy.float64]:
|
||||
"""
|
||||
Rotates offsets around a pivot point.
|
||||
|
||||
Args:
|
||||
offsets: Nx2 array, rows are (x, y) offsets
|
||||
pivot: (x, y) location to rotate around
|
||||
angle: rotation angle in radians
|
||||
|
||||
Returns:
|
||||
Nx2 ndarray of (x, y) position after the rotation is applied.
|
||||
"""
|
||||
offsets -= pivot
|
||||
offsets[:] = (rotation_matrix_2d(angle) @ offsets.T).T
|
||||
offsets += pivot
|
||||
return offsets
|
||||
|
||||
|
||||
def apply_transforms(
|
||||
outer: ArrayLike,
|
||||
inner: ArrayLike,
|
||||
tensor: bool = False,
|
||||
) -> NDArray[numpy.float64]:
|
||||
"""
|
||||
Apply a set of transforms (`outer`) to a second set (`inner`).
|
||||
This is used to find the "absolute" transform for nested `Ref`s.
|
||||
|
||||
The two transforms should be of shape Ox4 and Ix4.
|
||||
Rows should be of the form `(x_offset, y_offset, rotation_ccw_rad, mirror_across_x)`.
|
||||
The output will be of the form (O*I)x4 (if `tensor=False`) or OxIx4 (`tensor=True`).
|
||||
|
||||
Args:
|
||||
outer: Transforms for the container refs. Shape Ox4.
|
||||
inner: Transforms for the contained refs. Shape Ix4.
|
||||
tensor: If `True`, an OxIx4 array is returned, with `result[oo, ii, :]` corresponding
|
||||
to the `oo`th `outer` transform applied to the `ii`th inner transform.
|
||||
If `False` (default), this is concatenated into `(O*I)x4` to allow simple
|
||||
chaining into additional `apply_transforms()` calls.
|
||||
|
||||
Returns:
|
||||
OxIx4 or (O*I)x4 array. Final dimension is
|
||||
`(total_x, total_y, total_rotation_ccw_rad, net_mirrored_x)`.
|
||||
"""
|
||||
outer = numpy.atleast_2d(outer).astype(float, copy=False)
|
||||
inner = numpy.atleast_2d(inner).astype(float, copy=False)
|
||||
|
||||
# If mirrored, flip y's
|
||||
xy_mir = numpy.tile(inner[:, :2], (outer.shape[0], 1, 1)) # dims are outer, inner, xyrm
|
||||
xy_mir[outer[:, 3].astype(bool), :, 1] *= -1
|
||||
|
||||
rot_mats = [rotation_matrix_2d(angle) for angle in outer[:, 2]]
|
||||
xy = numpy.einsum('ort,oit->oir', rot_mats, xy_mir)
|
||||
|
||||
tot = numpy.empty((outer.shape[0], inner.shape[0], 4))
|
||||
tot[:, :, :2] = outer[:, None, :2] + xy
|
||||
tot[:, :, 2:] = outer[:, None, 2:] + inner[None, :, 2:] # sum rotations and mirrored
|
||||
tot[:, :, 2] %= 2 * pi # clamp rot
|
||||
tot[:, :, 3] %= 2 # clamp mirrored
|
||||
|
||||
if tensor:
|
||||
return tot
|
||||
return numpy.concatenate(tot)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from typing import Protocol
|
|||
|
||||
|
||||
layer_t = int | tuple[int, int] | str
|
||||
annotations_t = dict[str, list[int | float | str]] | None
|
||||
annotations_t = dict[str, list[int | float | str]]
|
||||
|
||||
|
||||
class SupportsBool(Protocol):
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@ def remove_duplicate_vertices(vertices: ArrayLike, closed_path: bool = True) ->
|
|||
`vertices` with no consecutive duplicates. This may be a view into the original array.
|
||||
"""
|
||||
vertices = numpy.asarray(vertices)
|
||||
duplicates = (vertices == numpy.roll(vertices, -1, axis=0)).all(axis=1)
|
||||
duplicates = (vertices == numpy.roll(vertices, 1, axis=0)).all(axis=1)
|
||||
if not closed_path:
|
||||
duplicates[-1] = False
|
||||
duplicates[0] = False
|
||||
return vertices[~duplicates]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ requires-python = ">=3.11"
|
|||
dynamic = ["version"]
|
||||
dependencies = [
|
||||
"numpy>=1.26",
|
||||
"klamath~=1.4",
|
||||
"klamath~=1.2",
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -56,7 +56,6 @@ dxf = ["ezdxf~=1.0.2"]
|
|||
svg = ["svgwrite"]
|
||||
visualize = ["matplotlib"]
|
||||
text = ["matplotlib", "freetype-py"]
|
||||
manhatanize_slow = ["float_raster"]
|
||||
|
||||
|
||||
[tool.ruff]
|
||||
|
|
@ -79,6 +78,7 @@ lint.ignore = [
|
|||
"ANN002", # *args
|
||||
"ANN003", # **kwargs
|
||||
"ANN401", # Any
|
||||
"ANN101", # self: Self
|
||||
"SIM108", # single-line if / else assignment
|
||||
"RET504", # x=y+z; return x
|
||||
"PIE790", # unnecessary pass
|
||||
|
|
@ -90,7 +90,3 @@ lint.ignore = [
|
|||
"TRY003", # Long exception message
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = "-rsXx"
|
||||
testpaths = ["masque"]
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue