From 69ac25078c5dee71138b8f14393601fe4e110247 Mon Sep 17 00:00:00 2001 From: jan Date: Fri, 6 Mar 2026 22:58:32 -0800 Subject: [PATCH] [Pather/RenderPather/Tool/PortPather] Add U-bends --- masque/builder/pather.py | 66 ++++++++++++++++++++++++++++++++++ masque/builder/pather_mixin.py | 30 ++++++++++++++++ masque/builder/renderpather.py | 60 +++++++++++++++++++++++++++++++ masque/builder/tools.py | 39 ++++++++++++++++++++ 4 files changed, 195 insertions(+) diff --git a/masque/builder/pather.py b/masque/builder/pather.py index 68dfcd9..e34aa00 100644 --- a/masque/builder/pather.py +++ b/masque/builder/pather.py @@ -255,6 +255,72 @@ class Pather(Builder, PatherMixin): return s + def _pathU( + self, + portspec: str, + jog: float, + *, + length: float = 0, + plug_into: str | None = None, + **kwargs, + ) -> Self: + """ + Create a U-shaped "wire"/"waveguide" and `plug` it into the port `portspec`, with the aim + of traveling exactly `length` distance along the axis of `portspec` and returning to + the same orientation with an offset `jog`. + + 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: Extra distance to travel along the port's axis. Default 0. + plug_into: If not None, attempts to plug the wire's output port into the provided + port on `self`. + + Returns: + self + """ + if self._dead: + logger.warning('Skipping geometry for _pathU() since device is dead') + + tool_port_names = ('A', 'B') + + tool = self.tools.get(portspec, self.tools[None]) + in_ptype = self.pattern[portspec].ptype + try: + tree = tool.pathU(jog, length=length, in_ptype=in_ptype, port_names=tool_port_names, **kwargs) + except (BuildError, NotImplementedError): + if self._uturn_fallback(tool, portspec, jog, length, in_ptype, plug_into, **kwargs): + return self + + if not self._dead: + raise + logger.warning("Tool pathU failed for dead pather. Using dummy extension.") + # Fallback for dead pather: manually update the port instead of plugging + port = self.pattern[portspec] + port_rot = port.rotation + assert port_rot is not None + out_port = Port((length, jog), rotation=0, ptype=in_ptype) + out_port.rotate_around((0, 0), pi + port_rot) + out_port.translate(port.offset) + self.pattern.ports[portspec] = out_port + self._log_port_update(portspec) + if plug_into is not None: + self.plugged({portspec: plug_into}) + 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 + + def plugged(self, connections: dict[str, str]) -> Self: + PortList.plugged(self, connections) + return self + def _path( self, portspec: str, diff --git a/masque/builder/pather_mixin.py b/masque/builder/pather_mixin.py index 0f5fa92..cb957b1 100644 --- a/masque/builder/pather_mixin.py +++ b/masque/builder/pather_mixin.py @@ -214,6 +214,19 @@ class PatherMixin(PortList, metaclass=ABCMeta): self._pathS(port, l_actual, offset, **bounds) return self + def uturn(self, portspec: str | Sequence[str], offset: float, length: float | None = None, **bounds) -> Self: + """ 180-degree turn extension. """ + if isinstance(portspec, str): + portspec = [portspec] + + for port in portspec: + l_actual = length + if l_actual is None: + # TODO: use bounds to determine length? + l_actual = 0 + self._pathU(port, offset, length=l_actual, **bounds) + return self + def trace_into( self, portspec_src: str, @@ -376,6 +389,18 @@ class PatherMixin(PortList, metaclass=ABCMeta): ) -> Self: pass + @abstractmethod + def _pathU( + self, + portspec: str, + jog: float, + *, + length: float = 0, + plug_into: str | None = None, + **kwargs, + ) -> Self: + pass + def path(self, *args, **kwargs) -> Self: import warnings warnings.warn("path() is deprecated; use trace(), straight(), or bend() instead", DeprecationWarning, stacklevel=2) @@ -386,6 +411,11 @@ class PatherMixin(PortList, metaclass=ABCMeta): warnings.warn("pathS() is deprecated; use jog() instead", DeprecationWarning, stacklevel=2) return self._pathS(*args, **kwargs) + def pathU(self, *args, **kwargs) -> Self: + import warnings + warnings.warn("pathU() is deprecated; use uturn() instead", DeprecationWarning, stacklevel=2) + return self._pathU(*args, **kwargs) + @abstractmethod def plug( self, diff --git a/masque/builder/renderpather.py b/masque/builder/renderpather.py index 93fd2e3..2f78f8e 100644 --- a/masque/builder/renderpather.py +++ b/masque/builder/renderpather.py @@ -377,6 +377,66 @@ class RenderPather(PatherMixin): PortList.plugged(self, connections) return self + def _pathU( + self, + portspec: str, + jog: float, + *, + length: float = 0, + plug_into: str | None = None, + **kwargs, + ) -> Self: + """ + Plan a U-shaped "wire"/"waveguide" extending from the port `portspec`, with the aim + of traveling exactly `length` distance and returning to the same orientation + with an offset `jog`. + + 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: Extra distance to travel along the port's axis. Default 0. + plug_into: If not None, attempts to plug the wire's output port into the provided + port on `self`. + + Returns: + self + """ + if self._dead: + logger.warning('Skipping geometry for _pathU() since device is dead') + + port = self.pattern[portspec] + in_ptype = port.ptype + port_rot = port.rotation + assert port_rot is not None + + tool = self.tools.get(portspec, self.tools[None]) + + try: + out_port, data = tool.planU(jog, length=length, in_ptype=in_ptype, **kwargs) + except (BuildError, NotImplementedError): + if self._uturn_fallback(tool, portspec, jog, length, in_ptype, plug_into, **kwargs): + return self + + if not self._dead: + raise + logger.warning("Tool planning failed for dead pather. Using dummy extension.") + out_port = Port((length, jog), rotation=0, ptype=in_ptype) + data = None + + if out_port is not None: + out_port.rotate_around((0, 0), pi + port_rot) + out_port.translate(port.offset) + if not self._dead: + step = RenderStep('U', tool, port.copy(), out_port.copy(), data) + self.paths[portspec].append(step) + self.pattern.ports[portspec] = out_port.copy() + self._log_port_update(portspec) + + if plug_into is not None: + self.plugged({portspec: plug_into}) + return self + def _path( self, portspec: str, diff --git a/masque/builder/tools.py b/masque/builder/tools.py index 3e6616a..8617541 100644 --- a/masque/builder/tools.py +++ b/masque/builder/tools.py @@ -260,6 +260,45 @@ class Tool: """ raise NotImplementedError(f'planS() not implemented for {type(self)}') + def pathU( + self, + 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 `jog` distance along the axis + perpendicular to its input port (i.e. a U-bend). + + Used by `Pather` and `RenderPather`. + + The output port must have an orientation identical to the input port. + + 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: + jog: The total offset from the input to output, along the perpendicular axis. + 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. + 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 U-shaped wire or waveguide + + Raises: + BuildError if an impossible or unsupported geometry is requested. + """ + raise NotImplementedError(f'pathU() not implemented for {type(self)}') + def planU( self, jog: float,