diff --git a/masque/builder/tools.py b/masque/builder/tools.py index 8617541..c964fe7 100644 --- a/masque/builder/tools.py +++ b/masque/builder/tools.py @@ -264,6 +264,7 @@ class Tool: self, jog: float, *, + length: float = 0, in_ptype: str | None = None, out_ptype: str | None = None, port_names: tuple[str, str] = ('A', 'B'), @@ -597,6 +598,14 @@ class AutoTool(Tool, metaclass=ABCMeta): b_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] """ 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) 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( self, 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) elif step.opcode == 'S': 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 @@ -1086,6 +1204,7 @@ class PathTool(Tool, metaclass=ABCMeta): port_rot = step.start_port.rotation # 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. + assert port_rot is not None if step.opcode == 'L': diff --git a/masque/test/test_pather_api.py b/masque/test/test_pather_api.py index 0803b77..5937ea1 100644 --- a/masque/test/test_pather_api.py +++ b/masque/test/test_pather_api.py @@ -1,6 +1,6 @@ import numpy 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 def test_pather_trace_basic() -> None: @@ -25,6 +25,7 @@ def test_pather_trace_basic() -> None: # (-5000, -500) rot pi - pi/2 = pi/2 # Add to start: (-10000, -500) rot pi/2 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) def test_pather_trace_to() -> None: @@ -151,8 +152,54 @@ def test_renderpather_uturn_fallback() -> None: assert rp.paths['A'][1].opcode == 'L' rp.render() + assert rp.pattern.ports['A'].rotation is not None 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: lib = Library() tool = PathTool(layer='M1', width=1000)