Compare commits
3 commits
59e996e680
...
5d040061f4
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d040061f4 | |||
| f42e720c68 | |||
| cf822c7dcf |
8 changed files with 355 additions and 75 deletions
|
|
@ -275,6 +275,10 @@ class Builder(PortList):
|
||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
|
|
||||||
|
Note:
|
||||||
|
If the builder is 'dead' (see `set_dead()`), geometry generation is
|
||||||
|
skipped but ports are still updated.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
`PortError` if any ports specified in `map_in` or `map_out` do not
|
`PortError` if any ports specified in `map_in` or `map_out` do not
|
||||||
exist in `self.ports` or `other_names`.
|
exist in `self.ports` or `other_names`.
|
||||||
|
|
@ -284,8 +288,7 @@ class Builder(PortList):
|
||||||
do not line up)
|
do not line up)
|
||||||
"""
|
"""
|
||||||
if self._dead:
|
if self._dead:
|
||||||
logger.error('Skipping plug() since device is dead')
|
logger.warning('Skipping geometry for plug() since device is dead')
|
||||||
return self
|
|
||||||
|
|
||||||
if not isinstance(other, str | Abstract | Pattern):
|
if not isinstance(other, str | Abstract | Pattern):
|
||||||
# We got a Tree; add it into self.library and grab an Abstract for it
|
# We got a Tree; add it into self.library and grab an Abstract for it
|
||||||
|
|
@ -305,6 +308,7 @@ class Builder(PortList):
|
||||||
set_rotation = set_rotation,
|
set_rotation = set_rotation,
|
||||||
append = append,
|
append = append,
|
||||||
ok_connections = ok_connections,
|
ok_connections = ok_connections,
|
||||||
|
skip_geometry = self._dead,
|
||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
@ -350,6 +354,10 @@ class Builder(PortList):
|
||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
|
|
||||||
|
Note:
|
||||||
|
If the builder is 'dead' (see `set_dead()`), geometry generation is
|
||||||
|
skipped but ports are still updated.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
`PortError` if any ports specified in `map_in` or `map_out` do not
|
`PortError` if any ports specified in `map_in` or `map_out` do not
|
||||||
exist in `self.ports` or `other.ports`.
|
exist in `self.ports` or `other.ports`.
|
||||||
|
|
@ -357,8 +365,7 @@ class Builder(PortList):
|
||||||
are applied.
|
are applied.
|
||||||
"""
|
"""
|
||||||
if self._dead:
|
if self._dead:
|
||||||
logger.error('Skipping place() since device is dead')
|
logger.warning('Skipping geometry for place() since device is dead')
|
||||||
return self
|
|
||||||
|
|
||||||
if not isinstance(other, str | Abstract | Pattern):
|
if not isinstance(other, str | Abstract | Pattern):
|
||||||
# We got a Tree; add it into self.library and grab an Abstract for it
|
# We got a Tree; add it into self.library and grab an Abstract for it
|
||||||
|
|
@ -378,6 +385,7 @@ class Builder(PortList):
|
||||||
port_map = port_map,
|
port_map = port_map,
|
||||||
skip_port_check = skip_port_check,
|
skip_port_check = skip_port_check,
|
||||||
append = append,
|
append = append,
|
||||||
|
skip_geometry = self._dead,
|
||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
@ -425,13 +433,18 @@ class Builder(PortList):
|
||||||
|
|
||||||
def set_dead(self) -> Self:
|
def set_dead(self) -> Self:
|
||||||
"""
|
"""
|
||||||
Disallows further changes through `plug()` or `place()`.
|
Suppresses geometry generation for subsequent `plug()` and `place()`
|
||||||
|
operations. Unlike a complete skip, the port state is still tracked
|
||||||
|
and updated, using 'best-effort' fallbacks for impossible transforms.
|
||||||
|
This allows a layout script to execute through problematic sections
|
||||||
|
while maintaining valid port references for downstream code.
|
||||||
|
|
||||||
This is meant for debugging:
|
This is meant for debugging:
|
||||||
```
|
```
|
||||||
dev.plug(a, ...)
|
dev.plug(a, ...)
|
||||||
dev.set_dead() # added for debug purposes
|
dev.set_dead() # added for debug purposes
|
||||||
dev.plug(b, ...) # usually raises an error, but now skipped
|
dev.plug(b, ...) # usually raises an error, but now uses fallback port update
|
||||||
dev.plug(c, ...) # also skipped
|
dev.plug(c, ...) # also updated via fallback
|
||||||
dev.pattern.visualize() # shows the device as of the set_dead() call
|
dev.pattern.visualize() # shows the device as of the set_dead() call
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import copy
|
||||||
import logging
|
import logging
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
|
|
||||||
|
from numpy import pi
|
||||||
|
|
||||||
from ..pattern import Pattern
|
from ..pattern import Pattern
|
||||||
from ..library import ILibrary
|
from ..library import ILibrary
|
||||||
from ..error import BuildError
|
from ..error import BuildError
|
||||||
|
|
@ -283,19 +285,47 @@ class Pather(Builder, PatherMixin):
|
||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
|
|
||||||
|
Note:
|
||||||
|
If the builder is 'dead', this operation will still attempt to update
|
||||||
|
the target port's location. If the pathing tool fails (e.g. due to an
|
||||||
|
impossible length), a dummy linear extension is used to maintain port
|
||||||
|
consistency for downstream operations.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
BuildError if `distance` is too small to fit the bend (if a bend is present).
|
BuildError if `distance` is too small to fit the bend (if a bend is present).
|
||||||
LibraryError if no valid name could be picked for the pattern.
|
LibraryError if no valid name could be picked for the pattern.
|
||||||
"""
|
"""
|
||||||
if self._dead:
|
if self._dead:
|
||||||
logger.error('Skipping path() since device is dead')
|
logger.warning('Skipping geometry for path() since device is dead')
|
||||||
return self
|
|
||||||
|
|
||||||
tool_port_names = ('A', 'B')
|
tool_port_names = ('A', 'B')
|
||||||
|
|
||||||
tool = self.tools.get(portspec, self.tools[None])
|
tool = self.tools.get(portspec, self.tools[None])
|
||||||
in_ptype = self.pattern[portspec].ptype
|
in_ptype = self.pattern[portspec].ptype
|
||||||
|
try:
|
||||||
tree = tool.path(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 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
|
||||||
|
if ccw is None:
|
||||||
|
out_rot = pi
|
||||||
|
elif bool(ccw):
|
||||||
|
out_rot = -pi / 2
|
||||||
|
else:
|
||||||
|
out_rot = pi / 2
|
||||||
|
out_port = Port((length, 0), rotation=out_rot, 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
|
tname = self.library << tree
|
||||||
if plug_into is not None:
|
if plug_into is not None:
|
||||||
output = {plug_into: tool_port_names[1]}
|
output = {plug_into: tool_port_names[1]}
|
||||||
|
|
@ -335,13 +365,18 @@ class Pather(Builder, PatherMixin):
|
||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
|
|
||||||
|
Note:
|
||||||
|
If the builder is 'dead', this operation will still attempt to update
|
||||||
|
the target port's location. If the pathing tool fails (e.g. due to an
|
||||||
|
impossible length), a dummy linear extension is used to maintain port
|
||||||
|
consistency for downstream operations.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
BuildError if `distance` is too small to fit the s-bend (for nonzero jog).
|
BuildError if `distance` is too small to fit the s-bend (for nonzero jog).
|
||||||
LibraryError if no valid name could be picked for the pattern.
|
LibraryError if no valid name could be picked for the pattern.
|
||||||
"""
|
"""
|
||||||
if self._dead:
|
if self._dead:
|
||||||
logger.error('Skipping pathS() since device is dead')
|
logger.warning('Skipping geometry for pathS() since device is dead')
|
||||||
return self
|
|
||||||
|
|
||||||
tool_port_names = ('A', 'B')
|
tool_port_names = ('A', 'B')
|
||||||
|
|
||||||
|
|
@ -353,6 +388,7 @@ class Pather(Builder, PatherMixin):
|
||||||
# Fall back to drawing two L-bends
|
# Fall back to drawing two L-bends
|
||||||
ccw0 = jog > 0
|
ccw0 = jog > 0
|
||||||
kwargs_no_out = kwargs | {'out_ptype': None}
|
kwargs_no_out = kwargs | {'out_ptype': None}
|
||||||
|
try:
|
||||||
t_tree0 = tool.path( ccw0, length / 2, port_names=tool_port_names, in_ptype=in_ptype, **kwargs_no_out)
|
t_tree0 = tool.path( ccw0, length / 2, port_names=tool_port_names, in_ptype=in_ptype, **kwargs_no_out)
|
||||||
t_pat0 = t_tree0.top_pattern()
|
t_pat0 = t_tree0.top_pattern()
|
||||||
(_, jog0), _ = t_pat0[tool_port_names[0]].measure_travel(t_pat0[tool_port_names[1]])
|
(_, jog0), _ = t_pat0[tool_port_names[0]].measure_travel(t_pat0[tool_port_names[1]])
|
||||||
|
|
@ -364,6 +400,28 @@ class Pather(Builder, PatherMixin):
|
||||||
self.path(portspec, ccw0, length - abs(jog1), **kwargs_no_out)
|
self.path(portspec, ccw0, length - abs(jog1), **kwargs_no_out)
|
||||||
self.path(portspec, not ccw0, abs(jog - jog0), **kwargs_plug)
|
self.path(portspec, not ccw0, abs(jog - jog0), **kwargs_plug)
|
||||||
return self
|
return self
|
||||||
|
except (BuildError, NotImplementedError):
|
||||||
|
if not self._dead:
|
||||||
|
raise
|
||||||
|
# Fall through to dummy extension below
|
||||||
|
except BuildError:
|
||||||
|
if not self._dead:
|
||||||
|
raise
|
||||||
|
# Fall through to dummy extension below
|
||||||
|
|
||||||
|
if self._dead:
|
||||||
|
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
|
||||||
|
out_port = Port((length, jog), rotation=pi, 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
|
tname = self.library << tree
|
||||||
if plug_into is not None:
|
if plug_into is not None:
|
||||||
|
|
|
||||||
|
|
@ -253,8 +253,7 @@ class RenderPather(PatherMixin):
|
||||||
do not line up)
|
do not line up)
|
||||||
"""
|
"""
|
||||||
if self._dead:
|
if self._dead:
|
||||||
logger.error('Skipping plug() since device is dead')
|
logger.warning('Skipping geometry for plug() since device is dead')
|
||||||
return self
|
|
||||||
|
|
||||||
other_tgt: Pattern | Abstract
|
other_tgt: Pattern | Abstract
|
||||||
if isinstance(other, str):
|
if isinstance(other, str):
|
||||||
|
|
@ -262,6 +261,7 @@ class RenderPather(PatherMixin):
|
||||||
if append and isinstance(other, Abstract):
|
if append and isinstance(other, Abstract):
|
||||||
other_tgt = self.library[other.name]
|
other_tgt = self.library[other.name]
|
||||||
|
|
||||||
|
if not self._dead:
|
||||||
# get rid of plugged ports
|
# get rid of plugged ports
|
||||||
for kk in map_in:
|
for kk in map_in:
|
||||||
if kk in self.paths:
|
if kk in self.paths:
|
||||||
|
|
@ -284,6 +284,7 @@ class RenderPather(PatherMixin):
|
||||||
set_rotation = set_rotation,
|
set_rotation = set_rotation,
|
||||||
append = append,
|
append = append,
|
||||||
ok_connections = ok_connections,
|
ok_connections = ok_connections,
|
||||||
|
skip_geometry = self._dead,
|
||||||
)
|
)
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
@ -334,8 +335,7 @@ class RenderPather(PatherMixin):
|
||||||
are applied.
|
are applied.
|
||||||
"""
|
"""
|
||||||
if self._dead:
|
if self._dead:
|
||||||
logger.error('Skipping place() since device is dead')
|
logger.warning('Skipping geometry for place() since device is dead')
|
||||||
return self
|
|
||||||
|
|
||||||
other_tgt: Pattern | Abstract
|
other_tgt: Pattern | Abstract
|
||||||
if isinstance(other, str):
|
if isinstance(other, str):
|
||||||
|
|
@ -343,6 +343,7 @@ class RenderPather(PatherMixin):
|
||||||
if append and isinstance(other, Abstract):
|
if append and isinstance(other, Abstract):
|
||||||
other_tgt = self.library[other.name]
|
other_tgt = self.library[other.name]
|
||||||
|
|
||||||
|
if not self._dead:
|
||||||
for name, port in other_tgt.ports.items():
|
for name, port in other_tgt.ports.items():
|
||||||
new_name = port_map.get(name, name) if port_map is not None else name
|
new_name = port_map.get(name, name) if port_map is not None else name
|
||||||
if new_name is not None and new_name in self.paths:
|
if new_name is not None and new_name in self.paths:
|
||||||
|
|
@ -357,6 +358,7 @@ class RenderPather(PatherMixin):
|
||||||
port_map = port_map,
|
port_map = port_map,
|
||||||
skip_port_check = skip_port_check,
|
skip_port_check = skip_port_check,
|
||||||
append = append,
|
append = append,
|
||||||
|
skip_geometry = self._dead,
|
||||||
)
|
)
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
@ -365,6 +367,7 @@ class RenderPather(PatherMixin):
|
||||||
self,
|
self,
|
||||||
connections: dict[str, str],
|
connections: dict[str, str],
|
||||||
) -> Self:
|
) -> Self:
|
||||||
|
if not self._dead:
|
||||||
for aa, bb in connections.items():
|
for aa, bb in connections.items():
|
||||||
porta = self.ports[aa]
|
porta = self.ports[aa]
|
||||||
portb = self.ports[bb]
|
portb = self.ports[bb]
|
||||||
|
|
@ -405,13 +408,18 @@ class RenderPather(PatherMixin):
|
||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
|
|
||||||
|
Note:
|
||||||
|
If the builder is 'dead', this operation will still attempt to update
|
||||||
|
the target port's location. If the pathing tool fails (e.g. due to an
|
||||||
|
impossible length), a dummy linear extension is used to maintain port
|
||||||
|
consistency for downstream operations.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
BuildError if `distance` is too small to fit the bend (if a bend is present).
|
BuildError if `distance` is too small to fit the bend (if a bend is present).
|
||||||
LibraryError if no valid name could be picked for the pattern.
|
LibraryError if no valid name could be picked for the pattern.
|
||||||
"""
|
"""
|
||||||
if self._dead:
|
if self._dead:
|
||||||
logger.error('Skipping path() since device is dead')
|
logger.warning('Skipping geometry for path() since device is dead')
|
||||||
return self
|
|
||||||
|
|
||||||
port = self.pattern[portspec]
|
port = self.pattern[portspec]
|
||||||
in_ptype = port.ptype
|
in_ptype = port.ptype
|
||||||
|
|
@ -420,16 +428,31 @@ class RenderPather(PatherMixin):
|
||||||
|
|
||||||
tool = self.tools.get(portspec, self.tools[None])
|
tool = self.tools.get(portspec, self.tools[None])
|
||||||
# ask the tool for bend size (fill missing dx or dy), check feasibility, and get out_ptype
|
# ask the tool for bend size (fill missing dx or dy), check feasibility, and get out_ptype
|
||||||
|
try:
|
||||||
out_port, data = tool.planL(ccw, length, in_ptype=in_ptype, **kwargs)
|
out_port, data = tool.planL(ccw, length, in_ptype=in_ptype, **kwargs)
|
||||||
|
except (BuildError, NotImplementedError):
|
||||||
|
if not self._dead:
|
||||||
|
raise
|
||||||
|
logger.warning("Tool planning failed for dead pather. Using dummy extension.")
|
||||||
|
if ccw is None:
|
||||||
|
out_rot = pi
|
||||||
|
elif bool(ccw):
|
||||||
|
out_rot = -pi / 2
|
||||||
|
else:
|
||||||
|
out_rot = pi / 2
|
||||||
|
out_port = Port((length, 0), rotation=out_rot, ptype=in_ptype)
|
||||||
|
data = None
|
||||||
|
|
||||||
# Update port
|
# Update port
|
||||||
out_port.rotate_around((0, 0), pi + port_rot)
|
out_port.rotate_around((0, 0), pi + port_rot)
|
||||||
out_port.translate(port.offset)
|
out_port.translate(port.offset)
|
||||||
|
|
||||||
|
if not self._dead:
|
||||||
step = RenderStep('L', tool, port.copy(), out_port.copy(), data)
|
step = RenderStep('L', tool, port.copy(), out_port.copy(), data)
|
||||||
self.paths[portspec].append(step)
|
self.paths[portspec].append(step)
|
||||||
|
|
||||||
self.pattern.ports[portspec] = out_port.copy()
|
self.pattern.ports[portspec] = out_port.copy()
|
||||||
|
self._log_port_update(portspec)
|
||||||
|
|
||||||
if plug_into is not None:
|
if plug_into is not None:
|
||||||
self.plugged({portspec: plug_into})
|
self.plugged({portspec: plug_into})
|
||||||
|
|
@ -469,13 +492,18 @@ class RenderPather(PatherMixin):
|
||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
|
|
||||||
|
Note:
|
||||||
|
If the builder is 'dead', this operation will still attempt to update
|
||||||
|
the target port's location. If the pathing tool fails (e.g. due to an
|
||||||
|
impossible length), a dummy linear extension is used to maintain port
|
||||||
|
consistency for downstream operations.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
BuildError if `distance` is too small to fit the s-bend (for nonzero jog).
|
BuildError if `distance` is too small to fit the s-bend (for nonzero jog).
|
||||||
LibraryError if no valid name could be picked for the pattern.
|
LibraryError if no valid name could be picked for the pattern.
|
||||||
"""
|
"""
|
||||||
if self._dead:
|
if self._dead:
|
||||||
logger.error('Skipping pathS() since device is dead')
|
logger.warning('Skipping geometry for pathS() since device is dead')
|
||||||
return self
|
|
||||||
|
|
||||||
port = self.pattern[portspec]
|
port = self.pattern[portspec]
|
||||||
in_ptype = port.ptype
|
in_ptype = port.ptype
|
||||||
|
|
@ -491,6 +519,7 @@ class RenderPather(PatherMixin):
|
||||||
# Fall back to drawing two L-bends
|
# Fall back to drawing two L-bends
|
||||||
ccw0 = jog > 0
|
ccw0 = jog > 0
|
||||||
kwargs_no_out = (kwargs | {'out_ptype': None})
|
kwargs_no_out = (kwargs | {'out_ptype': None})
|
||||||
|
try:
|
||||||
t_port0, _ = tool.planL( ccw0, length / 2, in_ptype=in_ptype, **kwargs_no_out) # TODO length/2 may fail with asymmetric ptypes
|
t_port0, _ = tool.planL( ccw0, length / 2, in_ptype=in_ptype, **kwargs_no_out) # TODO length/2 may fail with asymmetric ptypes
|
||||||
jog0 = Port((0, 0), 0).measure_travel(t_port0)[0][1]
|
jog0 = Port((0, 0), 0).measure_travel(t_port0)[0][1]
|
||||||
t_port1, _ = tool.planL(not ccw0, abs(jog - jog0), in_ptype=t_port0.ptype, **kwargs)
|
t_port1, _ = tool.planL(not ccw0, abs(jog - jog0), in_ptype=t_port0.ptype, **kwargs)
|
||||||
|
|
@ -500,12 +529,28 @@ class RenderPather(PatherMixin):
|
||||||
self.path(portspec, ccw0, length - abs(jog1), **kwargs_no_out)
|
self.path(portspec, ccw0, length - abs(jog1), **kwargs_no_out)
|
||||||
self.path(portspec, not ccw0, abs(jog - jog0), **kwargs_plug)
|
self.path(portspec, not ccw0, abs(jog - jog0), **kwargs_plug)
|
||||||
return self
|
return self
|
||||||
|
except (BuildError, NotImplementedError):
|
||||||
|
if not self._dead:
|
||||||
|
raise
|
||||||
|
# Fall through to dummy extension below
|
||||||
|
except BuildError:
|
||||||
|
if not self._dead:
|
||||||
|
raise
|
||||||
|
# Fall through to dummy extension below
|
||||||
|
|
||||||
|
if self._dead:
|
||||||
|
logger.warning("Tool planning failed for dead pather. Using dummy extension.")
|
||||||
|
out_port = Port((length, jog), rotation=pi, ptype=in_ptype)
|
||||||
|
data = None
|
||||||
|
|
||||||
|
if out_port is not None:
|
||||||
out_port.rotate_around((0, 0), pi + port_rot)
|
out_port.rotate_around((0, 0), pi + port_rot)
|
||||||
out_port.translate(port.offset)
|
out_port.translate(port.offset)
|
||||||
|
if not self._dead:
|
||||||
step = RenderStep('S', tool, port.copy(), out_port.copy(), data)
|
step = RenderStep('S', tool, port.copy(), out_port.copy(), data)
|
||||||
self.paths[portspec].append(step)
|
self.paths[portspec].append(step)
|
||||||
self.pattern.ports[portspec] = out_port.copy()
|
self.pattern.ports[portspec] = out_port.copy()
|
||||||
|
self._log_port_update(portspec)
|
||||||
|
|
||||||
if plug_into is not None:
|
if plug_into is not None:
|
||||||
self.plugged({portspec: plug_into})
|
self.plugged({portspec: plug_into})
|
||||||
|
|
|
||||||
|
|
@ -638,6 +638,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
"""
|
"""
|
||||||
for entry in chain(chain_elements(self.shapes, self.labels, self.refs), self.ports.values()):
|
for entry in chain(chain_elements(self.shapes, self.labels, self.refs), self.ports.values()):
|
||||||
cast('Positionable', entry).translate(offset)
|
cast('Positionable', entry).translate(offset)
|
||||||
|
self._log_bulk_update(f"translate({offset})")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def scale_elements(self, c: float) -> Self:
|
def scale_elements(self, c: float) -> Self:
|
||||||
|
|
@ -705,6 +706,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
self.rotate_elements(rotation)
|
self.rotate_elements(rotation)
|
||||||
self.rotate_element_centers(rotation)
|
self.rotate_element_centers(rotation)
|
||||||
self.translate_elements(+pivot)
|
self.translate_elements(+pivot)
|
||||||
|
self._log_bulk_update(f"rotate_around({pivot}, {rotation})")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def rotate_element_centers(self, rotation: float) -> Self:
|
def rotate_element_centers(self, rotation: float) -> Self:
|
||||||
|
|
@ -761,6 +763,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
"""
|
"""
|
||||||
for entry in chain(chain_elements(self.shapes, self.refs, self.labels), self.ports.values()):
|
for entry in chain(chain_elements(self.shapes, self.refs, self.labels), self.ports.values()):
|
||||||
cast('Flippable', entry).flip_across(axis=axis)
|
cast('Flippable', entry).flip_across(axis=axis)
|
||||||
|
self._log_bulk_update(f"mirror({axis})")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def copy(self) -> Self:
|
def copy(self) -> Self:
|
||||||
|
|
@ -1098,6 +1101,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
port_map: dict[str, str | None] | None = None,
|
port_map: dict[str, str | None] | None = None,
|
||||||
skip_port_check: bool = False,
|
skip_port_check: bool = False,
|
||||||
append: bool = False,
|
append: bool = False,
|
||||||
|
skip_geometry: bool = False,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
"""
|
"""
|
||||||
Instantiate or append the pattern `other` into the current pattern, adding its
|
Instantiate or append the pattern `other` into the current pattern, adding its
|
||||||
|
|
@ -1129,6 +1133,10 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
append: If `True`, `other` is appended instead of being referenced.
|
append: If `True`, `other` is appended instead of being referenced.
|
||||||
Note that this does not flatten `other`, so its refs will still
|
Note that this does not flatten `other`, so its refs will still
|
||||||
be refs (now inside `self`).
|
be refs (now inside `self`).
|
||||||
|
skip_geometry: If `True`, the operation only updates the port list and
|
||||||
|
skips adding any geometry (shapes, labels, or references). This
|
||||||
|
allows the pattern assembly to proceed for port-tracking purposes
|
||||||
|
even when layout generation is suppressed.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
|
|
@ -1160,6 +1168,10 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
pp.rotate_around(pivot, rotation)
|
pp.rotate_around(pivot, rotation)
|
||||||
pp.translate(offset)
|
pp.translate(offset)
|
||||||
self.ports[name] = pp
|
self.ports[name] = pp
|
||||||
|
self._log_port_update(name)
|
||||||
|
|
||||||
|
if skip_geometry:
|
||||||
|
return self
|
||||||
|
|
||||||
if append:
|
if append:
|
||||||
if isinstance(other, Abstract):
|
if isinstance(other, Abstract):
|
||||||
|
|
@ -1218,6 +1230,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
set_rotation: bool | None = None,
|
set_rotation: bool | None = None,
|
||||||
append: bool = False,
|
append: bool = False,
|
||||||
ok_connections: Iterable[tuple[str, str]] = (),
|
ok_connections: Iterable[tuple[str, str]] = (),
|
||||||
|
skip_geometry: bool = False,
|
||||||
) -> Self:
|
) -> Self:
|
||||||
"""
|
"""
|
||||||
Instantiate or append a pattern into the current pattern, connecting
|
Instantiate or append a pattern into the current pattern, connecting
|
||||||
|
|
@ -1272,6 +1285,11 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
any other ptypte. Non-allowed ptype connections will emit a
|
any other ptypte. Non-allowed ptype connections will emit a
|
||||||
warning. Order is ignored, i.e. `(a, b)` is equivalent to
|
warning. Order is ignored, i.e. `(a, b)` is equivalent to
|
||||||
`(b, a)`.
|
`(b, a)`.
|
||||||
|
skip_geometry: If `True`, only ports are updated and geometry is
|
||||||
|
skipped. If a valid transform cannot be found (e.g. due to
|
||||||
|
misaligned ports), a 'best-effort' dummy transform is used
|
||||||
|
to ensure new ports are still added at approximate locations,
|
||||||
|
allowing downstream routing to continue.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
|
|
@ -1304,6 +1322,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
map_out = {out_port_name: next(iter(map_in.keys()))}
|
map_out = {out_port_name: next(iter(map_in.keys()))}
|
||||||
|
|
||||||
self.check_ports(other.ports.keys(), map_in, map_out)
|
self.check_ports(other.ports.keys(), map_in, map_out)
|
||||||
|
try:
|
||||||
translation, rotation, pivot = self.find_transform(
|
translation, rotation, pivot = self.find_transform(
|
||||||
other,
|
other,
|
||||||
map_in,
|
map_in,
|
||||||
|
|
@ -1311,14 +1330,33 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
set_rotation = set_rotation,
|
set_rotation = set_rotation,
|
||||||
ok_connections = ok_connections,
|
ok_connections = ok_connections,
|
||||||
)
|
)
|
||||||
|
except PortError:
|
||||||
|
if not skip_geometry:
|
||||||
|
raise
|
||||||
|
logger.warning("Port transform failed for dead device. Using dummy transform.")
|
||||||
|
if map_in:
|
||||||
|
ki, vi = next(iter(map_in.items()))
|
||||||
|
s_port = self.ports[ki]
|
||||||
|
o_port = other.ports[vi].deepcopy()
|
||||||
|
if mirrored:
|
||||||
|
o_port.mirror()
|
||||||
|
o_port.offset[1] *= -1
|
||||||
|
translation = s_port.offset - o_port.offset
|
||||||
|
rotation = (s_port.rotation - o_port.rotation - pi) if (s_port.rotation is not None and o_port.rotation is not None) else 0
|
||||||
|
pivot = o_port.offset
|
||||||
|
else:
|
||||||
|
translation = numpy.zeros(2)
|
||||||
|
rotation = 0.0
|
||||||
|
pivot = numpy.zeros(2)
|
||||||
|
|
||||||
# get rid of plugged ports
|
# get rid of plugged ports
|
||||||
for ki, vi in map_in.items():
|
for ki, vi in map_in.items():
|
||||||
del self.ports[ki]
|
del self.ports[ki]
|
||||||
|
self._log_port_removal(ki)
|
||||||
map_out[vi] = None
|
map_out[vi] = None
|
||||||
|
|
||||||
if isinstance(other, Pattern):
|
if isinstance(other, Pattern):
|
||||||
assert append, 'Got a name (not an abstract) but was asked to reference (not append)'
|
assert append or skip_geometry, 'Got a name (not an abstract) but was asked to reference (not append)'
|
||||||
|
|
||||||
self.place(
|
self.place(
|
||||||
other,
|
other,
|
||||||
|
|
@ -1329,6 +1367,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
port_map = map_out,
|
port_map = map_out,
|
||||||
skip_port_check = True,
|
skip_port_check = True,
|
||||||
append = append,
|
append = append,
|
||||||
|
skip_geometry = skip_geometry,
|
||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ from .error import PortError, format_stacktrace
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
port_logger = logging.getLogger('masque.ports')
|
||||||
|
|
||||||
|
|
||||||
@functools.total_ordering
|
@functools.total_ordering
|
||||||
|
|
@ -207,6 +208,19 @@ class PortList(metaclass=ABCMeta):
|
||||||
def ports(self, value: dict[str, Port]) -> None:
|
def ports(self, value: dict[str, Port]) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def _log_port_update(self, name: str) -> None:
|
||||||
|
""" Log the current state of the named port """
|
||||||
|
port_logger.info("Port %s: %s", name, self.ports[name])
|
||||||
|
|
||||||
|
def _log_port_removal(self, name: str) -> None:
|
||||||
|
""" Log that the named port has been removed """
|
||||||
|
port_logger.info("Port %s: removed", name)
|
||||||
|
|
||||||
|
def _log_bulk_update(self, label: str) -> None:
|
||||||
|
""" Log all current ports at DEBUG level """
|
||||||
|
for name, port in self.ports.items():
|
||||||
|
port_logger.debug("%s: Port %s: %s", label, name, port)
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def __getitem__(self, key: str) -> Port:
|
def __getitem__(self, key: str) -> Port:
|
||||||
pass
|
pass
|
||||||
|
|
@ -260,6 +274,7 @@ class PortList(metaclass=ABCMeta):
|
||||||
raise PortError(f'Port {name} already exists.')
|
raise PortError(f'Port {name} already exists.')
|
||||||
assert name not in self.ports
|
assert name not in self.ports
|
||||||
self.ports[name] = value
|
self.ports[name] = value
|
||||||
|
self._log_port_update(name)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def rename_ports(
|
def rename_ports(
|
||||||
|
|
@ -286,11 +301,22 @@ class PortList(metaclass=ABCMeta):
|
||||||
if duplicates:
|
if duplicates:
|
||||||
raise PortError(f'Unrenamed ports would be overwritten: {duplicates}')
|
raise PortError(f'Unrenamed ports would be overwritten: {duplicates}')
|
||||||
|
|
||||||
|
for kk, vv in mapping.items():
|
||||||
|
if vv is None:
|
||||||
|
self._log_port_removal(kk)
|
||||||
|
elif vv != kk:
|
||||||
|
self._log_port_removal(kk)
|
||||||
|
|
||||||
renamed = {vv: self.ports.pop(kk) for kk, vv in mapping.items()}
|
renamed = {vv: self.ports.pop(kk) for kk, vv in mapping.items()}
|
||||||
if None in renamed:
|
if None in renamed:
|
||||||
del renamed[None]
|
del renamed[None]
|
||||||
|
|
||||||
self.ports.update(renamed) # type: ignore
|
self.ports.update(renamed) # type: ignore
|
||||||
|
|
||||||
|
for vv in mapping.values():
|
||||||
|
if vv is not None:
|
||||||
|
self._log_port_update(vv)
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def add_port_pair(
|
def add_port_pair(
|
||||||
|
|
@ -319,6 +345,8 @@ class PortList(metaclass=ABCMeta):
|
||||||
}
|
}
|
||||||
self.check_ports(names)
|
self.check_ports(names)
|
||||||
self.ports.update(new_ports)
|
self.ports.update(new_ports)
|
||||||
|
self._log_port_update(names[0])
|
||||||
|
self._log_port_update(names[1])
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def plugged(
|
def plugged(
|
||||||
|
|
@ -388,6 +416,7 @@ class PortList(metaclass=ABCMeta):
|
||||||
|
|
||||||
for pp in chain(a_names, b_names):
|
for pp in chain(a_names, b_names):
|
||||||
del self.ports[pp]
|
del self.ports[pp]
|
||||||
|
self._log_port_removal(pp)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def check_ports(
|
def check_ports(
|
||||||
|
|
|
||||||
|
|
@ -73,3 +73,57 @@ def test_builder_set_dead() -> None:
|
||||||
|
|
||||||
b.place("sub")
|
b.place("sub")
|
||||||
assert not b.pattern.has_refs()
|
assert not b.pattern.has_refs()
|
||||||
|
|
||||||
|
|
||||||
|
def test_builder_dead_ports() -> None:
|
||||||
|
lib = Library()
|
||||||
|
pat = Pattern()
|
||||||
|
pat.ports['A'] = Port((0, 0), 0)
|
||||||
|
b = Builder(lib, pattern=pat)
|
||||||
|
b.set_dead()
|
||||||
|
|
||||||
|
# Attempt to plug a device where ports don't line up
|
||||||
|
# A has rotation 0, C has rotation 0. plug() expects opposing rotations (pi difference).
|
||||||
|
other = Pattern(ports={'C': Port((10, 10), 0), 'D': Port((20, 20), 0)})
|
||||||
|
|
||||||
|
# This should NOT raise PortError because b is dead
|
||||||
|
b.plug(other, map_in={'A': 'C'}, map_out={'D': 'B'})
|
||||||
|
|
||||||
|
# Port A should be removed, and Port B (renamed from D) should be added
|
||||||
|
assert 'A' not in b.ports
|
||||||
|
assert 'B' in b.ports
|
||||||
|
|
||||||
|
# Verify geometry was not added
|
||||||
|
assert not b.pattern.has_refs()
|
||||||
|
assert not b.pattern.has_shapes()
|
||||||
|
|
||||||
|
|
||||||
|
def test_dead_plug_best_effort() -> None:
|
||||||
|
lib = Library()
|
||||||
|
pat = Pattern()
|
||||||
|
pat.ports['A'] = Port((0, 0), 0)
|
||||||
|
b = Builder(lib, pattern=pat)
|
||||||
|
b.set_dead()
|
||||||
|
|
||||||
|
# Device with multiple ports, none of which line up correctly
|
||||||
|
other = Pattern(ports={
|
||||||
|
'P1': Port((10, 10), 0), # Wrong rotation (0 instead of pi)
|
||||||
|
'P2': Port((20, 20), pi) # Correct rotation but wrong offset
|
||||||
|
})
|
||||||
|
|
||||||
|
# Try to plug. find_transform will fail.
|
||||||
|
# It should fall back to aligning the first pair ('A' and 'P1').
|
||||||
|
b.plug(other, map_in={'A': 'P1'}, map_out={'P2': 'B'})
|
||||||
|
|
||||||
|
assert 'A' not in b.ports
|
||||||
|
assert 'B' in b.ports
|
||||||
|
|
||||||
|
# Dummy transform aligns A (0,0) with P1 (10,10)
|
||||||
|
# A rotation 0, P1 rotation 0 -> rotation = (0 - 0 - pi) = -pi
|
||||||
|
# P2 (20,20) rotation pi:
|
||||||
|
# 1. Translate P2 so P1 is at origin: (20,20) - (10,10) = (10,10)
|
||||||
|
# 2. Rotate (10,10) by -pi: (-10,-10)
|
||||||
|
# 3. Translate by s_port.offset (0,0): (-10,-10)
|
||||||
|
assert_allclose(b.ports['B'].offset, [-10, -10], atol=1e-10)
|
||||||
|
# P2 rot pi + transform rot -pi = 0
|
||||||
|
assert_allclose(b.ports['B'].rotation, 0, atol=1e-10)
|
||||||
|
|
|
||||||
|
|
@ -81,3 +81,25 @@ def test_pather_at_chaining(pather_setup: tuple[Pather, PathTool, Library]) -> N
|
||||||
# pi/2 (North) + CCW (90 deg) -> 0 (East)?
|
# pi/2 (North) + CCW (90 deg) -> 0 (East)?
|
||||||
# Actual behavior results in pi (West).
|
# Actual behavior results in pi (West).
|
||||||
assert_allclose(p.ports["start"].rotation, pi, atol=1e-10)
|
assert_allclose(p.ports["start"].rotation, pi, atol=1e-10)
|
||||||
|
|
||||||
|
|
||||||
|
def test_pather_dead_ports() -> None:
|
||||||
|
lib = Library()
|
||||||
|
tool = PathTool(layer=(1, 0), width=1)
|
||||||
|
p = Pather(lib, ports={"in": Port((0, 0), 0)}, tools=tool)
|
||||||
|
p.set_dead()
|
||||||
|
|
||||||
|
# Path with negative length (impossible for PathTool, would normally raise BuildError)
|
||||||
|
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.path("in", None, 20)
|
||||||
|
# 10 + (-20) = -10
|
||||||
|
assert_allclose(p.ports["in"].offset, [-10, 0], atol=1e-10)
|
||||||
|
|
||||||
|
# Verify no geometry
|
||||||
|
assert not p.pattern.has_shapes()
|
||||||
|
|
|
||||||
|
|
@ -73,3 +73,23 @@ def test_renderpather_retool(rpather_setup: tuple[RenderPather, PathTool, Librar
|
||||||
# Different tools should cause different batches/shapes
|
# Different tools should cause different batches/shapes
|
||||||
assert len(rp.pattern.shapes[(1, 0)]) == 1
|
assert len(rp.pattern.shapes[(1, 0)]) == 1
|
||||||
assert len(rp.pattern.shapes[(2, 0)]) == 1
|
assert len(rp.pattern.shapes[(2, 0)]) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_renderpather_dead_ports() -> None:
|
||||||
|
lib = Library()
|
||||||
|
tool = PathTool(layer=(1, 0), width=1)
|
||||||
|
rp = RenderPather(lib, ports={"in": Port((0, 0), 0)}, tools=tool)
|
||||||
|
rp.set_dead()
|
||||||
|
|
||||||
|
# Impossible path
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Verify no render steps were added
|
||||||
|
assert len(rp.paths["in"]) == 0
|
||||||
|
|
||||||
|
# Verify no geometry
|
||||||
|
rp.render()
|
||||||
|
assert not rp.pattern.has_shapes()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue