Compare commits

..

10 commits

12 changed files with 1225 additions and 538 deletions

View file

@ -277,12 +277,6 @@ my_pattern.ref(_make_my_subpattern(), offset=..., ...)
## TODO ## TODO
* Rework naming/args for path-related (Builder, PortPather, path/pathL/pathS/pathU, path_to, mpath)
* PolyCollection & arrow-based read/write * PolyCollection & arrow-based read/write
* pather and renderpather examples, including .at() (PortPather)
* Bus-to-bus connections? * 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 * tuple / string layer auto-translation

View file

@ -106,7 +106,9 @@ def map_layer(layer: layer_t) -> layer_t:
'M2': (20, 0), 'M2': (20, 0),
'V1': (30, 0), 'V1': (30, 0),
} }
return layer_mapping.get(layer, layer) if isinstance(layer, str):
return layer_mapping.get(layer, layer)
return layer
def prepare_tools() -> tuple[Library, Tool, Tool]: def prepare_tools() -> tuple[Library, Tool, Tool]:
@ -224,19 +226,17 @@ def main() -> None:
# Path VCC forward (in this case south) and turn clockwise 90 degrees (ccw=False) # 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 # The total distance forward (including the bend's forward component) must be 6um
pather.path('VCC', ccw=False, length=6_000) pather.cw('VCC', 6_000)
# Now path VCC to x=0. This time, don't include any bend (ccw=None). # Now path VCC to x=0. This time, don't include any bend.
# Note that if we tried y=0 here, we would get an error since the VCC port is facing in the x-direction. # Note that if we tried y=0 here, we would get an error since the VCC port is facing in the x-direction.
pather.path_to('VCC', ccw=None, x=0) pather.straight('VCC', x=0)
# Path GND forward by 5um, turning clockwise 90 degrees. # Path GND forward by 5um, turning clockwise 90 degrees.
# This time we use shorthand (bool(0) == False) and omit the parameter labels pather.cw('GND', 5_000)
# 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. # This time, path GND until it matches the current x-coordinate of VCC. Don't place a bend.
pather.path_to('GND', None, x=pather['VCC'].offset[0]) pather.straight('GND', x=pather['VCC'].offset[0])
# Now, start using M1_tool for GND. # 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. # 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. # 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 # 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). # 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. # 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. # 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 # 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. # calculation), and its straight segment and bend will be drawn using M1 while VCC's are drawn with M2.
pather.mpath(['GND', 'VCC'], ccw=True, xmax=-10_000, spacing=5_000) pather.ccw(['GND', 'VCC'], xmax=-10_000, spacing=5_000)
# Now use M1_tool as the default tool for all ports/signals. # 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. # Since VCC does not have an explicitly assigned tool, it will now transition down to M1.
@ -262,35 +262,34 @@ def main() -> None:
# The total extension (travel distance along the forward direction) for the longest segment (in # 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. # this case the segment being added to GND) should be exactly 50um.
# After turning, the wire pitch should be reduced only 1.2um. # After turning, the wire pitch should be reduced only 1.2um.
pather.mpath(['GND', 'VCC'], ccw=True, emax=50_000, spacing=1_200) pather.ccw(['GND', 'VCC'], emax=50_000, spacing=1_200)
# Make a U-turn with the bundle and expand back out to 4.5um wire pitch. # 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 mpath() call # Here, emin specifies the travel distance for the shortest segment. For the first call
# that applies to VCC, and for teh second call, that applies to GND; the relative lengths of the # that applies to VCC, and for the second call, that applies to GND; the relative lengths of the
# segments depend on their starting positions and their ordering within the bundle. # segments depend on their starting positions and their ordering within the bundle.
pather.mpath(['GND', 'VCC'], ccw=False, emin=1_000, spacing=1_200) pather.cw(['GND', 'VCC'], emin=1_000, spacing=1_200)
pather.mpath(['GND', 'VCC'], ccw=False, emin=2_000, spacing=4_500) pather.cw(['GND', 'VCC'], 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 # Now, set the default tool back to M2_tool. Note that GND remains on M1 since it has been
# explicitly assigned a tool. We could `del pather.tools['GND']` to force it to use the default. # explicitly assigned a tool.
pather.retool(M2_tool) pather.retool(M2_tool)
# Now path both ports to x=-28_000. # Now path both ports to x=-28_000.
# When ccw is not None, xmin constrains the trailing/innermost port to stop at the target x coordinate, # With ccw=None, all ports stop at the same coordinate, and so specifying xmin= or xmax= is
# However, with ccw=None, all ports stop at the same coordinate, and so specifying xmin= or xmax= is
# equivalent. # equivalent.
pather.mpath(['GND', 'VCC'], None, xmin=-28_000) pather.straight(['GND', 'VCC'], xmin=-28_000)
# Further extend VCC out to x=-50_000, and specify that we would like to get an output on M1. # 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 # 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(). # when using pather.retool().
pather.path_to('VCC', None, -50_000, out_ptype='m1wire') pather.straight('VCC', x=-50_000, out_ptype='m1wire')
# Now extend GND out to x=-50_000, using M2 for a portion of the path. # 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. # We can use `pather.toolctx()` to temporarily retool, instead of calling `retool()` twice.
with pather.toolctx(M2_tool, keys=['GND']): with pather.toolctx(M2_tool, keys='GND'):
pather.path_to('GND', None, -40_000) pather.straight('GND', x=-40_000)
pather.path_to('GND', None, -50_000) pather.straight('GND', x=-50_000)
# Save the pather's pattern into our library # Save the pather's pattern into our library
library['Pather_and_AutoTool'] = pather.pattern library['Pather_and_AutoTool'] = pather.pattern

View file

@ -48,28 +48,28 @@ def main() -> None:
rpather.pattern.label(layer='M2', string='GND', offset=(18e3, 60e3)) rpather.pattern.label(layer='M2', string='GND', offset=(18e3, 60e3))
# ...and start routing the signals. # ...and start routing the signals.
rpather.path('VCC', ccw=False, length=6_000) rpather.cw('VCC', 6_000)
rpather.path_to('VCC', ccw=None, x=0) rpather.straight('VCC', x=0)
rpather.path('GND', 0, 5_000) rpather.cw('GND', 5_000)
rpather.path_to('GND', None, x=rpather['VCC'].x) rpather.straight('GND', x=rpather.pattern['VCC'].x)
# `PathTool` doesn't know how to transition betwen metal layers, so we have to # `PathTool` doesn't know how to transition betwen metal layers, so we have to
# `plug` the via into the GND wire ourselves. # `plug` the via into the GND wire ourselves.
rpather.plug('v1_via', {'GND': 'top'}) rpather.plug('v1_via', {'GND': 'top'})
rpather.retool(M1_ptool, keys=['GND']) rpather.retool(M1_ptool, keys='GND')
rpather.mpath(['GND', 'VCC'], ccw=True, xmax=-10_000, spacing=5_000) rpather.ccw(['GND', 'VCC'], xmax=-10_000, spacing=5_000)
# Same thing on the VCC wire when it goes down to M1. # Same thing on the VCC wire when it goes down to M1.
rpather.plug('v1_via', {'VCC': 'top'}) rpather.plug('v1_via', {'VCC': 'top'})
rpather.retool(M1_ptool) rpather.retool(M1_ptool)
rpather.mpath(['GND', 'VCC'], ccw=True, emax=50_000, spacing=1_200) rpather.ccw(['GND', 'VCC'], emax=50_000, spacing=1_200)
rpather.mpath(['GND', 'VCC'], ccw=False, emin=1_000, spacing=1_200) rpather.cw(['GND', 'VCC'], emin=1_000, spacing=1_200)
rpather.mpath(['GND', 'VCC'], ccw=False, emin=2_000, spacing=4_500) rpather.cw(['GND', 'VCC'], emin=2_000, spacing=4_500)
# And again when VCC goes back up to M2. # And again when VCC goes back up to M2.
rpather.plug('v1_via', {'VCC': 'bottom'}) rpather.plug('v1_via', {'VCC': 'bottom'})
rpather.retool(M2_ptool) rpather.retool(M2_ptool)
rpather.mpath(['GND', 'VCC'], None, xmin=-28_000) rpather.straight(['GND', 'VCC'], xmin=-28_000)
# Finally, since PathTool has no conception of transitions, we can't # 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. # 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] # 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 # would take into account the port orientations if we didn't already know they're along x
rpather.path_to('VCC', None, -50_000 + via_size) rpather.straight('VCC', x=-50_000 + via_size)
rpather.plug('v1_via', {'VCC': 'top'}) rpather.plug('v1_via', {'VCC': 'top'})
# Render the path we defined # Render the path we defined

View file

@ -28,7 +28,7 @@ class Pather(Builder, PatherMixin):
single-use patterns (e.g. wires or waveguides) and bundles / buses of such patterns. 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 `Pather` is mostly concerned with calculating how long each wire should be. It calls
out to `Tool.path` functions provided by subclasses of `Tool` to build the actual patterns. out to `Tool.traceL` 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 `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. a "default" `Tool` used for all ports which do not have a port-specific `Tool` assigned.
@ -63,7 +63,10 @@ class Pather(Builder, PatherMixin):
Examples: Adding to a pattern Examples: Adding to a pattern
============================= =============================
- `pather.path('my_port', ccw=True, distance)` creates a "wire" for which the output - `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
port is `distance` units away along the axis of `'my_port'` and rotated 90 degrees 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 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. into the existing `'my_port'`, causing the port to move to the wire's output.
@ -72,22 +75,15 @@ class Pather(Builder, PatherMixin):
there may be a significant width to the bend that is used to accomplish the 90 degree 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. turn. However, an error is raised if `distance` is too small to fit the bend.
- `pather.path('my_port', ccw=None, distance)` creates a straight wire with a length - `pather.trace_to('my_port', ccw=False, x=position)` creates a wire which starts at
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 `'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 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. To output is not specified, so `position` takes the form of a single coordinate.
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.mpath(['A', 'B', 'C'], ..., spacing=spacing)` is a superset of `path` - `pather.trace(['A', 'B', 'C'], ccw=True, spacing=spacing, xmax=position)` acts
and `path_to` which can act on multiple ports simultaneously. Each port's wire is on multiple ports simultaneously. Each port's wire is generated using its own
generated using its own `Tool` (or the default tool if left unspecified). `Tool` (or the default tool if left unspecified).
The output ports are spaced out by `spacing` along the input ports' axis, unless The output ports are spaced out by `spacing` along the input ports' axis.
`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' - `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`, of `pather.pattern`. If `wire` has only two ports (e.g. 'A' and 'B'), no `map_out`,
@ -141,8 +137,8 @@ class Pather(Builder, PatherMixin):
in which case it is interpreted as a name in `library`. in which case it is interpreted as a name in `library`.
Default `None` (no ports). Default `None` (no ports).
tools: A mapping of {port: tool} which specifies what `Tool` should be used tools: A mapping of {port: tool} which specifies what `Tool` should be used
to generate waveguide or wire segments when `path`/`path_to`/`mpath` to generate waveguide or wire segments when `trace`/`trace_to`/etc.
are called. Relies on `Tool.path` implementations. are called. Relies on `Tool.traceL` implementations.
name: If specified, `library[name]` is set to `self.pattern`. name: If specified, `library[name]` is set to `self.pattern`.
""" """
self._dead = False self._dead = False
@ -213,7 +209,7 @@ class Pather(Builder, PatherMixin):
and to which the new one should be added (if named). If not provided, and to which the new one should be added (if named). If not provided,
`source.library` must exist and will be used. `source.library` must exist and will be used.
tools: `Tool`s which will be used by the pather for generating new wires tools: `Tool`s which will be used by the pather for generating new wires
or waveguides (via `path`/`path_to`/`mpath`). or waveguides (via `trace`/`trace_to`).
in_prefix: Prepended to port names for newly-created ports with in_prefix: Prepended to port names for newly-created ports with
reversed directions compared to the current device. reversed directions compared to the current device.
out_prefix: Prepended to port names for ports which are directly out_prefix: Prepended to port names for ports which are directly
@ -255,7 +251,73 @@ class Pather(Builder, PatherMixin):
return s return s
def path( 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(
self, self,
portspec: str, portspec: str,
ccw: SupportsBool | None, ccw: SupportsBool | None,
@ -296,18 +358,18 @@ class Pather(Builder, PatherMixin):
LibraryError if no valid name could be picked for the pattern. LibraryError if no valid name could be picked for the pattern.
""" """
if self._dead: if self._dead:
logger.warning('Skipping geometry for path() since device is dead') logger.warning('Skipping geometry for _traceL() since device is dead')
tool_port_names = ('A', 'B') tool_port_names = ('A', 'B')
tool = self.tools.get(portspec, self.tools[None]) tool = self.tools.get(portspec, self.tools[None])
in_ptype = self.pattern[portspec].ptype in_ptype = self.pattern[portspec].ptype
try: try:
tree = tool.path(ccw, length, in_ptype=in_ptype, port_names=tool_port_names, **kwargs) tree = tool.traceL(ccw, length, in_ptype=in_ptype, port_names=tool_port_names, **kwargs)
except (BuildError, NotImplementedError): except (BuildError, NotImplementedError):
if not self._dead: if not self._dead:
raise raise
logger.warning("Tool path failed for dead pather. Using dummy extension.") logger.warning("Tool traceL failed for dead pather. Using dummy extension.")
# Fallback for dead pather: manually update the port instead of plugging # Fallback for dead pather: manually update the port instead of plugging
port = self.pattern[portspec] port = self.pattern[portspec]
port_rot = port.rotation port_rot = port.rotation
@ -335,7 +397,7 @@ class Pather(Builder, PatherMixin):
self.plug(tname, {portspec: tool_port_names[0], **output}) self.plug(tname, {portspec: tool_port_names[0], **output})
return self return self
def pathS( def _traceS(
self, self,
portspec: str, portspec: str,
length: float, length: float,
@ -346,20 +408,17 @@ class Pather(Builder, PatherMixin):
) -> Self: ) -> Self:
""" """
Create an S-shaped "wire"/"waveguide" and `plug` it into the port `portspec`, with the aim Create an S-shaped "wire"/"waveguide" and `plug` it into the port `portspec`, with the aim
of traveling exactly `length` distance with an offset `jog` along the other axis (+ve jog is of traveling exactly `length` distance.
left of direction of travel).
The output port will have the same orientation as the source port (`portspec`). 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
This function attempts to use `tool.planS()`, but falls back to `tool.planL()` if the former identical to the input port.
raises a NotImplementedError.
Args: Args:
portspec: The name of the port into which the wire will be plugged. portspec: The name of the port into which the wire will be plugged.
jog: Total manhattan distance perpendicular to the direction of travel. length: The total distance from input to output, along the input's axis only.
Positive values are to the left of the direction of travel. jog: Total distance perpendicular to the direction of travel. Positive values
length: The total manhattan distance from input to output, along the input's axis only. are to the left of the direction of travel.
(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 plug_into: If not None, attempts to plug the wire's output port into the provided
port on `self`. port on `self`.
@ -377,29 +436,29 @@ class Pather(Builder, PatherMixin):
LibraryError if no valid name could be picked for the pattern. LibraryError if no valid name could be picked for the pattern.
""" """
if self._dead: if self._dead:
logger.warning('Skipping geometry for pathS() since device is dead') logger.warning('Skipping geometry for _traceS() since device is dead')
tool_port_names = ('A', 'B') tool_port_names = ('A', 'B')
tool = self.tools.get(portspec, self.tools[None]) tool = self.tools.get(portspec, self.tools[None])
in_ptype = self.pattern[portspec].ptype in_ptype = self.pattern[portspec].ptype
try: try:
tree = tool.pathS(length, jog, in_ptype=in_ptype, port_names=tool_port_names, **kwargs) tree = tool.traceS(length, jog, in_ptype=in_ptype, port_names=tool_port_names, **kwargs)
except NotImplementedError: except NotImplementedError:
# Fall back to drawing two L-bends # Fall back to drawing two L-bends
ccw0 = jog > 0 ccw0 = jog > 0
kwargs_no_out = kwargs | {'out_ptype': None} kwargs_no_out = kwargs | {'out_ptype': None}
try: try:
t_tree0 = tool.path( ccw0, length / 2, port_names=tool_port_names, in_ptype=in_ptype, **kwargs_no_out) t_tree0 = tool.traceL( ccw0, length / 2, port_names=tool_port_names, in_ptype=in_ptype, **kwargs_no_out)
t_pat0 = t_tree0.top_pattern() t_pat0 = t_tree0.top_pattern()
(_, jog0), _ = t_pat0[tool_port_names[0]].measure_travel(t_pat0[tool_port_names[1]]) (_, jog0), _ = t_pat0[tool_port_names[0]].measure_travel(t_pat0[tool_port_names[1]])
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_tree1 = tool.traceL(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() t_pat1 = t_tree1.top_pattern()
(_, jog1), _ = t_pat1[tool_port_names[0]].measure_travel(t_pat1[tool_port_names[1]]) (_, jog1), _ = t_pat1[tool_port_names[0]].measure_travel(t_pat1[tool_port_names[1]])
kwargs_plug = kwargs | {'plug_into': plug_into} kwargs_plug = kwargs | {'plug_into': plug_into}
self.path(portspec, ccw0, length - abs(jog1), **kwargs_no_out) self._traceL(portspec, ccw0, length - abs(jog1), **kwargs_no_out)
self.path(portspec, not ccw0, abs(jog - jog0), **kwargs_plug) self._traceL(portspec, not ccw0, abs(jog - jog0), **kwargs_plug)
except (BuildError, NotImplementedError): except (BuildError, NotImplementedError):
if not self._dead: if not self._dead:
raise raise
@ -412,7 +471,7 @@ class Pather(Builder, PatherMixin):
# Fall through to dummy extension below # Fall through to dummy extension below
if self._dead: if self._dead:
logger.warning("Tool pathS failed for dead pather. Using dummy extension.") logger.warning("Tool traceS failed for dead pather. Using dummy extension.")
# Fallback for dead pather: manually update the port instead of plugging # Fallback for dead pather: manually update the port instead of plugging
port = self.pattern[portspec] port = self.pattern[portspec]
port_rot = port.rotation port_rot = port.rotation
@ -433,4 +492,3 @@ class Pather(Builder, PatherMixin):
output = {} output = {}
self.plug(tname, {portspec: tool_port_names[0], **output}) self.plug(tname, {portspec: tool_port_names[0], **output})
return self return self

View file

@ -1,5 +1,5 @@
from typing import Self, overload from typing import Self, overload
from collections.abc import Sequence, Iterator, Iterable from collections.abc import Sequence, Iterator, Iterable, Mapping
import logging import logging
from contextlib import contextmanager from contextlib import contextmanager
from abc import abstractmethod, ABCMeta from abc import abstractmethod, ABCMeta
@ -37,8 +37,338 @@ class PatherMixin(PortList, metaclass=ABCMeta):
(e.g wires or waveguides) to be plugged into this device. (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 @abstractmethod
def path( def _traceL(
self, self,
portspec: str, portspec: str,
ccw: SupportsBool | None, ccw: SupportsBool | None,
@ -50,7 +380,7 @@ class PatherMixin(PortList, metaclass=ABCMeta):
pass pass
@abstractmethod @abstractmethod
def pathS( def _traceS(
self, self,
portspec: str, portspec: str,
length: float, length: float,
@ -61,6 +391,33 @@ class PatherMixin(PortList, metaclass=ABCMeta):
) -> Self: ) -> Self:
pass 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 @abstractmethod
def plug( def plug(
self, self,
@ -76,6 +433,11 @@ class PatherMixin(PortList, metaclass=ABCMeta):
) -> Self: ) -> Self:
pass pass
@abstractmethod
def plugged(self, connections: dict[str, str]) -> Self:
""" Manual connection acknowledgment. """
pass
def retool( def retool(
self, self,
tool: Tool, tool: Tool,
@ -143,88 +505,13 @@ class PatherMixin(PortList, metaclass=ABCMeta):
**kwargs, **kwargs,
) -> Self: ) -> Self:
""" """
Build a "wire"/"waveguide" extending from the port `portspec`, with the aim [DEPRECATED] use trace_to() instead.
of ending exactly at a target position.
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: import warnings
logger.error('Skipping path_to() since device is dead') warnings.warn("path_to() is deprecated; use trace_to() instead", DeprecationWarning, stacklevel=2)
return self
pos_count = sum(vv is not None for vv in (position, x, y)) bounds = {kk: vv for kk, vv in (('position', position), ('x', x), ('y', y)) if vv is not None}
if pos_count > 1: return self.trace_to(portspec, ccw, plug_into=plug_into, **bounds, **kwargs)
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( def path_into(
self, self,
@ -237,100 +524,19 @@ class PatherMixin(PortList, metaclass=ABCMeta):
**kwargs, **kwargs,
) -> Self: ) -> Self:
""" """
Create a "wire"/"waveguide" traveling between the ports `portspec_src` and [DEPRECATED] use trace_into() instead.
`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).
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: import warnings
logger.error('Skipping path_into() since device is dead') warnings.warn("path_into() is deprecated; use trace_into() instead", DeprecationWarning, stacklevel=2)
return self
port_src = self.pattern[portspec_src] return self.trace_into(
port_dst = self.pattern[portspec_dst] portspec_src,
portspec_dst,
if out_ptype is None: out_ptype = out_ptype,
out_ptype = port_dst.ptype plug_destination = plug_destination,
thru = thru,
if port_src.rotation is None: **kwargs,
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( def mpath(
self, self,
@ -342,109 +548,12 @@ class PatherMixin(PortList, metaclass=ABCMeta):
**kwargs, **kwargs,
) -> Self: ) -> Self:
""" """
`mpath` is a superset of `path` and `path_to` which can act on bundles or buses [DEPRECATED] use trace() or trace_to() instead.
of "wires or "waveguides".
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: import warnings
logger.error('Skipping mpath() since device is dead') warnings.warn("mpath() is deprecated; use trace() or trace_to() instead", DeprecationWarning, stacklevel=2)
return self
bound_types = set() return self.trace(portspec, ccw, spacing=spacing, set_rotation=set_rotation, **kwargs)
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()? # TODO def bus_join()?
@ -488,61 +597,42 @@ class PortPather:
with self.pather.toolctx(tool, keys=self.ports): with self.pather.toolctx(tool, keys=self.ports):
yield self yield self
def path(self, *args, **kwargs) -> 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:
if len(self.ports) > 1: if len(self.ports) > 1:
logger.warning('Use path_each() when pathing multiple ports independently') raise BuildError(f'Unable use implicit trace_into() with {len(self.ports)} (>1) ports.')
for port in self.ports: self.pather.trace_into(self.ports[0], target_port, **kwargs)
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 return self
def plug( def plug(
@ -558,10 +648,13 @@ class PortPather:
self.pather.plug(other, {self.ports[0]: other_port}, *args, **kwargs) self.pather.plug(other, {self.ports[0]: other_port}, *args, **kwargs)
return self return self
def plugged(self, other_port: str) -> Self: def plugged(self, other_port: str | Mapping[str, str]) -> Self:
if len(self.ports) > 1: if isinstance(other_port, Mapping):
self.pather.plugged(dict(other_port))
elif len(self.ports) > 1:
raise BuildError(f'Unable use implicit plugged() with {len(self.ports)} (>1) ports.') raise BuildError(f'Unable use implicit plugged() with {len(self.ports)} (>1) ports.')
self.pather.plugged({self.ports[0]: other_port}) else:
self.pather.plugged({self.ports[0]: other_port})
return self return self
# #
@ -569,95 +662,91 @@ class PortPather:
# #
def set_ptype(self, ptype: str) -> Self: def set_ptype(self, ptype: str) -> Self:
for port in self.ports: for port in self.ports:
self.pather[port].set_ptype(ptype) self.pather.pattern[port].set_ptype(ptype)
return self return self
def translate(self, *args, **kwargs) -> Self: def translate(self, *args, **kwargs) -> Self:
for port in self.ports: for port in self.ports:
self.pather[port].translate(*args, **kwargs) self.pather.pattern[port].translate(*args, **kwargs)
return self return self
def mirror(self, *args, **kwargs) -> Self: def mirror(self, *args, **kwargs) -> Self:
for port in self.ports: for port in self.ports:
self.pather[port].mirror(*args, **kwargs) self.pather.pattern[port].mirror(*args, **kwargs)
return self return self
def rotate(self, rotation: float) -> Self: def rotate(self, rotation: float) -> Self:
for port in self.ports: for port in self.ports:
self.pather[port].rotate(rotation) self.pather.pattern[port].rotate(rotation)
return self return self
def set_rotation(self, rotation: float | None) -> Self: def set_rotation(self, rotation: float | None) -> Self:
for port in self.ports: for port in self.ports:
self.pather[port].set_rotation(rotation) self.pather.pattern[port].set_rotation(rotation)
return self return self
def rename_to(self, new_name: str) -> Self: def rename(self, name: str | Mapping[str, str | None]) -> Self:
if len(self.ports) > 1: """ Rename active ports. Replaces `rename_to`. """
BuildError('Use rename_ports() for >1 port') name_map: dict[str, str | None]
self.pather.rename_ports({self.ports[0]: new_name}) if isinstance(name, str):
self.ports[0] = new_name if len(self.ports) > 1:
return self raise BuildError('Use a mapping to rename >1 port')
name_map = {self.ports[0]: name}
def rename_from(self, old_name: str) -> Self: else:
if len(self.ports) > 1: name_map = dict(name)
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.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] self.ports = [mm for mm in [name_map.get(pp, pp) for pp in self.ports] if mm is not None]
return self return self
def add_ports(self, ports: Iterable[str]) -> Self: def select(self, ports: str | Iterable[str]) -> Self:
ports = list(ports) """ Add ports to the selection. Replaces `add_ports`. """
conflicts = set(ports) & set(self.ports) if isinstance(ports, str):
if conflicts: ports = [ports]
raise BuildError(f'ports {conflicts} already selected') for port in ports:
self.ports += ports if port not in self.ports:
self.ports.append(port)
return self return self
def add_port(self, port: str, index: int | None = None) -> Self: def deselect(self, ports: str | Iterable[str]) -> Self:
if port in self.ports: """ Remove ports from the selection. Replaces `drop_port`. """
raise BuildError(f'{port=} already selected') if isinstance(ports, str):
if index is not None: ports = [ports]
self.ports.insert(index, port) 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}
else: else:
self.ports.append(port) name_map = name
for src, dst in name_map.items():
self.pather.pattern.ports[dst] = self.pather.pattern[src].copy()
return self return self
def drop_port(self, port: str) -> Self: def fork(self, name: str | Mapping[str, str]) -> Self:
if port not in self.ports: """ Split and follow new name. Replaces `into_copy`. """
raise BuildError(f'{port=} already not selected') name_map: Mapping[str, str]
self.ports = [pp for pp in self.ports if pp != port] 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]
return self return self
def into_copy(self, new_name: str, src: str | None = None) -> Self: def drop(self) -> Self:
""" Copy a port and replace it with the copy """ """ Remove selected ports from the pattern and the PortPather. Replaces `delete(None)`. """
if not self.ports: for pp in self.ports:
raise BuildError('Have no ports to copy') del self.pather.pattern.ports[pp]
if len(self.ports) == 1: self.ports = []
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 return self
@overload @overload
@ -668,10 +757,8 @@ class PortPather:
def delete(self, name: str | None = None) -> Self | None: def delete(self, name: str | None = None) -> Self | None:
if name is None: if name is None:
for pp in self.ports: self.drop()
del self.pather.ports[pp]
return None return None
del self.pather.ports[name] del self.pather.pattern.ports[name]
self.ports = [pp for pp in self.ports if pp != name] self.ports = [pp for pp in self.ports if pp != name]
return self return self

