diff --git a/README.md b/README.md index 6ebc5ab..62b13bb 100644 --- a/README.md +++ b/README.md @@ -277,6 +277,12 @@ my_pattern.ref(_make_my_subpattern(), offset=..., ...) ## TODO +* Rework naming/args for path-related (Builder, PortPather, path/pathL/pathS/pathU, path_to, mpath) * PolyCollection & arrow-based read/write +* pather and renderpather examples, including .at() (PortPather) * Bus-to-bus connections? +* Tests tests tests +* Better interface for polygon operations (e.g. with `pyclipper`) + - de-embedding + - boolean ops * tuple / string layer auto-translation diff --git a/examples/tutorial/pather.py b/examples/tutorial/pather.py index f7bbdb2..a9d9af9 100644 --- a/examples/tutorial/pather.py +++ b/examples/tutorial/pather.py @@ -106,9 +106,7 @@ def map_layer(layer: layer_t) -> layer_t: 'M2': (20, 0), 'V1': (30, 0), } - if isinstance(layer, str): - return layer_mapping.get(layer, layer) - return layer + return layer_mapping.get(layer, layer) def prepare_tools() -> tuple[Library, Tool, Tool]: @@ -226,17 +224,19 @@ def main() -> None: # Path VCC forward (in this case south) and turn clockwise 90 degrees (ccw=False) # The total distance forward (including the bend's forward component) must be 6um - pather.cw('VCC', 6_000) + pather.path('VCC', ccw=False, length=6_000) - # Now path VCC to x=0. This time, don't include any bend. + # Now path VCC to x=0. This time, don't include any bend (ccw=None). # Note that if we tried y=0 here, we would get an error since the VCC port is facing in the x-direction. - pather.straight('VCC', x=0) + pather.path_to('VCC', ccw=None, x=0) # Path GND forward by 5um, turning clockwise 90 degrees. - pather.cw('GND', 5_000) + # This time we use shorthand (bool(0) == False) and omit the parameter labels + # Note that although ccw=0 is equivalent to ccw=False, ccw=None is not! + pather.path('GND', 0, 5_000) # This time, path GND until it matches the current x-coordinate of VCC. Don't place a bend. - pather.straight('GND', x=pather['VCC'].offset[0]) + pather.path_to('GND', None, x=pather['VCC'].offset[0]) # Now, start using M1_tool for GND. # Since we have defined an M2-to-M1 transition for Pather, we don't need to place one ourselves. @@ -244,7 +244,7 @@ def main() -> None: # and achieve the same result without having to define any transitions in M1_tool. # Note that even though we have changed the tool used for GND, the via doesn't get placed until # the next time we draw a path on GND (the pather.mpath() statement below). - pather.retool(M1_tool, keys='GND') + pather.retool(M1_tool, keys=['GND']) # Bundle together GND and VCC, and path the bundle forward and counterclockwise. # Pick the distance so that the leading/outermost wire (in this case GND) ends up at x=-10_000. @@ -252,7 +252,7 @@ def main() -> None: # # Since we recently retooled GND, its path starts with a via down to M1 (included in the distance # calculation), and its straight segment and bend will be drawn using M1 while VCC's are drawn with M2. - pather.ccw(['GND', 'VCC'], xmax=-10_000, spacing=5_000) + pather.mpath(['GND', 'VCC'], ccw=True, xmax=-10_000, spacing=5_000) # Now use M1_tool as the default tool for all ports/signals. # Since VCC does not have an explicitly assigned tool, it will now transition down to M1. @@ -262,34 +262,35 @@ def main() -> None: # The total extension (travel distance along the forward direction) for the longest segment (in # this case the segment being added to GND) should be exactly 50um. # After turning, the wire pitch should be reduced only 1.2um. - pather.ccw(['GND', 'VCC'], emax=50_000, spacing=1_200) + pather.mpath(['GND', 'VCC'], ccw=True, emax=50_000, spacing=1_200) # Make a U-turn with the bundle and expand back out to 4.5um wire pitch. - # Here, emin specifies the travel distance for the shortest segment. For the first call - # that applies to VCC, and for the second call, that applies to GND; the relative lengths of the + # Here, emin specifies the travel distance for the shortest segment. For the first mpath() call + # that applies to VCC, and for teh second call, that applies to GND; the relative lengths of the # segments depend on their starting positions and their ordering within the bundle. - pather.cw(['GND', 'VCC'], emin=1_000, spacing=1_200) - pather.cw(['GND', 'VCC'], emin=2_000, spacing=4_500) + pather.mpath(['GND', 'VCC'], ccw=False, emin=1_000, spacing=1_200) + pather.mpath(['GND', 'VCC'], ccw=False, emin=2_000, spacing=4_500) # Now, set the default tool back to M2_tool. Note that GND remains on M1 since it has been - # explicitly assigned a tool. + # explicitly assigned a tool. We could `del pather.tools['GND']` to force it to use the default. pather.retool(M2_tool) # Now path both ports to x=-28_000. - # With ccw=None, all ports stop at the same coordinate, and so specifying xmin= or xmax= is + # When ccw is not None, xmin constrains the trailing/innermost port to stop at the target x coordinate, + # However, with ccw=None, all ports stop at the same coordinate, and so specifying xmin= or xmax= is # equivalent. - pather.straight(['GND', 'VCC'], xmin=-28_000) + pather.mpath(['GND', 'VCC'], None, xmin=-28_000) # Further extend VCC out to x=-50_000, and specify that we would like to get an output on M1. # This results in a via at the end of the wire (instead of having one at the start like we got # when using pather.retool(). - pather.straight('VCC', x=-50_000, out_ptype='m1wire') + pather.path_to('VCC', None, -50_000, out_ptype='m1wire') # Now extend GND out to x=-50_000, using M2 for a portion of the path. # We can use `pather.toolctx()` to temporarily retool, instead of calling `retool()` twice. - with pather.toolctx(M2_tool, keys='GND'): - pather.straight('GND', x=-40_000) - pather.straight('GND', x=-50_000) + with pather.toolctx(M2_tool, keys=['GND']): + pather.path_to('GND', None, -40_000) + pather.path_to('GND', None, -50_000) # Save the pather's pattern into our library library['Pather_and_AutoTool'] = pather.pattern diff --git a/examples/tutorial/renderpather.py b/examples/tutorial/renderpather.py index 7b75f5d..ecc8bc8 100644 --- a/examples/tutorial/renderpather.py +++ b/examples/tutorial/renderpather.py @@ -48,28 +48,28 @@ def main() -> None: rpather.pattern.label(layer='M2', string='GND', offset=(18e3, 60e3)) # ...and start routing the signals. - rpather.cw('VCC', 6_000) - rpather.straight('VCC', x=0) - rpather.cw('GND', 5_000) - rpather.straight('GND', x=rpather.pattern['VCC'].x) + rpather.path('VCC', ccw=False, length=6_000) + rpather.path_to('VCC', ccw=None, x=0) + rpather.path('GND', 0, 5_000) + rpather.path_to('GND', None, x=rpather['VCC'].x) # `PathTool` doesn't know how to transition betwen metal layers, so we have to # `plug` the via into the GND wire ourselves. rpather.plug('v1_via', {'GND': 'top'}) - rpather.retool(M1_ptool, keys='GND') - rpather.ccw(['GND', 'VCC'], xmax=-10_000, spacing=5_000) + rpather.retool(M1_ptool, keys=['GND']) + rpather.mpath(['GND', 'VCC'], ccw=True, xmax=-10_000, spacing=5_000) # Same thing on the VCC wire when it goes down to M1. rpather.plug('v1_via', {'VCC': 'top'}) rpather.retool(M1_ptool) - rpather.ccw(['GND', 'VCC'], emax=50_000, spacing=1_200) - rpather.cw(['GND', 'VCC'], emin=1_000, spacing=1_200) - rpather.cw(['GND', 'VCC'], emin=2_000, spacing=4_500) + rpather.mpath(['GND', 'VCC'], ccw=True, emax=50_000, spacing=1_200) + rpather.mpath(['GND', 'VCC'], ccw=False, emin=1_000, spacing=1_200) + rpather.mpath(['GND', 'VCC'], ccw=False, emin=2_000, spacing=4_500) # And again when VCC goes back up to M2. rpather.plug('v1_via', {'VCC': 'bottom'}) rpather.retool(M2_ptool) - rpather.straight(['GND', 'VCC'], xmin=-28_000) + rpather.mpath(['GND', 'VCC'], None, xmin=-28_000) # Finally, since PathTool has no conception of transitions, we can't # just ask it to transition to an 'm1wire' port at the end of the final VCC segment. @@ -80,7 +80,7 @@ def main() -> None: # alternatively, via_size = v1pat.ports['top'].measure_travel(v1pat.ports['bottom'])[0][0] # would take into account the port orientations if we didn't already know they're along x - rpather.straight('VCC', x=-50_000 + via_size) + rpather.path_to('VCC', None, -50_000 + via_size) rpather.plug('v1_via', {'VCC': 'top'}) # Render the path we defined diff --git a/masque/builder/pather.py b/masque/builder/pather.py index df00cc0..a3c4dc5 100644 --- a/masque/builder/pather.py +++ b/masque/builder/pather.py @@ -28,7 +28,7 @@ class Pather(Builder, PatherMixin): single-use patterns (e.g. wires or waveguides) and bundles / buses of such patterns. `Pather` is mostly concerned with calculating how long each wire should be. It calls - out to `Tool.traceL` functions provided by subclasses of `Tool` to build the actual patterns. + out to `Tool.path` functions provided by subclasses of `Tool` to build the actual patterns. `Tool`s are assigned on a per-port basis and stored in `.tools`; a key of `None` represents a "default" `Tool` used for all ports which do not have a port-specific `Tool` assigned. @@ -63,10 +63,7 @@ class Pather(Builder, PatherMixin): Examples: Adding to a pattern ============================= - - `pather.straight('my_port', distance)` creates a straight wire with a length - of `distance` and `plug`s it into `'my_port'`. - - - `pather.bend('my_port', ccw=True, distance)` creates a "wire" for which the output + - `pather.path('my_port', ccw=True, distance)` creates a "wire" for which the output port is `distance` units away along the axis of `'my_port'` and rotated 90 degrees counterclockwise (since `ccw=True`) relative to `'my_port'`. The wire is `plug`ged into the existing `'my_port'`, causing the port to move to the wire's output. @@ -75,15 +72,22 @@ class Pather(Builder, PatherMixin): there may be a significant width to the bend that is used to accomplish the 90 degree turn. However, an error is raised if `distance` is too small to fit the bend. - - `pather.trace_to('my_port', ccw=False, x=position)` creates a wire which starts at + - `pather.path('my_port', ccw=None, distance)` creates a straight wire with a length + of `distance` and `plug`s it into `'my_port'`. + + - `pather.path_to('my_port', ccw=False, position)` creates a wire which starts at `'my_port'` and has its output at the specified `position`, pointing 90 degrees clockwise relative to the input. Again, the off-axis position or distance to the - output is not specified, so `position` takes the form of a single coordinate. + output is not specified, so `position` takes the form of a single coordinate. To + ease debugging, position may be specified as `x=position` or `y=position` and an + error will be raised if the wrong coordinate is given. - - `pather.trace(['A', 'B', 'C'], ccw=True, spacing=spacing, xmax=position)` acts - on multiple ports simultaneously. Each port's wire is generated using its own - `Tool` (or the default tool if left unspecified). - The output ports are spaced out by `spacing` along the input ports' axis. + - `pather.mpath(['A', 'B', 'C'], ..., spacing=spacing)` is a superset of `path` + and `path_to` which can act on multiple ports simultaneously. Each port's wire is + generated using its own `Tool` (or the default tool if left unspecified). + The output ports are spaced out by `spacing` along the input ports' axis, unless + `ccw=None` is specified (i.e. no bends) in which case they all end at the same + destination coordinate. - `pather.plug(wire, {'myport': 'A'})` places port 'A' of `wire` at 'myport' of `pather.pattern`. If `wire` has only two ports (e.g. 'A' and 'B'), no `map_out`, @@ -137,8 +141,8 @@ class Pather(Builder, PatherMixin): in which case it is interpreted as a name in `library`. Default `None` (no ports). tools: A mapping of {port: tool} which specifies what `Tool` should be used - to generate waveguide or wire segments when `trace`/`trace_to`/etc. - are called. Relies on `Tool.traceL` implementations. + to generate waveguide or wire segments when `path`/`path_to`/`mpath` + are called. Relies on `Tool.path` implementations. name: If specified, `library[name]` is set to `self.pattern`. """ self._dead = False @@ -209,7 +213,7 @@ class Pather(Builder, PatherMixin): and to which the new one should be added (if named). If not provided, `source.library` must exist and will be used. tools: `Tool`s which will be used by the pather for generating new wires - or waveguides (via `trace`/`trace_to`). + or waveguides (via `path`/`path_to`/`mpath`). in_prefix: Prepended to port names for newly-created ports with reversed directions compared to the current device. out_prefix: Prepended to port names for ports which are directly @@ -251,73 +255,7 @@ class Pather(Builder, PatherMixin): return s - def _traceU( - 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 _traceU() 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.traceU(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 traceU 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 _traceL( + def path( self, portspec: str, ccw: SupportsBool | None, @@ -358,18 +296,18 @@ class Pather(Builder, PatherMixin): LibraryError if no valid name could be picked for the pattern. """ if self._dead: - logger.warning('Skipping geometry for _traceL() since device is dead') + logger.warning('Skipping geometry for path() 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.traceL(ccw, length, in_ptype=in_ptype, port_names=tool_port_names, **kwargs) + tree = tool.path(ccw, length, in_ptype=in_ptype, port_names=tool_port_names, **kwargs) except (BuildError, NotImplementedError): if not self._dead: raise - logger.warning("Tool traceL failed for dead pather. Using dummy extension.") + logger.warning("Tool path 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 @@ -397,7 +335,7 @@ class Pather(Builder, PatherMixin): self.plug(tname, {portspec: tool_port_names[0], **output}) return self - def _traceS( + def pathS( self, portspec: str, length: float, @@ -408,17 +346,20 @@ class Pather(Builder, PatherMixin): ) -> Self: """ Create an S-shaped "wire"/"waveguide" and `plug` it into the port `portspec`, with the aim - of traveling exactly `length` distance. + of traveling exactly `length` distance with an offset `jog` along the other axis (+ve jog is + left of direction of travel). - The wire will travel `length` distance along the port's axis, and exactly `jog` - distance in the perpendicular direction. The output port will have an orientation - identical to the input port. + 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. - length: The total distance from input to output, along the input's axis only. - jog: Total distance perpendicular to the direction of travel. Positive values - are to the left of the direction of travel. + 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`. @@ -436,29 +377,29 @@ class Pather(Builder, PatherMixin): LibraryError if no valid name could be picked for the pattern. """ if self._dead: - logger.warning('Skipping geometry for _traceS() since device is dead') + logger.warning('Skipping geometry for pathS() 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.traceS(length, jog, in_ptype=in_ptype, port_names=tool_port_names, **kwargs) + 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} try: - t_tree0 = tool.traceL( ccw0, length / 2, port_names=tool_port_names, in_ptype=in_ptype, **kwargs_no_out) + 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.traceL(not ccw0, abs(jog - jog0), port_names=tool_port_names, in_ptype=t_pat0[tool_port_names[1]].ptype, **kwargs) + t_tree1 = tool.path(not ccw0, abs(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]]) kwargs_plug = kwargs | {'plug_into': plug_into} - self._traceL(portspec, ccw0, length - abs(jog1), **kwargs_no_out) - self._traceL(portspec, not ccw0, abs(jog - jog0), **kwargs_plug) + self.path(portspec, ccw0, length - abs(jog1), **kwargs_no_out) + self.path(portspec, not ccw0, abs(jog - jog0), **kwargs_plug) except (BuildError, NotImplementedError): if not self._dead: raise @@ -471,7 +412,7 @@ class Pather(Builder, PatherMixin): # Fall through to dummy extension below if self._dead: - logger.warning("Tool traceS failed for dead pather. Using dummy extension.") + logger.warning("Tool pathS 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 @@ -492,3 +433,4 @@ class Pather(Builder, PatherMixin): output = {} self.plug(tname, {portspec: tool_port_names[0], **output}) return self + diff --git a/masque/builder/pather_mixin.py b/masque/builder/pather_mixin.py index eeae7ad..1655329 100644 --- a/masque/builder/pather_mixin.py +++ b/masque/builder/pather_mixin.py @@ -1,5 +1,5 @@ from typing import Self, overload -from collections.abc import Sequence, Iterator, Iterable, Mapping +from collections.abc import Sequence, Iterator, Iterable import logging from contextlib import contextmanager from abc import abstractmethod, ABCMeta @@ -37,338 +37,8 @@ class PatherMixin(PortList, metaclass=ABCMeta): (e.g wires or waveguides) to be plugged into this device. """ - def trace( - self, - portspec: str | Sequence[str], - ccw: SupportsBool | None, - length: float | None = None, - *, - spacing: float | ArrayLike | None = None, - **bounds, - ) -> Self: - """ - Create a "wire"/"waveguide" extending from the port(s) `portspec`. - - Args: - portspec: The name(s) of the port(s) into which the wire(s) will be plugged. - ccw: If `None`, the output should be along the same axis as the input. - Otherwise, cast to bool and turn counterclockwise if True - and clockwise otherwise. - length: The total distance from input to output, along the input's axis only. - Length is only allowed with a single port. - spacing: Center-to-center distance between output ports along the input port's axis. - Only used when routing multiple ports with a bend. - bounds: Boundary constraints for the trace. - - each: results in each port being extended by `each` distance. - - emin, emax, pmin, pmax, xmin, xmax, ymin, ymax: bundle routing via `ell()`. - - set_rotation: explicit rotation for ports without one. - - Returns: - self - """ - if isinstance(portspec, str): - portspec = [portspec] - - if length is not None: - if len(portspec) > 1: - raise BuildError('length is only allowed with a single port in trace()') - if bounds: - raise BuildError('length and bounds are mutually exclusive in trace()') - return self._traceL(portspec[0], ccw, length) - - if 'each' in bounds: - each = bounds.pop('each') - if bounds: - raise BuildError('each and other bounds are mutually exclusive in trace()') - for port in portspec: - self._traceL(port, ccw, each) - return self - - # Bundle routing (formerly mpath logic) - bound_types = set() - if 'bound_type' in bounds: - bound_types.add(bounds.pop('bound_type')) - bound = bounds.pop('bound') - for bt in ('emin', 'emax', 'pmin', 'pmax', 'xmin', 'xmax', 'ymin', 'ymax', 'min_past_furthest'): - if bt in bounds: - bound_types.add(bt) - bound = bounds.pop(bt) - - if not bound_types: - raise BuildError('No bound type specified for trace()') - if len(bound_types) > 1: - raise BuildError(f'Too many bound types specified: {bound_types}') - bound_type = tuple(bound_types)[0] - - ports = self.pattern[tuple(portspec)] - set_rotation = bounds.pop('set_rotation', None) - - extensions = ell(ports, ccw, spacing=spacing, bound=bound, bound_type=bound_type, set_rotation=set_rotation) - - for port_name, ext_len in extensions.items(): - self._traceL(port_name, ccw, ext_len, **bounds) - return self - - def trace_to( - self, - portspec: str | Sequence[str], - ccw: SupportsBool | None, - *, - spacing: float | ArrayLike | None = None, - **bounds, - ) -> Self: - """ - Create a "wire"/"waveguide" extending from the port(s) `portspec` to a target position. - - Args: - portspec: The name(s) of the port(s) into which the wire(s) will be plugged. - ccw: If `None`, the output should be along the same axis as the input. - Otherwise, cast to bool and turn counterclockwise if True - and clockwise otherwise. - spacing: Center-to-center distance between output ports along the input port's axis. - Only used when routing multiple ports with a bend. - bounds: Boundary constraints for the target position. - - p, x, y, pos, position: Coordinate of the target position. Error if used with multiple ports. - - pmin, pmax, xmin, xmax, ymin, ymax, emin, emax: bundle routing via `ell()`. - - Returns: - self - """ - if isinstance(portspec, str): - portspec = [portspec] - - pos_bounds = {kk: bounds[kk] for kk in ('p', 'x', 'y', 'pos', 'position') if kk in bounds} - if pos_bounds: - if len(portspec) > 1: - raise BuildError(f'{tuple(pos_bounds.keys())} bounds are only allowed with a single port in trace_to()') - if len(pos_bounds) > 1: - raise BuildError(f'Too many position bounds: {tuple(pos_bounds.keys())}') - - k, v = next(iter(pos_bounds.items())) - k = 'position' if k in ('p', 'pos') else k - - # Logic hoisted from path_to() - port_name = portspec[0] - port = self.pattern[port_name] - if port.rotation is None: - raise PortError(f'Port {port_name} has no rotation and cannot be used for trace_to()') - - if not numpy.isclose(port.rotation % (pi / 2), 0): - raise BuildError('trace_to was asked to route from non-manhattan port') - - is_horizontal = numpy.isclose(port.rotation % pi, 0) - if is_horizontal: - if k == 'y': - raise BuildError('Asked to trace to y-coordinate, but port is horizontal') - target = v - else: - if k == 'x': - raise BuildError('Asked to trace to x-coordinate, but port is vertical') - target = v - - x0, y0 = port.offset - if is_horizontal: - if numpy.sign(numpy.cos(port.rotation)) == numpy.sign(target - x0): - raise BuildError(f'trace_to routing to behind source port: x0={x0:g} to {target:g}') - length = numpy.abs(target - x0) - else: - if numpy.sign(numpy.sin(port.rotation)) == numpy.sign(target - y0): - raise BuildError(f'trace_to routing to behind source port: y0={y0:g} to {target:g}') - length = numpy.abs(target - y0) - - other_bounds = {bk: bv for bk, bv in bounds.items() if bk not in pos_bounds and bk != 'length'} - if 'length' in bounds and bounds['length'] is not None: - raise BuildError('Cannot specify both relative length and absolute position in trace_to()') - - return self._traceL(port_name, ccw, length, **other_bounds) - - # Bundle routing (delegate to trace which handles ell) - return self.trace(portspec, ccw, spacing=spacing, **bounds) - - def straight(self, portspec: str | Sequence[str], length: float | None = None, **bounds) -> Self: - """ Straight extension. Replaces `path(ccw=None)` and `path_to(ccw=None)` """ - return self.trace_to(portspec, None, length=length, **bounds) - - def bend(self, portspec: str | Sequence[str], ccw: SupportsBool, length: float | None = None, **bounds) -> Self: - """ Bend extension. Replaces `path(ccw=True/False)` and `path_to(ccw=True/False)` """ - return self.trace_to(portspec, ccw, length=length, **bounds) - - def ccw(self, portspec: str | Sequence[str], length: float | None = None, **bounds) -> Self: - """ Counter-clockwise bend extension. """ - return self.bend(portspec, True, length, **bounds) - - def cw(self, portspec: str | Sequence[str], length: float | None = None, **bounds) -> Self: - """ Clockwise bend extension. """ - return self.bend(portspec, False, length, **bounds) - - def jog(self, portspec: str | Sequence[str], offset: float, length: float | None = None, **bounds) -> Self: - """ Jog extension. Replaces `pathS`. """ - if isinstance(portspec, str): - portspec = [portspec] - - for port in portspec: - l_actual = length - if l_actual is None: - # TODO: use bounds to determine length? - raise BuildError('jog() currently requires a length') - self._traceS(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._traceU(port, offset, length=l_actual, **bounds) - return self - - def trace_into( - self, - portspec_src: str, - portspec_dst: str, - *, - out_ptype: str | None = None, - plug_destination: bool = True, - thru: str | None = None, - **kwargs, - ) -> Self: - """ - Create a "wire"/"waveguide" traveling between the ports `portspec_src` and - `portspec_dst`, and `plug` it into both (or just the source port). - - Only unambiguous scenarios are allowed: - - Straight connector between facing ports - - Single 90 degree bend - - Jog between facing ports - (jog is done as late as possible, i.e. only 2 L-shaped segments are used) - - By default, the destination's `pytpe` will be used as the `out_ptype` for the - wire, and the `portspec_dst` will be plugged (i.e. removed). - - Args: - portspec_src: The name of the starting port into which the wire will be plugged. - portspec_dst: The name of the destination port. - out_ptype: Passed to the pathing tool in order to specify the desired port type - to be generated at the destination end. If `None` (default), the destination - port's `ptype` will be used. - thru: If not `None`, the port by this name will be renamed to `portspec_src`. - This can be used when routing a signal through a pre-placed 2-port device. - - Returns: - self - - Raises: - PortError if either port does not have a specified rotation. - BuildError if an invalid port config is encountered: - - Non-manhattan ports - - U-bend - - Destination too close to (or behind) source - """ - if self._dead: - logger.error('Skipping trace_into() since device is dead') - return self - - port_src = self.pattern[portspec_src] - port_dst = self.pattern[portspec_dst] - - if out_ptype is None: - out_ptype = port_dst.ptype - - if port_src.rotation is None: - raise PortError(f'Port {portspec_src} has no rotation and cannot be used for trace_into()') - if port_dst.rotation is None: - raise PortError(f'Port {portspec_dst} has no rotation and cannot be used for trace_into()') - - if not numpy.isclose(port_src.rotation % (pi / 2), 0): - raise BuildError('trace_into was asked to route from non-manhattan port') - if not numpy.isclose(port_dst.rotation % (pi / 2), 0): - raise BuildError('trace_into was asked to route to non-manhattan port') - - src_is_horizontal = numpy.isclose(port_src.rotation % pi, 0) - dst_is_horizontal = numpy.isclose(port_dst.rotation % pi, 0) - xs, ys = port_src.offset - xd, yd = port_dst.offset - - angle = (port_dst.rotation - port_src.rotation) % (2 * pi) - - dst_extra_args = {'out_ptype': out_ptype} - if plug_destination: - dst_extra_args['plug_into'] = portspec_dst - - src_args = {**kwargs} - dst_args = {**src_args, **dst_extra_args} - if src_is_horizontal and not dst_is_horizontal: - # single bend should suffice - self.trace_to(portspec_src, angle > pi, x=xd, **src_args) - self.trace_to(portspec_src, None, y=yd, **dst_args) - elif dst_is_horizontal and not src_is_horizontal: - # single bend should suffice - self.trace_to(portspec_src, angle > pi, y=yd, **src_args) - self.trace_to(portspec_src, None, x=xd, **dst_args) - elif numpy.isclose(angle, pi): - if src_is_horizontal and ys == yd: - # straight connector - self.trace_to(portspec_src, None, x=xd, **dst_args) - elif not src_is_horizontal and xs == xd: - # straight connector - self.trace_to(portspec_src, None, y=yd, **dst_args) - else: - # S-bend - (travel, jog), _ = port_src.measure_travel(port_dst) - self.jog(portspec_src, -jog, -travel, **dst_args) - elif numpy.isclose(angle, 0): - # U-bend - (travel, jog), _ = port_src.measure_travel(port_dst) - self.uturn(portspec_src, -jog, length=-travel, **dst_args) - else: - raise BuildError(f"Don't know how to route ports with relative angle {angle}") - - if thru is not None: - self.rename_ports({thru: portspec_src}) - - return self - - def _uturn_fallback( - self, - tool: Tool, - portspec: str, - jog: float, - length: float, - in_ptype: str | None, - plug_into: str | None, - **kwargs, - ) -> bool: - """ - Attempt to perform a U-turn using two L-bends. - Returns True if successful, False if planL failed. - """ - # Fall back to drawing two L-bends - ccw = jog > 0 - kwargs_no_out = kwargs | {'out_ptype': None} - try: - # First, find R by planning a minimal L-bend. - # Use a large length to ensure we don't hit tool-specific minimum length constraints. - dummy_port, _ = tool.planL(ccw, 1e9, in_ptype=in_ptype, **kwargs_no_out) - R = abs(dummy_port.y) - - L1 = length + R - L2 = abs(jog) - R - - kwargs_plug = kwargs | {'plug_into': plug_into} - self._traceL(portspec, ccw, L1, **kwargs_no_out) - self._traceL(portspec, ccw, L2, **kwargs_plug) - except (BuildError, NotImplementedError): - return False - else: - return True - @abstractmethod - def _traceL( + def path( self, portspec: str, ccw: SupportsBool | None, @@ -380,7 +50,7 @@ class PatherMixin(PortList, metaclass=ABCMeta): pass @abstractmethod - def _traceS( + def pathS( self, portspec: str, length: float, @@ -391,33 +61,6 @@ class PatherMixin(PortList, metaclass=ABCMeta): ) -> Self: pass - @abstractmethod - def _traceU( - 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) - return self._traceL(*args, **kwargs) - - def pathS(self, *args, **kwargs) -> Self: - import warnings - warnings.warn("pathS() is deprecated; use jog() instead", DeprecationWarning, stacklevel=2) - return self._traceS(*args, **kwargs) - - def pathU(self, *args, **kwargs) -> Self: - import warnings - warnings.warn("pathU() is deprecated; use uturn() instead", DeprecationWarning, stacklevel=2) - return self._traceU(*args, **kwargs) - @abstractmethod def plug( self, @@ -433,11 +76,6 @@ class PatherMixin(PortList, metaclass=ABCMeta): ) -> Self: pass - @abstractmethod - def plugged(self, connections: dict[str, str]) -> Self: - """ Manual connection acknowledgment. """ - pass - def retool( self, tool: Tool, @@ -505,13 +143,88 @@ class PatherMixin(PortList, metaclass=ABCMeta): **kwargs, ) -> Self: """ - [DEPRECATED] use trace_to() instead. - """ - import warnings - warnings.warn("path_to() is deprecated; use trace_to() instead", DeprecationWarning, stacklevel=2) + Build a "wire"/"waveguide" extending from the port `portspec`, with the aim + of ending exactly at a target position. - bounds = {kk: vv for kk, vv in (('position', position), ('x', x), ('y', y)) if vv is not None} - return self.trace_to(portspec, ccw, plug_into=plug_into, **bounds, **kwargs) + The wire will travel so that the output port will be placed at exactly the target + position along the input port's axis. There can be an unspecified (tool-dependent) + offset in the perpendicular direction. The output port will be rotated (or not) + based on the `ccw` parameter. + + If using `RenderPather`, `RenderPather.render` must be called after all paths have been fully planned. + + Args: + portspec: The name of the port into which the wire will be plugged. + ccw: If `None`, the output should be along the same axis as the input. + Otherwise, cast to bool and turn counterclockwise if True + and clockwise otherwise. + position: The final port position, along the input's axis only. + (There may be a tool-dependent offset along the other axis.) + Only one of `position`, `x`, and `y` may be specified. + x: The final port position along the x axis. + `portspec` must refer to a horizontal port if `x` is passed, otherwise a + BuildError will be raised. + y: The final port position along the y axis. + `portspec` must refer to a vertical port if `y` is passed, otherwise a + BuildError will be raised. + plug_into: If not None, attempts to plug the wire's output port into the provided + port on `self`. + + Returns: + self + + Raises: + BuildError if `position`, `x`, or `y` is too close to fit the bend (if a bend + is present). + BuildError if `x` or `y` is specified but does not match the axis of `portspec`. + BuildError if more than one of `x`, `y`, and `position` is specified. + """ + if self._dead: + logger.error('Skipping path_to() since device is dead') + return self + + pos_count = sum(vv is not None for vv in (position, x, y)) + if pos_count > 1: + raise BuildError('Only one of `position`, `x`, and `y` may be specified at once') + if pos_count < 1: + raise BuildError('One of `position`, `x`, and `y` must be specified') + + port = self.pattern[portspec] + if port.rotation is None: + raise PortError(f'Port {portspec} has no rotation and cannot be used for path_to()') + + if not numpy.isclose(port.rotation % (pi / 2), 0): + raise BuildError('path_to was asked to route from non-manhattan port') + + is_horizontal = numpy.isclose(port.rotation % pi, 0) + if is_horizontal: + if y is not None: + raise BuildError('Asked to path to y-coordinate, but port is horizontal') + if position is None: + position = x + else: + if x is not None: + raise BuildError('Asked to path to x-coordinate, but port is vertical') + if position is None: + position = y + + x0, y0 = port.offset + if is_horizontal: + if numpy.sign(numpy.cos(port.rotation)) == numpy.sign(position - x0): + raise BuildError(f'path_to routing to behind source port: x0={x0:g} to {position:g}') + length = numpy.abs(position - x0) + else: + if numpy.sign(numpy.sin(port.rotation)) == numpy.sign(position - y0): + raise BuildError(f'path_to routing to behind source port: y0={y0:g} to {position:g}') + length = numpy.abs(position - y0) + + return self.path( + portspec, + ccw, + length, + plug_into = plug_into, + **kwargs, + ) def path_into( self, @@ -524,19 +237,100 @@ class PatherMixin(PortList, metaclass=ABCMeta): **kwargs, ) -> Self: """ - [DEPRECATED] use trace_into() instead. - """ - import warnings - warnings.warn("path_into() is deprecated; use trace_into() instead", DeprecationWarning, stacklevel=2) + Create a "wire"/"waveguide" traveling between the ports `portspec_src` and + `portspec_dst`, and `plug` it into both (or just the source port). - return self.trace_into( - portspec_src, - portspec_dst, - out_ptype = out_ptype, - plug_destination = plug_destination, - thru = thru, - **kwargs, - ) + Only unambiguous scenarios are allowed: + - Straight connector between facing ports + - Single 90 degree bend + - Jog between facing ports + (jog is done as late as possible, i.e. only 2 L-shaped segments are used) + + By default, the destination's `pytpe` will be used as the `out_ptype` for the + wire, and the `portspec_dst` will be plugged (i.e. removed). + + If using `RenderPather`, `RenderPather.render` must be called after all paths have been fully planned. + + Args: + portspec_src: The name of the starting port into which the wire will be plugged. + portspec_dst: The name of the destination port. + out_ptype: Passed to the pathing tool in order to specify the desired port type + to be generated at the destination end. If `None` (default), the destination + port's `ptype` will be used. + thru: If not `None`, the port by this name will be rename to `portspec_src`. + This can be used when routing a signal through a pre-placed 2-port device. + + Returns: + self + + Raises: + PortError if either port does not have a specified rotation. + BuildError if and invalid port config is encountered: + - Non-manhattan ports + - U-bend + - Destination too close to (or behind) source + """ + if self._dead: + logger.error('Skipping path_into() since device is dead') + return self + + port_src = self.pattern[portspec_src] + port_dst = self.pattern[portspec_dst] + + if out_ptype is None: + out_ptype = port_dst.ptype + + if port_src.rotation is None: + raise PortError(f'Port {portspec_src} has no rotation and cannot be used for path_into()') + if port_dst.rotation is None: + raise PortError(f'Port {portspec_dst} has no rotation and cannot be used for path_into()') + + if not numpy.isclose(port_src.rotation % (pi / 2), 0): + raise BuildError('path_into was asked to route from non-manhattan port') + if not numpy.isclose(port_dst.rotation % (pi / 2), 0): + raise BuildError('path_into was asked to route to non-manhattan port') + + src_is_horizontal = numpy.isclose(port_src.rotation % pi, 0) + dst_is_horizontal = numpy.isclose(port_dst.rotation % pi, 0) + xs, ys = port_src.offset + xd, yd = port_dst.offset + + angle = (port_dst.rotation - port_src.rotation) % (2 * pi) + + dst_extra_args = {'out_ptype': out_ptype} + if plug_destination: + dst_extra_args['plug_into'] = portspec_dst + + src_args = {**kwargs} + dst_args = {**src_args, **dst_extra_args} + if src_is_horizontal and not dst_is_horizontal: + # single bend should suffice + self.path_to(portspec_src, angle > pi, x=xd, **src_args) + self.path_to(portspec_src, None, y=yd, **dst_args) + elif dst_is_horizontal and not src_is_horizontal: + # single bend should suffice + self.path_to(portspec_src, angle > pi, y=yd, **src_args) + self.path_to(portspec_src, None, x=xd, **dst_args) + elif numpy.isclose(angle, pi): + if src_is_horizontal and ys == yd: + # straight connector + self.path_to(portspec_src, None, x=xd, **dst_args) + elif not src_is_horizontal and xs == xd: + # straight connector + self.path_to(portspec_src, None, y=yd, **dst_args) + else: + # S-bend, delegate to implementations + (travel, jog), _ = port_src.measure_travel(port_dst) + self.pathS(portspec_src, -travel, -jog, **dst_args) + elif numpy.isclose(angle, 0): + raise BuildError('Don\'t know how to route a U-bend yet (TODO)!') + else: + raise BuildError(f'Don\'t know how to route ports with relative angle {angle}') + + if thru is not None: + self.rename_ports({thru: portspec_src}) + + return self def mpath( self, @@ -548,12 +342,109 @@ class PatherMixin(PortList, metaclass=ABCMeta): **kwargs, ) -> Self: """ - [DEPRECATED] use trace() or trace_to() instead. - """ - import warnings - warnings.warn("mpath() is deprecated; use trace() or trace_to() instead", DeprecationWarning, stacklevel=2) + `mpath` is a superset of `path` and `path_to` which can act on bundles or buses + of "wires or "waveguides". - return self.trace(portspec, ccw, spacing=spacing, set_rotation=set_rotation, **kwargs) + The wires will travel so that the output ports will be placed at well-defined + locations along the axis of their input ports, but may have arbitrary (tool- + dependent) offsets in the perpendicular direction. + + If `ccw` is not `None`, the wire bundle will turn 90 degres in either the + clockwise (`ccw=False`) or counter-clockwise (`ccw=True`) direction. Within the + bundle, the center-to-center wire spacings after the turn are set by `spacing`, + which is required when `ccw` is not `None`. The final position of bundle as a + whole can be set in a number of ways: + + =A>---------------------------V turn direction: `ccw=False` + =B>-------------V | + =C>-----------------------V | + =D=>----------------V | + | + + x---x---x---x `spacing` (can be scalar or array) + + <--------------> `emin=` + <------> `bound_type='min_past_furthest', bound=` + <--------------------------------> `emax=` + x `pmin=` + x `pmax=` + + - `emin=`, equivalent to `bound_type='min_extension', bound=` + The total extension value for the furthest-out port (B in the diagram). + - `emax=`, equivalent to `bound_type='max_extension', bound=`: + The total extension value for the closest-in port (C in the diagram). + - `pmin=`, equivalent to `xmin=`, `ymin=`, or `bound_type='min_position', bound=`: + The coordinate of the innermost bend (D's bend). + The x/y versions throw an error if they do not match the port axis (for debug) + - `pmax=`, `xmax=`, `ymax=`, or `bound_type='max_position', bound=`: + The coordinate of the outermost bend (A's bend). + The x/y versions throw an error if they do not match the port axis (for debug) + - `bound_type='min_past_furthest', bound=`: + The distance between furthest out-port (B) and the innermost bend (D's bend). + + If `ccw=None`, final output positions (along the input axis) of all wires will be + identical (i.e. wires will all be cut off evenly). In this case, `spacing=None` is + required. In this case, `emin=` and `emax=` are equivalent to each other, and + `pmin=`, `pmax=`, `xmin=`, etc. are also equivalent to each other. + + If using `RenderPather`, `RenderPather.render` must be called after all paths have been fully planned. + + Args: + portspec: The names of the ports which are to be routed. + ccw: If `None`, the outputs should be along the same axis as the inputs. + Otherwise, cast to bool and turn 90 degrees counterclockwise if `True` + and clockwise otherwise. + spacing: Center-to-center distance between output ports along the input port's axis. + Must be provided if (and only if) `ccw` is not `None`. + set_rotation: If the provided ports have `rotation=None`, this can be used + to set a rotation for them. + + Returns: + self + + Raises: + BuildError if the implied length for any wire is too close to fit the bend + (if a bend is requested). + BuildError if `xmin`/`xmax` or `ymin`/`ymax` is specified but does not + match the axis of `portspec`. + BuildError if an incorrect bound type or spacing is specified. + """ + if self._dead: + logger.error('Skipping mpath() since device is dead') + return self + + bound_types = set() + if 'bound_type' in kwargs: + bound_types.add(kwargs.pop('bound_type')) + bound = kwargs.pop('bound') + for bt in ('emin', 'emax', 'pmin', 'pmax', 'xmin', 'xmax', 'ymin', 'ymax', 'min_past_furthest'): + if bt in kwargs: + bound_types.add(bt) + bound = kwargs.pop(bt) + + if not bound_types: + raise BuildError('No bound type specified for mpath') + if len(bound_types) > 1: + raise BuildError(f'Too many bound types specified for mpath: {bound_types}') + bound_type = tuple(bound_types)[0] + + if isinstance(portspec, str): + portspec = [portspec] + ports = self.pattern[tuple(portspec)] + + extensions = ell(ports, ccw, spacing=spacing, bound=bound, bound_type=bound_type, set_rotation=set_rotation) + + #if container: + # assert not getattr(self, 'render'), 'Containers not implemented for RenderPather' + # bld = self.interface(source=ports, library=self.library, tools=self.tools) + # for port_name, length in extensions.items(): + # bld.path(port_name, ccw, length, **kwargs) + # self.library[container] = bld.pattern + # self.plug(Abstract(container, bld.pattern.ports), {sp: 'in_' + sp for sp in ports}) # TODO safe to use 'in_'? + #else: + for port_name, length in extensions.items(): + self.path(port_name, ccw, length, **kwargs) + return self # TODO def bus_join()? @@ -597,42 +488,61 @@ class PortPather: with self.pather.toolctx(tool, keys=self.ports): yield self - def trace(self, ccw: SupportsBool | None, length: float | None = None, **kwargs) -> Self: - self.pather.trace(self.ports, ccw, length, **kwargs) - return self - - def trace_to(self, ccw: SupportsBool | None, **kwargs) -> Self: - self.pather.trace_to(self.ports, ccw, **kwargs) - return self - - def straight(self, length: float | None = None, **kwargs) -> Self: - self.pather.straight(self.ports, length, **kwargs) - return self - - def bend(self, ccw: SupportsBool, length: float | None = None, **kwargs) -> Self: - self.pather.bend(self.ports, ccw, length, **kwargs) - return self - - def ccw(self, length: float | None = None, **kwargs) -> Self: - self.pather.ccw(self.ports, length, **kwargs) - return self - - def cw(self, length: float | None = None, **kwargs) -> Self: - self.pather.cw(self.ports, length, **kwargs) - return self - - def jog(self, offset: float, length: float | None = None, **kwargs) -> Self: - self.pather.jog(self.ports, offset, length, **kwargs) - return self - - def uturn(self, offset: float, length: float | None = None, **kwargs) -> Self: - self.pather.uturn(self.ports, offset, length, **kwargs) - return self - - def trace_into(self, target_port: str, **kwargs) -> Self: + def path(self, *args, **kwargs) -> Self: if len(self.ports) > 1: - raise BuildError(f'Unable use implicit trace_into() with {len(self.ports)} (>1) ports.') - self.pather.trace_into(self.ports[0], target_port, **kwargs) + logger.warning('Use path_each() when pathing multiple ports independently') + for port in self.ports: + self.pather.path(port, *args, **kwargs) + return self + + def path_each(self, *args, **kwargs) -> Self: + for port in self.ports: + self.pather.path(port, *args, **kwargs) + return self + + def pathS(self, *args, **kwargs) -> Self: + if len(self.ports) > 1: + logger.warning('Use pathS_each() when pathing multiple ports independently') + for port in self.ports: + self.pather.pathS(port, *args, **kwargs) + return self + + def pathS_each(self, *args, **kwargs) -> Self: + for port in self.ports: + self.pather.pathS(port, *args, **kwargs) + return self + + def path_to(self, *args, **kwargs) -> Self: + if len(self.ports) > 1: + logger.warning('Use path_each_to() when pathing multiple ports independently') + for port in self.ports: + self.pather.path_to(port, *args, **kwargs) + return self + + def path_each_to(self, *args, **kwargs) -> Self: + for port in self.ports: + self.pather.path_to(port, *args, **kwargs) + return self + + def mpath(self, *args, **kwargs) -> Self: + self.pather.mpath(self.ports, *args, **kwargs) + return self + + def path_into(self, *args, **kwargs) -> Self: + """ Path_into, using the current port as the source """ + if len(self.ports) > 1: + raise BuildError(f'Unable use implicit path_into() with {len(self.ports)} (>1) ports.') + self.pather.path_into(self.ports[0], *args, **kwargs) + return self + + def path_from(self, *args, **kwargs) -> Self: + """ Path_into, using the current port as the destination """ + if len(self.ports) > 1: + raise BuildError(f'Unable use implicit path_from() with {len(self.ports)} (>1) ports.') + thru = kwargs.pop('thru', None) + self.pather.path_into(args[0], self.ports[0], *args[1:], **kwargs) + if thru is not None: + self.rename_from(thru) return self def plug( @@ -648,13 +558,10 @@ class PortPather: self.pather.plug(other, {self.ports[0]: other_port}, *args, **kwargs) return self - def plugged(self, other_port: str | Mapping[str, str]) -> Self: - if isinstance(other_port, Mapping): - self.pather.plugged(dict(other_port)) - elif len(self.ports) > 1: + def plugged(self, other_port: str) -> Self: + if len(self.ports) > 1: raise BuildError(f'Unable use implicit plugged() with {len(self.ports)} (>1) ports.') - else: - self.pather.plugged({self.ports[0]: other_port}) + self.pather.plugged({self.ports[0]: other_port}) return self # @@ -662,91 +569,95 @@ class PortPather: # def set_ptype(self, ptype: str) -> Self: for port in self.ports: - self.pather.pattern[port].set_ptype(ptype) + self.pather[port].set_ptype(ptype) return self def translate(self, *args, **kwargs) -> Self: for port in self.ports: - self.pather.pattern[port].translate(*args, **kwargs) + self.pather[port].translate(*args, **kwargs) return self def mirror(self, *args, **kwargs) -> Self: for port in self.ports: - self.pather.pattern[port].mirror(*args, **kwargs) + self.pather[port].mirror(*args, **kwargs) return self def rotate(self, rotation: float) -> Self: for port in self.ports: - self.pather.pattern[port].rotate(rotation) + self.pather[port].rotate(rotation) return self def set_rotation(self, rotation: float | None) -> Self: for port in self.ports: - self.pather.pattern[port].set_rotation(rotation) + self.pather[port].set_rotation(rotation) return self - def rename(self, name: str | Mapping[str, str | None]) -> Self: - """ Rename active ports. Replaces `rename_to`. """ - name_map: dict[str, str | None] - if isinstance(name, str): - if len(self.ports) > 1: - raise BuildError('Use a mapping to rename >1 port') - name_map = {self.ports[0]: name} - else: - name_map = dict(name) + def rename_to(self, new_name: str) -> Self: + if len(self.ports) > 1: + BuildError('Use rename_ports() for >1 port') + self.pather.rename_ports({self.ports[0]: new_name}) + self.ports[0] = new_name + return self + + def rename_from(self, old_name: str) -> Self: + if len(self.ports) > 1: + BuildError('Use rename_ports() for >1 port') + self.pather.rename_ports({old_name: self.ports[0]}) + return self + + def rename_ports(self, name_map: dict[str, str | None]) -> Self: self.pather.rename_ports(name_map) self.ports = [mm for mm in [name_map.get(pp, pp) for pp in self.ports] if mm is not None] return self - def select(self, ports: str | Iterable[str]) -> Self: - """ Add ports to the selection. Replaces `add_ports`. """ - if isinstance(ports, str): - ports = [ports] - for port in ports: - if port not in self.ports: - self.ports.append(port) + def add_ports(self, ports: Iterable[str]) -> Self: + ports = list(ports) + conflicts = set(ports) & set(self.ports) + if conflicts: + raise BuildError(f'ports {conflicts} already selected') + self.ports += ports return self - def deselect(self, ports: str | Iterable[str]) -> Self: - """ Remove ports from the selection. Replaces `drop_port`. """ - if isinstance(ports, str): - ports = [ports] - ports_set = set(ports) - self.ports = [pp for pp in self.ports if pp not in ports_set] - return self - - def mark(self, name: str | Mapping[str, str]) -> Self: - """ Bookmark current port(s). Replaces `save_copy`. """ - name_map: Mapping[str, str] - if isinstance(name, str): - if len(self.ports) > 1: - raise BuildError('Use a mapping to mark >1 port') - name_map = {self.ports[0]: name} + def add_port(self, port: str, index: int | None = None) -> Self: + if port in self.ports: + raise BuildError(f'{port=} already selected') + if index is not None: + self.ports.insert(index, port) else: - name_map = name - for src, dst in name_map.items(): - self.pather.pattern.ports[dst] = self.pather.pattern[src].copy() + self.ports.append(port) return self - def fork(self, name: str | Mapping[str, str]) -> Self: - """ Split and follow new name. Replaces `into_copy`. """ - name_map: Mapping[str, str] - if isinstance(name, str): - if len(self.ports) > 1: - raise BuildError('Use a mapping to fork >1 port') - name_map = {self.ports[0]: name} - else: - name_map = name - for src, dst in name_map.items(): - self.pather.pattern.ports[dst] = self.pather.pattern[src].copy() - self.ports = [(dst if pp == src else pp) for pp in self.ports] + def drop_port(self, port: str) -> Self: + if port not in self.ports: + raise BuildError(f'{port=} already not selected') + self.ports = [pp for pp in self.ports if pp != port] return self - def drop(self) -> Self: - """ Remove selected ports from the pattern and the PortPather. Replaces `delete(None)`. """ - for pp in self.ports: - del self.pather.pattern.ports[pp] - self.ports = [] + def into_copy(self, new_name: str, src: str | None = None) -> Self: + """ Copy a port and replace it with the copy """ + if not self.ports: + raise BuildError('Have no ports to copy') + if len(self.ports) == 1: + src = self.ports[0] + elif src is None: + raise BuildError('Must specify src when >1 port is available') + if src not in self.ports: + raise BuildError(f'{src=} not available') + self.pather.ports[new_name] = self.pather[src].copy() + self.ports = [(new_name if pp == src else pp) for pp in self.ports] + return self + + def save_copy(self, new_name: str, src: str | None = None) -> Self: + """ Copy a port and but keep using the original """ + if not self.ports: + raise BuildError('Have no ports to copy') + if len(self.ports) == 1: + src = self.ports[0] + elif src is None: + raise BuildError('Must specify src when >1 port is available') + if src not in self.ports: + raise BuildError(f'{src=} not available') + self.pather.ports[new_name] = self.pather[src].copy() return self @overload @@ -757,8 +668,10 @@ class PortPather: def delete(self, name: str | None = None) -> Self | None: if name is None: - self.drop() + for pp in self.ports: + del self.pather.ports[pp] return None - del self.pather.pattern.ports[name] + del self.pather.ports[name] self.ports = [pp for pp in self.ports if pp != name] return self + diff --git a/masque/builder/renderpather.py b/masque/builder/renderpather.py index 0e16c7b..fae975a 100644 --- a/masque/builder/renderpather.py +++ b/masque/builder/renderpather.py @@ -9,9 +9,8 @@ from collections import defaultdict from functools import wraps from pprint import pformat -import numpy from numpy import pi -from numpy.typing import ArrayLike, NDArray +from numpy.typing import ArrayLike from ..pattern import Pattern from ..library import ILibrary, TreeView @@ -28,7 +27,7 @@ logger = logging.getLogger(__name__) class RenderPather(PatherMixin): """ - `RenderPather` is an alternative to `Pather` which uses the `trace`/`trace_to` + `RenderPather` is an alternative to `Pather` which uses the `path`/`path_to`/`mpath` functions to plan out wire paths without incrementally generating the layout. Instead, it waits until `render` is called, at which point it draws all the planned segments simultaneously. This allows it to e.g. draw each wire using a single `Path` or @@ -97,7 +96,7 @@ class RenderPather(PatherMixin): in which case it is interpreted as a name in `library`. Default `None` (no ports). tools: A mapping of {port: tool} which specifies what `Tool` should be used - to generate waveguide or wire segments when `trace`/`trace_to` + to generate waveguide or wire segments when `path`/`path_to`/`mpath` are called. Relies on `Tool.planL` and `Tool.render` implementations. name: If specified, `library[name]` is set to `self.pattern`. """ @@ -150,7 +149,7 @@ class RenderPather(PatherMixin): and to which the new one should be added (if named). If not provided, `source.library` must exist and will be used. tools: `Tool`s which will be used by the pather for generating new wires - or waveguides (via `trace`/`trace_to`). + or waveguides (via `path`/`path_to`/`mpath`). in_prefix: Prepended to port names for newly-created ports with reversed directions compared to the current device. out_prefix: Prepended to port names for ports which are directly @@ -377,67 +376,7 @@ class RenderPather(PatherMixin): PortList.plugged(self, connections) return self - def _traceU( - 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 _traceU() 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 _traceL( + def path( self, portspec: str, ccw: SupportsBool | None, @@ -480,7 +419,7 @@ class RenderPather(PatherMixin): LibraryError if no valid name could be picked for the pattern. """ if self._dead: - logger.warning('Skipping geometry for _traceL() since device is dead') + logger.warning('Skipping geometry for path() since device is dead') port = self.pattern[portspec] in_ptype = port.ptype @@ -520,7 +459,7 @@ class RenderPather(PatherMixin): return self - def _traceS( + def pathS( self, portspec: str, length: float, @@ -564,7 +503,7 @@ class RenderPather(PatherMixin): LibraryError if no valid name could be picked for the pattern. """ if self._dead: - logger.warning('Skipping geometry for _traceS() since device is dead') + logger.warning('Skipping geometry for pathS() since device is dead') port = self.pattern[portspec] in_ptype = port.ptype @@ -587,8 +526,8 @@ class RenderPather(PatherMixin): jog1 = Port((0, 0), 0).measure_travel(t_port1)[0][1] kwargs_plug = kwargs | {'plug_into': plug_into} - self._traceL(portspec, ccw0, length - abs(jog1), **kwargs_no_out) - self._traceL(portspec, not ccw0, abs(jog - jog0), **kwargs_plug) + self.path(portspec, ccw0, length - abs(jog1), **kwargs_no_out) + self.path(portspec, not ccw0, abs(jog - jog0), **kwargs_plug) except (BuildError, NotImplementedError): if not self._dead: raise @@ -624,7 +563,7 @@ class RenderPather(PatherMixin): append: bool = True, ) -> Self: """ - Generate the geometry which has been planned out with `trace`/`trace_to`/etc. + Generate the geometry which has been planned out with `path`/`path_to`/etc. Args: append: If `True`, the rendered geometry will be directly appended to @@ -640,54 +579,22 @@ class RenderPather(PatherMixin): def render_batch(portspec: str, batch: list[RenderStep], append: bool) -> None: assert batch[0].tool is not None - # Tools render in local space (first port at 0,0, rotation 0). - tree = batch[0].tool.render(batch, port_names=tool_port_names) - - actual_in, actual_out = tool_port_names - name = lib << tree - - # To plug the segment at its intended location, we create a - # 'stationary' port in our temporary pattern that matches - # the batch's planned start. - if portspec in pat.ports: - del pat.ports[portspec] - - stationary_port = batch[0].start_port.copy() - pat.ports[portspec] = stationary_port - + name = lib << batch[0].tool.render(batch, port_names=tool_port_names) + pat.ports[portspec] = batch[0].start_port.copy() if append: - # pat.plug() translates and rotates the tool's local output to the start port. - pat.plug(lib[name], {portspec: actual_in}, append=append) - del lib[name] + pat.plug(lib[name], {portspec: tool_port_names[0]}, append=append) + del lib[name] # NOTE if the rendered pattern has refs, those are now in `pat` but not flattened else: - pat.plug(lib.abstract(name), {portspec: actual_in}, append=append) - - # Rename output back to portspec for the next batch. - if portspec not in pat.ports and actual_out in pat.ports: - pat.rename_ports({actual_out: portspec}, overwrite=True) + pat.plug(lib.abstract(name), {portspec: tool_port_names[0]}, append=append) for portspec, steps in self.paths.items(): - if not steps: - continue - batch: list[RenderStep] = [] - # Initialize continuity check with the start of the entire path. - prev_end = steps[0].start_port - for step in steps: appendable_op = step.opcode in ('L', 'S', 'U') same_tool = batch and step.tool == batch[0].tool - # Check continuity with tolerance - offsets_match = numpy.allclose(step.start_port.offset, prev_end.offset) - rotations_match = (step.start_port.rotation is None and prev_end.rotation is None) or ( - step.start_port.rotation is not None and prev_end.rotation is not None and - numpy.isclose(step.start_port.rotation, prev_end.rotation) - ) - continuous = offsets_match and rotations_match - # If we can't continue a batch, render it - if batch and (not appendable_op or not same_tool or not continuous): + if batch and (not appendable_op or not same_tool): render_batch(portspec, batch, append) batch = [] @@ -696,14 +603,8 @@ class RenderPather(PatherMixin): batch.append(step) # Opcodes which break the batch go below this line - if not appendable_op: - if portspec in pat.ports: - del pat.ports[portspec] - # Plugged ports should be tracked - if step.opcode == 'P' and portspec in pat.ports: - del pat.ports[portspec] - - prev_end = step.end_port + if not appendable_op and portspec in pat.ports: + del pat.ports[portspec] #If the last batch didn't end yet if batch: @@ -725,11 +626,7 @@ class RenderPather(PatherMixin): Returns: self """ - offset_arr: NDArray[numpy.float64] = numpy.asarray(offset) - self.pattern.translate_elements(offset_arr) - for steps in self.paths.values(): - for i, step in enumerate(steps): - steps[i] = step.transformed(offset_arr, 0, numpy.zeros(2)) + self.pattern.translate_elements(offset) return self def rotate_around(self, pivot: ArrayLike, angle: float) -> Self: @@ -743,11 +640,7 @@ class RenderPather(PatherMixin): Returns: self """ - pivot_arr: NDArray[numpy.float64] = numpy.asarray(pivot) - self.pattern.rotate_around(pivot_arr, angle) - for steps in self.paths.values(): - for i, step in enumerate(steps): - steps[i] = step.transformed(numpy.zeros(2), angle, pivot_arr) + self.pattern.rotate_around(pivot, angle) return self def mirror(self, axis: int) -> Self: @@ -761,9 +654,6 @@ class RenderPather(PatherMixin): self """ self.pattern.mirror(axis) - for steps in self.paths.values(): - for i, step in enumerate(steps): - steps[i] = step.mirrored(axis) return self def set_dead(self) -> Self: @@ -803,3 +693,4 @@ class RenderPather(PatherMixin): def rect(self, *args, **kwargs) -> Self: self.pattern.rect(*args, **kwargs) return self + diff --git a/masque/builder/tools.py b/masque/builder/tools.py index f8a72fb..27bc27e 100644 --- a/masque/builder/tools.py +++ b/masque/builder/tools.py @@ -47,43 +47,6 @@ class RenderStep: if self.opcode != 'P' and self.tool is None: raise BuildError('Got tool=None but the opcode is not "P"') - def transformed(self, translation: NDArray[numpy.float64], rotation: float, pivot: NDArray[numpy.float64]) -> 'RenderStep': - """ - Return a new RenderStep with transformed start and end ports. - """ - new_start = self.start_port.copy() - new_end = self.end_port.copy() - - for pp in (new_start, new_end): - pp.rotate_around(pivot, rotation) - pp.translate(translation) - - return RenderStep( - opcode = self.opcode, - tool = self.tool, - start_port = new_start, - end_port = new_end, - data = self.data, - ) - - def mirrored(self, axis: int) -> 'RenderStep': - """ - Return a new RenderStep with mirrored start and end ports. - """ - new_start = self.start_port.copy() - new_end = self.end_port.copy() - - new_start.mirror(axis) - new_end.mirror(axis) - - return RenderStep( - opcode = self.opcode, - tool = self.tool, - start_port = new_start, - end_port = new_end, - data = self.data, - ) - class Tool: """ @@ -93,7 +56,7 @@ class Tool: unimplemented (e.g. in cases where they don't make sense or the required components are impractical or unavailable). """ - def traceL( + def path( self, ccw: SupportsBool | None, length: float, @@ -136,9 +99,9 @@ class Tool: Raises: BuildError if an impossible or unsupported geometry is requested. """ - raise NotImplementedError(f'traceL() not implemented for {type(self)}') + raise NotImplementedError(f'path() not implemented for {type(self)}') - def traceS( + def pathS( self, length: float, jog: float, @@ -178,7 +141,7 @@ class Tool: Raises: BuildError if an impossible or unsupported geometry is requested. """ - raise NotImplementedError(f'traceS() not implemented for {type(self)}') + raise NotImplementedError(f'path() not implemented for {type(self)}') def planL( self, @@ -260,46 +223,6 @@ class Tool: """ raise NotImplementedError(f'planS() not implemented for {type(self)}') - def traceU( - 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: - """ - 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'traceU() not implemented for {type(self)}') - def planU( self, jog: float, @@ -467,7 +390,7 @@ class SimpleTool(Tool, metaclass=ABCMeta): pat.plug(bend, {port_names[1]: inport}, mirrored=mirrored) return tree - def traceL( + def path( self, ccw: SupportsBool | None, length: float, @@ -484,7 +407,7 @@ class SimpleTool(Tool, metaclass=ABCMeta): out_ptype = out_ptype, ) - tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL') + tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path') pat.add_port_pair(names=port_names, ptype='unk' if in_ptype is None else in_ptype) self._renderL(data=data, tree=tree, port_names=port_names, straight_kwargs=kwargs) return tree @@ -497,7 +420,7 @@ class SimpleTool(Tool, metaclass=ABCMeta): **kwargs, ) -> ILibrary: - tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL') + tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path') pat.add_port_pair(names=(port_names[0], port_names[1])) for step in batch: @@ -598,14 +521,6 @@ 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 """ @@ -774,7 +689,7 @@ class AutoTool(Tool, metaclass=ABCMeta): pat.plug(data.out_transition.abstract, {port_names[1]: data.out_transition.our_port_name}) return tree - def traceL( + def path( self, ccw: SupportsBool | None, length: float, @@ -791,7 +706,7 @@ class AutoTool(Tool, metaclass=ABCMeta): out_ptype = out_ptype, ) - tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL') + tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path') pat.add_port_pair(names=port_names, ptype='unk' if in_ptype is None else in_ptype) self._renderL(data=data, tree=tree, port_names=port_names, straight_kwargs=kwargs) return tree @@ -930,7 +845,7 @@ class AutoTool(Tool, metaclass=ABCMeta): pat.plug(data.out_transition.abstract, {port_names[1]: data.out_transition.our_port_name}) return tree - def traceS( + def pathS( self, length: float, jog: float, @@ -946,118 +861,11 @@ class AutoTool(Tool, metaclass=ABCMeta): in_ptype = in_ptype, out_ptype = out_ptype, ) - tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceS') + 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 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 traceU( - 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 + 'traceU') - 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], @@ -1066,7 +874,7 @@ class AutoTool(Tool, metaclass=ABCMeta): **kwargs, ) -> ILibrary: - tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL') + tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path') pat.add_port_pair(names=(port_names[0], port_names[1])) for step in batch: @@ -1075,8 +883,6 @@ 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 @@ -1107,7 +913,7 @@ class PathTool(Tool, metaclass=ABCMeta): # self.width = width # self.ptype: str - def traceL( + def path( self, ccw: SupportsBool | None, length: float, @@ -1124,7 +930,7 @@ class PathTool(Tool, metaclass=ABCMeta): out_ptype=out_ptype, ) - tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL') + tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path') pat.path(layer=self.layer, width=self.width, vertices=[(0, 0), (length, 0)]) if ccw is None: @@ -1185,44 +991,29 @@ class PathTool(Tool, metaclass=ABCMeta): **kwargs, # noqa: ARG002 (unused) ) -> ILibrary: - # Transform the batch so the first port is local (at 0,0) but retains its global rotation. - # This allows the path to be rendered with its original orientation, simplified by - # translation to the origin. RenderPather.render will handle the final placement - # (including rotation alignment) via `pat.plug`. - first_port = batch[0].start_port - translation = -first_port.offset - rotation = 0 - pivot = first_port.offset - - # Localize the batch for rendering - local_batch = [step.transformed(translation, rotation, pivot) for step in batch] - - path_vertices = [local_batch[0].start_port.offset] - for step in local_batch: + path_vertices = [batch[0].start_port.offset] + for step in batch: assert step.tool == self 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': - - length, _ = step.data + length, bend_run = step.data dxy = rotation_matrix_2d(port_rot + pi) @ (length, 0) + #path_vertices.append(step.start_port.offset) path_vertices.append(step.start_port.offset + dxy) else: raise BuildError(f'Unrecognized opcode "{step.opcode}"') - # Check if the last vertex added is already at the end port location - if not numpy.allclose(path_vertices[-1], local_batch[-1].end_port.offset): + if (path_vertices[-1] != batch[-1].end_port.offset).any(): # If the path ends in a bend, we need to add the final vertex - path_vertices.append(local_batch[-1].end_port.offset) + path_vertices.append(batch[-1].end_port.offset) - tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL') + tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path') pat.path(layer=self.layer, width=self.width, vertices=path_vertices) pat.ports = { - port_names[0]: local_batch[0].start_port.copy().rotate(pi), - port_names[1]: local_batch[-1].end_port.copy().rotate(pi), + port_names[0]: batch[0].start_port.copy().rotate(pi), + port_names[1]: batch[-1].end_port.copy().rotate(pi), } return tree diff --git a/masque/test/test_advanced_routing.py b/masque/test/test_advanced_routing.py index 7033159..5afcc21 100644 --- a/masque/test/test_advanced_routing.py +++ b/masque/test/test_advanced_routing.py @@ -18,23 +18,23 @@ def advanced_pather() -> tuple[Pather, PathTool, Library]: def test_path_into_straight(advanced_pather: tuple[Pather, PathTool, Library]) -> None: - p, _tool, _lib = advanced_pather + p, tool, lib = advanced_pather # Facing ports p.ports["src"] = Port((0, 0), 0, ptype="wire") # Facing East (into device) # Forward (+pi relative to port) is West (-x). # Put destination at (-20, 0) pointing East (pi). p.ports["dst"] = Port((-20, 0), pi, ptype="wire") - p.trace_into("src", "dst") + p.path_into("src", "dst") assert "src" not in p.ports assert "dst" not in p.ports - # Pather._traceL adds a Reference to the generated pattern + # Pather.path adds a Reference to the generated pattern assert len(p.pattern.refs) == 1 def test_path_into_bend(advanced_pather: tuple[Pather, PathTool, Library]) -> None: - p, _tool, _lib = advanced_pather + p, tool, lib = advanced_pather # Source at (0,0) rot 0 (facing East). Forward is West (-x). p.ports["src"] = Port((0, 0), 0, ptype="wire") # Destination at (-20, -20) rot pi (facing West). Forward is East (+x). @@ -43,7 +43,7 @@ def test_path_into_bend(advanced_pather: tuple[Pather, PathTool, Library]) -> No # Forward for South is North (+y). p.ports["dst"] = Port((-20, -20), 3 * pi / 2, ptype="wire") - p.trace_into("src", "dst") + p.path_into("src", "dst") assert "src" not in p.ports assert "dst" not in p.ports @@ -52,24 +52,35 @@ def test_path_into_bend(advanced_pather: tuple[Pather, PathTool, Library]) -> No def test_path_into_sbend(advanced_pather: tuple[Pather, PathTool, Library]) -> None: - p, _tool, _lib = advanced_pather + p, tool, lib = advanced_pather # Facing but offset ports p.ports["src"] = Port((0, 0), 0, ptype="wire") # Forward is West (-x) p.ports["dst"] = Port((-20, -10), pi, ptype="wire") # Facing East (rot pi) - p.trace_into("src", "dst") + p.path_into("src", "dst") + + assert "src" not in p.ports + assert "dst" not in p.ports + + +def test_path_from(advanced_pather: tuple[Pather, PathTool, Library]) -> None: + p, tool, lib = advanced_pather + p.ports["src"] = Port((0, 0), 0, ptype="wire") + p.ports["dst"] = Port((-20, 0), pi, ptype="wire") + + p.at("dst").path_from("src") assert "src" not in p.ports assert "dst" not in p.ports def test_path_into_thru(advanced_pather: tuple[Pather, PathTool, Library]) -> None: - p, _tool, _lib = advanced_pather + p, tool, lib = advanced_pather p.ports["src"] = Port((0, 0), 0, ptype="wire") p.ports["dst"] = Port((-20, 0), pi, ptype="wire") p.ports["other"] = Port((10, 10), 0) - p.trace_into("src", "dst", thru="other") + p.path_into("src", "dst", thru="other") assert "src" in p.ports assert_equal(p.ports["src"].offset, [10, 10]) diff --git a/masque/test/test_autotool.py b/masque/test/test_autotool.py index e03994e..5686193 100644 --- a/masque/test/test_autotool.py +++ b/masque/test/test_autotool.py @@ -54,11 +54,11 @@ def autotool_setup() -> tuple[Pather, AutoTool, Library]: def test_autotool_transition(autotool_setup: tuple[Pather, AutoTool, Library]) -> None: - p, _tool, _lib = autotool_setup + p, tool, lib = autotool_setup # Route m1 from an m2 port. Should trigger via. # length 10. Via length is 1. So straight m1 should be 9. - p.straight("start", 10) + p.path("start", ccw=None, length=10) # Start at (0,0) rot pi (facing West). # Forward (+pi relative to port) is East (+x). diff --git a/masque/test/test_pather.py b/masque/test/test_pather.py index 47cae29..35e9f53 100644 --- a/masque/test/test_pather.py +++ b/masque/test/test_pather.py @@ -24,7 +24,7 @@ def pather_setup() -> tuple[Pather, PathTool, Library]: def test_pather_straight(pather_setup: tuple[Pather, PathTool, Library]) -> None: p, tool, lib = pather_setup # Route 10um "forward" - p.straight("start", 10) + p.path("start", ccw=None, length=10) # port rot pi/2 (North). Travel +pi relative to port -> South. assert_allclose(p.ports["start"].offset, [0, -10], atol=1e-10) @@ -37,7 +37,7 @@ def test_pather_bend(pather_setup: tuple[Pather, PathTool, Library]) -> None: # Start (0,0) rot pi/2 (North). # Path 10um "forward" (South), then turn Clockwise (ccw=False). # Facing South, turn Right -> West. - p.cw("start", 10) + p.path("start", ccw=False, length=10) # PathTool.planL(ccw=False, length=10) returns out_port at (10, -1) relative to (0,0) rot 0. # Transformed by port rot pi/2 (North) + pi (to move "forward" away from device): @@ -55,7 +55,7 @@ def test_pather_path_to(pather_setup: tuple[Pather, PathTool, Library]) -> None: p, tool, lib = pather_setup # start at (0,0) rot pi/2 (North) # path "forward" (South) to y=-50 - p.straight("start", y=-50) + p.path_to("start", ccw=None, y=-50) assert_equal(p.ports["start"].offset, [0, -50]) @@ -65,7 +65,7 @@ def test_pather_mpath(pather_setup: tuple[Pather, PathTool, Library]) -> None: p.ports["B"] = Port((10, 0), pi / 2, ptype="wire") # Path both "forward" (South) to y=-20 - p.straight(["A", "B"], ymin=-20) + p.mpath(["A", "B"], ccw=None, ymin=-20) assert_equal(p.ports["A"].offset, [0, -20]) assert_equal(p.ports["B"].offset, [10, -20]) @@ -73,7 +73,7 @@ def test_pather_mpath(pather_setup: tuple[Pather, PathTool, Library]) -> None: def test_pather_at_chaining(pather_setup: tuple[Pather, PathTool, Library]) -> None: p, tool, lib = pather_setup # Fluent API test - p.at("start").straight(10).ccw(10) + p.at("start").path(ccw=None, length=10).path(ccw=True, length=10) # 10um South -> (0, -10) rot pi/2 # then 10um South and turn CCW (Facing South, CCW is East) # PathTool.planL(ccw=True, length=10) -> out_port=(10, 1) rot -pi/2 relative to rot 0 @@ -93,14 +93,14 @@ def test_pather_dead_ports() -> None: p.set_dead() # Path with negative length (impossible for PathTool, would normally raise BuildError) - p.straight("in", -10) + p.path("in", None, -10) # Port 'in' should be updated by dummy extension despite tool failure # port_rot=0, forward is -x. path(-10) means moving -10 in -x direction -> +10 in x. assert_allclose(p.ports["in"].offset, [10, 0], atol=1e-10) # Downstream path should work correctly using the dummy port location - p.straight("in", 20) + p.path("in", None, 20) # 10 + (-20) = -10 assert_allclose(p.ports["in"].offset, [-10, 0], atol=1e-10) diff --git a/masque/test/test_pather_api.py b/masque/test/test_pather_api.py deleted file mode 100644 index 9ac1b78..0000000 --- a/masque/test/test_pather_api.py +++ /dev/null @@ -1,242 +0,0 @@ -import numpy -from numpy import pi -from masque import Pather, RenderPather, Library, Pattern, Port -from masque.builder.tools import PathTool - -def test_pather_trace_basic() -> None: - lib = Library() - tool = PathTool(layer='M1', width=1000) - p = Pather(lib, tools=tool) - - # Port rotation 0 points in +x (INTO device). - # To extend it, we move in -x direction. - p.pattern.ports['A'] = Port((0, 0), rotation=0) - - # Trace single port - p.at('A').trace(None, 5000) - assert numpy.allclose(p.pattern.ports['A'].offset, (-5000, 0)) - - # Trace with bend - p.at('A').trace(True, 5000) # CCW bend - # Port was at (-5000, 0) rot 0. - # New wire starts at (-5000, 0) rot 0. - # Output port of wire before rotation: (5000, 500) rot -pi/2 - # Rotate by pi (since dev port rot is 0 and tool port rot is 0): - # (-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: - lib = Library() - tool = PathTool(layer='M1', width=1000) - p = Pather(lib, tools=tool) - - p.pattern.ports['A'] = Port((0, 0), rotation=0) - - # Trace to x=-10000 - p.at('A').trace_to(None, x=-10000) - assert numpy.allclose(p.pattern.ports['A'].offset, (-10000, 0)) - - # Trace to position=-20000 - p.at('A').trace_to(None, p=-20000) - assert numpy.allclose(p.pattern.ports['A'].offset, (-20000, 0)) - -def test_pather_bundle_trace() -> None: - lib = Library() - tool = PathTool(layer='M1', width=1000) - p = Pather(lib, tools=tool) - - p.pattern.ports['A'] = Port((0, 0), rotation=0) - p.pattern.ports['B'] = Port((0, 2000), rotation=0) - - # Straight bundle - all should align to same x - p.at(['A', 'B']).straight(xmin=-10000) - assert numpy.isclose(p.pattern.ports['A'].offset[0], -10000) - assert numpy.isclose(p.pattern.ports['B'].offset[0], -10000) - - # Bundle with bend - p.at(['A', 'B']).ccw(xmin=-20000, spacing=2000) - # Traveling in -x direction. CCW turn turns towards -y. - # A is at y=0, B is at y=2000. - # Rotation center is at y = -R. - # A is closer to center than B. So A is inner, B is outer. - # xmin is coordinate of innermost bend (A). - assert numpy.isclose(p.pattern.ports['A'].offset[0], -20000) - # B's bend is further out (more negative x) - assert numpy.isclose(p.pattern.ports['B'].offset[0], -22000) - -def test_pather_each_bound() -> None: - lib = Library() - tool = PathTool(layer='M1', width=1000) - p = Pather(lib, tools=tool) - - p.pattern.ports['A'] = Port((0, 0), rotation=0) - p.pattern.ports['B'] = Port((-1000, 2000), rotation=0) - - # Each should move by 5000 (towards -x) - p.at(['A', 'B']).trace(None, each=5000) - assert numpy.allclose(p.pattern.ports['A'].offset, (-5000, 0)) - assert numpy.allclose(p.pattern.ports['B'].offset, (-6000, 2000)) - -def test_selection_management() -> None: - lib = Library() - p = Pather(lib) - p.pattern.ports['A'] = Port((0, 0), rotation=0) - p.pattern.ports['B'] = Port((0, 0), rotation=0) - - pp = p.at('A') - assert pp.ports == ['A'] - - pp.select('B') - assert pp.ports == ['A', 'B'] - - pp.deselect('A') - assert pp.ports == ['B'] - - pp.select(['A']) - assert pp.ports == ['B', 'A'] - - pp.drop() - assert 'A' not in p.pattern.ports - assert 'B' not in p.pattern.ports - assert pp.ports == [] - -def test_mark_fork() -> None: - lib = Library() - p = Pather(lib) - p.pattern.ports['A'] = Port((100, 200), rotation=1) - - pp = p.at('A') - pp.mark('B') - assert 'B' in p.pattern.ports - assert numpy.allclose(p.pattern.ports['B'].offset, (100, 200)) - assert p.pattern.ports['B'].rotation == 1 - assert pp.ports == ['A'] # mark keeps current selection - - pp.fork('C') - assert 'C' in p.pattern.ports - assert pp.ports == ['C'] # fork switches to new name - -def test_rename() -> None: - lib = Library() - p = Pather(lib) - p.pattern.ports['A'] = Port((0, 0), rotation=0) - - p.at('A').rename('B') - assert 'A' not in p.pattern.ports - assert 'B' in p.pattern.ports - - p.pattern.ports['C'] = Port((0, 0), rotation=0) - pp = p.at(['B', 'C']) - pp.rename({'B': 'D', 'C': 'E'}) - assert 'B' not in p.pattern.ports - assert 'C' not in p.pattern.ports - assert 'D' in p.pattern.ports - assert 'E' in p.pattern.ports - assert set(pp.ports) == {'D', 'E'} - -def test_renderpather_uturn_fallback() -> None: - lib = Library() - tool = PathTool(layer='M1', width=1000) - rp = RenderPather(lib, tools=tool) - rp.pattern.ports['A'] = Port((0, 0), rotation=0) - - # PathTool doesn't implement planU, so it should fall back to two planL calls - rp.at('A').uturn(offset=10000, length=5000) - - # Two steps should be added - assert len(rp.paths['A']) == 2 - assert rp.paths['A'][0].opcode == 'L' - 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) - p = Pather(lib, tools=tool) - - # 1. Straight connector - p.pattern.ports['A'] = Port((0, 0), rotation=0) - p.pattern.ports['B'] = Port((-10000, 0), rotation=pi) - p.at('A').trace_into('B', plug_destination=False) - assert 'B' in p.pattern.ports - assert 'A' in p.pattern.ports - assert numpy.allclose(p.pattern.ports['A'].offset, (-10000, 0)) - - # 2. Single bend - p.pattern.ports['C'] = Port((0, 0), rotation=0) - p.pattern.ports['D'] = Port((-5000, 5000), rotation=pi/2) - p.at('C').trace_into('D', plug_destination=False) - assert 'D' in p.pattern.ports - assert 'C' in p.pattern.ports - assert numpy.allclose(p.pattern.ports['C'].offset, (-5000, 5000)) - - # 3. Jog (S-bend) - p.pattern.ports['E'] = Port((0, 0), rotation=0) - p.pattern.ports['F'] = Port((-10000, 2000), rotation=pi) - p.at('E').trace_into('F', plug_destination=False) - assert 'F' in p.pattern.ports - assert 'E' in p.pattern.ports - assert numpy.allclose(p.pattern.ports['E'].offset, (-10000, 2000)) - - # 4. U-bend (0 deg angle) - p.pattern.ports['G'] = Port((0, 0), rotation=0) - p.pattern.ports['H'] = Port((-10000, 2000), rotation=0) - p.at('G').trace_into('H', plug_destination=False) - assert 'H' in p.pattern.ports - assert 'G' in p.pattern.ports - # A U-bend with length=-travel=10000 and jog=-2000 from (0,0) rot 0 - # ends up at (-10000, 2000) rot pi. - assert numpy.allclose(p.pattern.ports['G'].offset, (-10000, 2000)) - assert p.pattern.ports['G'].rotation is not None - assert numpy.isclose(p.pattern.ports['G'].rotation, pi) diff --git a/masque/test/test_renderpather.py b/masque/test/test_renderpather.py index cbeef3a..5d2c8c3 100644 --- a/masque/test/test_renderpather.py +++ b/masque/test/test_renderpather.py @@ -24,7 +24,7 @@ def rpather_setup() -> tuple[RenderPather, PathTool, Library]: def test_renderpather_basic(rpather_setup: tuple[RenderPather, PathTool, Library]) -> None: rp, tool, lib = rpather_setup # Plan two segments - rp.at("start").straight(10).straight(10) + rp.at("start").path(ccw=None, length=10).path(ccw=None, length=10) # Before rendering, no shapes in pattern assert not rp.pattern.has_shapes() @@ -49,7 +49,7 @@ def test_renderpather_basic(rpather_setup: tuple[RenderPather, PathTool, Library def test_renderpather_bend(rpather_setup: tuple[RenderPather, PathTool, Library]) -> None: rp, tool, lib = rpather_setup # Plan straight then bend - rp.at("start").straight(10).cw(10) + rp.at("start").path(ccw=None, length=10).path(ccw=False, length=10) rp.render() path_shape = cast("Path", rp.pattern.shapes[(1, 0)][0]) @@ -69,9 +69,9 @@ def test_renderpather_retool(rpather_setup: tuple[RenderPather, PathTool, Librar rp, tool1, lib = rpather_setup tool2 = PathTool(layer=(2, 0), width=4, ptype="wire") - rp.at("start").straight(10) + rp.at("start").path(ccw=None, length=10) rp.retool(tool2, keys=["start"]) - rp.at("start").straight(10) + rp.at("start").path(ccw=None, length=10) rp.render() # Different tools should cause different batches/shapes @@ -86,7 +86,7 @@ def test_renderpather_dead_ports() -> None: rp.set_dead() # Impossible path - rp.straight("in", -10) + rp.path("in", None, -10) # port_rot=0, forward is -x. path(-10) means moving -10 in -x direction -> +10 in x. assert_allclose(rp.ports["in"].offset, [10, 0], atol=1e-10)