Compare commits

..

5 Commits

5 changed files with 290 additions and 56 deletions

View File

@ -266,7 +266,7 @@ class Pather(Builder, PatherMixin):
Create a "wire"/"waveguide" and `plug` it into the port `portspec`, with the aim Create a "wire"/"waveguide" and `plug` it into the port `portspec`, with the aim
of traveling exactly `length` distance. of traveling exactly `length` distance.
The wire will travel `length` distance along the port's axis, an an unspecified The wire will travel `length` distance along the port's axis, and an unspecified
(tool-dependent) distance in the perpendicular direction. The output port will (tool-dependent) distance in the perpendicular direction. The output port will
be rotated (or not) based on the `ccw` parameter. be rotated (or not) based on the `ccw` parameter.
@ -296,11 +296,79 @@ class Pather(Builder, PatherMixin):
tool = self.tools.get(portspec, self.tools[None]) tool = self.tools.get(portspec, self.tools[None])
in_ptype = self.pattern[portspec].ptype in_ptype = self.pattern[portspec].ptype
tree = tool.path(ccw, length, in_ptype=in_ptype, port_names=tool_port_names, **kwargs) tree = tool.path(ccw, length, in_ptype=in_ptype, port_names=tool_port_names, **kwargs)
abstract = self.library << tree # TODO this seems like a name, not an abstract tname = self.library << tree
if plug_into is not None: if plug_into is not None:
output = {plug_into: tool_port_names[1]} output = {plug_into: tool_port_names[1]}
else: else:
output = {} output = {}
self.plug(abstract, {portspec: tool_port_names[0], **output}) self.plug(tname, {portspec: tool_port_names[0], **output})
return self
def pathS(
self,
portspec: str,
length: float,
jog: float,
*,
plug_into: str | None = None,
**kwargs,
) -> Self:
"""
Create an S-shaped "wire"/"waveguide" and `plug` it into the port `portspec`, with the aim
of traveling exactly `length` distance with an offset `jog` along the other axis (+ve jog is
left of direction of travel).
The output port will have the same orientation as the source port (`portspec`).
This function attempts to use `tool.planS()`, but falls back to `tool.planL()` if the former
raises a NotImplementedError.
Args:
portspec: The name of the port into which the wire will be plugged.
jog: Total manhattan distance perpendicular to the direction of travel.
Positive values are to the left of the direction of travel.
length: The total manhattan distance from input to output, along the input's axis only.
(There may be a tool-dependent offset along the other axis.)
plug_into: If not None, attempts to plug the wire's output port into the provided
port on `self`.
Returns:
self
Raises:
BuildError if `distance` is too small to fit the s-bend (for nonzero jog).
LibraryError if no valid name could be picked for the pattern.
"""
if self._dead:
logger.error('Skipping pathS() 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
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, 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]])
self.path(portspec, ccw0, length - jog1, **kwargs_no_out)
self.path(portspec, not ccw0, jog - jog0, **kwargs)
return self
tname = 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

View File

