diff --git a/masque/builder/pather.py b/masque/builder/pather.py index 3a0dee4..e9cd125 100644 --- a/masque/builder/pather.py +++ b/masque/builder/pather.py @@ -266,7 +266,7 @@ class Pather(Builder, PatherMixin): Create a "wire"/"waveguide" and `plug` it into the port `portspec`, with the aim of traveling exactly `length` distance. - The wire will travel `length` distance along the port's axis, and an unspecified + The wire will travel `length` distance along the port's axis, an an unspecified (tool-dependent) distance in the perpendicular direction. The output port will be rotated (or not) based on the `ccw` parameter. @@ -296,79 +296,11 @@ class Pather(Builder, PatherMixin): tool = self.tools.get(portspec, self.tools[None]) in_ptype = self.pattern[portspec].ptype tree = tool.path(ccw, length, in_ptype=in_ptype, port_names=tool_port_names, **kwargs) - tname = self.library << tree + abstract = self.library << tree # TODO this seems like a name, not an abstract 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 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}) + self.plug(abstract, {portspec: tool_port_names[0], **output}) return self diff --git a/masque/builder/pather_mixin.py b/masque/builder/pather_mixin.py index a60aaeb..e2c7eee 100644 --- a/masque/builder/pather_mixin.py +++ b/masque/builder/pather_mixin.py @@ -48,18 +48,6 @@ 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, @@ -278,6 +266,8 @@ 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... @@ -307,10 +297,20 @@ 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: - # S-bend, delegate to implementations - tj, _ = port_src.measure_travel(port_dst) - self.pathS(*tj, **dst_args) + # 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) elif numpy.isclose(angle, 0): raise BuildError('Don\'t know how to route a U-bend yet (TODO)!') else: diff --git a/masque/builder/renderpather.py b/masque/builder/renderpather.py index eb78cb8..c546f8b 100644 --- a/masque/builder/renderpather.py +++ b/masque/builder/renderpather.py @@ -420,81 +420,6 @@ 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, diff --git a/masque/builder/tools.py b/masque/builder/tools.py index 08aaf4c..2b88d24 100644 --- a/masque/builder/tools.py +++ b/masque/builder/tools.py @@ -70,7 +70,7 @@ class Tool: Create a wire or waveguide that travels exactly `length` distance along the axis of its input port. - Used by `Pather` and `RenderPather`. + Used by `Pather`. 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 @@ -101,48 +101,6 @@ 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, @@ -177,7 +135,7 @@ class Tool: kwargs: Custom tool-specific parameters. Returns: - The calculated output `Port` for the wire, assuming an input port at (0, 0) with rotation 0. + The calculated output `Port` for the wire. Any tool-specifc data, to be stored in `RenderStep.data`, for use during rendering. Raises: @@ -215,7 +173,7 @@ class Tool: kwargs: Custom tool-specific parameters. Returns: - The calculated output `Port` for the wire, assuming an input port at (0, 0) with rotation 0. + The calculated output `Port` for the wire. Any tool-specifc data, to be stored in `RenderStep.data`, for use during rendering. Raises: @@ -246,14 +204,14 @@ class Tool: 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) + A positive number implies a rightwards shift (i.e. clockwise bend followed + by a counterclockwise 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. Returns: - The calculated output `Port` for the wire, assuming an input port at (0, 0) with rotation 0. + The calculated output `Port` for the wire. Any tool-specifc data, to be stored in `RenderStep.data`, for use during rendering. Raises: @@ -424,7 +382,6 @@ class SimpleTool(Tool, metaclass=ABCMeta): self._renderL(data=step.data, tree=tree, port_names=port_names, straight_kwargs=kwargs) return tree - @dataclass class AutoTool(Tool, metaclass=ABCMeta): """ @@ -450,12 +407,11 @@ class AutoTool(Tool, metaclass=ABCMeta): fn: Callable[[float], Pattern] | Callable[[float], Library] """ Generator function. `jog` (only argument) is assumed to be left (ccw) relative to travel - and may be negative for a jog in the opposite direction. Won't be called if jog=0. + and may be negative for a jog i the opposite direction. Won't be called if jog=0. """ in_port_name: str out_port_name: str - jog_range: tuple[float, float] = (0, numpy.inf) @dataclass(frozen=True, slots=True) class Bend: @@ -540,8 +496,15 @@ 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 - bend_dxy, bend_angle = bend.in_port.measure_travel(bend.out_port) - assert bend_angle is not None + + 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 + if bool(ccw): bend_dxy[1] *= -1 bend_angle *= -1 @@ -554,15 +517,21 @@ 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() - 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 @staticmethod def _itransition2dxy(in_transition: Transition | None) -> NDArray[numpy.float64]: if in_transition is None: return numpy.zeros(2) - dxy, _ = in_transition.their_port.measure_travel(in_transition.our_port) - return dxy + 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 @staticmethod def _otransition2dxy(out_transition: Transition | None, bend_angle: float) -> NDArray[numpy.float64]: @@ -580,7 +549,7 @@ class AutoTool(Tool, metaclass=ABCMeta): *, in_ptype: str | None = None, out_ptype: str | None = None, - **kwargs, + **kwargs, # noqa: ARG002 (unused) ) -> tuple[Port, LData]: success = False @@ -733,36 +702,21 @@ class AutoTool(Tool, metaclass=ABCMeta): itrans_dxy = self._itransition2dxy(in_transition) jog_remaining = jog - itrans_dxy[1] - otrans_dxy[1] - 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 + 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 - - 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: + else: # Failed to break raise BuildError( - f'Failed to find a valid s-bend configuration for {length=:,g}, {jog=:,g}, {in_ptype=}, {out_ptype=}' - ) from l2_err + 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}' + ) if out_transition is not None: out_ptype_actual = out_transition.their_port.ptype @@ -815,28 +769,6 @@ 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], diff --git a/masque/ports.py b/masque/ports.py index 17431c2..b56ad70 100644 --- a/masque/ports.py +++ b/masque/ports.py @@ -11,7 +11,7 @@ from numpy import pi from numpy.typing import ArrayLike, NDArray from .traits import PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable -from .utils import rotate_offsets_around, rotation_matrix_2d +from .utils import rotate_offsets_around from .error import PortError, format_stacktrace @@ -143,28 +143,6 @@ class Port(PositionableImpl, Rotatable, PivotableImpl, Copyable, Mirrorable): 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): __slots__ = () # Allow subclasses to use __slots__ @@ -574,4 +552,3 @@ class PortList(metaclass=ABCMeta): raise PortError(msg) return translations[0], rotations[0], o_offsets[0] -