[set_dead / skip_geometry] Improve dead pathers so more "broken" layouts can be successfully executed
This commit is contained in:
parent
cf822c7dcf
commit
f42e720c68
7 changed files with 273 additions and 73 deletions
|
|
@ -284,8 +284,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 +304,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
|
||||||
|
|
||||||
|
|
@ -357,8 +357,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 +377,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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -288,14 +290,36 @@ class Pather(Builder, PatherMixin):
|
||||||
LibraryError if no valid name could be picked for the pattern.
|
LibraryError if no valid name could be picked for the pattern.
|
||||||
"""
|
"""
|
||||||
if self._dead:
|
if self._dead:
|
||||||
logger.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
|
||||||
tree = tool.path(ccw, length, in_ptype=in_ptype, port_names=tool_port_names, **kwargs)
|
try:
|
||||||
|
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]}
|
||||||
|
|
@ -340,8 +364,7 @@ class Pather(Builder, PatherMixin):
|
||||||
LibraryError if no valid name could be picked for the pattern.
|
LibraryError if no valid name could be picked for the pattern.
|
||||||
"""
|
"""
|
||||||
if self._dead:
|
if self._dead:
|
||||||
logger.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,16 +376,39 @@ 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}
|
||||||
t_tree0 = tool.path( ccw0, length / 2, port_names=tool_port_names, in_ptype=in_ptype, **kwargs_no_out)
|
try:
|
||||||
t_pat0 = t_tree0.top_pattern()
|
t_tree0 = tool.path( ccw0, length / 2, port_names=tool_port_names, in_ptype=in_ptype, **kwargs_no_out)
|
||||||
(_, jog0), _ = t_pat0[tool_port_names[0]].measure_travel(t_pat0[tool_port_names[1]])
|
t_pat0 = t_tree0.top_pattern()
|
||||||
t_tree1 = tool.path(not ccw0, abs(jog - jog0), port_names=tool_port_names, in_ptype=t_pat0[tool_port_names[1]].ptype, **kwargs)
|
(_, jog0), _ = t_pat0[tool_port_names[0]].measure_travel(t_pat0[tool_port_names[1]])
|
||||||
t_pat1 = t_tree1.top_pattern()
|
t_tree1 = tool.path(not ccw0, abs(jog - jog0), port_names=tool_port_names, in_ptype=t_pat0[tool_port_names[1]].ptype, **kwargs)
|
||||||
(_, jog1), _ = t_pat1[tool_port_names[0]].measure_travel(t_pat1[tool_port_names[1]])
|
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}
|
kwargs_plug = kwargs | {'plug_into': plug_into}
|
||||||
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
|
||||||
|
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
|
return self
|
||||||
|
|
||||||
tname = self.library << tree
|
tname = self.library << tree
|
||||||
|
|
|
||||||
|
|
@ -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,18 +261,19 @@ 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]
|
||||||
|
|
||||||
# get rid of plugged ports
|
if not self._dead:
|
||||||
for kk in map_in:
|
# get rid of plugged ports
|
||||||
if kk in self.paths:
|
for kk in map_in:
|
||||||
self.paths[kk].append(RenderStep('P', None, self.ports[kk].copy(), self.ports[kk].copy(), None))
|
if kk in self.paths:
|
||||||
|
self.paths[kk].append(RenderStep('P', None, self.ports[kk].copy(), self.ports[kk].copy(), None))
|
||||||
|
|
||||||
plugged = map_in.values()
|
plugged = map_in.values()
|
||||||
for name, port in other_tgt.ports.items():
|
for name, port in other_tgt.ports.items():
|
||||||
if name in plugged:
|
if name in plugged:
|
||||||
continue
|
continue
|
||||||
new_name = map_out.get(name, name) if map_out is not None else name
|
new_name = map_out.get(name, name) if map_out 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:
|
||||||
self.paths[new_name].append(RenderStep('P', None, port.copy(), port.copy(), None))
|
self.paths[new_name].append(RenderStep('P', None, port.copy(), port.copy(), None))
|
||||||
|
|
||||||
self.pattern.plug(
|
self.pattern.plug(
|
||||||
other = other_tgt,
|
other = other_tgt,
|
||||||
|
|
@ -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,10 +343,11 @@ 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]
|
||||||
|
|
||||||
for name, port in other_tgt.ports.items():
|
if not self._dead:
|
||||||
new_name = port_map.get(name, name) if port_map is not None else name
|
for name, port in other_tgt.ports.items():
|
||||||
if new_name is not None and new_name in self.paths:
|
new_name = port_map.get(name, name) if port_map is not None else name
|
||||||
self.paths[new_name].append(RenderStep('P', None, port.copy(), port.copy(), None))
|
if new_name is not None and new_name in self.paths:
|
||||||
|
self.paths[new_name].append(RenderStep('P', None, port.copy(), port.copy(), None))
|
||||||
|
|
||||||
self.pattern.place(
|
self.pattern.place(
|
||||||
other = other_tgt,
|
other = other_tgt,
|
||||||
|
|
@ -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,11 +367,12 @@ class RenderPather(PatherMixin):
|
||||||
self,
|
self,
|
||||||
connections: dict[str, str],
|
connections: dict[str, str],
|
||||||
) -> Self:
|
) -> Self:
|
||||||
for aa, bb in connections.items():
|
if not self._dead:
|
||||||
porta = self.ports[aa]
|
for aa, bb in connections.items():
|
||||||
portb = self.ports[bb]
|
porta = self.ports[aa]
|
||||||
self.paths[aa].append(RenderStep('P', None, porta.copy(), porta.copy(), None))
|
portb = self.ports[bb]
|
||||||
self.paths[bb].append(RenderStep('P', None, portb.copy(), portb.copy(), None))
|
self.paths[aa].append(RenderStep('P', None, porta.copy(), porta.copy(), None))
|
||||||
|
self.paths[bb].append(RenderStep('P', None, portb.copy(), portb.copy(), None))
|
||||||
PortList.plugged(self, connections)
|
PortList.plugged(self, connections)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
@ -410,8 +413,7 @@ class RenderPather(PatherMixin):
|
||||||
LibraryError if no valid name could be picked for the pattern.
|
LibraryError if no valid name could be picked for the pattern.
|
||||||
"""
|
"""
|
||||||
if self._dead:
|
if self._dead:
|
||||||
logger.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,14 +422,28 @@ 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
|
||||||
out_port, data = tool.planL(ccw, length, in_ptype=in_ptype, **kwargs)
|
try:
|
||||||
|
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)
|
||||||
|
|
||||||
step = RenderStep('L', tool, port.copy(), out_port.copy(), data)
|
if not self._dead:
|
||||||
self.paths[portspec].append(step)
|
step = RenderStep('L', tool, port.copy(), out_port.copy(), data)
|
||||||
|
self.paths[portspec].append(step)
|
||||||
|
|
||||||
self.pattern.ports[portspec] = out_port.copy()
|
self.pattern.ports[portspec] = out_port.copy()
|
||||||
self._log_port_update(portspec)
|
self._log_port_update(portspec)
|
||||||
|
|
@ -475,8 +491,7 @@ class RenderPather(PatherMixin):
|
||||||
LibraryError if no valid name could be picked for the pattern.
|
LibraryError if no valid name could be picked for the pattern.
|
||||||
"""
|
"""
|
||||||
if self._dead:
|
if self._dead:
|
||||||
logger.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
|
||||||
|
|
@ -492,22 +507,38 @@ 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})
|
||||||
t_port0, _ = tool.planL( ccw0, length / 2, in_ptype=in_ptype, **kwargs_no_out) # TODO length/2 may fail with asymmetric ptypes
|
try:
|
||||||
jog0 = Port((0, 0), 0).measure_travel(t_port0)[0][1]
|
t_port0, _ = tool.planL( ccw0, length / 2, in_ptype=in_ptype, **kwargs_no_out) # TODO length/2 may fail with asymmetric ptypes
|
||||||
t_port1, _ = tool.planL(not ccw0, abs(jog - jog0), in_ptype=t_port0.ptype, **kwargs)
|
jog0 = Port((0, 0), 0).measure_travel(t_port0)[0][1]
|
||||||
jog1 = Port((0, 0), 0).measure_travel(t_port1)[0][1]
|
t_port1, _ = tool.planL(not ccw0, abs(jog - jog0), in_ptype=t_port0.ptype, **kwargs)
|
||||||
|
jog1 = Port((0, 0), 0).measure_travel(t_port1)[0][1]
|
||||||
|
|
||||||
kwargs_plug = kwargs | {'plug_into': plug_into}
|
kwargs_plug = kwargs | {'plug_into': plug_into}
|
||||||
self.path(portspec, ccw0, length - abs(jog1), **kwargs_no_out)
|
self.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
|
||||||
|
|
||||||
out_port.rotate_around((0, 0), pi + port_rot)
|
if self._dead:
|
||||||
out_port.translate(port.offset)
|
logger.warning("Tool planning failed for dead pather. Using dummy extension.")
|
||||||
step = RenderStep('S', tool, port.copy(), out_port.copy(), data)
|
out_port = Port((length, jog), rotation=pi, ptype=in_ptype)
|
||||||
self.paths[portspec].append(step)
|
data = None
|
||||||
self.pattern.ports[portspec] = out_port.copy()
|
|
||||||
self._log_port_update(portspec)
|
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('S', 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:
|
if plug_into is not None:
|
||||||
self.plugged({portspec: plug_into})
|
self.plugged({portspec: plug_into})
|
||||||
|
|
|
||||||
|
|
@ -1101,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
|
||||||
|
|
@ -1132,6 +1133,7 @@ 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`, only ports are added; geometry is skipped.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
|
|
@ -1165,6 +1167,9 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
self.ports[name] = pp
|
self.ports[name] = pp
|
||||||
self._log_port_update(name)
|
self._log_port_update(name)
|
||||||
|
|
||||||
|
if skip_geometry:
|
||||||
|
return self
|
||||||
|
|
||||||
if append:
|
if append:
|
||||||
if isinstance(other, Abstract):
|
if isinstance(other, Abstract):
|
||||||
raise PatternError('Must provide a full `Pattern` (not an `Abstract`) when appending!')
|
raise PatternError('Must provide a full `Pattern` (not an `Abstract`) when appending!')
|
||||||
|
|
@ -1222,6 +1227,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
|
||||||
|
|
@ -1276,6 +1282,7 @@ 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; geometry is skipped.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
self
|
self
|
||||||
|
|
@ -1308,13 +1315,32 @@ 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)
|
||||||
translation, rotation, pivot = self.find_transform(
|
try:
|
||||||
other,
|
translation, rotation, pivot = self.find_transform(
|
||||||
map_in,
|
other,
|
||||||
mirrored = mirrored,
|
map_in,
|
||||||
set_rotation = set_rotation,
|
mirrored = mirrored,
|
||||||
ok_connections = ok_connections,
|
set_rotation = set_rotation,
|
||||||
)
|
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():
|
||||||
|
|
@ -1323,7 +1349,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
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,
|
||||||
|
|
@ -1334,6 +1360,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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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