View file

@ -9,8 +9,9 @@ from collections import defaultdict
from functools import wraps from functools import wraps
from pprint import pformat from pprint import pformat
import numpy
from numpy import pi from numpy import pi
from numpy.typing import ArrayLike from numpy.typing import ArrayLike, NDArray
from ..pattern import Pattern from ..pattern import Pattern
from ..library import ILibrary, TreeView from ..library import ILibrary, TreeView
@ -27,7 +28,7 @@ logger = logging.getLogger(__name__)
class RenderPather(PatherMixin): class RenderPather(PatherMixin):
""" """
`RenderPather` is an alternative to `Pather` which uses the `path`/`path_to`/`mpath` `RenderPather` is an alternative to `Pather` which uses the `trace`/`trace_to`
functions to plan out wire paths without incrementally generating the layout. Instead, 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 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 simultaneously. This allows it to e.g. draw each wire using a single `Path` or
@ -96,7 +97,7 @@ class RenderPather(PatherMixin):
in which case it is interpreted as a name in `library`. in which case it is interpreted as a name in `library`.
Default `None` (no ports). Default `None` (no ports).
tools: A mapping of {port: tool} which specifies what `Tool` should be used tools: A mapping of {port: tool} which specifies what `Tool` should be used
to generate waveguide or wire segments when `path`/`path_to`/`mpath` to generate waveguide or wire segments when `trace`/`trace_to`
are called. Relies on `Tool.planL` and `Tool.render` implementations. are called. Relies on `Tool.planL` and `Tool.render` implementations.
name: If specified, `library[name]` is set to `self.pattern`. name: If specified, `library[name]` is set to `self.pattern`.
""" """
@ -149,7 +150,7 @@ class RenderPather(PatherMixin):
and to which the new one should be added (if named). If not provided, and to which the new one should be added (if named). If not provided,
`source.library` must exist and will be used. `source.library` must exist and will be used.
tools: `Tool`s which will be used by the pather for generating new wires tools: `Tool`s which will be used by the pather for generating new wires
or waveguides (via `path`/`path_to`/`mpath`). or waveguides (via `trace`/`trace_to`).
in_prefix: Prepended to port names for newly-created ports with in_prefix: Prepended to port names for newly-created ports with
reversed directions compared to the current device. reversed directions compared to the current device.
out_prefix: Prepended to port names for ports which are directly out_prefix: Prepended to port names for ports which are directly
@ -376,7 +377,67 @@ class RenderPather(PatherMixin):
PortList.plugged(self, connections) PortList.plugged(self, connections)
return self return self
def path( 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(
self, self,
portspec: str, portspec: str,
ccw: SupportsBool | None, ccw: SupportsBool | None,
@ -419,7 +480,7 @@ class RenderPather(PatherMixin):
LibraryError if no valid name could be picked for the pattern. LibraryError if no valid name could be picked for the pattern.
""" """
if self._dead: if self._dead:
logger.warning('Skipping geometry for path() since device is dead') logger.warning('Skipping geometry for _traceL() since device is dead')
port = self.pattern[portspec] port = self.pattern[portspec]
in_ptype = port.ptype in_ptype = port.ptype
@ -459,7 +520,7 @@ class RenderPather(PatherMixin):
return self return self
def pathS( def _traceS(
self, self,
portspec: str, portspec: str,
length: float, length: float,
@ -503,7 +564,7 @@ class RenderPather(PatherMixin):
LibraryError if no valid name could be picked for the pattern. LibraryError if no valid name could be picked for the pattern.
""" """
if self._dead: if self._dead:
logger.warning('Skipping geometry for pathS() since device is dead') logger.warning('Skipping geometry for _traceS() since device is dead')
port = self.pattern[portspec] port = self.pattern[portspec]
in_ptype = port.ptype in_ptype = port.ptype
@ -526,8 +587,8 @@ class RenderPather(PatherMixin):
jog1 = Port((0, 0), 0).measure_travel(t_port1)[0][1] jog1 = Port((0, 0), 0).measure_travel(t_port1)[0][1]
kwargs_plug = kwargs | {'plug_into': plug_into} kwargs_plug = kwargs | {'plug_into': plug_into}
self.path(portspec, ccw0, length - abs(jog1), **kwargs_no_out) self._traceL(portspec, ccw0, length - abs(jog1), **kwargs_no_out)
self.path(portspec, not ccw0, abs(jog - jog0), **kwargs_plug) self._traceL(portspec, not ccw0, abs(jog - jog0), **kwargs_plug)
except (BuildError, NotImplementedError): except (BuildError, NotImplementedError):
if not self._dead: if not self._dead:
raise raise
@ -563,7 +624,7 @@ class RenderPather(PatherMixin):
append: bool = True, append: bool = True,
) -> Self: ) -> Self:
""" """
Generate the geometry which has been planned out with `path`/`path_to`/etc. Generate the geometry which has been planned out with `trace`/`trace_to`/etc.
Args: Args:
append: If `True`, the rendered geometry will be directly appended to append: If `True`, the rendered geometry will be directly appended to
@ -579,22 +640,54 @@ class RenderPather(PatherMixin):
def render_batch(portspec: str, batch: list[RenderStep], append: bool) -> None: def render_batch(portspec: str, batch: list[RenderStep], append: bool) -> None:
assert batch[0].tool is not None assert batch[0].tool is not None
name = lib << batch[0].tool.render(batch, port_names=tool_port_names) # Tools render in local space (first port at 0,0, rotation 0).
pat.ports[portspec] = batch[0].start_port.copy() 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
if append: if append:
pat.plug(lib[name], {portspec: tool_port_names[0]}, append=append) # pat.plug() translates and rotates the tool's local output to the start port.
del lib[name] # NOTE if the rendered pattern has refs, those are now in `pat` but not flattened pat.plug(lib[name], {portspec: actual_in}, append=append)
del lib[name]
else: else:
pat.plug(lib.abstract(name), {portspec: tool_port_names[0]}, append=append) 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)
for portspec, steps in self.paths.items(): for portspec, steps in self.paths.items():
if not steps:
continue
batch: list[RenderStep] = [] batch: list[RenderStep] = []
# Initialize continuity check with the start of the entire path.
prev_end = steps[0].start_port
for step in steps: for step in steps:
appendable_op = step.opcode in ('L', 'S', 'U') appendable_op = step.opcode in ('L', 'S', 'U')
same_tool = batch and step.tool == batch[0].tool 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 we can't continue a batch, render it
if batch and (not appendable_op or not same_tool): if batch and (not appendable_op or not same_tool or not continuous):
render_batch(portspec, batch, append) render_batch(portspec, batch, append)
batch = [] batch = []
@ -603,8 +696,14 @@ class RenderPather(PatherMixin):
batch.append(step) batch.append(step)
# Opcodes which break the batch go below this line # Opcodes which break the batch go below this line
if not appendable_op and portspec in pat.ports: if not appendable_op:
del pat.ports[portspec] 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 the last batch didn't end yet #If the last batch didn't end yet
if batch: if batch:
@ -626,7 +725,11 @@ class RenderPather(PatherMixin):
Returns: Returns:
self self
""" """
self.pattern.translate_elements(offset) 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))
return self return self
def rotate_around(self, pivot: ArrayLike, angle: float) -> Self: def rotate_around(self, pivot: ArrayLike, angle: float) -> Self:
@ -640,7 +743,11 @@ class RenderPather(PatherMixin):
Returns: Returns:
self self
""" """
self.pattern.rotate_around(pivot, angle) 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)
return self return self
def mirror(self, axis: int) -> Self: def mirror(self, axis: int) -> Self:
@ -654,6 +761,9 @@ class RenderPather(PatherMixin):
self self
""" """
self.pattern.mirror(axis) self.pattern.mirror(axis)
for steps in self.paths.values():
for i, step in enumerate(steps):
steps[i] = step.mirrored(axis)
return self return self
def set_dead(self) -> Self: def set_dead(self) -> Self:
@ -693,4 +803,3 @@ class RenderPather(PatherMixin):
def rect(self, *args, **kwargs) -> Self: def rect(self, *args, **kwargs) -> Self:
self.pattern.rect(*args, **kwargs) self.pattern.rect(*args, **kwargs)
return self return self

