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