[Tool / AutoTool / Pather / RenderPather / PatherMixin] add support for S-bends
This commit is contained in:
parent
dbaa6fc1f3
commit
049098ade5
@ -304,3 +304,71 @@ class Pather(Builder, PatherMixin):
|
||||
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
|
||||
|
||||
|
||||
@ -48,6 +48,18 @@ class PatherMixin(metaclass=ABCMeta):
|
||||
) -> Self:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def pathS(
|
||||
self,
|
||||
portspec: str,
|
||||
length: float,
|
||||
jog: float,
|
||||
*,
|
||||
plug_into: str | None = None,
|
||||
**kwargs,
|
||||
) -> Self:
|
||||
pass
|
||||
|
||||
def retool(
|
||||
self,
|
||||
tool: Tool,
|
||||
@ -266,8 +278,6 @@ class PatherMixin(metaclass=ABCMeta):
|
||||
|
||||
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...
|
||||
@ -297,20 +307,10 @@ class PatherMixin(metaclass=ABCMeta):
|
||||
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)
|
||||
# S-bend, delegate to implementations
|
||||
tj, _ = port_src.measure_travel(port_dst)
|
||||
self.pathS(*tj, **dst_args)
|
||||
elif numpy.isclose(angle, 0):
|
||||
raise BuildError('Don\'t know how to route a U-bend yet (TODO)!')
|
||||
else:
|
||||
|
||||
@ -420,6 +420,81 @@ class RenderPather(PortList, PatherMixin):
|
||||
|
||||
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(
|
||||
self,
|
||||
append: bool = True,
|
||||
|
||||
@ -101,6 +101,48 @@ class Tool:
|
||||
"""
|
||||
raise NotImplementedError(f'path() not implemented for {type(self)}')
|
||||
|
||||
def pathS(
|
||||
self,
|
||||
length: float,
|
||||
jog: float,
|
||||
*,
|
||||
in_ptype: str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
port_names: tuple[str, str] = ('A', 'B'),
|
||||
**kwargs,
|
||||
) -> Library:
|
||||
"""
|
||||
Create a wire or waveguide that travels exactly `length` distance along the axis
|
||||
of its input port, and `jog` distance on the perpendicular axis.
|
||||
`jog` is positive when moving left of the direction of travel (from input to ouput port).
|
||||
|
||||
Used by `Pather` and `RenderPather`.
|
||||
|
||||
The output port should be rotated to face the input port (i.e. plugging the device
|
||||
into a port will move that port but keep its orientation).
|
||||
|
||||
The input and output ports should be compatible with `in_ptype` and
|
||||
`out_ptype`, respectively. They should also be named `port_names[0]` and
|
||||
`port_names[1]`, respectively.
|
||||
|
||||
Args:
|
||||
length: The total distance from input to output, along the input's axis only.
|
||||
jog: The total distance from input to output, along the second axis. Positive indicates
|
||||
a leftward shift when moving from input to output port.
|
||||
in_ptype: The `ptype` of the port into which this wire's input will be `plug`ged.
|
||||
out_ptype: The `ptype` of the port into which this wire's output will be `plug`ged.
|
||||
port_names: The output pattern will have its input port named `port_names[0]` and
|
||||
its output named `port_names[1]`.
|
||||
kwargs: Custom tool-specific parameters.
|
||||
|
||||
Returns:
|
||||
A pattern tree containing the requested S-shaped (or straight) wire or waveguide
|
||||
|
||||
Raises:
|
||||
BuildError if an impossible or unsupported geometry is requested.
|
||||
"""
|
||||
raise NotImplementedError(f'path() not implemented for {type(self)}')
|
||||
|
||||
def planL(
|
||||
self,
|
||||
ccw: SupportsBool | None,
|
||||
@ -204,8 +246,8 @@ class Tool:
|
||||
|
||||
Args:
|
||||
jog: The total offset from the input to output, along the perpendicular axis.
|
||||
A positive number implies a rightwards shift (i.e. clockwise bend followed
|
||||
by a counterclockwise bend)
|
||||
A positive number implies a leftwards shift (i.e. counterclockwise bend
|
||||
followed by a clockwise bend)
|
||||
in_ptype: The `ptype` of the port into which this wire's input will be `plug`ged.
|
||||
out_ptype: The `ptype` of the port into which this wire's output will be `plug`ged.
|
||||
kwargs: Custom tool-specific parameters.
|
||||
@ -413,6 +455,7 @@ class AutoTool(Tool, metaclass=ABCMeta):
|
||||
|
||||
in_port_name: str
|
||||
out_port_name: str
|
||||
jog_range: tuple[float, float] = (0, numpy.inf)
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Bend:
|
||||
@ -497,15 +540,8 @@ class AutoTool(Tool, metaclass=ABCMeta):
|
||||
def _bend2dxy(bend: Bend, ccw: SupportsBool | None) -> tuple[NDArray[numpy.float64], float]:
|
||||
if ccw is None:
|
||||
return numpy.zeros(2), pi
|
||||
|
||||
angle_in = bend.in_port.rotation
|
||||
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
|
||||
|
||||
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
|
||||
@ -518,21 +554,15 @@ class AutoTool(Tool, metaclass=ABCMeta):
|
||||
|
||||
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()
|
||||
|
||||
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)
|
||||
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)
|
||||
irot = in_transition.their_port.rotation
|
||||
assert irot is not None
|
||||
itrans_dxy = rotation_matrix_2d(-irot) @ (in_transition.our_port.offset - in_transition.their_port.offset)
|
||||
return itrans_dxy
|
||||
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]:
|
||||
@ -550,7 +580,7 @@ class AutoTool(Tool, metaclass=ABCMeta):
|
||||
*,
|
||||
in_ptype: str | None = None,
|
||||
out_ptype: str | None = None,
|
||||
**kwargs, # noqa: ARG002 (unused)
|
||||
**kwargs,
|
||||
) -> tuple[Port, LData]:
|
||||
|
||||
success = False
|
||||
@ -703,21 +733,36 @@ class AutoTool(Tool, metaclass=ABCMeta):
|
||||
itrans_dxy = self._itransition2dxy(in_transition)
|
||||
|
||||
jog_remaining = jog - itrans_dxy[1] - otrans_dxy[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 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
|
||||
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
|
||||
raise BuildError(
|
||||
f'Asked to draw S-path with total length {length:,g}, shorter than required bends and transitions:\n'
|
||||
f'sbend: {sbend_dxy[0]:,g} in_trans: {itrans_dxy[0]:,g}\n'
|
||||
f'out_trans: {otrans_dxy[0]:,g} bend_trans: {btrans_dxy[0]:,g}'
|
||||
)
|
||||
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
|
||||
@ -770,6 +815,28 @@ class AutoTool(Tool, metaclass=ABCMeta):
|
||||
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],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user