View file

@ -47,6 +47,43 @@ class RenderStep:
if self.opcode != 'P' and self.tool is None: if self.opcode != 'P' and self.tool is None:
raise BuildError('Got tool=None but the opcode is not "P"') 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: class Tool:
""" """
@ -56,7 +93,7 @@ class Tool:
unimplemented (e.g. in cases where they don't make sense or the required components unimplemented (e.g. in cases where they don't make sense or the required components
are impractical or unavailable). are impractical or unavailable).
""" """
def path( def traceL(
self, self,
ccw: SupportsBool | None, ccw: SupportsBool | None,
length: float, length: float,
@ -99,9 +136,9 @@ class Tool:
Raises: Raises:
BuildError if an impossible or unsupported geometry is requested. BuildError if an impossible or unsupported geometry is requested.
""" """
raise NotImplementedError(f'path() not implemented for {type(self)}') raise NotImplementedError(f'traceL() not implemented for {type(self)}')
def pathS( def traceS(
self, self,
length: float, length: float,
jog: float, jog: float,
@ -141,7 +178,7 @@ class Tool:
Raises: Raises:
BuildError if an impossible or unsupported geometry is requested. BuildError if an impossible or unsupported geometry is requested.
""" """
raise NotImplementedError(f'path() not implemented for {type(self)}') raise NotImplementedError(f'traceS() not implemented for {type(self)}')
def planL( def planL(
self, self,
@ -223,6 +260,46 @@ class Tool:
""" """
raise NotImplementedError(f'planS() not implemented for {type(self)}') 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( def planU(
self, self,
jog: float, jog: float,
@ -390,7 +467,7 @@ class SimpleTool(Tool, metaclass=ABCMeta):
pat.plug(bend, {port_names[1]: inport}, mirrored=mirrored) pat.plug(bend, {port_names[1]: inport}, mirrored=mirrored)
return tree return tree
def path( def traceL(
self, self,
ccw: SupportsBool | None, ccw: SupportsBool | None,
length: float, length: float,
@ -407,7 +484,7 @@ class SimpleTool(Tool, metaclass=ABCMeta):
out_ptype = out_ptype, out_ptype = out_ptype,
) )
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path') tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL')
pat.add_port_pair(names=port_names, ptype='unk' if in_ptype is None else in_ptype) 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) self._renderL(data=data, tree=tree, port_names=port_names, straight_kwargs=kwargs)
return tree return tree
@ -420,7 +497,7 @@ class SimpleTool(Tool, metaclass=ABCMeta):
**kwargs, **kwargs,
) -> ILibrary: ) -> ILibrary:
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path') tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL')
pat.add_port_pair(names=(port_names[0], port_names[1])) pat.add_port_pair(names=(port_names[0], port_names[1]))
for step in batch: for step in batch:
@ -521,6 +598,14 @@ class AutoTool(Tool, metaclass=ABCMeta):
b_transition: 'AutoTool.Transition | None' b_transition: 'AutoTool.Transition | None'
out_transition: 'AutoTool.Transition | None' out_transition: 'AutoTool.Transition | None'
@dataclass(frozen=True, slots=True)
class UData:
""" Data for planU """
ldata0: 'AutoTool.LData'
ldata1: 'AutoTool.LData'
straight2: 'AutoTool.Straight'
l2_length: float
straights: list[Straight] straights: list[Straight]
""" List of straight-generators to choose from, in order of priority """ """ List of straight-generators to choose from, in order of priority """
@ -689,7 +774,7 @@ class AutoTool(Tool, metaclass=ABCMeta):
pat.plug(data.out_transition.abstract, {port_names[1]: data.out_transition.our_port_name}) pat.plug(data.out_transition.abstract, {port_names[1]: data.out_transition.our_port_name})
return tree return tree
def path( def traceL(
self, self,
ccw: SupportsBool | None, ccw: SupportsBool | None,
length: float, length: float,
@ -706,7 +791,7 @@ class AutoTool(Tool, metaclass=ABCMeta):
out_ptype = out_ptype, out_ptype = out_ptype,
) )
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path') tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL')
pat.add_port_pair(names=port_names, ptype='unk' if in_ptype is None else in_ptype) 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) self._renderL(data=data, tree=tree, port_names=port_names, straight_kwargs=kwargs)
return tree return tree
@ -845,7 +930,7 @@ class AutoTool(Tool, metaclass=ABCMeta):
pat.plug(data.out_transition.abstract, {port_names[1]: data.out_transition.our_port_name}) pat.plug(data.out_transition.abstract, {port_names[1]: data.out_transition.our_port_name})
return tree return tree
def pathS( def traceS(
self, self,
length: float, length: float,
jog: float, jog: float,
@ -861,11 +946,118 @@ class AutoTool(Tool, metaclass=ABCMeta):
in_ptype = in_ptype, in_ptype = in_ptype,
out_ptype = out_ptype, out_ptype = out_ptype,
) )
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'pathS') tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceS')
pat.add_port_pair(names=port_names, ptype='unk' if in_ptype is None else in_ptype) 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) self._renderS(data=data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
return tree return tree
def planU(
self,
jog: float,
*,
length: float = 0,
in_ptype: str | None = None,
out_ptype: str | None = None,
**kwargs,
) -> tuple[Port, UData]:
ccw = jog > 0
kwargs_no_out = kwargs | {'out_ptype': None}
# Use loops to find a combination of straights and bends that fits
success = False
for _straight1 in self.straights:
for _bend1 in self.bends:
for straight2 in self.straights:
for _bend2 in self.bends:
try:
# We need to know R1 and R2 to calculate the lengths.
# Use large dummy lengths to probe the bends.
p_probe1, _ = self.planL(ccw, 1e9, in_ptype=in_ptype, **kwargs_no_out)
R1 = abs(Port((0, 0), 0).measure_travel(p_probe1)[0][1])
p_probe2, _ = self.planL(ccw, 1e9, in_ptype=p_probe1.ptype, out_ptype=out_ptype, **kwargs)
R2 = abs(Port((0, 0), 0).measure_travel(p_probe2)[0][1])
# Final x will be: x = l1_straight + R1 - R2
# We want final x = length. So: l1_straight = length - R1 + R2
# Total length for planL(0) is l1 = l1_straight + R1 = length + R2
l1 = length + R2
# Final y will be: y = R1 + l2_straight + R2 = abs(jog)
# So: l2_straight = abs(jog) - R1 - R2
l2_length = abs(jog) - R1 - R2
if l2_length >= straight2.length_range[0] and l2_length < straight2.length_range[1]:
p0, ldata0 = self.planL(ccw, l1, in_ptype=in_ptype, **kwargs_no_out)
# For the second bend, we want straight length = 0.
# Total length for planL(1) is l2 = 0 + R2 = R2.
p1, ldata1 = self.planL(ccw, R2, in_ptype=p0.ptype, out_ptype=out_ptype, **kwargs)
success = True
break
except BuildError:
continue
if success:
break
if success:
break
if success:
break
if not success:
raise BuildError(f"AutoTool failed to plan U-turn with {jog=}, {length=}")
data = self.UData(ldata0, ldata1, straight2, l2_length)
# Final port is at (length, jog) rot pi relative to input
out_port = Port((length, jog), rotation=pi, ptype=p1.ptype)
return out_port, data
def _renderU(
self,
data: UData,
tree: ILibrary,
port_names: tuple[str, str],
gen_kwargs: dict[str, Any],
) -> ILibrary:
pat = tree.top_pattern()
# 1. First L-bend
self._renderL(data.ldata0, tree, port_names, gen_kwargs)
# 2. Connecting straight
if not numpy.isclose(data.l2_length, 0):
s2_pat_or_tree = data.straight2.fn(data.l2_length, **(gen_kwargs | data.ldata0.straight_kwargs))
pmap = {port_names[1]: data.straight2.in_port_name}
if isinstance(s2_pat_or_tree, Pattern):
pat.plug(s2_pat_or_tree, pmap, append=True)
else:
s2_tree = s2_pat_or_tree
top = s2_tree.top()
s2_tree.flatten(top, dangling_ok=True)
pat.plug(s2_tree[top], pmap, append=True)
# 3. Second L-bend
self._renderL(data.ldata1, tree, port_names, gen_kwargs)
return tree
def 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( def render(
self, self,
batch: Sequence[RenderStep], batch: Sequence[RenderStep],
@ -874,7 +1066,7 @@ class AutoTool(Tool, metaclass=ABCMeta):
**kwargs, **kwargs,
) -> ILibrary: ) -> ILibrary:
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path') tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL')
pat.add_port_pair(names=(port_names[0], port_names[1])) pat.add_port_pair(names=(port_names[0], port_names[1]))
for step in batch: for step in batch:
@ -883,6 +1075,8 @@ class AutoTool(Tool, metaclass=ABCMeta):
self._renderL(data=step.data, tree=tree, port_names=port_names, straight_kwargs=kwargs) self._renderL(data=step.data, tree=tree, port_names=port_names, straight_kwargs=kwargs)
elif step.opcode == 'S': elif step.opcode == 'S':
self._renderS(data=step.data, tree=tree, port_names=port_names, gen_kwargs=kwargs) self._renderS(data=step.data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
elif step.opcode == 'U':
self._renderU(data=step.data, tree=tree, port_names=port_names, gen_kwargs=kwargs)
return tree return tree
@ -913,7 +1107,7 @@ class PathTool(Tool, metaclass=ABCMeta):
# self.width = width # self.width = width
# self.ptype: str # self.ptype: str
def path( def traceL(
self, self,
ccw: SupportsBool | None, ccw: SupportsBool | None,
length: float, length: float,
@ -930,7 +1124,7 @@ class PathTool(Tool, metaclass=ABCMeta):
out_ptype=out_ptype, out_ptype=out_ptype,
) )
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path') tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL')
pat.path(layer=self.layer, width=self.width, vertices=[(0, 0), (length, 0)]) pat.path(layer=self.layer, width=self.width, vertices=[(0, 0), (length, 0)])
if ccw is None: if ccw is None:
@ -991,29 +1185,44 @@ class PathTool(Tool, metaclass=ABCMeta):
**kwargs, # noqa: ARG002 (unused) **kwargs, # noqa: ARG002 (unused)
) -> ILibrary: ) -> ILibrary:
path_vertices = [batch[0].start_port.offset] # Transform the batch so the first port is local (at 0,0) but retains its global rotation.
for step in batch: # 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:
assert step.tool == self assert step.tool == self
port_rot = step.start_port.rotation 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 assert port_rot is not None
if step.opcode == 'L': if step.opcode == 'L':
length, bend_run = step.data
length, _ = step.data
dxy = rotation_matrix_2d(port_rot + pi) @ (length, 0) dxy = rotation_matrix_2d(port_rot + pi) @ (length, 0)
#path_vertices.append(step.start_port.offset)
path_vertices.append(step.start_port.offset + dxy) path_vertices.append(step.start_port.offset + dxy)
else: else:
raise BuildError(f'Unrecognized opcode "{step.opcode}"') raise BuildError(f'Unrecognized opcode "{step.opcode}"')
if (path_vertices[-1] != batch[-1].end_port.offset).any(): # 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 the path ends in a bend, we need to add the final vertex # If the path ends in a bend, we need to add the final vertex
path_vertices.append(batch[-1].end_port.offset) path_vertices.append(local_batch[-1].end_port.offset)
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path') tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL')
pat.path(layer=self.layer, width=self.width, vertices=path_vertices) pat.path(layer=self.layer, width=self.width, vertices=path_vertices)
pat.ports = { pat.ports = {
port_names[0]: batch[0].start_port.copy().rotate(pi), port_names[0]: local_batch[0].start_port.copy().rotate(pi),
port_names[1]: batch[-1].end_port.copy().rotate(pi), port_names[1]: local_batch[-1].end_port.copy().rotate(pi),
} }
return tree return tree