@ -48,6 +48,18 @@ class PatherMixin(metaclass=ABCMeta):
) -> Self: ) -> Self:
pass pass
@abstractmethod
def pathS(
self,
portspec: str,
length: float,
jog: float,
*,
plug_into: str | None = None,
**kwargs,
) -> Self:
pass
def retool( def retool(
self, self,
tool: Tool, tool: Tool,
@ -266,8 +278,6 @@ class PatherMixin(metaclass=ABCMeta):
angle = (port_dst.rotation - port_src.rotation) % (2 * pi) 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: def get_jog(ccw: SupportsBool, length: float) -> float:
tool = self.tools.get(portspec_src, self.tools[None]) 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... in_ptype = 'unk' # Could use port_src.ptype, but we're assuming this is after one bend already...
@ -297,20 +307,10 @@ class PatherMixin(metaclass=ABCMeta):
elif not src_is_horizontal and xs == xd: elif not src_is_horizontal and xs == xd:
# straight connector # straight connector
self.path_to(portspec_src, None, y=yd, **dst_args) 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: else:
# figure out how much y our x-segment (2nd) takes up, then path based on that # S-bend, delegate to implementations
x_len = numpy.abs(xd - xs) tj, _ = port_src.measure_travel(port_dst)
ccw2 = src_ne != (xd < xs) self.pathS(*tj, **dst_args)
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): elif numpy.isclose(angle, 0):
raise BuildError('Don\'t know how to route a U-bend yet (TODO)!') raise BuildError('Don\'t know how to route a U-bend yet (TODO)!')
else: else:

View File

@ -420,6 +420,81 @@ class RenderPather(PortList, PatherMixin):
return self return self
def pathS(
self,
portspec: str,
length: float,
jog: float,
*,
plug_into: str | None = None,
**kwargs,
) -> Self:
"""
Create an S-shaped "wire"/"waveguide" and `plug` it into the port `portspec`, with the aim
of traveling exactly `length` distance with an offset `jog` along the other axis (+ve jog is
left of direction of travel).
The output port will have the same orientation as the source port (`portspec`).
`RenderPather.render` must be called after all paths have been fully planned.
This function attempts to use `tool.planS()`, but falls back to `tool.planL()` if the former
raises a NotImplementedError.
Args:
portspec: The name of the port into which the wire will be plugged.
jog: Total manhattan distance perpendicular to the direction of travel.
Positive values are to the left of the direction of travel.
length: The total manhattan distance from input to output, along the input's axis only.
(There may be a tool-dependent offset along the other axis.)
plug_into: If not None, attempts to plug the wire's output port into the provided
port on `self`.
Returns:
self
Raises:
BuildError if `distance` is too small to fit the s-bend (for nonzero jog).
LibraryError if no valid name could be picked for the pattern.
"""
if self._dead:
logger.error('Skipping pathS() since device is dead')
return self
port = self.pattern[portspec]
in_ptype = port.ptype
port_rot = port.rotation
assert port_rot is not None # TODO allow manually setting rotation for RenderPather.path()?
tool = self.tools.get(portspec, self.tools[None])
# check feasibility, get output port and data
try:
out_port, data = tool.planS(length, jog, in_ptype=in_ptype, **kwargs)
except NotImplementedError:
# Fall back to drawing two L-bends
ccw0 = jog > 0
kwargs_no_out = (kwargs | {'out_ptype': None})
t_port0, _ = tool.planL( ccw0, length / 2, in_ptype=in_ptype, **kwargs_no_out)
(_, jog0), _ = Port((0, 0), 0).measure_travel(t_port0)
t_port1, _ = tool.planL(not ccw0, jog - jog0, in_ptype=t_port0.ptype, **kwargs)
(_, jog1), _ = Port((0, 0), 0).measure_travel(t_port1)
self.path(portspec, ccw0, length - jog1, **kwargs_no_out)
self.path(portspec, not ccw0, jog - jog0, **kwargs)
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()
if plug_into is not None:
self.plugged({portspec: plug_into})
return self
def render( def render(
self, self,
append: bool = True, append: bool = True,

View File

@ -70,7 +70,7 @@ class Tool:
Create a wire or waveguide that travels exactly `length` distance along the axis Create a wire or waveguide that travels exactly `length` distance along the axis
of its input port. of its input port.
Used by `Pather`. Used by `Pather` and `RenderPather`.
The output port must be exactly `length` away along the input port's axis, but The output port must be exactly `length` away along the input port's axis, but
may be placed an additional (unspecified) distance away along the perpendicular may be placed an additional (unspecified) distance away along the perpendicular
@ -101,6 +101,48 @@ class Tool:
""" """
raise NotImplementedError(f'path() not implemented for {type(self)}') raise NotImplementedError(f'path() not implemented for {type(self)}')
def pathS(
self,
length: float,
jog: float,
*,
in_ptype: str | None = None,
out_ptype: str | None = None,
port_names: tuple[str, str] = ('A', 'B'),
**kwargs,
) -> Library:
"""
Create a wire or waveguide that travels exactly `length` distance along the axis
of its input port, and `jog` distance on the perpendicular axis.
`jog` is positive when moving left of the direction of travel (from input to ouput port).
Used by `Pather` and `RenderPather`.
The output port should be rotated to face the input port (i.e. plugging the device
into a port will move that port but keep its orientation).
The input and output ports should be compatible with `in_ptype` and
`out_ptype`, respectively. They should also be named `port_names[0]` and
`port_names[1]`, respectively.
Args:
length: The total distance from input to output, along the input's axis only.
jog: The total distance from input to output, along the second axis. Positive indicates
a leftward shift when moving from input to output port.
in_ptype: The `ptype` of the port into which this wire's input will be `plug`ged.
out_ptype: The `ptype` of the port into which this wire's output will be `plug`ged.
port_names: The output pattern will have its input port named `port_names[0]` and
its output named `port_names[1]`.
kwargs: Custom tool-specific parameters.
Returns:
A pattern tree containing the requested S-shaped (or straight) wire or waveguide
Raises:
BuildError if an impossible or unsupported geometry is requested.
"""
raise NotImplementedError(f'path() not implemented for {type(self)}')
def planL( def planL(
self, self,
ccw: SupportsBool | None, ccw: SupportsBool | None,
@ -135,7 +177,7 @@ class Tool:
kwargs: Custom tool-specific parameters. kwargs: Custom tool-specific parameters.
Returns: Returns:
The calculated output `Port` for the wire. The calculated output `Port` for the wire, assuming an input port at (0, 0) with rotation 0.
Any tool-specifc data, to be stored in `RenderStep.data`, for use during rendering. Any tool-specifc data, to be stored in `RenderStep.data`, for use during rendering.
Raises: Raises:
@ -173,7 +215,7 @@ class Tool:
kwargs: Custom tool-specific parameters. kwargs: Custom tool-specific parameters.
Returns: Returns:
The calculated output `Port` for the wire. The calculated output `Port` for the wire, assuming an input port at (0, 0) with rotation 0.
Any tool-specifc data, to be stored in `RenderStep.data`, for use during rendering. Any tool-specifc data, to be stored in `RenderStep.data`, for use during rendering.
Raises: Raises:
@ -204,14 +246,14 @@ class Tool:
Args: Args:
jog: The total offset from the input to output, along the perpendicular axis. jog: The total offset from the input to output, along the perpendicular axis.
A positive number implies a rightwards shift (i.e. clockwise bend followed A positive number implies a leftwards shift (i.e. counterclockwise bend
by a counterclockwise bend) followed by a clockwise bend)
in_ptype: The `ptype` of the port into which this wire's input will be `plug`ged. in_ptype: The `ptype` of the port into which this wire's input will be `plug`ged.
out_ptype: The `ptype` of the port into which this wire's output will be `plug`ged. out_ptype: The `ptype` of the port into which this wire's output will be `plug`ged.
kwargs: Custom tool-specific parameters. kwargs: Custom tool-specific parameters.
Returns: Returns:
The calculated output `Port` for the wire. The calculated output `Port` for the wire, assuming an input port at (0, 0) with rotation 0.
Any tool-specifc data, to be stored in `RenderStep.data`, for use during rendering. Any tool-specifc data, to be stored in `RenderStep.data`, for use during rendering.
Raises: Raises:
@ -382,6 +424,7 @@ class SimpleTool(Tool, metaclass=ABCMeta):
self._renderL(data=step.data, tree=tree, port_names=port_names, straight_kwargs=kwargs) self._renderL(data=step.data, tree=tree, port_names=port_names, straight_kwargs=kwargs)
return tree return tree
@dataclass @dataclass
class AutoTool(Tool, metaclass=ABCMeta): class AutoTool(Tool, metaclass=ABCMeta):
""" """
@ -407,11 +450,12 @@ class AutoTool(Tool, metaclass=ABCMeta):
fn: Callable[[float], Pattern] | Callable[[float], Library] fn: Callable[[float], Pattern] | Callable[[float], Library]
""" """
Generator function. `jog` (only argument) is assumed to be left (ccw) relative to travel Generator function. `jog` (only argument) is assumed to be left (ccw) relative to travel
and may be negative for a jog i the opposite direction. Won't be called if jog=0. and may be negative for a jog in the opposite direction. Won't be called if jog=0.
""" """
in_port_name: str in_port_name: str
out_port_name: str out_port_name: str
jog_range: tuple[float, float] = (0, numpy.inf)
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class Bend: class Bend:
@ -496,15 +540,8 @@ class AutoTool(Tool, metaclass=ABCMeta):
def _bend2dxy(bend: Bend, ccw: SupportsBool | None) -> tuple[NDArray[numpy.float64], float]: def _bend2dxy(bend: Bend, ccw: SupportsBool | None) -> tuple[NDArray[numpy.float64], float]:
if ccw is None: if ccw is None:
return numpy.zeros(2), pi return numpy.zeros(2), pi
bend_dxy, bend_angle = bend.in_port.measure_travel(bend.out_port)
angle_in = bend.in_port.rotation assert bend_angle is not None
angle_out = bend.out_port.rotation
assert angle_in is not None
assert angle_out is not None
bend_dxy = rotation_matrix_2d(-angle_in) @ (bend.out_port.offset - bend.in_port.offset)
bend_angle = angle_out - angle_in
if bool(ccw): if bool(ccw):
bend_dxy[1] *= -1 bend_dxy[1] *= -1
bend_angle *= -1 bend_angle *= -1
@ -517,21 +554,15 @@ class AutoTool(Tool, metaclass=ABCMeta):
sbend_pat_or_tree = sbend.fn(jog) sbend_pat_or_tree = sbend.fn(jog)
sbpat = sbend_pat_or_tree if isinstance(sbend_pat_or_tree, Pattern) else sbend_pat_or_tree.top_pattern() 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])
angle_in = sbpat[sbend.in_port_name].rotation
assert angle_in is not None
dxy = rotation_matrix_2d(-angle_in) @ (sbpat[sbend.out_port_name].offset - sbpat[sbend.in_port_name].offset)
return dxy return dxy
@staticmethod @staticmethod
def _itransition2dxy(in_transition: Transition | None) -> NDArray[numpy.float64]: def _itransition2dxy(in_transition: Transition | None) -> NDArray[numpy.float64]:
if in_transition is None: if in_transition is None:
return numpy.zeros(2) return numpy.zeros(2)
irot = in_transition.their_port.rotation dxy, _ = in_transition.their_port.measure_travel(in_transition.our_port)
assert irot is not None return dxy
itrans_dxy = rotation_matrix_2d(-irot) @ (in_transition.our_port.offset - in_transition.their_port.offset)
return itrans_dxy
@staticmethod @staticmethod
def _otransition2dxy(out_transition: Transition | None, bend_angle: float) -> NDArray[numpy.float64]: def _otransition2dxy(out_transition: Transition | None, bend_angle: float) -> NDArray[numpy.float64]:
@ -549,7 +580,7 @@ class AutoTool(Tool, metaclass=ABCMeta):
*, *,
in_ptype: str | None = None, in_ptype: str | None = None,
out_ptype: str | None = None, out_ptype: str | None = None,
**kwargs, # noqa: ARG002 (unused) **kwargs,
) -> tuple[Port, LData]: ) -> tuple[Port, LData]:
success = False success = False
@ -702,21 +733,36 @@ class AutoTool(Tool, metaclass=ABCMeta):
itrans_dxy = self._itransition2dxy(in_transition) itrans_dxy = self._itransition2dxy(in_transition)
jog_remaining = jog - itrans_dxy[1] - otrans_dxy[1] jog_remaining = jog - itrans_dxy[1] - otrans_dxy[1]
sbend_dxy = self._sbend2dxy(sbend, jog_remaining) if sbend.jog_range[0] <= jog_remaining < sbend.jog_range[1]:
success = numpy.isclose(length, sbend_dxy[0] + itrans_dxy[1] + otrans_dxy[1]) sbend_dxy = self._sbend2dxy(sbend, jog_remaining)
if success: success = numpy.isclose(length, sbend_dxy[0] + itrans_dxy[1] + otrans_dxy[1])
b_transition = None if success:
straight_length = 0 b_transition = None
break straight_length = 0
break
if success: if success:
break break
else:
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
if not success:
# Failed to break # Failed to break
raise BuildError( raise BuildError(
f'Asked to draw S-path with total length {length:,g}, shorter than required bends and transitions:\n' f'Failed to find a valid s-bend configuration for {length=:,g}, {jog=:,g}, {in_ptype=}, {out_ptype=}'
f'sbend: {sbend_dxy[0]:,g} in_trans: {itrans_dxy[0]:,g}\n' ) from l2_err
f'out_trans: {otrans_dxy[0]:,g} bend_trans: {btrans_dxy[0]:,g}'
)
if out_transition is not None: if out_transition is not None:
out_ptype_actual = out_transition.their_port.ptype out_ptype_actual = out_transition.their_port.ptype
@ -769,6 +815,28 @@ class AutoTool(Tool, metaclass=ABCMeta):
pat.plug(data.out_transition.abstract, {port_names[1]: data.out_transition.our_port_name}) pat.plug(data.out_transition.abstract, {port_names[1]: data.out_transition.our_port_name})
return tree 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( def render(
self, self,
batch: Sequence[RenderStep], batch: Sequence[RenderStep],

View File

@ -11,7 +11,7 @@ from numpy import pi
from numpy.typing import ArrayLike, NDArray from numpy.typing import ArrayLike, NDArray
from .traits import PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable from .traits import PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable
from .utils import rotate_offsets_around from .utils import rotate_offsets_around, rotation_matrix_2d
from .error import PortError, format_stacktrace from .error import PortError, format_stacktrace
@ -143,6 +143,28 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable):
and self.rotation == other.rotation and self.rotation == other.rotation
) )
def measure_travel(self, destination: 'Port') -> tuple[NDArray[numpy.float64], float | None]:
"""
Find the (travel, jog) distances and rotation angle from the current port to the provided
`destination` port.
Travel is along the source port's axis (into the device interior), and jog is perpendicular,
with left of the travel direction corresponding to a positive jog.
Args:
(self): Source `Port`
destination: Destination `Port`
Returns
[travel, jog], rotation
"""
angle_in = self.rotation
angle_out = destination.rotation
assert angle_in is not None
dxy = rotation_matrix_2d(-angle_in) @ (destination.offset - self.offset)
angle = ((angle_out - angle_in) % (2 * pi)) if angle_out is not None else None
return dxy, angle
class PortList(metaclass=ABCMeta): class PortList(metaclass=ABCMeta):
__slots__ = () # Allow subclasses to use __slots__ __slots__ = () # Allow subclasses to use __slots__
@ -552,3 +574,4 @@ class PortList(metaclass=ABCMeta):
raise PortError(msg) raise PortError(msg)
return translations[0], rotations[0], o_offsets[0] return translations[0], rotations[0], o_offsets[0]