[Pather/RenderPather/PortPather] Rework pathing verbs *BREAKING CHANGE*

This commit is contained in:
jan 2026-03-06 22:58:03 -08:00
commit babbe78daa
5 changed files with 468 additions and 416 deletions

View file

@ -255,7 +255,7 @@ class Pather(Builder, PatherMixin):
return s return s
def path( def _path(
self, self,
portspec: str, portspec: str,
ccw: SupportsBool | None, ccw: SupportsBool | None,
@ -296,7 +296,7 @@ class Pather(Builder, PatherMixin):
LibraryError if no valid name could be picked for the pattern. LibraryError if no valid name could be picked for the pattern.
""" """
if self._dead: if self._dead:
logger.warning('Skipping geometry for path() since device is dead') logger.warning('Skipping geometry for _path() since device is dead')
tool_port_names = ('A', 'B') tool_port_names = ('A', 'B')
@ -335,7 +335,7 @@ class Pather(Builder, PatherMixin):
self.plug(tname, {portspec: tool_port_names[0], **output}) self.plug(tname, {portspec: tool_port_names[0], **output})
return self return self
def pathS( def _pathS(
self, self,
portspec: str, portspec: str,
length: float, length: float,
@ -346,20 +346,17 @@ class Pather(Builder, PatherMixin):
) -> Self: ) -> Self:
""" """
Create an S-shaped "wire"/"waveguide" and `plug` it into the port `portspec`, with the aim Create an S-shaped "wire"/"waveguide" and `plug` it into the port `portspec`, with the aim
of traveling exactly `length` distance with an offset `jog` along the other axis (+ve jog is of traveling exactly `length` distance.
left of direction of travel).
The output port will have the same orientation as the source port (`portspec`). The wire will travel `length` distance along the port's axis, and exactly `jog`
distance in the perpendicular direction. The output port will have an orientation
This function attempts to use `tool.planS()`, but falls back to `tool.planL()` if the former identical to the input port.
raises a NotImplementedError.
Args: Args:
portspec: The name of the port into which the wire will be plugged. portspec: The name of the port into which the wire will be plugged.
jog: Total manhattan distance perpendicular to the direction of travel. length: The total distance from input to output, along the input's axis only.
Positive values are to the left of the direction of travel. jog: Total distance perpendicular to the direction of travel. Positive values
length: The total manhattan distance from input to output, along the input's axis only. are to the left of the direction of travel.
(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 plug_into: If not None, attempts to plug the wire's output port into the provided
port on `self`. port on `self`.
@ -377,7 +374,7 @@ class Pather(Builder, PatherMixin):
LibraryError if no valid name could be picked for the pattern. LibraryError if no valid name could be picked for the pattern.
""" """
if self._dead: if self._dead:
logger.warning('Skipping geometry for pathS() since device is dead') logger.warning('Skipping geometry for _pathS() since device is dead')
tool_port_names = ('A', 'B') tool_port_names = ('A', 'B')
@ -398,8 +395,8 @@ class Pather(Builder, PatherMixin):
(_, jog1), _ = t_pat1[tool_port_names[0]].measure_travel(t_pat1[tool_port_names[1]]) (_, jog1), _ = t_pat1[tool_port_names[0]].measure_travel(t_pat1[tool_port_names[1]])
kwargs_plug = kwargs | {'plug_into': plug_into} kwargs_plug = kwargs | {'plug_into': plug_into}
self.path(portspec, ccw0, length - abs(jog1), **kwargs_no_out) self._path(portspec, ccw0, length - abs(jog1), **kwargs_no_out)
self.path(portspec, not ccw0, abs(jog - jog0), **kwargs_plug) self._path(portspec, not ccw0, abs(jog - jog0), **kwargs_plug)
except (BuildError, NotImplementedError): except (BuildError, NotImplementedError):
if not self._dead: if not self._dead:
raise raise
@ -433,4 +430,3 @@ class Pather(Builder, PatherMixin):
output = {} output = {}
self.plug(tname, {portspec: tool_port_names[0], **output}) self.plug(tname, {portspec: tool_port_names[0], **output})
return self return self

View file

@ -1,5 +1,5 @@
from typing import Self, overload from typing import Self, overload
from collections.abc import Sequence, Iterator, Iterable from collections.abc import Sequence, Iterator, Iterable, Mapping
import logging import logging
from contextlib import contextmanager from contextlib import contextmanager
from abc import abstractmethod, ABCMeta from abc import abstractmethod, ABCMeta
@ -37,8 +37,323 @@ class PatherMixin(PortList, metaclass=ABCMeta):
(e.g wires or waveguides) to be plugged into this device. (e.g wires or waveguides) to be plugged into this device.
""" """
def trace(
self,
portspec: str | Sequence[str],
ccw: SupportsBool | None,
length: float | None = None,
*,
spacing: float | ArrayLike | None = None,
**bounds,
) -> Self:
"""
Create a "wire"/"waveguide" extending from the port(s) `portspec`.
Args:
portspec: The name(s) of the port(s) into which the wire(s) will be plugged.
ccw: If `None`, the output should be along the same axis as the input.
Otherwise, cast to bool and turn counterclockwise if True
and clockwise otherwise.
length: The total distance from input to output, along the input's axis only.
Length is only allowed with a single port.
spacing: Center-to-center distance between output ports along the input port's axis.
Only used when routing multiple ports with a bend.
bounds: Boundary constraints for the trace.
- each: results in each port being extended by `each` distance.
- emin, emax, pmin, pmax, xmin, xmax, ymin, ymax: bundle routing via `ell()`.
- set_rotation: explicit rotation for ports without one.
Returns:
self
"""
if isinstance(portspec, str):
portspec = [portspec]
if length is not None:
if len(portspec) > 1:
raise BuildError('length is only allowed with a single port in trace()')
if bounds:
raise BuildError('length and bounds are mutually exclusive in trace()')
return self._path(portspec[0], ccw, length)
if 'each' in bounds:
each = bounds.pop('each')
if bounds:
raise BuildError('each and other bounds are mutually exclusive in trace()')
for port in portspec:
self._path(port, ccw, each)
return self
# Bundle routing (formerly mpath logic)
bound_types = set()
if 'bound_type' in bounds:
bound_types.add(bounds.pop('bound_type'))
bound = bounds.pop('bound')
for bt in ('emin', 'emax', 'pmin', 'pmax', 'xmin', 'xmax', 'ymin', 'ymax', 'min_past_furthest'):
if bt in bounds:
bound_types.add(bt)
bound = bounds.pop(bt)
if not bound_types:
raise BuildError('No bound type specified for trace()')
if len(bound_types) > 1:
raise BuildError(f'Too many bound types specified: {bound_types}')
bound_type = tuple(bound_types)[0]
ports = self.pattern[tuple(portspec)]
set_rotation = bounds.pop('set_rotation', None)
extensions = ell(ports, ccw, spacing=spacing, bound=bound, bound_type=bound_type, set_rotation=set_rotation)
for port_name, ext_len in extensions.items():
self._path(port_name, ccw, ext_len, **bounds)
return self
def trace_to(
self,
portspec: str | Sequence[str],
ccw: SupportsBool | None,
*,
spacing: float | ArrayLike | None = None,
**bounds,
) -> Self:
"""
Create a "wire"/"waveguide" extending from the port(s) `portspec` to a target position.
Args:
portspec: The name(s) of the port(s) into which the wire(s) will be plugged.
ccw: If `None`, the output should be along the same axis as the input.
Otherwise, cast to bool and turn counterclockwise if True
and clockwise otherwise.
spacing: Center-to-center distance between output ports along the input port's axis.
Only used when routing multiple ports with a bend.
bounds: Boundary constraints for the target position.
- p, x, y, pos, position: Coordinate of the target position. Error if used with multiple ports.
- pmin, pmax, xmin, xmax, ymin, ymax, emin, emax: bundle routing via `ell()`.
Returns:
self
"""
if isinstance(portspec, str):
portspec = [portspec]
pos_bounds = {kk: bounds[kk] for kk in ('p', 'x', 'y', 'pos', 'position') if kk in bounds}
if pos_bounds:
if len(portspec) > 1:
raise BuildError(f'{tuple(pos_bounds.keys())} bounds are only allowed with a single port in trace_to()')
if len(pos_bounds) > 1:
raise BuildError(f'Too many position bounds: {tuple(pos_bounds.keys())}')
k, v = next(iter(pos_bounds.items()))
k = 'position' if k in ('p', 'pos') else k
# Logic hoisted from path_to()
port_name = portspec[0]
port = self.pattern[port_name]
if port.rotation is None:
raise PortError(f'Port {port_name} has no rotation and cannot be used for trace_to()')
if not numpy.isclose(port.rotation % (pi / 2), 0):
raise BuildError('trace_to was asked to route from non-manhattan port')
is_horizontal = numpy.isclose(port.rotation % pi, 0)
if is_horizontal:
if k == 'y':
raise BuildError('Asked to trace to y-coordinate, but port is horizontal')
target = v
else:
if k == 'x':
raise BuildError('Asked to trace to x-coordinate, but port is vertical')
target = v
x0, y0 = port.offset
if is_horizontal:
if numpy.sign(numpy.cos(port.rotation)) == numpy.sign(target - x0):
raise BuildError(f'trace_to routing to behind source port: x0={x0:g} to {target:g}')
length = numpy.abs(target - x0)
else:
if numpy.sign(numpy.sin(port.rotation)) == numpy.sign(target - y0):
raise BuildError(f'trace_to routing to behind source port: y0={y0:g} to {target:g}')
length = numpy.abs(target - y0)
other_bounds = {bk: bv for bk, bv in bounds.items() if bk not in pos_bounds and bk != 'length'}
if 'length' in bounds and bounds['length'] is not None:
raise BuildError('Cannot specify both relative length and absolute position in trace_to()')
return self._path(port_name, ccw, length, **other_bounds)
# Bundle routing (delegate to trace which handles ell)
return self.trace(portspec, ccw, spacing=spacing, **bounds)
def straight(self, portspec: str | Sequence[str], length: float | None = None, **bounds) -> Self:
""" Straight extension. Replaces `path(ccw=None)` and `path_to(ccw=None)` """
return self.trace_to(portspec, None, length=length, **bounds)
def bend(self, portspec: str | Sequence[str], ccw: SupportsBool, length: float | None = None, **bounds) -> Self:
""" Bend extension. Replaces `path(ccw=True/False)` and `path_to(ccw=True/False)` """
return self.trace_to(portspec, ccw, length=length, **bounds)
def ccw(self, portspec: str | Sequence[str], length: float | None = None, **bounds) -> Self:
""" Counter-clockwise bend extension. """
return self.bend(portspec, True, length, **bounds)
def cw(self, portspec: str | Sequence[str], length: float | None = None, **bounds) -> Self:
""" Clockwise bend extension. """
return self.bend(portspec, False, length, **bounds)
def jog(self, portspec: str | Sequence[str], offset: float, length: float | None = None, **bounds) -> Self:
""" Jog extension. Replaces `pathS`. """
if isinstance(portspec, str):
portspec = [portspec]
for port in portspec:
l_actual = length
if l_actual is None:
# TODO: use bounds to determine length?
raise BuildError('jog() currently requires a length')
self._pathS(port, l_actual, offset, **bounds)
return self
def trace_into(
self,
portspec_src: str,
portspec_dst: str,
*,
out_ptype: str | None = None,
plug_destination: bool = True,
thru: str | None = None,
**kwargs,
) -> Self:
"""
Create a "wire"/"waveguide" traveling between the ports `portspec_src` and
`portspec_dst`, and `plug` it into both (or just the source port).
Only unambiguous scenarios are allowed:
- Straight connector between facing ports
- Single 90 degree bend
- Jog between facing ports
(jog is done as late as possible, i.e. only 2 L-shaped segments are used)
By default, the destination's `pytpe` will be used as the `out_ptype` for the
wire, and the `portspec_dst` will be plugged (i.e. removed).
Args:
portspec_src: The name of the starting port into which the wire will be plugged.
portspec_dst: The name of the destination port.
out_ptype: Passed to the pathing tool in order to specify the desired port type
to be generated at the destination end. If `None` (default), the destination
port's `ptype` will be used.
thru: If not `None`, the port by this name will be renamed to `portspec_src`.
This can be used when routing a signal through a pre-placed 2-port device.
Returns:
self
Raises:
PortError if either port does not have a specified rotation.
BuildError if an invalid port config is encountered:
- Non-manhattan ports
- U-bend
- Destination too close to (or behind) source
"""
if self._dead:
logger.error('Skipping trace_into() since device is dead')
return self
port_src = self.pattern[portspec_src]
port_dst = self.pattern[portspec_dst]
if out_ptype is None:
out_ptype = port_dst.ptype
if port_src.rotation is None:
raise PortError(f'Port {portspec_src} has no rotation and cannot be used for trace_into()')
if port_dst.rotation is None:
raise PortError(f'Port {portspec_dst} has no rotation and cannot be used for trace_into()')
if not numpy.isclose(port_src.rotation % (pi / 2), 0):
raise BuildError('trace_into was asked to route from non-manhattan port')
if not numpy.isclose(port_dst.rotation % (pi / 2), 0):
raise BuildError('trace_into was asked to route to non-manhattan port')
src_is_horizontal = numpy.isclose(port_src.rotation % pi, 0)
dst_is_horizontal = numpy.isclose(port_dst.rotation % pi, 0)
xs, ys = port_src.offset
xd, yd = port_dst.offset
angle = (port_dst.rotation - port_src.rotation) % (2 * pi)
dst_extra_args = {'out_ptype': out_ptype}
if plug_destination:
dst_extra_args['plug_into'] = portspec_dst
src_args = {**kwargs}
dst_args = {**src_args, **dst_extra_args}
if src_is_horizontal and not dst_is_horizontal:
# single bend should suffice
self.trace_to(portspec_src, angle > pi, x=xd, **src_args)
self.trace_to(portspec_src, None, y=yd, **dst_args)
elif dst_is_horizontal and not src_is_horizontal:
# single bend should suffice
self.trace_to(portspec_src, angle > pi, y=yd, **src_args)
self.trace_to(portspec_src, None, x=xd, **dst_args)
elif numpy.isclose(angle, pi):
if src_is_horizontal and ys == yd:
# straight connector
self.trace_to(portspec_src, None, x=xd, **dst_args)
elif not src_is_horizontal and xs == xd:
# straight connector
self.trace_to(portspec_src, None, y=yd, **dst_args)
else:
# S-bend
(travel, jog), _ = port_src.measure_travel(port_dst)
self.jog(portspec_src, -jog, -travel, **dst_args)
elif numpy.isclose(angle, 0):
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 _uturn_fallback(
self,
tool: Tool,
portspec: str,
jog: float,
length: float,
in_ptype: str | None,
plug_into: str | None,
**kwargs,
) -> bool:
"""
Attempt to perform a U-turn using two L-bends.
Returns True if successful, False if planL failed.
"""
# Fall back to drawing two L-bends
ccw = jog > 0
kwargs_no_out = kwargs | {'out_ptype': None}
try:
# First, find R by planning a minimal L-bend.
# Use a large length to ensure we don't hit tool-specific minimum length constraints.
dummy_port, _ = tool.planL(ccw, 1e9, in_ptype=in_ptype, **kwargs_no_out)
R = abs(dummy_port.y)
L1 = length + R
L2 = abs(jog) - R
kwargs_plug = kwargs | {'plug_into': plug_into}
self._path(portspec, ccw, L1, **kwargs_no_out)
self._path(portspec, ccw, L2, **kwargs_plug)
except (BuildError, NotImplementedError):
return False
else:
return True
@abstractmethod @abstractmethod
def path( def _path(
self, self,
portspec: str, portspec: str,
ccw: SupportsBool | None, ccw: SupportsBool | None,
@ -50,7 +365,7 @@ class PatherMixin(PortList, metaclass=ABCMeta):
pass pass
@abstractmethod @abstractmethod
def pathS( def _pathS(
self, self,
portspec: str, portspec: str,
length: float, length: float,
@ -61,6 +376,16 @@ class PatherMixin(PortList, metaclass=ABCMeta):
) -> Self: ) -> Self:
pass pass
def path(self, *args, **kwargs) -> Self:
import warnings
warnings.warn("path() is deprecated; use trace(), straight(), or bend() instead", DeprecationWarning, stacklevel=2)
return self._path(*args, **kwargs)
def pathS(self, *args, **kwargs) -> Self:
import warnings
warnings.warn("pathS() is deprecated; use jog() instead", DeprecationWarning, stacklevel=2)
return self._pathS(*args, **kwargs)
@abstractmethod @abstractmethod
def plug( def plug(
self, self,
@ -76,6 +401,11 @@ class PatherMixin(PortList, metaclass=ABCMeta):
) -> Self: ) -> Self:
pass pass
@abstractmethod
def plugged(self, connections: dict[str, str]) -> Self:
""" Manual connection acknowledgment. """
pass
def retool( def retool(
self, self,
tool: Tool, tool: Tool,
@ -143,88 +473,13 @@ class PatherMixin(PortList, metaclass=ABCMeta):
**kwargs, **kwargs,
) -> Self: ) -> Self:
""" """
Build a "wire"/"waveguide" extending from the port `portspec`, with the aim [DEPRECATED] use trace_to() instead.
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: import warnings
logger.error('Skipping path_to() since device is dead') warnings.warn("path_to() is deprecated; use trace_to() instead", DeprecationWarning, stacklevel=2)
return self
pos_count = sum(vv is not None for vv in (position, x, y)) bounds = {kk: vv for kk, vv in (('position', position), ('x', x), ('y', y)) if vv is not None}
if pos_count > 1: return self.trace_to(portspec, ccw, plug_into=plug_into, **bounds, **kwargs)
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( def path_into(
self, self,
@ -237,100 +492,19 @@ class PatherMixin(PortList, metaclass=ABCMeta):
**kwargs, **kwargs,
) -> Self: ) -> Self:
""" """
Create a "wire"/"waveguide" traveling between the ports `portspec_src` and [DEPRECATED] use trace_into() instead.
`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: import warnings
logger.error('Skipping path_into() since device is dead') warnings.warn("path_into() is deprecated; use trace_into() instead", DeprecationWarning, stacklevel=2)
return self
port_src = self.pattern[portspec_src] return self.trace_into(
port_dst = self.pattern[portspec_dst] portspec_src,
portspec_dst,
if out_ptype is None: out_ptype = out_ptype,
out_ptype = port_dst.ptype plug_destination = plug_destination,
thru = thru,
if port_src.rotation is None: **kwargs,
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( def mpath(
self, self,
@ -342,109 +516,12 @@ class PatherMixin(PortList, metaclass=ABCMeta):
**kwargs, **kwargs,
) -> Self: ) -> Self:
""" """
`mpath` is a superset of `path` and `path_to` which can act on bundles or buses [DEPRECATED] use trace() or trace_to() instead.
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: import warnings
logger.error('Skipping mpath() since device is dead') warnings.warn("mpath() is deprecated; use trace() or trace_to() instead", DeprecationWarning, stacklevel=2)
return self
bound_types = set() return self.trace(portspec, ccw, spacing=spacing, set_rotation=set_rotation, **kwargs)
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()? # TODO def bus_join()?
@ -488,61 +565,42 @@ class PortPather:
with self.pather.toolctx(tool, keys=self.ports): with self.pather.toolctx(tool, keys=self.ports):
yield self yield self
def path(self, *args, **kwargs) -> Self: def trace(self, ccw: SupportsBool | None, length: float | None = None, **kwargs) -> Self:
self.pather.trace(self.ports, ccw, length, **kwargs)
return self
def trace_to(self, ccw: SupportsBool | None, **kwargs) -> Self:
self.pather.trace_to(self.ports, ccw, **kwargs)
return self
def straight(self, length: float | None = None, **kwargs) -> Self:
self.pather.straight(self.ports, length, **kwargs)
return self
def bend(self, ccw: SupportsBool, length: float | None = None, **kwargs) -> Self:
self.pather.bend(self.ports, ccw, length, **kwargs)
return self
def ccw(self, length: float | None = None, **kwargs) -> Self:
self.pather.ccw(self.ports, length, **kwargs)
return self
def cw(self, length: float | None = None, **kwargs) -> Self:
self.pather.cw(self.ports, length, **kwargs)
return self
def jog(self, offset: float, length: float | None = None, **kwargs) -> Self:
self.pather.jog(self.ports, offset, length, **kwargs)
return self
def uturn(self, offset: float, length: float | None = None, **kwargs) -> Self:
self.pather.uturn(self.ports, offset, length, **kwargs)
return self
def trace_into(self, target_port: str, **kwargs) -> Self:
if len(self.ports) > 1: if len(self.ports) > 1:
logger.warning('Use path_each() when pathing multiple ports independently') raise BuildError(f'Unable use implicit trace_into() with {len(self.ports)} (>1) ports.')
for port in self.ports: self.pather.trace_into(self.ports[0], target_port, **kwargs)
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 return self
def plug( def plug(
@ -558,10 +616,13 @@ class PortPather:
self.pather.plug(other, {self.ports[0]: other_port}, *args, **kwargs) self.pather.plug(other, {self.ports[0]: other_port}, *args, **kwargs)
return self return self
def plugged(self, other_port: str) -> Self: def plugged(self, other_port: str | Mapping[str, str]) -> Self:
if len(self.ports) > 1: if isinstance(other_port, Mapping):
self.pather.plugged(dict(other_port))
elif len(self.ports) > 1:
raise BuildError(f'Unable use implicit plugged() with {len(self.ports)} (>1) ports.') raise BuildError(f'Unable use implicit plugged() with {len(self.ports)} (>1) ports.')
self.pather.plugged({self.ports[0]: other_port}) else:
self.pather.plugged({self.ports[0]: other_port})
return self return self
# #
@ -569,95 +630,91 @@ class PortPather:
# #
def set_ptype(self, ptype: str) -> Self: def set_ptype(self, ptype: str) -> Self:
for port in self.ports: for port in self.ports:
self.pather[port].set_ptype(ptype) self.pather.pattern[port].set_ptype(ptype)
return self return self
def translate(self, *args, **kwargs) -> Self: def translate(self, *args, **kwargs) -> Self:
for port in self.ports: for port in self.ports:
self.pather[port].translate(*args, **kwargs) self.pather.pattern[port].translate(*args, **kwargs)
return self return self
def mirror(self, *args, **kwargs) -> Self: def mirror(self, *args, **kwargs) -> Self:
for port in self.ports: for port in self.ports:
self.pather[port].mirror(*args, **kwargs) self.pather.pattern[port].mirror(*args, **kwargs)
return self return self
def rotate(self, rotation: float) -> Self: def rotate(self, rotation: float) -> Self:
for port in self.ports: for port in self.ports:
self.pather[port].rotate(rotation) self.pather.pattern[port].rotate(rotation)
return self return self
def set_rotation(self, rotation: float | None) -> Self: def set_rotation(self, rotation: float | None) -> Self:
for port in self.ports: for port in self.ports:
self.pather[port].set_rotation(rotation) self.pather.pattern[port].set_rotation(rotation)
return self return self
def rename_to(self, new_name: str) -> Self: def rename(self, name: str | Mapping[str, str | None]) -> Self:
if len(self.ports) > 1: """ Rename active ports. Replaces `rename_to`. """
BuildError('Use rename_ports() for >1 port') name_map: dict[str, str | None]
self.pather.rename_ports({self.ports[0]: new_name}) if isinstance(name, str):
self.ports[0] = new_name if len(self.ports) > 1:
return self raise BuildError('Use a mapping to rename >1 port')
name_map = {self.ports[0]: name}
def rename_from(self, old_name: str) -> Self: else:
if len(self.ports) > 1: name_map = dict(name)
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.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] self.ports = [mm for mm in [name_map.get(pp, pp) for pp in self.ports] if mm is not None]
return self return self
def add_ports(self, ports: Iterable[str]) -> Self: def select(self, ports: str | Iterable[str]) -> Self:
ports = list(ports) """ Add ports to the selection. Replaces `add_ports`. """
conflicts = set(ports) & set(self.ports) if isinstance(ports, str):
if conflicts: ports = [ports]
raise BuildError(f'ports {conflicts} already selected') for port in ports:
self.ports += ports if port not in self.ports:
self.ports.append(port)
return self return self
def add_port(self, port: str, index: int | None = None) -> Self: def deselect(self, ports: str | Iterable[str]) -> Self:
if port in self.ports: """ Remove ports from the selection. Replaces `drop_port`. """
raise BuildError(f'{port=} already selected') if isinstance(ports, str):
if index is not None: ports = [ports]
self.ports.insert(index, port) ports_set = set(ports)
self.ports = [pp for pp in self.ports if pp not in ports_set]
return self
def mark(self, name: str | Mapping[str, str]) -> Self:
""" Bookmark current port(s). Replaces `save_copy`. """
name_map: Mapping[str, str]
if isinstance(name, str):
if len(self.ports) > 1:
raise BuildError('Use a mapping to mark >1 port')
name_map = {self.ports[0]: name}
else: else:
self.ports.append(port) name_map = name
for src, dst in name_map.items():
self.pather.pattern.ports[dst] = self.pather.pattern[src].copy()
return self return self
def drop_port(self, port: str) -> Self: def fork(self, name: str | Mapping[str, str]) -> Self:
if port not in self.ports: """ Split and follow new name. Replaces `into_copy`. """
raise BuildError(f'{port=} already not selected') name_map: Mapping[str, str]
self.ports = [pp for pp in self.ports if pp != port] if isinstance(name, str):
if len(self.ports) > 1:
raise BuildError('Use a mapping to fork >1 port')
name_map = {self.ports[0]: name}
else:
name_map = name
for src, dst in name_map.items():
self.pather.pattern.ports[dst] = self.pather.pattern[src].copy()
self.ports = [(dst if pp == src else pp) for pp in self.ports]
return self return self
def into_copy(self, new_name: str, src: str | None = None) -> Self: def drop(self) -> Self:
""" Copy a port and replace it with the copy """ """ Remove selected ports from the pattern and the PortPather. Replaces `delete(None)`. """
if not self.ports: for pp in self.ports:
raise BuildError('Have no ports to copy') del self.pather.pattern.ports[pp]
if len(self.ports) == 1: self.ports = []
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 return self
@overload @overload
@ -668,10 +725,9 @@ class PortPather:
def delete(self, name: str | None = None) -> Self | None: def delete(self, name: str | None = None) -> Self | None:
if name is None: if name is None:
for pp in self.ports: self.drop()
del self.pather.ports[pp]
return None return None
del self.pather.ports[name] del self.pather.pattern.ports[name]
self.ports = [pp for pp in self.ports if pp != name] self.ports = [pp for pp in self.ports if pp != name]
return self return self

View file

@ -28,7 +28,7 @@ logger = logging.getLogger(__name__)
class RenderPather(PatherMixin): class RenderPather(PatherMixin):
""" """
`RenderPather` is an alternative to `Pather` which uses the `path`/`path_to`/`mpath` `RenderPather` is an alternative to `Pather` which uses the `trace`/`trace_to`
functions to plan out wire paths without incrementally generating the layout. Instead, functions to plan out wire paths without incrementally generating the layout. Instead,
it waits until `render` is called, at which point it draws all the planned segments it waits until `render` is called, at which point it draws all the planned segments
simultaneously. This allows it to e.g. draw each wire using a single `Path` or simultaneously. This allows it to e.g. draw each wire using a single `Path` or
@ -97,7 +97,7 @@ class RenderPather(PatherMixin):
in which case it is interpreted as a name in `library`. in which case it is interpreted as a name in `library`.
Default `None` (no ports). Default `None` (no ports).
tools: A mapping of {port: tool} which specifies what `Tool` should be used tools: A mapping of {port: tool} which specifies what `Tool` should be used
to generate waveguide or wire segments when `path`/`path_to`/`mpath` to generate waveguide or wire segments when `trace`/`trace_to`
are called. Relies on `Tool.planL` and `Tool.render` implementations. are called. Relies on `Tool.planL` and `Tool.render` implementations.
name: If specified, `library[name]` is set to `self.pattern`. name: If specified, `library[name]` is set to `self.pattern`.
""" """
@ -150,7 +150,7 @@ class RenderPather(PatherMixin):
and to which the new one should be added (if named). If not provided, and to which the new one should be added (if named). If not provided,
`source.library` must exist and will be used. `source.library` must exist and will be used.
tools: `Tool`s which will be used by the pather for generating new wires tools: `Tool`s which will be used by the pather for generating new wires
or waveguides (via `path`/`path_to`/`mpath`). or waveguides (via `trace`/`trace_to`).
in_prefix: Prepended to port names for newly-created ports with in_prefix: Prepended to port names for newly-created ports with
reversed directions compared to the current device. reversed directions compared to the current device.
out_prefix: Prepended to port names for ports which are directly out_prefix: Prepended to port names for ports which are directly
@ -377,7 +377,7 @@ class RenderPather(PatherMixin):
PortList.plugged(self, connections) PortList.plugged(self, connections)
return self return self
def path( def _path(
self, self,
portspec: str, portspec: str,
ccw: SupportsBool | None, ccw: SupportsBool | None,
@ -420,7 +420,7 @@ class RenderPather(PatherMixin):
LibraryError if no valid name could be picked for the pattern. LibraryError if no valid name could be picked for the pattern.
""" """
if self._dead: if self._dead:
logger.warning('Skipping geometry for path() since device is dead') logger.warning('Skipping geometry for _path() since device is dead')
port = self.pattern[portspec] port = self.pattern[portspec]
in_ptype = port.ptype in_ptype = port.ptype
@ -460,7 +460,7 @@ class RenderPather(PatherMixin):
return self return self
def pathS( def _pathS(
self, self,
portspec: str, portspec: str,
length: float, length: float,
@ -504,7 +504,7 @@ class RenderPather(PatherMixin):
LibraryError if no valid name could be picked for the pattern. LibraryError if no valid name could be picked for the pattern.
""" """
if self._dead: if self._dead:
logger.warning('Skipping geometry for pathS() since device is dead') logger.warning('Skipping geometry for _pathS() since device is dead')
port = self.pattern[portspec] port = self.pattern[portspec]
in_ptype = port.ptype in_ptype = port.ptype
@ -527,8 +527,8 @@ class RenderPather(PatherMixin):
jog1 = Port((0, 0), 0).measure_travel(t_port1)[0][1] jog1 = Port((0, 0), 0).measure_travel(t_port1)[0][1]
kwargs_plug = kwargs | {'plug_into': plug_into} kwargs_plug = kwargs | {'plug_into': plug_into}
self.path(portspec, ccw0, length - abs(jog1), **kwargs_no_out) self._path(portspec, ccw0, length - abs(jog1), **kwargs_no_out)
self.path(portspec, not ccw0, abs(jog - jog0), **kwargs_plug) self._path(portspec, not ccw0, abs(jog - jog0), **kwargs_plug)
except (BuildError, NotImplementedError): except (BuildError, NotImplementedError):
if not self._dead: if not self._dead:
raise raise
@ -564,7 +564,7 @@ class RenderPather(PatherMixin):
append: bool = True, append: bool = True,
) -> Self: ) -> Self:
""" """
Generate the geometry which has been planned out with `path`/`path_to`/etc. Generate the geometry which has been planned out with `trace`/`trace_to`/etc.
Args: Args:
append: If `True`, the rendered geometry will be directly appended to append: If `True`, the rendered geometry will be directly appended to

View file

@ -24,7 +24,7 @@ def pather_setup() -> tuple[Pather, PathTool, Library]:
def test_pather_straight(pather_setup: tuple[Pather, PathTool, Library]) -> None: def test_pather_straight(pather_setup: tuple[Pather, PathTool, Library]) -> None:
p, tool, lib = pather_setup p, tool, lib = pather_setup
# Route 10um "forward" # Route 10um "forward"
p.path("start", ccw=None, length=10) p.straight("start", 10)
# port rot pi/2 (North). Travel +pi relative to port -> South. # port rot pi/2 (North). Travel +pi relative to port -> South.
assert_allclose(p.ports["start"].offset, [0, -10], atol=1e-10) assert_allclose(p.ports["start"].offset, [0, -10], atol=1e-10)
@ -37,7 +37,7 @@ def test_pather_bend(pather_setup: tuple[Pather, PathTool, Library]) -> None:
# Start (0,0) rot pi/2 (North). # Start (0,0) rot pi/2 (North).
# Path 10um "forward" (South), then turn Clockwise (ccw=False). # Path 10um "forward" (South), then turn Clockwise (ccw=False).
# Facing South, turn Right -> West. # Facing South, turn Right -> West.
p.path("start", ccw=False, length=10) p.cw("start", 10)
# PathTool.planL(ccw=False, length=10) returns out_port at (10, -1) relative to (0,0) rot 0. # PathTool.planL(ccw=False, length=10) returns out_port at (10, -1) relative to (0,0) rot 0.
# Transformed by port rot pi/2 (North) + pi (to move "forward" away from device): # Transformed by port rot pi/2 (North) + pi (to move "forward" away from device):
@ -55,7 +55,7 @@ def test_pather_path_to(pather_setup: tuple[Pather, PathTool, Library]) -> None:
p, tool, lib = pather_setup p, tool, lib = pather_setup
# start at (0,0) rot pi/2 (North) # start at (0,0) rot pi/2 (North)
# path "forward" (South) to y=-50 # path "forward" (South) to y=-50
p.path_to("start", ccw=None, y=-50) p.straight("start", y=-50)
assert_equal(p.ports["start"].offset, [0, -50]) assert_equal(p.ports["start"].offset, [0, -50])
@ -65,7 +65,7 @@ def test_pather_mpath(pather_setup: tuple[Pather, PathTool, Library]) -> None:
p.ports["B"] = Port((10, 0), pi / 2, ptype="wire") p.ports["B"] = Port((10, 0), pi / 2, ptype="wire")
# Path both "forward" (South) to y=-20 # Path both "forward" (South) to y=-20
p.mpath(["A", "B"], ccw=None, ymin=-20) p.straight(["A", "B"], ymin=-20)
assert_equal(p.ports["A"].offset, [0, -20]) assert_equal(p.ports["A"].offset, [0, -20])
assert_equal(p.ports["B"].offset, [10, -20]) assert_equal(p.ports["B"].offset, [10, -20])
@ -73,7 +73,7 @@ def test_pather_mpath(pather_setup: tuple[Pather, PathTool, Library]) -> None:
def test_pather_at_chaining(pather_setup: tuple[Pather, PathTool, Library]) -> None: def test_pather_at_chaining(pather_setup: tuple[Pather, PathTool, Library]) -> None:
p, tool, lib = pather_setup p, tool, lib = pather_setup
# Fluent API test # Fluent API test
p.at("start").path(ccw=None, length=10).path(ccw=True, length=10) p.at("start").straight(10).ccw(10)
# 10um South -> (0, -10) rot pi/2 # 10um South -> (0, -10) rot pi/2
# then 10um South and turn CCW (Facing South, CCW is East) # then 10um South and turn CCW (Facing South, CCW is East)
# PathTool.planL(ccw=True, length=10) -> out_port=(10, 1) rot -pi/2 relative to rot 0 # PathTool.planL(ccw=True, length=10) -> out_port=(10, 1) rot -pi/2 relative to rot 0
@ -93,14 +93,14 @@ def test_pather_dead_ports() -> None:
p.set_dead() p.set_dead()
# Path with negative length (impossible for PathTool, would normally raise BuildError) # Path with negative length (impossible for PathTool, would normally raise BuildError)
p.path("in", None, -10) p.straight("in", -10)
# Port 'in' should be updated by dummy extension despite tool failure # Port 'in' should be updated by dummy extension despite tool failure
# port_rot=0, forward is -x. path(-10) means moving -10 in -x direction -> +10 in x. # port_rot=0, forward is -x. path(-10) means moving -10 in -x direction -> +10 in x.
assert_allclose(p.ports["in"].offset, [10, 0], atol=1e-10) assert_allclose(p.ports["in"].offset, [10, 0], atol=1e-10)
# Downstream path should work correctly using the dummy port location # Downstream path should work correctly using the dummy port location
p.path("in", None, 20) p.straight("in", 20)
# 10 + (-20) = -10 # 10 + (-20) = -10
assert_allclose(p.ports["in"].offset, [-10, 0], atol=1e-10) assert_allclose(p.ports["in"].offset, [-10, 0], atol=1e-10)

View file

@ -24,7 +24,7 @@ def rpather_setup() -> tuple[RenderPather, PathTool, Library]:
def test_renderpather_basic(rpather_setup: tuple[RenderPather, PathTool, Library]) -> None: def test_renderpather_basic(rpather_setup: tuple[RenderPather, PathTool, Library]) -> None:
rp, tool, lib = rpather_setup rp, tool, lib = rpather_setup
# Plan two segments # Plan two segments
rp.at("start").path(ccw=None, length=10).path(ccw=None, length=10) rp.at("start").straight(10).straight(10)
# Before rendering, no shapes in pattern # Before rendering, no shapes in pattern
assert not rp.pattern.has_shapes() assert not rp.pattern.has_shapes()
@ -49,7 +49,7 @@ def test_renderpather_basic(rpather_setup: tuple[RenderPather, PathTool, Library
def test_renderpather_bend(rpather_setup: tuple[RenderPather, PathTool, Library]) -> None: def test_renderpather_bend(rpather_setup: tuple[RenderPather, PathTool, Library]) -> None:
rp, tool, lib = rpather_setup rp, tool, lib = rpather_setup
# Plan straight then bend # Plan straight then bend
rp.at("start").path(ccw=None, length=10).path(ccw=False, length=10) rp.at("start").straight(10).cw(10)
rp.render() rp.render()
path_shape = cast("Path", rp.pattern.shapes[(1, 0)][0]) path_shape = cast("Path", rp.pattern.shapes[(1, 0)][0])
@ -69,9 +69,9 @@ def test_renderpather_retool(rpather_setup: tuple[RenderPather, PathTool, Librar
rp, tool1, lib = rpather_setup rp, tool1, lib = rpather_setup
tool2 = PathTool(layer=(2, 0), width=4, ptype="wire") tool2 = PathTool(layer=(2, 0), width=4, ptype="wire")
rp.at("start").path(ccw=None, length=10) rp.at("start").straight(10)
rp.retool(tool2, keys=["start"]) rp.retool(tool2, keys=["start"])
rp.at("start").path(ccw=None, length=10) rp.at("start").straight(10)
rp.render() rp.render()
# Different tools should cause different batches/shapes # Different tools should cause different batches/shapes
@ -86,7 +86,7 @@ def test_renderpather_dead_ports() -> None:
rp.set_dead() rp.set_dead()
# Impossible path # Impossible path
rp.path("in", None, -10) rp.straight("in", -10)
# port_rot=0, forward is -x. path(-10) means moving -10 in -x direction -> +10 in x. # port_rot=0, forward is -x. path(-10) means moving -10 in -x direction -> +10 in x.
assert_allclose(rp.ports["in"].offset, [10, 0], atol=1e-10) assert_allclose(rp.ports["in"].offset, [10, 0], atol=1e-10)