View file

@ -18,23 +18,23 @@ def advanced_pather() -> tuple[Pather, PathTool, Library]:
def test_path_into_straight(advanced_pather: tuple[Pather, PathTool, Library]) -> None: def test_path_into_straight(advanced_pather: tuple[Pather, PathTool, Library]) -> None:
p, tool, lib = advanced_pather p, _tool, _lib = advanced_pather
# Facing ports # Facing ports
p.ports["src"] = Port((0, 0), 0, ptype="wire") # Facing East (into device) p.ports["src"] = Port((0, 0), 0, ptype="wire") # Facing East (into device)
# Forward (+pi relative to port) is West (-x). # Forward (+pi relative to port) is West (-x).
# Put destination at (-20, 0) pointing East (pi). # Put destination at (-20, 0) pointing East (pi).
p.ports["dst"] = Port((-20, 0), pi, ptype="wire") p.ports["dst"] = Port((-20, 0), pi, ptype="wire")
p.path_into("src", "dst") p.trace_into("src", "dst")
assert "src" not in p.ports assert "src" not in p.ports
assert "dst" not in p.ports assert "dst" not in p.ports
# Pather.path adds a Reference to the generated pattern # Pather._traceL adds a Reference to the generated pattern
assert len(p.pattern.refs) == 1 assert len(p.pattern.refs) == 1
def test_path_into_bend(advanced_pather: tuple[Pather, PathTool, Library]) -> None: 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). # Source at (0,0) rot 0 (facing East). Forward is West (-x).
p.ports["src"] = Port((0, 0), 0, ptype="wire") p.ports["src"] = Port((0, 0), 0, ptype="wire")
# Destination at (-20, -20) rot pi (facing West). Forward is East (+x). # 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). # Forward for South is North (+y).
p.ports["dst"] = Port((-20, -20), 3 * pi / 2, ptype="wire") p.ports["dst"] = Port((-20, -20), 3 * pi / 2, ptype="wire")
p.path_into("src", "dst") p.trace_into("src", "dst")
assert "src" not in p.ports assert "src" not in p.ports
assert "dst" not in p.ports assert "dst" not in p.ports
@ -52,35 +52,24 @@ def test_path_into_bend(advanced_pather: tuple[Pather, PathTool, Library]) -> No
def test_path_into_sbend(advanced_pather: tuple[Pather, PathTool, Library]) -> None: 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 # Facing but offset ports
p.ports["src"] = Port((0, 0), 0, ptype="wire") # Forward is West (-x) 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.ports["dst"] = Port((-20, -10), pi, ptype="wire") # Facing East (rot pi)
p.path_into("src", "dst") p.trace_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 "src" not in p.ports
assert "dst" not in p.ports assert "dst" not in p.ports
def test_path_into_thru(advanced_pather: tuple[Pather, PathTool, Library]) -> None: 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["src"] = Port((0, 0), 0, ptype="wire")
p.ports["dst"] = Port((-20, 0), pi, ptype="wire") p.ports["dst"] = Port((-20, 0), pi, ptype="wire")
p.ports["other"] = Port((10, 10), 0) p.ports["other"] = Port((10, 10), 0)
p.path_into("src", "dst", thru="other") p.trace_into("src", "dst", thru="other")
assert "src" in p.ports assert "src" in p.ports
assert_equal(p.ports["src"].offset, [10, 10]) assert_equal(p.ports["src"].offset, [10, 10])

