Compare commits
No commits in common. "26e6a44559ed6c4394d88783657055811aecdc8e" and "4332cf14c05b03e789e00a5081fe4ae3acc85bd4" have entirely different histories.
26e6a44559
...
4332cf14c0
12 changed files with 538 additions and 1225 deletions
|
|
@ -277,6 +277,12 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -106,9 +106,7 @@ def map_layer(layer: layer_t) -> layer_t:
|
||||||
'M2': (20, 0),
|
'M2': (20, 0),
|
||||||
'V1': (30, 0),
|
'V1': (30, 0),
|
||||||
}
|
}
|
||||||
if isinstance(layer, str):
|
return layer_mapping.get(layer, layer)
|
||||||
return layer_mapping.get(layer, layer)
|
|
||||||
return layer
|
|
||||||
|
|
||||||
|
|
||||||
def prepare_tools() -> tuple[Library, Tool, Tool]:
|
def prepare_tools() -> tuple[Library, Tool, Tool]:
|
||||||
|
|
@ -226,17 +224,19 @@ def main() -> None:
|
||||||
|
|
||||||
# Path VCC forward (in this case south) and turn clockwise 90 degrees (ccw=False)
|
# 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.cw('VCC', 6_000)
|
pather.path('VCC', ccw=False, length=6_000)
|
||||||
|
|
||||||
# Now path VCC to x=0. This time, don't include any bend.
|
# Now path VCC to x=0. This time, don't include any bend (ccw=None).
|
||||||
# Note that if we tried y=0 here, we would get an error since the VCC port is facing in the x-direction.
|
# Note that if we tried y=0 here, we would get an error since the VCC port is facing in the x-direction.
|
||||||
pather.straight('VCC', x=0)
|
pather.path_to('VCC', ccw=None, x=0)
|
||||||
|
|
||||||
# Path GND forward by 5um, turning clockwise 90 degrees.
|
# Path GND forward by 5um, turning clockwise 90 degrees.
|
||||||
pather.cw('GND', 5_000)
|
# This time we use shorthand (bool(0) == False) and omit the parameter labels
|
||||||
|
# Note that although ccw=0 is equivalent to ccw=False, ccw=None is not!
|
||||||
|
pather.path('GND', 0, 5_000)
|
||||||
|
|
||||||
# This time, path GND until it matches the current x-coordinate of VCC. Don't place a bend.
|
# This time, path GND until it matches the current x-coordinate of VCC. Don't place a bend.
|
||||||
pather.straight('GND', x=pather['VCC'].offset[0])
|
pather.path_to('GND', None, x=pather['VCC'].offset[0])
|
||||||
|
|
||||||
# Now, start using M1_tool for GND.
|
# 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.ccw(['GND', 'VCC'], xmax=-10_000, spacing=5_000)
|
pather.mpath(['GND', 'VCC'], ccw=True, xmax=-10_000, spacing=5_000)
|
||||||
|
|
||||||
# Now use M1_tool as the default tool for all ports/signals.
|
# 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,34 +262,35 @@ 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.ccw(['GND', 'VCC'], emax=50_000, spacing=1_200)
|
pather.mpath(['GND', 'VCC'], ccw=True, emax=50_000, spacing=1_200)
|
||||||
|
|
||||||
# Make a U-turn with the bundle and expand back out to 4.5um wire pitch.
|
# Make a U-turn with the bundle and expand back out to 4.5um wire pitch.
|
||||||
# Here, emin specifies the travel distance for the shortest segment. For the first call
|
# Here, emin specifies the travel distance for the shortest segment. For the first mpath() call
|
||||||
# that applies to VCC, and for the second call, that applies to GND; the relative lengths of the
|
# that applies to VCC, and for teh second call, that applies to GND; the relative lengths of the
|
||||||
# segments depend on their starting positions and their ordering within the bundle.
|
# segments depend on their starting positions and their ordering within the bundle.
|
||||||
pather.cw(['GND', 'VCC'], emin=1_000, spacing=1_200)
|
pather.mpath(['GND', 'VCC'], ccw=False, emin=1_000, spacing=1_200)
|
||||||
pather.cw(['GND', 'VCC'], emin=2_000, spacing=4_500)
|
pather.mpath(['GND', 'VCC'], ccw=False, emin=2_000, spacing=4_500)
|
||||||
|
|
||||||
# Now, set the default tool back to M2_tool. Note that GND remains on M1 since it has been
|
# Now, set the default tool back to M2_tool. Note that GND remains on M1 since it has been
|
||||||
# explicitly assigned a tool.
|
# explicitly assigned a tool. We could `del pather.tools['GND']` to force it to use the default.
|
||||||
pather.retool(M2_tool)
|
pather.retool(M2_tool)
|
||||||
|
|
||||||
# Now path both ports to x=-28_000.
|
# Now path both ports to x=-28_000.
|
||||||
# With ccw=None, all ports stop at the same coordinate, and so specifying xmin= or xmax= is
|
# When ccw is not None, xmin constrains the trailing/innermost port to stop at the target x coordinate,
|
||||||
|
# However, with ccw=None, all ports stop at the same coordinate, and so specifying xmin= or xmax= is
|
||||||
# equivalent.
|
# equivalent.
|
||||||
pather.straight(['GND', 'VCC'], xmin=-28_000)
|
pather.mpath(['GND', 'VCC'], None, xmin=-28_000)
|
||||||
|
|
||||||
# Further extend VCC out to x=-50_000, and specify that we would like to get an output on M1.
|
# 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.straight('VCC', x=-50_000, out_ptype='m1wire')
|
pather.path_to('VCC', None, -50_000, out_ptype='m1wire')
|
||||||
|
|
||||||
# Now extend GND out to x=-50_000, using M2 for a portion of the path.
|
# 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.straight('GND', x=-40_000)
|
pather.path_to('GND', None, -40_000)
|
||||||
pather.straight('GND', x=-50_000)
|
pather.path_to('GND', None, -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
|
||||||
|
|
|
||||||
|
|
@ -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.cw('VCC', 6_000)
|
rpather.path('VCC', ccw=False, length=6_000)
|
||||||
rpather.straight('VCC', x=0)
|
rpather.path_to('VCC', ccw=None, x=0)
|
||||||
rpather.cw('GND', 5_000)
|
rpather.path('GND', 0, 5_000)
|
||||||
rpather.straight('GND', x=rpather.pattern['VCC'].x)
|
rpather.path_to('GND', None, x=rpather['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.ccw(['GND', 'VCC'], xmax=-10_000, spacing=5_000)
|
rpather.mpath(['GND', 'VCC'], ccw=True, 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.ccw(['GND', 'VCC'], emax=50_000, spacing=1_200)
|
rpather.mpath(['GND', 'VCC'], ccw=True, emax=50_000, spacing=1_200)
|
||||||
rpather.cw(['GND', 'VCC'], emin=1_000, spacing=1_200)
|
rpather.mpath(['GND', 'VCC'], ccw=False, emin=1_000, spacing=1_200)
|
||||||
rpather.cw(['GND', 'VCC'], emin=2_000, spacing=4_500)
|
rpather.mpath(['GND', 'VCC'], ccw=False, 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.straight(['GND', 'VCC'], xmin=-28_000)
|
rpather.mpath(['GND', 'VCC'], None, xmin=-28_000)
|
||||||
|
|
||||||
# Finally, since PathTool has no conception of transitions, we can't
|
# 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.straight('VCC', x=-50_000 + via_size)
|
rpather.path_to('VCC', None, -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
|
||||||
|
|
|
||||||
|
|
@ -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.traceL` functions provided by subclasses of `Tool` to build the actual patterns.
|
out to `Tool.path` functions provided by subclasses of `Tool` to build the actual patterns.
|
||||||
`Tool`s are assigned on a per-port basis and stored in `.tools`; a key of `None` represents
|
`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,10 +63,7 @@ class Pather(Builder, PatherMixin):
|
||||||
|
|
||||||
Examples: Adding to a pattern
|
Examples: Adding to a pattern
|
||||||
=============================
|
=============================
|
||||||
- `pather.straight('my_port', distance)` creates a straight wire with a length
|
- `pather.path('my_port', ccw=True, distance)` creates a "wire" for which the output
|
||||||
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.
|
||||||
|
|
@ -75,15 +72,22 @@ class Pather(Builder, PatherMixin):
|
||||||
there may be a significant width to the bend that is used to accomplish the 90 degree
|
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.trace_to('my_port', ccw=False, x=position)` creates a wire which starts at
|
- `pather.path('my_port', ccw=None, distance)` creates a straight wire with a length
|
||||||
|
of `distance` and `plug`s it into `'my_port'`.
|
||||||
|
|
||||||
|
- `pather.path_to('my_port', ccw=False, position)` creates a wire which starts at
|
||||||
`'my_port'` and has its output at the specified `position`, pointing 90 degrees
|
`'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.
|
output is not specified, so `position` takes the form of a single coordinate. To
|
||||||
|
ease debugging, position may be specified as `x=position` or `y=position` and an
|
||||||
|
error will be raised if the wrong coordinate is given.
|
||||||
|
|
||||||
- `pather.trace(['A', 'B', 'C'], ccw=True, spacing=spacing, xmax=position)` acts
|
- `pather.mpath(['A', 'B', 'C'], ..., spacing=spacing)` is a superset of `path`
|
||||||
on multiple ports simultaneously. Each port's wire is generated using its own
|
and `path_to` which can act on multiple ports simultaneously. Each port's wire is
|
||||||
`Tool` (or the default tool if left unspecified).
|
generated using its own `Tool` (or the default tool if left unspecified).
|
||||||
The output ports are spaced out by `spacing` along the input ports' axis.
|
The output ports are spaced out by `spacing` along the input ports' axis, unless
|
||||||
|
`ccw=None` is specified (i.e. no bends) in which case they all end at the same
|
||||||
|
destination coordinate.
|
||||||
|
|
||||||
- `pather.plug(wire, {'myport': 'A'})` places port 'A' of `wire` at 'myport'
|
- `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`,
|
||||||
|
|
@ -137,8 +141,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 `trace`/`trace_to`/etc.
|
to generate waveguide or wire segments when `path`/`path_to`/`mpath`
|
||||||
are called. Relies on `Tool.traceL` implementations.
|
are called. Relies on `Tool.path` 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
|
||||||
|
|
@ -209,7 +213,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 `trace`/`trace_to`).
|
or waveguides (via `path`/`path_to`/`mpath`).
|
||||||
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
|
||||||
|
|
@ -251,73 +255,7 @@ class Pather(Builder, PatherMixin):
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
def _traceU(
|
def path(
|
||||||
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,
|
||||||
|
|
@ -358,18 +296,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 _traceL() since device is dead')
|
logger.warning('Skipping geometry for path() 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.traceL(ccw, length, in_ptype=in_ptype, port_names=tool_port_names, **kwargs)
|
tree = tool.path(ccw, length, in_ptype=in_ptype, port_names=tool_port_names, **kwargs)
|
||||||
except (BuildError, NotImplementedError):
|
except (BuildError, NotImplementedError):
|
||||||
if not self._dead:
|
if not self._dead:
|
||||||
raise
|
raise
|
||||||
logger.warning("Tool traceL failed for dead pather. Using dummy extension.")
|
logger.warning("Tool path failed for dead pather. Using dummy extension.")
|
||||||
# Fallback for dead pather: manually update the port instead of plugging
|
# 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
|
||||||
|
|
@ -397,7 +335,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 _traceS(
|
def pathS(
|
||||||
self,
|
self,
|
||||||
portspec: str,
|
portspec: str,
|
||||||
length: float,
|
length: float,
|
||||||
|
|
@ -408,17 +346,20 @@ 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.
|
of traveling exactly `length` distance with an offset `jog` along the other axis (+ve jog is
|
||||||
|
left of direction of travel).
|
||||||
|
|
||||||
The wire will travel `length` distance along the port's axis, and exactly `jog`
|
The output port will have the same orientation as the source port (`portspec`).
|
||||||
distance in the perpendicular direction. The output port will have an orientation
|
|
||||||
identical to the input port.
|
This function attempts to use `tool.planS()`, but falls back to `tool.planL()` if the former
|
||||||
|
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.
|
||||||
length: The total distance from input to output, along the input's axis only.
|
jog: Total manhattan distance perpendicular to the direction of travel.
|
||||||
jog: Total distance perpendicular to the direction of travel. Positive values
|
Positive values are to the left of the direction of travel.
|
||||||
are to the left of the direction of travel.
|
length: The total manhattan distance from input to output, along the input's axis only.
|
||||||
|
(There may be a tool-dependent offset along the other axis.)
|
||||||
plug_into: If not None, attempts to plug the wire's output port into the provided
|
plug_into: If not None, attempts to plug the wire's output port into the provided
|
||||||
port on `self`.
|
port on `self`.
|
||||||
|
|
||||||
|
|
@ -436,29 +377,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 _traceS() since device is dead')
|
logger.warning('Skipping geometry for pathS() 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.traceS(length, jog, in_ptype=in_ptype, port_names=tool_port_names, **kwargs)
|
tree = tool.pathS(length, jog, in_ptype=in_ptype, port_names=tool_port_names, **kwargs)
|
||||||
except NotImplementedError:
|
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.traceL( ccw0, length / 2, port_names=tool_port_names, in_ptype=in_ptype, **kwargs_no_out)
|
t_tree0 = tool.path( ccw0, length / 2, port_names=tool_port_names, in_ptype=in_ptype, **kwargs_no_out)
|
||||||
t_pat0 = t_tree0.top_pattern()
|
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.traceL(not ccw0, abs(jog - jog0), port_names=tool_port_names, in_ptype=t_pat0[tool_port_names[1]].ptype, **kwargs)
|
t_tree1 = tool.path(not ccw0, abs(jog - jog0), port_names=tool_port_names, in_ptype=t_pat0[tool_port_names[1]].ptype, **kwargs)
|
||||||
t_pat1 = t_tree1.top_pattern()
|
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._traceL(portspec, ccw0, length - abs(jog1), **kwargs_no_out)
|
self.path(portspec, ccw0, length - abs(jog1), **kwargs_no_out)
|
||||||
self._traceL(portspec, not ccw0, abs(jog - jog0), **kwargs_plug)
|
self.path(portspec, not ccw0, abs(jog - jog0), **kwargs_plug)
|
||||||
except (BuildError, NotImplementedError):
|
except (BuildError, NotImplementedError):
|
||||||
if not self._dead:
|
if not self._dead:
|
||||||
raise
|
raise
|
||||||
|
|
@ -471,7 +412,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 traceS failed for dead pather. Using dummy extension.")
|
logger.warning("Tool pathS failed for dead pather. Using dummy extension.")
|
||||||
# Fallback for dead pather: manually update the port instead of plugging
|
# 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
|
||||||
|
|
@ -492,3 +433,4 @@ 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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from typing import Self, overload
|
from typing import Self, overload
|
||||||
from collections.abc import Sequence, Iterator, Iterable, Mapping
|
from collections.abc import Sequence, Iterator, Iterable
|
||||||
import logging
|
import logging
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from abc import abstractmethod, ABCMeta
|
from abc import abstractmethod, ABCMeta
|
||||||
|
|
@ -37,338 +37,8 @@ class PatherMixin(PortList, metaclass=ABCMeta):
|
||||||
(e.g wires or waveguides) to be plugged into this device.
|
(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 _traceL(
|
def path(
|
||||||
self,
|
self,
|
||||||
portspec: str,
|
portspec: str,
|
||||||
ccw: SupportsBool | None,
|
ccw: SupportsBool | None,
|
||||||
|
|
@ -380,7 +50,7 @@ class PatherMixin(PortList, metaclass=ABCMeta):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def _traceS(
|
def pathS(
|
||||||
self,
|
self,
|
||||||
portspec: str,
|
portspec: str,
|
||||||
length: float,
|
length: float,
|
||||||
|
|
@ -391,33 +61,6 @@ 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,
|
||||||
|
|
@ -433,11 +76,6 @@ 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,
|
||||||
|
|
@ -505,13 +143,88 @@ class PatherMixin(PortList, metaclass=ABCMeta):
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
"""
|
"""
|
||||||
[DEPRECATED] use trace_to() instead.
|
Build a "wire"/"waveguide" extending from the port `portspec`, with the aim
|
||||||
"""
|
of ending exactly at a target position.
|
||||||
import warnings
|
|
||||||
warnings.warn("path_to() is deprecated; use trace_to() instead", DeprecationWarning, stacklevel=2)
|
|
||||||
|
|
||||||
bounds = {kk: vv for kk, vv in (('position', position), ('x', x), ('y', y)) if vv is not None}
|
The wire will travel so that the output port will be placed at exactly the target
|
||||||
return self.trace_to(portspec, ccw, plug_into=plug_into, **bounds, **kwargs)
|
position along the input port's axis. There can be an unspecified (tool-dependent)
|
||||||
|
offset in the perpendicular direction. The output port will be rotated (or not)
|
||||||
|
based on the `ccw` parameter.
|
||||||
|
|
||||||
|
If using `RenderPather`, `RenderPather.render` must be called after all paths have been fully planned.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
portspec: The name of the port into which the wire will be plugged.
|
||||||
|
ccw: If `None`, the output should be along the same axis as the input.
|
||||||
|
Otherwise, cast to bool and turn counterclockwise if True
|
||||||
|
and clockwise otherwise.
|
||||||
|
position: The final port position, along the input's axis only.
|
||||||
|
(There may be a tool-dependent offset along the other axis.)
|
||||||
|
Only one of `position`, `x`, and `y` may be specified.
|
||||||
|
x: The final port position along the x axis.
|
||||||
|
`portspec` must refer to a horizontal port if `x` is passed, otherwise a
|
||||||
|
BuildError will be raised.
|
||||||
|
y: The final port position along the y axis.
|
||||||
|
`portspec` must refer to a vertical port if `y` is passed, otherwise a
|
||||||
|
BuildError will be raised.
|
||||||
|
plug_into: If not None, attempts to plug the wire's output port into the provided
|
||||||
|
port on `self`.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
BuildError if `position`, `x`, or `y` is too close to fit the bend (if a bend
|
||||||
|
is present).
|
||||||
|
BuildError if `x` or `y` is specified but does not match the axis of `portspec`.
|
||||||
|
BuildError if more than one of `x`, `y`, and `position` is specified.
|
||||||
|
"""
|
||||||
|
if self._dead:
|
||||||
|
logger.error('Skipping path_to() since device is dead')
|
||||||
|
return self
|
||||||
|
|
||||||
|
pos_count = sum(vv is not None for vv in (position, x, y))
|
||||||
|
if pos_count > 1:
|
||||||
|
raise BuildError('Only one of `position`, `x`, and `y` may be specified at once')
|
||||||
|
if pos_count < 1:
|
||||||
|
raise BuildError('One of `position`, `x`, and `y` must be specified')
|
||||||
|
|
||||||
|
port = self.pattern[portspec]
|
||||||
|
if port.rotation is None:
|
||||||
|
raise PortError(f'Port {portspec} has no rotation and cannot be used for path_to()')
|
||||||
|
|
||||||
|
if not numpy.isclose(port.rotation % (pi / 2), 0):
|
||||||
|
raise BuildError('path_to was asked to route from non-manhattan port')
|
||||||
|
|
||||||
|
is_horizontal = numpy.isclose(port.rotation % pi, 0)
|
||||||
|
if is_horizontal:
|
||||||
|
if y is not None:
|
||||||
|
raise BuildError('Asked to path to y-coordinate, but port is horizontal')
|
||||||
|
if position is None:
|
||||||
|
position = x
|
||||||
|
else:
|
||||||
|
if x is not None:
|
||||||
|
raise BuildError('Asked to path to x-coordinate, but port is vertical')
|
||||||
|
if position is None:
|
||||||
|
position = y
|
||||||
|
|
||||||
|
x0, y0 = port.offset
|
||||||
|
if is_horizontal:
|
||||||
|
if numpy.sign(numpy.cos(port.rotation)) == numpy.sign(position - x0):
|
||||||
|
raise BuildError(f'path_to routing to behind source port: x0={x0:g} to {position:g}')
|
||||||
|
length = numpy.abs(position - x0)
|
||||||
|
else:
|
||||||
|
if numpy.sign(numpy.sin(port.rotation)) == numpy.sign(position - y0):
|
||||||
|
raise BuildError(f'path_to routing to behind source port: y0={y0:g} to {position:g}')
|
||||||
|
length = numpy.abs(position - y0)
|
||||||
|
|
||||||
|
return self.path(
|
||||||
|
portspec,
|
||||||
|
ccw,
|
||||||
|
length,
|
||||||
|
plug_into = plug_into,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
def path_into(
|
def path_into(
|
||||||
self,
|
self,
|
||||||
|
|
@ -524,19 +237,100 @@ class PatherMixin(PortList, metaclass=ABCMeta):
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
"""
|
"""
|
||||||
[DEPRECATED] use trace_into() instead.
|
Create a "wire"/"waveguide" traveling between the ports `portspec_src` and
|
||||||
"""
|
`portspec_dst`, and `plug` it into both (or just the source port).
|
||||||
import warnings
|
|
||||||
warnings.warn("path_into() is deprecated; use trace_into() instead", DeprecationWarning, stacklevel=2)
|
|
||||||
|
|
||||||
return self.trace_into(
|
Only unambiguous scenarios are allowed:
|
||||||
portspec_src,
|
- Straight connector between facing ports
|
||||||
portspec_dst,
|
- Single 90 degree bend
|
||||||
out_ptype = out_ptype,
|
- Jog between facing ports
|
||||||
plug_destination = plug_destination,
|
(jog is done as late as possible, i.e. only 2 L-shaped segments are used)
|
||||||
thru = thru,
|
|
||||||
**kwargs,
|
By default, the destination's `pytpe` will be used as the `out_ptype` for the
|
||||||
)
|
wire, and the `portspec_dst` will be plugged (i.e. removed).
|
||||||
|
|
||||||
|
If using `RenderPather`, `RenderPather.render` must be called after all paths have been fully planned.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
portspec_src: The name of the starting port into which the wire will be plugged.
|
||||||
|
portspec_dst: The name of the destination port.
|
||||||
|
out_ptype: Passed to the pathing tool in order to specify the desired port type
|
||||||
|
to be generated at the destination end. If `None` (default), the destination
|
||||||
|
port's `ptype` will be used.
|
||||||
|
thru: If not `None`, the port by this name will be rename to `portspec_src`.
|
||||||
|
This can be used when routing a signal through a pre-placed 2-port device.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
PortError if either port does not have a specified rotation.
|
||||||
|
BuildError if and invalid port config is encountered:
|
||||||
|
- Non-manhattan ports
|
||||||
|
- U-bend
|
||||||
|
- Destination too close to (or behind) source
|
||||||
|
"""
|
||||||
|
if self._dead:
|
||||||
|
logger.error('Skipping path_into() since device is dead')
|
||||||
|
return self
|
||||||
|
|
||||||
|
port_src = self.pattern[portspec_src]
|
||||||
|
port_dst = self.pattern[portspec_dst]
|
||||||
|
|
||||||
|
if out_ptype is None:
|
||||||
|
out_ptype = port_dst.ptype
|
||||||
|
|
||||||
|
if port_src.rotation is None:
|
||||||
|
raise PortError(f'Port {portspec_src} has no rotation and cannot be used for path_into()')
|
||||||
|
if port_dst.rotation is None:
|
||||||
|
raise PortError(f'Port {portspec_dst} has no rotation and cannot be used for path_into()')
|
||||||
|
|
||||||
|
if not numpy.isclose(port_src.rotation % (pi / 2), 0):
|
||||||
|
raise BuildError('path_into was asked to route from non-manhattan port')
|
||||||
|
if not numpy.isclose(port_dst.rotation % (pi / 2), 0):
|
||||||
|
raise BuildError('path_into was asked to route to non-manhattan port')
|
||||||
|
|
||||||
|
src_is_horizontal = numpy.isclose(port_src.rotation % pi, 0)
|
||||||
|
dst_is_horizontal = numpy.isclose(port_dst.rotation % pi, 0)
|
||||||
|
xs, ys = port_src.offset
|
||||||
|
xd, yd = port_dst.offset
|
||||||
|
|
||||||
|
angle = (port_dst.rotation - port_src.rotation) % (2 * pi)
|
||||||
|
|
||||||
|
dst_extra_args = {'out_ptype': out_ptype}
|
||||||
|
if plug_destination:
|
||||||
|
dst_extra_args['plug_into'] = portspec_dst
|
||||||
|
|
||||||
|
src_args = {**kwargs}
|
||||||
|
dst_args = {**src_args, **dst_extra_args}
|
||||||
|
if src_is_horizontal and not dst_is_horizontal:
|
||||||
|
# single bend should suffice
|
||||||
|
self.path_to(portspec_src, angle > pi, x=xd, **src_args)
|
||||||
|
self.path_to(portspec_src, None, y=yd, **dst_args)
|
||||||
|
elif dst_is_horizontal and not src_is_horizontal:
|
||||||
|
# single bend should suffice
|
||||||
|
self.path_to(portspec_src, angle > pi, y=yd, **src_args)
|
||||||
|
self.path_to(portspec_src, None, x=xd, **dst_args)
|
||||||
|
elif numpy.isclose(angle, pi):
|
||||||
|
if src_is_horizontal and ys == yd:
|
||||||
|
# straight connector
|
||||||
|
self.path_to(portspec_src, None, x=xd, **dst_args)
|
||||||
|
elif not src_is_horizontal and xs == xd:
|
||||||
|
# straight connector
|
||||||
|
self.path_to(portspec_src, None, y=yd, **dst_args)
|
||||||
|
else:
|
||||||
|
# S-bend, delegate to implementations
|
||||||
|
(travel, jog), _ = port_src.measure_travel(port_dst)
|
||||||
|
self.pathS(portspec_src, -travel, -jog, **dst_args)
|
||||||
|
elif numpy.isclose(angle, 0):
|
||||||
|
raise BuildError('Don\'t know how to route a U-bend yet (TODO)!')
|
||||||
|
else:
|
||||||
|
raise BuildError(f'Don\'t know how to route ports with relative angle {angle}')
|
||||||
|
|
||||||
|
if thru is not None:
|
||||||
|
self.rename_ports({thru: portspec_src})
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
def mpath(
|
def mpath(
|
||||||
self,
|
self,
|
||||||
|
|
@ -548,12 +342,109 @@ class PatherMixin(PortList, metaclass=ABCMeta):
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
"""
|
"""
|
||||||
[DEPRECATED] use trace() or trace_to() instead.
|
`mpath` is a superset of `path` and `path_to` which can act on bundles or buses
|
||||||
"""
|
of "wires or "waveguides".
|
||||||
import warnings
|
|
||||||
warnings.warn("mpath() is deprecated; use trace() or trace_to() instead", DeprecationWarning, stacklevel=2)
|
|
||||||
|
|
||||||
return self.trace(portspec, ccw, spacing=spacing, set_rotation=set_rotation, **kwargs)
|
The wires will travel so that the output ports will be placed at well-defined
|
||||||
|
locations along the axis of their input ports, but may have arbitrary (tool-
|
||||||
|
dependent) offsets in the perpendicular direction.
|
||||||
|
|
||||||
|
If `ccw` is not `None`, the wire bundle will turn 90 degres in either the
|
||||||
|
clockwise (`ccw=False`) or counter-clockwise (`ccw=True`) direction. Within the
|
||||||
|
bundle, the center-to-center wire spacings after the turn are set by `spacing`,
|
||||||
|
which is required when `ccw` is not `None`. The final position of bundle as a
|
||||||
|
whole can be set in a number of ways:
|
||||||
|
|
||||||
|
=A>---------------------------V turn direction: `ccw=False`
|
||||||
|
=B>-------------V |
|
||||||
|
=C>-----------------------V |
|
||||||
|
=D=>----------------V |
|
||||||
|
|
|
||||||
|
|
||||||
|
x---x---x---x `spacing` (can be scalar or array)
|
||||||
|
|
||||||
|
<--------------> `emin=`
|
||||||
|
<------> `bound_type='min_past_furthest', bound=`
|
||||||
|
<--------------------------------> `emax=`
|
||||||
|
x `pmin=`
|
||||||
|
x `pmax=`
|
||||||
|
|
||||||
|
- `emin=`, equivalent to `bound_type='min_extension', bound=`
|
||||||
|
The total extension value for the furthest-out port (B in the diagram).
|
||||||
|
- `emax=`, equivalent to `bound_type='max_extension', bound=`:
|
||||||
|
The total extension value for the closest-in port (C in the diagram).
|
||||||
|
- `pmin=`, equivalent to `xmin=`, `ymin=`, or `bound_type='min_position', bound=`:
|
||||||
|
The coordinate of the innermost bend (D's bend).
|
||||||
|
The x/y versions throw an error if they do not match the port axis (for debug)
|
||||||
|
- `pmax=`, `xmax=`, `ymax=`, or `bound_type='max_position', bound=`:
|
||||||
|
The coordinate of the outermost bend (A's bend).
|
||||||
|
The x/y versions throw an error if they do not match the port axis (for debug)
|
||||||
|
- `bound_type='min_past_furthest', bound=`:
|
||||||
|
The distance between furthest out-port (B) and the innermost bend (D's bend).
|
||||||
|
|
||||||
|
If `ccw=None`, final output positions (along the input axis) of all wires will be
|
||||||
|
identical (i.e. wires will all be cut off evenly). In this case, `spacing=None` is
|
||||||
|
required. In this case, `emin=` and `emax=` are equivalent to each other, and
|
||||||
|
`pmin=`, `pmax=`, `xmin=`, etc. are also equivalent to each other.
|
||||||
|
|
||||||
|
If using `RenderPather`, `RenderPather.render` must be called after all paths have been fully planned.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
portspec: The names of the ports which are to be routed.
|
||||||
|
ccw: If `None`, the outputs should be along the same axis as the inputs.
|
||||||
|
Otherwise, cast to bool and turn 90 degrees counterclockwise if `True`
|
||||||
|
and clockwise otherwise.
|
||||||
|
spacing: Center-to-center distance between output ports along the input port's axis.
|
||||||
|
Must be provided if (and only if) `ccw` is not `None`.
|
||||||
|
set_rotation: If the provided ports have `rotation=None`, this can be used
|
||||||
|
to set a rotation for them.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
BuildError if the implied length for any wire is too close to fit the bend
|
||||||
|
(if a bend is requested).
|
||||||
|
BuildError if `xmin`/`xmax` or `ymin`/`ymax` is specified but does not
|
||||||
|
match the axis of `portspec`.
|
||||||
|
BuildError if an incorrect bound type or spacing is specified.
|
||||||
|
"""
|
||||||
|
if self._dead:
|
||||||
|
logger.error('Skipping mpath() since device is dead')
|
||||||
|
return self
|
||||||
|
|
||||||
|
bound_types = set()
|
||||||
|
if 'bound_type' in kwargs:
|
||||||
|
bound_types.add(kwargs.pop('bound_type'))
|
||||||
|
bound = kwargs.pop('bound')
|
||||||
|
for bt in ('emin', 'emax', 'pmin', 'pmax', 'xmin', 'xmax', 'ymin', 'ymax', 'min_past_furthest'):
|
||||||
|
if bt in kwargs:
|
||||||
|
bound_types.add(bt)
|
||||||
|
bound = kwargs.pop(bt)
|
||||||
|
|
||||||
|
if not bound_types:
|
||||||
|
raise BuildError('No bound type specified for mpath')
|
||||||
|
if len(bound_types) > 1:
|
||||||
|
raise BuildError(f'Too many bound types specified for mpath: {bound_types}')
|
||||||
|
bound_type = tuple(bound_types)[0]
|
||||||
|
|
||||||
|
if isinstance(portspec, str):
|
||||||
|
portspec = [portspec]
|
||||||
|
ports = self.pattern[tuple(portspec)]
|
||||||
|
|
||||||
|
extensions = ell(ports, ccw, spacing=spacing, bound=bound, bound_type=bound_type, set_rotation=set_rotation)
|
||||||
|
|
||||||
|
#if container:
|
||||||
|
# assert not getattr(self, 'render'), 'Containers not implemented for RenderPather'
|
||||||
|
# bld = self.interface(source=ports, library=self.library, tools=self.tools)
|
||||||
|
# for port_name, length in extensions.items():
|
||||||
|
# bld.path(port_name, ccw, length, **kwargs)
|
||||||
|
# self.library[container] = bld.pattern
|
||||||
|
# self.plug(Abstract(container, bld.pattern.ports), {sp: 'in_' + sp for sp in ports}) # TODO safe to use 'in_'?
|
||||||
|
#else:
|
||||||
|
for port_name, length in extensions.items():
|
||||||
|
self.path(port_name, ccw, length, **kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
# TODO def bus_join()?
|
# TODO def bus_join()?
|
||||||
|
|
||||||
|
|
@ -597,42 +488,61 @@ class PortPather:
|
||||||
with self.pather.toolctx(tool, keys=self.ports):
|
with self.pather.toolctx(tool, keys=self.ports):
|
||||||
yield self
|
yield self
|
||||||
|
|
||||||
def trace(self, ccw: SupportsBool | None, length: float | None = None, **kwargs) -> Self:
|
def path(self, *args, **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:
|
||||||
raise BuildError(f'Unable use implicit trace_into() with {len(self.ports)} (>1) ports.')
|
logger.warning('Use path_each() when pathing multiple ports independently')
|
||||||
self.pather.trace_into(self.ports[0], target_port, **kwargs)
|
for port in self.ports:
|
||||||
|
self.pather.path(port, *args, **kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def path_each(self, *args, **kwargs) -> Self:
|
||||||
|
for port in self.ports:
|
||||||
|
self.pather.path(port, *args, **kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def pathS(self, *args, **kwargs) -> Self:
|
||||||
|
if len(self.ports) > 1:
|
||||||
|
logger.warning('Use pathS_each() when pathing multiple ports independently')
|
||||||
|
for port in self.ports:
|
||||||
|
self.pather.pathS(port, *args, **kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def pathS_each(self, *args, **kwargs) -> Self:
|
||||||
|
for port in self.ports:
|
||||||
|
self.pather.pathS(port, *args, **kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def path_to(self, *args, **kwargs) -> Self:
|
||||||
|
if len(self.ports) > 1:
|
||||||
|
logger.warning('Use path_each_to() when pathing multiple ports independently')
|
||||||
|
for port in self.ports:
|
||||||
|
self.pather.path_to(port, *args, **kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def path_each_to(self, *args, **kwargs) -> Self:
|
||||||
|
for port in self.ports:
|
||||||
|
self.pather.path_to(port, *args, **kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def mpath(self, *args, **kwargs) -> Self:
|
||||||
|
self.pather.mpath(self.ports, *args, **kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def path_into(self, *args, **kwargs) -> Self:
|
||||||
|
""" Path_into, using the current port as the source """
|
||||||
|
if len(self.ports) > 1:
|
||||||
|
raise BuildError(f'Unable use implicit path_into() with {len(self.ports)} (>1) ports.')
|
||||||
|
self.pather.path_into(self.ports[0], *args, **kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def path_from(self, *args, **kwargs) -> Self:
|
||||||
|
""" Path_into, using the current port as the destination """
|
||||||
|
if len(self.ports) > 1:
|
||||||
|
raise BuildError(f'Unable use implicit path_from() with {len(self.ports)} (>1) ports.')
|
||||||
|
thru = kwargs.pop('thru', None)
|
||||||
|
self.pather.path_into(args[0], self.ports[0], *args[1:], **kwargs)
|
||||||
|
if thru is not None:
|
||||||
|
self.rename_from(thru)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def plug(
|
def plug(
|
||||||
|
|
@ -648,13 +558,10 @@ 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 | Mapping[str, str]) -> Self:
|
def plugged(self, other_port: str) -> Self:
|
||||||
if isinstance(other_port, Mapping):
|
if len(self.ports) > 1:
|
||||||
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.')
|
||||||
else:
|
self.pather.plugged({self.ports[0]: other_port})
|
||||||
self.pather.plugged({self.ports[0]: other_port})
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|
@ -662,91 +569,95 @@ 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.pattern[port].set_ptype(ptype)
|
self.pather[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.pattern[port].translate(*args, **kwargs)
|
self.pather[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.pattern[port].mirror(*args, **kwargs)
|
self.pather[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.pattern[port].rotate(rotation)
|
self.pather[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.pattern[port].set_rotation(rotation)
|
self.pather[port].set_rotation(rotation)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def rename(self, name: str | Mapping[str, str | None]) -> Self:
|
def rename_to(self, new_name: str) -> Self:
|
||||||
""" Rename active ports. Replaces `rename_to`. """
|
if len(self.ports) > 1:
|
||||||
name_map: dict[str, str | None]
|
BuildError('Use rename_ports() for >1 port')
|
||||||
if isinstance(name, str):
|
self.pather.rename_ports({self.ports[0]: new_name})
|
||||||
if len(self.ports) > 1:
|
self.ports[0] = new_name
|
||||||
raise BuildError('Use a mapping to rename >1 port')
|
return self
|
||||||
name_map = {self.ports[0]: name}
|
|
||||||
else:
|
def rename_from(self, old_name: str) -> Self:
|
||||||
name_map = dict(name)
|
if len(self.ports) > 1:
|
||||||
|
BuildError('Use rename_ports() for >1 port')
|
||||||
|
self.pather.rename_ports({old_name: self.ports[0]})
|
||||||
|
return self
|
||||||
|
|
||||||
|
def rename_ports(self, name_map: dict[str, str | None]) -> Self:
|
||||||
self.pather.rename_ports(name_map)
|
self.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 select(self, ports: str | Iterable[str]) -> Self:
|
def add_ports(self, ports: Iterable[str]) -> Self:
|
||||||
""" Add ports to the selection. Replaces `add_ports`. """
|
ports = list(ports)
|
||||||
if isinstance(ports, str):
|
conflicts = set(ports) & set(self.ports)
|
||||||
ports = [ports]
|
if conflicts:
|
||||||
for port in ports:
|
raise BuildError(f'ports {conflicts} already selected')
|
||||||
if port not in self.ports:
|
self.ports += ports
|
||||||
self.ports.append(port)
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def deselect(self, ports: str | Iterable[str]) -> Self:
|
def add_port(self, port: str, index: int | None = None) -> Self:
|
||||||
""" Remove ports from the selection. Replaces `drop_port`. """
|
if port in self.ports:
|
||||||
if isinstance(ports, str):
|
raise BuildError(f'{port=} already selected')
|
||||||
ports = [ports]
|
if index is not None:
|
||||||
ports_set = set(ports)
|
self.ports.insert(index, port)
|
||||||
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:
|
||||||
name_map = name
|
self.ports.append(port)
|
||||||
for src, dst in name_map.items():
|
|
||||||
self.pather.pattern.ports[dst] = self.pather.pattern[src].copy()
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def fork(self, name: str | Mapping[str, str]) -> Self:
|
def drop_port(self, port: str) -> Self:
|
||||||
""" Split and follow new name. Replaces `into_copy`. """
|
if port not in self.ports:
|
||||||
name_map: Mapping[str, str]
|
raise BuildError(f'{port=} already not selected')
|
||||||
if isinstance(name, str):
|
self.ports = [pp for pp in self.ports if pp != port]
|
||||||
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 drop(self) -> Self:
|
def into_copy(self, new_name: str, src: str | None = None) -> Self:
|
||||||
""" Remove selected ports from the pattern and the PortPather. Replaces `delete(None)`. """
|
""" Copy a port and replace it with the copy """
|
||||||
for pp in self.ports:
|
if not self.ports:
|
||||||
del self.pather.pattern.ports[pp]
|
raise BuildError('Have no ports to copy')
|
||||||
self.ports = []
|
if len(self.ports) == 1:
|
||||||
|
src = self.ports[0]
|
||||||
|
elif src is None:
|
||||||
|
raise BuildError('Must specify src when >1 port is available')
|
||||||
|
if src not in self.ports:
|
||||||
|
raise BuildError(f'{src=} not available')
|
||||||
|
self.pather.ports[new_name] = self.pather[src].copy()
|
||||||
|
self.ports = [(new_name if pp == src else pp) for pp in self.ports]
|
||||||
|
return self
|
||||||
|
|
||||||
|
def save_copy(self, new_name: str, src: str | None = None) -> Self:
|
||||||
|
""" Copy a port and but keep using the original """
|
||||||
|
if not self.ports:
|
||||||
|
raise BuildError('Have no ports to copy')
|
||||||
|
if len(self.ports) == 1:
|
||||||
|
src = self.ports[0]
|
||||||
|
elif src is None:
|
||||||
|
raise BuildError('Must specify src when >1 port is available')
|
||||||
|
if src not in self.ports:
|
||||||
|
raise BuildError(f'{src=} not available')
|
||||||
|
self.pather.ports[new_name] = self.pather[src].copy()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
|
|
@ -757,8 +668,10 @@ 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:
|
||||||
self.drop()
|
for pp in self.ports:
|
||||||
|
del self.pather.ports[pp]
|
||||||
return None
|
return None
|
||||||
del self.pather.pattern.ports[name]
|
del self.pather.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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,8 @@ 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, NDArray
|
from numpy.typing import ArrayLike
|
||||||
|
|
||||||
from ..pattern import Pattern
|
from ..pattern import Pattern
|
||||||
from ..library import ILibrary, TreeView
|
from ..library import ILibrary, TreeView
|
||||||
|
|
@ -28,7 +27,7 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class RenderPather(PatherMixin):
|
class RenderPather(PatherMixin):
|
||||||
"""
|
"""
|
||||||
`RenderPather` is an alternative to `Pather` which uses the `trace`/`trace_to`
|
`RenderPather` is an alternative to `Pather` which uses the `path`/`path_to`/`mpath`
|
||||||
functions to plan out wire paths without incrementally generating the layout. Instead,
|
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
|
||||||
|
|
@ -97,7 +96,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 `trace`/`trace_to`
|
to generate waveguide or wire segments when `path`/`path_to`/`mpath`
|
||||||
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`.
|
||||||
"""
|
"""
|
||||||
|
|
@ -150,7 +149,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 `trace`/`trace_to`).
|
or waveguides (via `path`/`path_to`/`mpath`).
|
||||||
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
|
||||||
|
|
@ -377,67 +376,7 @@ class RenderPather(PatherMixin):
|
||||||
PortList.plugged(self, connections)
|
PortList.plugged(self, connections)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def _traceU(
|
def path(
|
||||||
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,
|
||||||
|
|
@ -480,7 +419,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 _traceL() since device is dead')
|
logger.warning('Skipping geometry for path() since device is dead')
|
||||||
|
|
||||||
port = self.pattern[portspec]
|
port = self.pattern[portspec]
|
||||||
in_ptype = port.ptype
|
in_ptype = port.ptype
|
||||||
|
|
@ -520,7 +459,7 @@ class RenderPather(PatherMixin):
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def _traceS(
|
def pathS(
|
||||||
self,
|
self,
|
||||||
portspec: str,
|
portspec: str,
|
||||||
length: float,
|
length: float,
|
||||||
|
|
@ -564,7 +503,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 _traceS() since device is dead')
|
logger.warning('Skipping geometry for pathS() since device is dead')
|
||||||
|
|
||||||
port = self.pattern[portspec]
|
port = self.pattern[portspec]
|
||||||
in_ptype = port.ptype
|
in_ptype = port.ptype
|
||||||
|
|
@ -587,8 +526,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._traceL(portspec, ccw0, length - abs(jog1), **kwargs_no_out)
|
self.path(portspec, ccw0, length - abs(jog1), **kwargs_no_out)
|
||||||
self._traceL(portspec, not ccw0, abs(jog - jog0), **kwargs_plug)
|
self.path(portspec, not ccw0, abs(jog - jog0), **kwargs_plug)
|
||||||
except (BuildError, NotImplementedError):
|
except (BuildError, NotImplementedError):
|
||||||
if not self._dead:
|
if not self._dead:
|
||||||
raise
|
raise
|
||||||
|
|
@ -624,7 +563,7 @@ class RenderPather(PatherMixin):
|
||||||
append: bool = True,
|
append: bool = True,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
"""
|
"""
|
||||||
Generate the geometry which has been planned out with `trace`/`trace_to`/etc.
|
Generate the geometry which has been planned out with `path`/`path_to`/etc.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
append: If `True`, the rendered geometry will be directly appended to
|
append: If `True`, the rendered geometry will be directly appended to
|
||||||
|
|
@ -640,54 +579,22 @@ class RenderPather(PatherMixin):
|
||||||
|
|
||||||
def render_batch(portspec: str, batch: list[RenderStep], append: bool) -> None:
|
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
|
||||||
# Tools render in local space (first port at 0,0, rotation 0).
|
name = lib << batch[0].tool.render(batch, port_names=tool_port_names)
|
||||||
tree = batch[0].tool.render(batch, port_names=tool_port_names)
|
pat.ports[portspec] = batch[0].start_port.copy()
|
||||||
|
|
||||||
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() translates and rotates the tool's local output to the start port.
|
pat.plug(lib[name], {portspec: tool_port_names[0]}, append=append)
|
||||||
pat.plug(lib[name], {portspec: actual_in}, append=append)
|
del lib[name] # NOTE if the rendered pattern has refs, those are now in `pat` but not flattened
|
||||||
del lib[name]
|
|
||||||
else:
|
else:
|
||||||
pat.plug(lib.abstract(name), {portspec: actual_in}, append=append)
|
pat.plug(lib.abstract(name), {portspec: tool_port_names[0]}, 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 or not continuous):
|
if batch and (not appendable_op or not same_tool):
|
||||||
render_batch(portspec, batch, append)
|
render_batch(portspec, batch, append)
|
||||||
batch = []
|
batch = []
|
||||||
|
|
||||||
|
|
@ -696,14 +603,8 @@ 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:
|
if not appendable_op and portspec in pat.ports:
|
||||||
if portspec in pat.ports:
|
del pat.ports[portspec]
|
||||||
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:
|
||||||
|
|
@ -725,11 +626,7 @@ class RenderPather(PatherMixin):
|
||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
offset_arr: NDArray[numpy.float64] = numpy.asarray(offset)
|
self.pattern.translate_elements(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:
|
||||||
|
|
@ -743,11 +640,7 @@ class RenderPather(PatherMixin):
|
||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
pivot_arr: NDArray[numpy.float64] = numpy.asarray(pivot)
|
self.pattern.rotate_around(pivot, angle)
|
||||||
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:
|
||||||
|
|
@ -761,9 +654,6 @@ 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:
|
||||||
|
|
@ -803,3 +693,4 @@ 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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,43 +47,6 @@ 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:
|
||||||
"""
|
"""
|
||||||
|
|
@ -93,7 +56,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 traceL(
|
def path(
|
||||||
self,
|
self,
|
||||||
ccw: SupportsBool | None,
|
ccw: SupportsBool | None,
|
||||||
length: float,
|
length: float,
|
||||||
|
|
@ -136,9 +99,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'traceL() not implemented for {type(self)}')
|
raise NotImplementedError(f'path() not implemented for {type(self)}')
|
||||||
|
|
||||||
def traceS(
|
def pathS(
|
||||||
self,
|
self,
|
||||||
length: float,
|
length: float,
|
||||||
jog: float,
|
jog: float,
|
||||||
|
|
@ -178,7 +141,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'traceS() not implemented for {type(self)}')
|
raise NotImplementedError(f'path() not implemented for {type(self)}')
|
||||||
|
|
||||||
def planL(
|
def planL(
|
||||||
self,
|
self,
|
||||||
|
|
@ -260,46 +223,6 @@ 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,
|
||||||
|
|
@ -467,7 +390,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 traceL(
|
def path(
|
||||||
self,
|
self,
|
||||||
ccw: SupportsBool | None,
|
ccw: SupportsBool | None,
|
||||||
length: float,
|
length: float,
|
||||||
|
|
@ -484,7 +407,7 @@ class SimpleTool(Tool, metaclass=ABCMeta):
|
||||||
out_ptype = out_ptype,
|
out_ptype = out_ptype,
|
||||||
)
|
)
|
||||||
|
|
||||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL')
|
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path')
|
||||||
pat.add_port_pair(names=port_names, ptype='unk' if in_ptype is None else in_ptype)
|
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
|
||||||
|
|
@ -497,7 +420,7 @@ class SimpleTool(Tool, metaclass=ABCMeta):
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> ILibrary:
|
) -> ILibrary:
|
||||||
|
|
||||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL')
|
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path')
|
||||||
pat.add_port_pair(names=(port_names[0], port_names[1]))
|
pat.add_port_pair(names=(port_names[0], port_names[1]))
|
||||||
|
|
||||||
for step in batch:
|
for step in batch:
|
||||||
|
|
@ -598,14 +521,6 @@ 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 """
|
||||||
|
|
||||||
|
|
@ -774,7 +689,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 traceL(
|
def path(
|
||||||
self,
|
self,
|
||||||
ccw: SupportsBool | None,
|
ccw: SupportsBool | None,
|
||||||
length: float,
|
length: float,
|
||||||
|
|
@ -791,7 +706,7 @@ class AutoTool(Tool, metaclass=ABCMeta):
|
||||||
out_ptype = out_ptype,
|
out_ptype = out_ptype,
|
||||||
)
|
)
|
||||||
|
|
||||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL')
|
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path')
|
||||||
pat.add_port_pair(names=port_names, ptype='unk' if in_ptype is None else in_ptype)
|
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
|
||||||
|
|
@ -930,7 +845,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 traceS(
|
def pathS(
|
||||||
self,
|
self,
|
||||||
length: float,
|
length: float,
|
||||||
jog: float,
|
jog: float,
|
||||||
|
|
@ -946,118 +861,11 @@ 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 + 'traceS')
|
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'pathS')
|
||||||
pat.add_port_pair(names=port_names, ptype='unk' if in_ptype is None else in_ptype)
|
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],
|
||||||
|
|
@ -1066,7 +874,7 @@ class AutoTool(Tool, metaclass=ABCMeta):
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> ILibrary:
|
) -> ILibrary:
|
||||||
|
|
||||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL')
|
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path')
|
||||||
pat.add_port_pair(names=(port_names[0], port_names[1]))
|
pat.add_port_pair(names=(port_names[0], port_names[1]))
|
||||||
|
|
||||||
for step in batch:
|
for step in batch:
|
||||||
|
|
@ -1075,8 +883,6 @@ class AutoTool(Tool, metaclass=ABCMeta):
|
||||||
self._renderL(data=step.data, tree=tree, port_names=port_names, straight_kwargs=kwargs)
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1107,7 +913,7 @@ class PathTool(Tool, metaclass=ABCMeta):
|
||||||
# self.width = width
|
# self.width = width
|
||||||
# self.ptype: str
|
# self.ptype: str
|
||||||
|
|
||||||
def traceL(
|
def path(
|
||||||
self,
|
self,
|
||||||
ccw: SupportsBool | None,
|
ccw: SupportsBool | None,
|
||||||
length: float,
|
length: float,
|
||||||
|
|
@ -1124,7 +930,7 @@ class PathTool(Tool, metaclass=ABCMeta):
|
||||||
out_ptype=out_ptype,
|
out_ptype=out_ptype,
|
||||||
)
|
)
|
||||||
|
|
||||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL')
|
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path')
|
||||||
pat.path(layer=self.layer, width=self.width, vertices=[(0, 0), (length, 0)])
|
pat.path(layer=self.layer, width=self.width, vertices=[(0, 0), (length, 0)])
|
||||||
|
|
||||||
if ccw is None:
|
if ccw is None:
|
||||||
|
|
@ -1185,44 +991,29 @@ class PathTool(Tool, metaclass=ABCMeta):
|
||||||
**kwargs, # noqa: ARG002 (unused)
|
**kwargs, # noqa: ARG002 (unused)
|
||||||
) -> ILibrary:
|
) -> ILibrary:
|
||||||
|
|
||||||
# Transform the batch so the first port is local (at 0,0) but retains its global rotation.
|
path_vertices = [batch[0].start_port.offset]
|
||||||
# This allows the path to be rendered with its original orientation, simplified by
|
for step in batch:
|
||||||
# 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}"')
|
||||||
|
|
||||||
# Check if the last vertex added is already at the end port location
|
if (path_vertices[-1] != batch[-1].end_port.offset).any():
|
||||||
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(local_batch[-1].end_port.offset)
|
path_vertices.append(batch[-1].end_port.offset)
|
||||||
|
|
||||||
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'traceL')
|
tree, pat = Library.mktree(SINGLE_USE_PREFIX + 'path')
|
||||||
pat.path(layer=self.layer, width=self.width, vertices=path_vertices)
|
pat.path(layer=self.layer, width=self.width, vertices=path_vertices)
|
||||||
pat.ports = {
|
pat.ports = {
|
||||||
port_names[0]: local_batch[0].start_port.copy().rotate(pi),
|
port_names[0]: batch[0].start_port.copy().rotate(pi),
|
||||||
port_names[1]: local_batch[-1].end_port.copy().rotate(pi),
|
port_names[1]: batch[-1].end_port.copy().rotate(pi),
|
||||||
}
|
}
|
||||||
return tree
|
return tree
|
||||||
|
|
|
||||||
|
|
@ -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.trace_into("src", "dst")
|
p.path_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._traceL adds a Reference to the generated pattern
|
# Pather.path 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.trace_into("src", "dst")
|
p.path_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,24 +52,35 @@ def test_path_into_bend(advanced_pather: tuple[Pather, PathTool, Library]) -> No
|
||||||
|
|
||||||
|
|
||||||
def test_path_into_sbend(advanced_pather: tuple[Pather, PathTool, Library]) -> None:
|
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.trace_into("src", "dst")
|
p.path_into("src", "dst")
|
||||||
|
|
||||||
|
assert "src" not in p.ports
|
||||||
|
assert "dst" not in p.ports
|
||||||
|
|
||||||
|
|
||||||
|
def test_path_from(advanced_pather: tuple[Pather, PathTool, Library]) -> None:
|
||||||
|
p, tool, lib = advanced_pather
|
||||||
|
p.ports["src"] = Port((0, 0), 0, ptype="wire")
|
||||||
|
p.ports["dst"] = Port((-20, 0), pi, ptype="wire")
|
||||||
|
|
||||||
|
p.at("dst").path_from("src")
|
||||||
|
|
||||||
assert "src" not in p.ports
|
assert "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.trace_into("src", "dst", thru="other")
|
p.path_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])
|
||||||
|
|
|
||||||
|
|
@ -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.straight("start", 10)
|
p.path("start", ccw=None, length=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).
|
||||||
|
|
|
||||||
|
|
@ -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.straight("start", 10)
|
p.path("start", ccw=None, length=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.cw("start", 10)
|
p.path("start", ccw=False, length=10)
|
||||||
|
|
||||||
# PathTool.planL(ccw=False, length=10) returns out_port at (10, -1) relative to (0,0) rot 0.
|
# 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.straight("start", y=-50)
|
p.path_to("start", ccw=None, 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.straight(["A", "B"], ymin=-20)
|
p.mpath(["A", "B"], ccw=None, ymin=-20)
|
||||||
assert_equal(p.ports["A"].offset, [0, -20])
|
assert_equal(p.ports["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").straight(10).ccw(10)
|
p.at("start").path(ccw=None, length=10).path(ccw=True, length=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.straight("in", -10)
|
p.path("in", None, -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.straight("in", 20)
|
p.path("in", None, 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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,242 +0,0 @@
|
||||||
import numpy
|
|
||||||
from numpy import pi
|
|
||||||
from masque import Pather, RenderPather, Library, Pattern, Port
|
|
||||||
from masque.builder.tools import PathTool
|
|
||||||
|
|
||||||
def test_pather_trace_basic() -> None:
|
|
||||||
lib = Library()
|
|
||||||
tool = PathTool(layer='M1', width=1000)
|
|
||||||
p = Pather(lib, tools=tool)
|
|
||||||
|
|
||||||
# Port rotation 0 points in +x (INTO device).
|
|
||||||
# To extend it, we move in -x direction.
|
|
||||||
p.pattern.ports['A'] = Port((0, 0), rotation=0)
|
|
||||||
|
|
||||||
# Trace single port
|
|
||||||
p.at('A').trace(None, 5000)
|
|
||||||
assert numpy.allclose(p.pattern.ports['A'].offset, (-5000, 0))
|
|
||||||
|
|
||||||
# Trace with bend
|
|
||||||
p.at('A').trace(True, 5000) # CCW bend
|
|
||||||
# Port was at (-5000, 0) rot 0.
|
|
||||||
# New wire starts at (-5000, 0) rot 0.
|
|
||||||
# Output port of wire before rotation: (5000, 500) rot -pi/2
|
|
||||||
# Rotate by pi (since dev port rot is 0 and tool port rot is 0):
|
|
||||||
# (-5000, -500) rot pi - pi/2 = pi/2
|
|
||||||
# Add to start: (-10000, -500) rot pi/2
|
|
||||||
assert numpy.allclose(p.pattern.ports['A'].offset, (-10000, -500))
|
|
||||||
assert p.pattern.ports['A'].rotation is not None
|
|
||||||
assert numpy.isclose(p.pattern.ports['A'].rotation, pi/2)
|
|
||||||
|
|
||||||
def test_pather_trace_to() -> None:
|
|
||||||
lib = Library()
|
|
||||||
tool = PathTool(layer='M1', width=1000)
|
|
||||||
p = Pather(lib, tools=tool)
|
|
||||||
|
|
||||||
p.pattern.ports['A'] = Port((0, 0), rotation=0)
|
|
||||||
|
|
||||||
# Trace to x=-10000
|
|
||||||
p.at('A').trace_to(None, x=-10000)
|
|
||||||
assert numpy.allclose(p.pattern.ports['A'].offset, (-10000, 0))
|
|
||||||
|
|
||||||
# Trace to position=-20000
|
|
||||||
p.at('A').trace_to(None, p=-20000)
|
|
||||||
assert numpy.allclose(p.pattern.ports['A'].offset, (-20000, 0))
|
|
||||||
|
|
||||||
def test_pather_bundle_trace() -> None:
|
|
||||||
lib = Library()
|
|
||||||
tool = PathTool(layer='M1', width=1000)
|
|
||||||
p = Pather(lib, tools=tool)
|
|
||||||
|
|
||||||
p.pattern.ports['A'] = Port((0, 0), rotation=0)
|
|
||||||
p.pattern.ports['B'] = Port((0, 2000), rotation=0)
|
|
||||||
|
|
||||||
# Straight bundle - all should align to same x
|
|
||||||
p.at(['A', 'B']).straight(xmin=-10000)
|
|
||||||
assert numpy.isclose(p.pattern.ports['A'].offset[0], -10000)
|
|
||||||
assert numpy.isclose(p.pattern.ports['B'].offset[0], -10000)
|
|
||||||
|
|
||||||
# Bundle with bend
|
|
||||||
p.at(['A', 'B']).ccw(xmin=-20000, spacing=2000)
|
|
||||||
# Traveling in -x direction. CCW turn turns towards -y.
|
|
||||||
# A is at y=0, B is at y=2000.
|
|
||||||
# Rotation center is at y = -R.
|
|
||||||
# A is closer to center than B. So A is inner, B is outer.
|
|
||||||
# xmin is coordinate of innermost bend (A).
|
|
||||||
assert numpy.isclose(p.pattern.ports['A'].offset[0], -20000)
|
|
||||||
# B's bend is further out (more negative x)
|
|
||||||
assert numpy.isclose(p.pattern.ports['B'].offset[0], -22000)
|
|
||||||
|
|
||||||
def test_pather_each_bound() -> None:
|
|
||||||
lib = Library()
|
|
||||||
tool = PathTool(layer='M1', width=1000)
|
|
||||||
p = Pather(lib, tools=tool)
|
|
||||||
|
|
||||||
p.pattern.ports['A'] = Port((0, 0), rotation=0)
|
|
||||||
p.pattern.ports['B'] = Port((-1000, 2000), rotation=0)
|
|
||||||
|
|
||||||
# Each should move by 5000 (towards -x)
|
|
||||||
p.at(['A', 'B']).trace(None, each=5000)
|
|
||||||
assert numpy.allclose(p.pattern.ports['A'].offset, (-5000, 0))
|
|
||||||
assert numpy.allclose(p.pattern.ports['B'].offset, (-6000, 2000))
|
|
||||||
|
|
||||||
def test_selection_management() -> None:
|
|
||||||
lib = Library()
|
|
||||||
p = Pather(lib)
|
|
||||||
p.pattern.ports['A'] = Port((0, 0), rotation=0)
|
|
||||||
p.pattern.ports['B'] = Port((0, 0), rotation=0)
|
|
||||||
|
|
||||||
pp = p.at('A')
|
|
||||||
assert pp.ports == ['A']
|
|
||||||
|
|
||||||
pp.select('B')
|
|
||||||
assert pp.ports == ['A', 'B']
|
|
||||||
|
|
||||||
pp.deselect('A')
|
|
||||||
assert pp.ports == ['B']
|
|
||||||
|
|
||||||
pp.select(['A'])
|
|
||||||
assert pp.ports == ['B', 'A']
|
|
||||||
|
|
||||||
pp.drop()
|
|
||||||
assert 'A' not in p.pattern.ports
|
|
||||||
assert 'B' not in p.pattern.ports
|
|
||||||
assert pp.ports == []
|
|
||||||
|
|
||||||
def test_mark_fork() -> None:
|
|
||||||
lib = Library()
|
|
||||||
p = Pather(lib)
|
|
||||||
p.pattern.ports['A'] = Port((100, 200), rotation=1)
|
|
||||||
|
|
||||||
pp = p.at('A')
|
|
||||||
pp.mark('B')
|
|
||||||
assert 'B' in p.pattern.ports
|
|
||||||
assert numpy.allclose(p.pattern.ports['B'].offset, (100, 200))
|
|
||||||
assert p.pattern.ports['B'].rotation == 1
|
|
||||||
assert pp.ports == ['A'] # mark keeps current selection
|
|
||||||
|
|
||||||
pp.fork('C')
|
|
||||||
assert 'C' in p.pattern.ports
|
|
||||||
assert pp.ports == ['C'] # fork switches to new name
|
|
||||||
|
|
||||||
def test_rename() -> None:
|
|
||||||
lib = Library()
|
|
||||||
p = Pather(lib)
|
|
||||||
p.pattern.ports['A'] = Port((0, 0), rotation=0)
|
|
||||||
|
|
||||||
p.at('A').rename('B')
|
|
||||||
assert 'A' not in p.pattern.ports
|
|
||||||
assert 'B' in p.pattern.ports
|
|
||||||
|
|
||||||
p.pattern.ports['C'] = Port((0, 0), rotation=0)
|
|
||||||
pp = p.at(['B', 'C'])
|
|
||||||
pp.rename({'B': 'D', 'C': 'E'})
|
|
||||||
assert 'B' not in p.pattern.ports
|
|
||||||
assert 'C' not in p.pattern.ports
|
|
||||||
assert 'D' in p.pattern.ports
|
|
||||||
assert 'E' in p.pattern.ports
|
|
||||||
assert set(pp.ports) == {'D', 'E'}
|
|
||||||
|
|
||||||
def test_renderpather_uturn_fallback() -> None:
|
|
||||||
lib = Library()
|
|
||||||
tool = PathTool(layer='M1', width=1000)
|
|
||||||
rp = RenderPather(lib, tools=tool)
|
|
||||||
rp.pattern.ports['A'] = Port((0, 0), rotation=0)
|
|
||||||
|
|
||||||
# PathTool doesn't implement planU, so it should fall back to two planL calls
|
|
||||||
rp.at('A').uturn(offset=10000, length=5000)
|
|
||||||
|
|
||||||
# Two steps should be added
|
|
||||||
assert len(rp.paths['A']) == 2
|
|
||||||
assert rp.paths['A'][0].opcode == 'L'
|
|
||||||
assert rp.paths['A'][1].opcode == 'L'
|
|
||||||
|
|
||||||
rp.render()
|
|
||||||
assert rp.pattern.ports['A'].rotation is not None
|
|
||||||
assert numpy.isclose(rp.pattern.ports['A'].rotation, pi)
|
|
||||||
|
|
||||||
def test_autotool_uturn() -> None:
|
|
||||||
from masque.builder.tools import AutoTool
|
|
||||||
lib = Library()
|
|
||||||
|
|
||||||
# Setup AutoTool with a simple straight and a bend
|
|
||||||
def make_straight(length: float) -> Pattern:
|
|
||||||
pat = Pattern()
|
|
||||||
pat.rect(layer='M1', xmin=0, xmax=length, yctr=0, ly=1000)
|
|
||||||
pat.ports['in'] = Port((0, 0), 0)
|
|
||||||
pat.ports['out'] = Port((length, 0), pi)
|
|
||||||
return pat
|
|
||||||
|
|
||||||
bend_pat = Pattern()
|
|
||||||
bend_pat.polygon(layer='M1', vertices=[(0, -500), (0, 500), (1000, -500)])
|
|
||||||
bend_pat.ports['in'] = Port((0, 0), 0)
|
|
||||||
bend_pat.ports['out'] = Port((500, -500), pi/2)
|
|
||||||
lib['bend'] = bend_pat
|
|
||||||
|
|
||||||
tool = AutoTool(
|
|
||||||
straights=[AutoTool.Straight(ptype='wire', fn=make_straight, in_port_name='in', out_port_name='out')],
|
|
||||||
bends=[AutoTool.Bend(abstract=lib.abstract('bend'), in_port_name='in', out_port_name='out', clockwise=True)],
|
|
||||||
sbends=[],
|
|
||||||
transitions={},
|
|
||||||
default_out_ptype='wire'
|
|
||||||
)
|
|
||||||
|
|
||||||
p = Pather(lib, tools=tool)
|
|
||||||
p.pattern.ports['A'] = Port((0, 0), 0)
|
|
||||||
|
|
||||||
# CW U-turn (jog < 0)
|
|
||||||
# R = 500. jog = -2000. length = 1000.
|
|
||||||
# p0 = planL(length=1000) -> out at (1000, -500) rot pi/2
|
|
||||||
# R2 = 500.
|
|
||||||
# l2_length = abs(-2000) - abs(-500) - 500 = 1000.
|
|
||||||
p.at('A').uturn(offset=-2000, length=1000)
|
|
||||||
|
|
||||||
# Final port should be at (-1000, 2000) rot pi
|
|
||||||
# Start: (0,0) rot 0. Wire direction is rot + pi = pi (West, -x).
|
|
||||||
# Tool planU returns (length, jog) = (1000, -2000) relative to (0,0) rot 0.
|
|
||||||
# Rotation of pi transforms (1000, -2000) to (-1000, 2000).
|
|
||||||
# Final rotation: 0 + pi = pi.
|
|
||||||
assert numpy.allclose(p.pattern.ports['A'].offset, (-1000, 2000))
|
|
||||||
assert p.pattern.ports['A'].rotation is not None
|
|
||||||
assert numpy.isclose(p.pattern.ports['A'].rotation, pi)
|
|
||||||
|
|
||||||
def test_pather_trace_into() -> None:
|
|
||||||
lib = Library()
|
|
||||||
tool = PathTool(layer='M1', width=1000)
|
|
||||||
p = Pather(lib, tools=tool)
|
|
||||||
|
|
||||||
# 1. Straight connector
|
|
||||||
p.pattern.ports['A'] = Port((0, 0), rotation=0)
|
|
||||||
p.pattern.ports['B'] = Port((-10000, 0), rotation=pi)
|
|
||||||
p.at('A').trace_into('B', plug_destination=False)
|
|
||||||
assert 'B' in p.pattern.ports
|
|
||||||
assert 'A' in p.pattern.ports
|
|
||||||
assert numpy.allclose(p.pattern.ports['A'].offset, (-10000, 0))
|
|
||||||
|
|
||||||
# 2. Single bend
|
|
||||||
p.pattern.ports['C'] = Port((0, 0), rotation=0)
|
|
||||||
p.pattern.ports['D'] = Port((-5000, 5000), rotation=pi/2)
|
|
||||||
p.at('C').trace_into('D', plug_destination=False)
|
|
||||||
assert 'D' in p.pattern.ports
|
|
||||||
assert 'C' in p.pattern.ports
|
|
||||||
assert numpy.allclose(p.pattern.ports['C'].offset, (-5000, 5000))
|
|
||||||
|
|
||||||
# 3. Jog (S-bend)
|
|
||||||
p.pattern.ports['E'] = Port((0, 0), rotation=0)
|
|
||||||
p.pattern.ports['F'] = Port((-10000, 2000), rotation=pi)
|
|
||||||
p.at('E').trace_into('F', plug_destination=False)
|
|
||||||
assert 'F' in p.pattern.ports
|
|
||||||
assert 'E' in p.pattern.ports
|
|
||||||
assert numpy.allclose(p.pattern.ports['E'].offset, (-10000, 2000))
|
|
||||||
|
|
||||||
# 4. U-bend (0 deg angle)
|
|
||||||
p.pattern.ports['G'] = Port((0, 0), rotation=0)
|
|
||||||
p.pattern.ports['H'] = Port((-10000, 2000), rotation=0)
|
|
||||||
p.at('G').trace_into('H', plug_destination=False)
|
|
||||||
assert 'H' in p.pattern.ports
|
|
||||||
assert 'G' in p.pattern.ports
|
|
||||||
# A U-bend with length=-travel=10000 and jog=-2000 from (0,0) rot 0
|
|
||||||
# ends up at (-10000, 2000) rot pi.
|
|
||||||
assert numpy.allclose(p.pattern.ports['G'].offset, (-10000, 2000))
|
|
||||||
assert p.pattern.ports['G'].rotation is not None
|
|
||||||
assert numpy.isclose(p.pattern.ports['G'].rotation, pi)
|
|
||||||
|
|
@ -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").straight(10).straight(10)
|
rp.at("start").path(ccw=None, length=10).path(ccw=None, length=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").straight(10).cw(10)
|
rp.at("start").path(ccw=None, length=10).path(ccw=False, length=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").straight(10)
|
rp.at("start").path(ccw=None, length=10)
|
||||||
rp.retool(tool2, keys=["start"])
|
rp.retool(tool2, keys=["start"])
|
||||||
rp.at("start").straight(10)
|
rp.at("start").path(ccw=None, length=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.straight("in", -10)
|
rp.path("in", None, -10)
|
||||||
|
|
||||||
# port_rot=0, forward is -x. path(-10) means moving -10 in -x direction -> +10 in x.
|
# 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)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue