[AutoTool] add U-bend

This commit is contained in:
jan 2026-03-06 23:51:56 -08:00
commit 1070815730
2 changed files with 167 additions and 1 deletions

View file

@ -264,6 +264,7 @@ class Tool:
self, self,
jog: float, jog: float,
*, *,
length: float = 0,
in_ptype: str | None = None, in_ptype: str | None = None,
out_ptype: str | None = None, out_ptype: str | None = None,
port_names: tuple[str, str] = ('A', 'B'), port_names: tuple[str, str] = ('A', 'B'),
@ -597,6 +598,14 @@ class AutoTool(Tool, metaclass=ABCMeta):
b_transition: 'AutoTool.Transition | None' b_transition: 'AutoTool.Transition | None'
out_transition: 'AutoTool.Transition | None' out_transition: 'AutoTool.Transition | None'
@dataclass(frozen=True, slots=True)
class UData:
""" Data for planU """
ldata0: 'AutoTool.LData'
ldata1: 'AutoTool.LData'
straight2: 'AutoTool.Straight'
l2_length: float
straights: list[Straight] straights: list[Straight]
""" List of straight-generators to choose from, in order of priority """ """ List of straight-generators to choose from, in order of priority """
@ -942,6 +951,113 @@ class AutoTool(Tool, metaclass=ABCMeta):
self._renderS(data=data, tree=tree, port_names=port_names, gen_kwargs=kwargs) self._renderS(data=data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
return tree return tree
def planU(
self,
jog: float,
*,
length: float = 0,
in_ptype: str | None = None,
out_ptype: str | None = None,
**kwargs,
) -> tuple[Port, UData]:
ccw = jog > 0
kwargs_no_out = kwargs | {'out_ptype': None}
# Use loops to find a combination of straights and bends that fits
success = False
for _straight1 in self.straights:
for _bend1 in self.bends:
for straight2 in self.straights:
for _bend2 in self.bends:
try:
# We need to know R1 and R2 to calculate the lengths.
# Use large dummy lengths to probe the bends.
p_probe1, _ = self.planL(ccw, 1e9, in_ptype=in_ptype, **kwargs_no_out)
R1 = abs(Port((0, 0), 0).measure_travel(p_probe1)[0][1])
p_probe2, _ = self.planL(ccw, 1e9, in_ptype=p_probe1.ptype, out_ptype=out_ptype, **kwargs)
R2 = abs(Port((0, 0), 0).measure_travel(p_probe2)[0][1])
# Final x will be: x = l1_straight + R1 - R2
# We want final x = length. So: l1_straight = length - R1 + R2
# Total length for planL(0) is l1 = l1_straight + R1 = length + R2
l1 = length + R2
# Final y will be: y = R1 + l2_straight + R2 = abs(jog)
# So: l2_straight = abs(jog) - R1 - R2
l2_length = abs(jog) - R1 - R2
if l2_length >= straight2.length_range[0] and l2_length < straight2.length_range[1]:
p0, ldata0 = self.planL(ccw, l1, in_ptype=in_ptype, **kwargs_no_out)
# For the second bend, we want straight length = 0.
# Total length for planL(1) is l2 = 0 + R2 = R2.
p1, ldata1 = self.planL(ccw, R2, in_ptype=p0.ptype, out_ptype=out_ptype, **kwargs)
success = True
break
except BuildError:
continue
if success:
break
if success:
break
if success:
break
if not success:
raise BuildError(f"AutoTool failed to plan U-turn with {jog=}, {length=}")
data = self.UData(ldata0, ldata1, straight2, l2_length)
# Final port is at (length, jog) rot pi relative to input
out_port = Port((length, jog), rotation=pi, ptype=p1.ptype)
return out_port, data
def _renderU(
self,
data: UData,
tree: ILibrary,
port_names: tuple[str, str],
gen_kwargs: dict[str, Any],
) -> ILibrary:
pat = tree.top_pattern()
# 1. First L-bend
self._renderL(data.ldata0, tree, port_names, gen_kwargs)
# 2. Connecting straight
if not numpy.isclose(data.l2_length, 0):
s2_pat_or_tree = data.straight2.fn(data.l2_length, **(gen_kwargs | data.ldata0.straight_kwargs))
pmap = {port_names[1]: data.straight2.in_port_name}
if isinstance(s2_pat_or_tree, Pattern):
pat.plug(s2_pat_or_tree, pmap, append=True)
else:
s2_tree = s2_pat_or_tree
top = s2_tree.top()
s2_tree.flatten(top, dangling_ok=True)
pat.plug(s2_tree[top], pmap, append=True)
# 3. Second L-bend
self._renderL(data.ldata1, tree, port_names, gen_kwargs)
return tree
def pathU(
self,
jog: float,
*,
length: float = 0,
in_ptype: str | None = None,
out_ptype: str | None = None,
port_names: tuple[str, str] = ('A', 'B'),
**kwargs,
) -> Library:
_out_port, data = self.planU(
jog,
length = length,
in_ptype = in_ptype,
out_ptype = out_ptype,
**kwargs,
)
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'pathU')
pat.add_port_pair(names=port_names, ptype='unk' if in_ptype is None else in_ptype)
self._renderU(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],
@ -959,6 +1075,8 @@ class AutoTool(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)
elif step.opcode == 'S': elif step.opcode == 'S':
self._renderS(data=step.data, tree=tree, port_names=port_names, gen_kwargs=kwargs) self._renderS(data=step.data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
elif step.opcode == 'U':
self._renderU(data=step.data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
return tree return tree
@ -1086,6 +1204,7 @@ class PathTool(Tool, metaclass=ABCMeta):
port_rot = step.start_port.rotation port_rot = step.start_port.rotation
# Masque convention: Port rotation points INTO the device. # Masque convention: Port rotation points INTO the device.
# So the direction of travel for the path is AWAY from the port, i.e., port_rot + pi. # So the direction of travel for the path is AWAY from the port, i.e., port_rot + pi.
assert port_rot is not None
if step.opcode == 'L': if step.opcode == 'L':

View file

@ -1,6 +1,6 @@
import numpy import numpy
from numpy import pi from numpy import pi
from masque import Pather, RenderPather, Library, Port from masque import Pather, RenderPather, Library, Pattern, Port
from masque.builder.tools import PathTool from masque.builder.tools import PathTool
def test_pather_trace_basic() -> None: def test_pather_trace_basic() -> None:
@ -25,6 +25,7 @@ def test_pather_trace_basic() -> None:
# (-5000, -500) rot pi - pi/2 = pi/2 # (-5000, -500) rot pi - pi/2 = pi/2
# Add to start: (-10000, -500) rot pi/2 # Add to start: (-10000, -500) rot pi/2
assert numpy.allclose(p.pattern.ports['A'].offset, (-10000, -500)) assert numpy.allclose(p.pattern.ports['A'].offset, (-10000, -500))
assert p.pattern.ports['A'].rotation is not None
assert numpy.isclose(p.pattern.ports['A'].rotation, pi/2) assert numpy.isclose(p.pattern.ports['A'].rotation, pi/2)
def test_pather_trace_to() -> None: def test_pather_trace_to() -> None:
@ -151,8 +152,54 @@ def test_renderpather_uturn_fallback() -> None:
assert rp.paths['A'][1].opcode == 'L' assert rp.paths['A'][1].opcode == 'L'
rp.render() rp.render()
assert rp.pattern.ports['A'].rotation is not None
assert numpy.isclose(rp.pattern.ports['A'].rotation, pi) assert numpy.isclose(rp.pattern.ports['A'].rotation, pi)
def test_autotool_uturn() -> None:
from masque.builder.tools import AutoTool
lib = Library()
# Setup AutoTool with a simple straight and a bend
def make_straight(length: float) -> Pattern:
pat = Pattern()
pat.rect(layer='M1', xmin=0, xmax=length, yctr=0, ly=1000)
pat.ports['in'] = Port((0, 0), 0)
pat.ports['out'] = Port((length, 0), pi)
return pat
bend_pat = Pattern()
bend_pat.polygon(layer='M1', vertices=[(0, -500), (0, 500), (1000, -500)])
bend_pat.ports['in'] = Port((0, 0), 0)
bend_pat.ports['out'] = Port((500, -500), pi/2)
lib['bend'] = bend_pat
tool = AutoTool(
straights=[AutoTool.Straight(ptype='wire', fn=make_straight, in_port_name='in', out_port_name='out')],
bends=[AutoTool.Bend(abstract=lib.abstract('bend'), in_port_name='in', out_port_name='out', clockwise=True)],
sbends=[],
transitions={},
default_out_ptype='wire'
)
p = Pather(lib, tools=tool)
p.pattern.ports['A'] = Port((0, 0), 0)
# CW U-turn (jog < 0)
# R = 500. jog = -2000. length = 1000.
# p0 = planL(length=1000) -> out at (1000, -500) rot pi/2
# R2 = 500.
# l2_length = abs(-2000) - abs(-500) - 500 = 1000.
p.at('A').uturn(offset=-2000, length=1000)
# Final port should be at (-1000, 2000) rot pi
# Start: (0,0) rot 0. Wire direction is rot + pi = pi (West, -x).
# Tool planU returns (length, jog) = (1000, -2000) relative to (0,0) rot 0.
# Rotation of pi transforms (1000, -2000) to (-1000, 2000).
# Final rotation: 0 + pi = pi.
assert numpy.allclose(p.pattern.ports['A'].offset, (-1000, 2000))
assert p.pattern.ports['A'].rotation is not None
assert numpy.isclose(p.pattern.ports['A'].rotation, pi)
def test_pather_trace_into() -> None: def test_pather_trace_into() -> None:
lib = Library() lib = Library()
tool = PathTool(layer='M1', width=1000) tool = PathTool(layer='M1', width=1000)