View file

@ -54,11 +54,11 @@ def autotool_setup() -> tuple[Pather, AutoTool, Library]:
def test_autotool_transition(autotool_setup: tuple[Pather, AutoTool, Library]) -> None: 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. # Route m1 from an m2 port. Should trigger via.
# length 10. Via length is 1. So straight m1 should be 9. # length 10. Via length is 1. So straight m1 should be 9.
p.path("start", ccw=None, length=10) p.straight("start", 10)
# Start at (0,0) rot pi (facing West). # Start at (0,0) rot pi (facing West).
# Forward (+pi relative to port) is East (+x). # Forward (+pi relative to port) is East (+x).

View file

@ -24,7 +24,7 @@ def pather_setup() -> tuple[Pather, PathTool, Library]:
def test_pather_straight(pather_setup: tuple[Pather, PathTool, Library]) -> None: def test_pather_straight(pather_setup: tuple[Pather, PathTool, Library]) -> None:
p, tool, lib = pather_setup p, tool, lib = pather_setup
# Route 10um "forward" # Route 10um "forward"
p.path("start", ccw=None, length=10) p.straight("start", 10)
# port rot pi/2 (North). Travel +pi relative to port -> South. # port rot pi/2 (North). Travel +pi relative to port -> South.
assert_allclose(p.ports["start"].offset, [0, -10], atol=1e-10) 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). # Start (0,0) rot pi/2 (North).
# Path 10um "forward" (South), then turn Clockwise (ccw=False). # Path 10um "forward" (South), then turn Clockwise (ccw=False).
# Facing South, turn Right -> West. # Facing South, turn Right -> West.
p.path("start", ccw=False, length=10) p.cw("start", 10)
# PathTool.planL(ccw=False, length=10) returns out_port at (10, -1) relative to (0,0) rot 0. # 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): # 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 p, tool, lib = pather_setup
# start at (0,0) rot pi/2 (North) # start at (0,0) rot pi/2 (North)
# path "forward" (South) to y=-50 # path "forward" (South) to y=-50
p.path_to("start", ccw=None, y=-50) p.straight("start", y=-50)
assert_equal(p.ports["start"].offset, [0, -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") p.ports["B"] = Port((10, 0), pi / 2, ptype="wire")
# Path both "forward" (South) to y=-20 # Path both "forward" (South) to y=-20
p.mpath(["A", "B"], ccw=None, ymin=-20) p.straight(["A", "B"], ymin=-20)
assert_equal(p.ports["A"].offset, [0, -20]) assert_equal(p.ports["A"].offset, [0, -20])
assert_equal(p.ports["B"].offset, [10, -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: def test_pather_at_chaining(pather_setup: tuple[Pather, PathTool, Library]) -> None:
p, tool, lib = pather_setup p, tool, lib = pather_setup
# Fluent API test # Fluent API test
p.at("start").path(ccw=None, length=10).path(ccw=True, length=10) p.at("start").straight(10).ccw(10)
# 10um South -> (0, -10) rot pi/2 # 10um South -> (0, -10) rot pi/2
# then 10um South and turn CCW (Facing South, CCW is East) # 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 # 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() p.set_dead()
# Path with negative length (impossible for PathTool, would normally raise BuildError) # Path with negative length (impossible for PathTool, would normally raise BuildError)
p.path("in", None, -10) p.straight("in", -10)
# Port 'in' should be updated by dummy extension despite tool failure # 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. # 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) assert_allclose(p.ports["in"].offset, [10, 0], atol=1e-10)
# Downstream path should work correctly using the dummy port location # Downstream path should work correctly using the dummy port location
p.path("in", None, 20) p.straight("in", 20)
# 10 + (-20) = -10 # 10 + (-20) = -10
assert_allclose(p.ports["in"].offset, [-10, 0], atol=1e-10) assert_allclose(p.ports["in"].offset, [-10, 0], atol=1e-10)

View file

@ -0,0 +1,242 @@
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)

View file

@ -24,7 +24,7 @@ def rpather_setup() -> tuple[RenderPather, PathTool, Library]:
def test_renderpather_basic(rpather_setup: tuple[RenderPather, PathTool, Library]) -> None: def test_renderpather_basic(rpather_setup: tuple[RenderPather, PathTool, Library]) -> None:
rp, tool, lib = rpather_setup rp, tool, lib = rpather_setup
# Plan two segments # Plan two segments
rp.at("start").path(ccw=None, length=10).path(ccw=None, length=10) rp.at("start").straight(10).straight(10)
# Before rendering, no shapes in pattern # Before rendering, no shapes in pattern
assert not rp.pattern.has_shapes() 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: def test_renderpather_bend(rpather_setup: tuple[RenderPather, PathTool, Library]) -> None:
rp, tool, lib = rpather_setup rp, tool, lib = rpather_setup
# Plan straight then bend # Plan straight then bend
rp.at("start").path(ccw=None, length=10).path(ccw=False, length=10) rp.at("start").straight(10).cw(10)
rp.render() rp.render()
path_shape = cast("Path", rp.pattern.shapes[(1, 0)][0]) 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 rp, tool1, lib = rpather_setup
tool2 = PathTool(layer=(2, 0), width=4, ptype="wire") tool2 = PathTool(layer=(2, 0), width=4, ptype="wire")
rp.at("start").path(ccw=None, length=10) rp.at("start").straight(10)
rp.retool(tool2, keys=["start"]) rp.retool(tool2, keys=["start"])
rp.at("start").path(ccw=None, length=10) rp.at("start").straight(10)
rp.render() rp.render()
# Different tools should cause different batches/shapes # Different tools should cause different batches/shapes
@ -86,7 +86,7 @@ def test_renderpather_dead_ports() -> None:
rp.set_dead() rp.set_dead()
# Impossible path # Impossible path
rp.path("in", None, -10) rp.straight("in", -10)
# port_rot=0, forward is -x. path(-10) means moving -10 in -x direction -> +10 in x. # 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) assert_allclose(rp.ports["in"].offset, [10, 0], atol=1e-10)