[Pather] improve bounds handling for bundles
This commit is contained in:
parent
2c5243237e
commit
8100d8095a
2 changed files with 141 additions and 10 deletions
|
|
@ -96,6 +96,11 @@ class Pather(PortList):
|
|||
_POSITION_KEYS: tuple[str, ...] = ('p', 'x', 'y', 'pos', 'position')
|
||||
""" Single-port position bounds accepted by `trace_to()` and `jog()` """
|
||||
|
||||
_BUNDLE_BOUND_KEYS: tuple[str, ...] = (
|
||||
'emin', 'emax', 'pmin', 'pmax', 'xmin', 'xmax', 'ymin', 'ymax', 'min_past_furthest',
|
||||
)
|
||||
""" Bounds accepted by `trace()` / `trace_to()` when solving bundle extensions """
|
||||
|
||||
@property
|
||||
def ports(self) -> dict[str, Port]:
|
||||
return self.pattern.ports
|
||||
|
|
@ -401,6 +406,86 @@ class Pather(PortList):
|
|||
(travel, _jog), _ = port.measure_travel(target)
|
||||
return key, value, -float(travel)
|
||||
|
||||
@staticmethod
|
||||
def _format_route_key_list(keys: Sequence[str]) -> str:
|
||||
return ', '.join(keys)
|
||||
|
||||
@staticmethod
|
||||
def _present_keys(bounds: Mapping[str, Any], keys: Sequence[str]) -> list[str]:
|
||||
return [key for key in keys if bounds.get(key) is not None]
|
||||
|
||||
def _present_bundle_bounds(self, bounds: Mapping[str, Any]) -> list[str]:
|
||||
return self._present_keys(bounds, self._BUNDLE_BOUND_KEYS)
|
||||
|
||||
def _validate_trace_args(
|
||||
self,
|
||||
portspec: Sequence[str],
|
||||
*,
|
||||
length: float | None,
|
||||
spacing: float | ArrayLike | None,
|
||||
bounds: Mapping[str, Any],
|
||||
) -> None:
|
||||
bundle_bounds = self._present_bundle_bounds(bounds)
|
||||
if len(bundle_bounds) > 1:
|
||||
args = self._format_route_key_list(bundle_bounds)
|
||||
raise BuildError(f'Provide exactly one bundle bound for trace(); got {args}')
|
||||
|
||||
invalid_with_length = self._present_keys(bounds, ('each', 'set_rotation')) + bundle_bounds
|
||||
invalid_with_each = self._present_keys(bounds, ('set_rotation',)) + bundle_bounds
|
||||
|
||||
if length is not None:
|
||||
if len(portspec) > 1:
|
||||
raise BuildError('length only allowed with a single port')
|
||||
if spacing is not None:
|
||||
invalid_with_length.append('spacing')
|
||||
if invalid_with_length:
|
||||
args = self._format_route_key_list(invalid_with_length)
|
||||
raise BuildError(f'length cannot be combined with other routing bounds: {args}')
|
||||
return
|
||||
|
||||
if bounds.get('each') is not None:
|
||||
if spacing is not None:
|
||||
invalid_with_each.append('spacing')
|
||||
if invalid_with_each:
|
||||
args = self._format_route_key_list(invalid_with_each)
|
||||
raise BuildError(f'each cannot be combined with other routing bounds: {args}')
|
||||
return
|
||||
|
||||
if not bundle_bounds:
|
||||
raise BuildError('No bound type specified for trace()')
|
||||
|
||||
def _validate_trace_to_positional_args(
|
||||
self,
|
||||
*,
|
||||
spacing: float | ArrayLike | None,
|
||||
bounds: Mapping[str, Any],
|
||||
) -> None:
|
||||
invalid = self._present_keys(bounds, ('each', 'set_rotation')) + self._present_bundle_bounds(bounds)
|
||||
if spacing is not None:
|
||||
invalid.append('spacing')
|
||||
if invalid:
|
||||
args = self._format_route_key_list(invalid)
|
||||
raise BuildError(f'Positional bounds cannot be combined with other routing bounds: {args}')
|
||||
|
||||
def _validate_jog_args(self, *, length: float | None, bounds: Mapping[str, Any]) -> None:
|
||||
invalid = self._present_keys(bounds, ('each', 'set_rotation')) + self._present_bundle_bounds(bounds)
|
||||
if length is not None:
|
||||
invalid = self._present_keys(bounds, self._POSITION_KEYS) + invalid
|
||||
if invalid:
|
||||
args = self._format_route_key_list(invalid)
|
||||
raise BuildError(f'length cannot be combined with other routing bounds in jog(): {args}')
|
||||
return
|
||||
|
||||
if invalid:
|
||||
args = self._format_route_key_list(invalid)
|
||||
raise BuildError(f'Unsupported routing bounds for jog(): {args}')
|
||||
|
||||
def _validate_uturn_args(self, bounds: Mapping[str, Any]) -> None:
|
||||
invalid = self._present_keys(bounds, self._POSITION_KEYS + ('each', 'set_rotation')) + self._present_bundle_bounds(bounds)
|
||||
if invalid:
|
||||
args = self._format_route_key_list(invalid)
|
||||
raise BuildError(f'Unsupported routing bounds for uturn(): {args}')
|
||||
|
||||
def _validate_fallback_endpoint(
|
||||
self,
|
||||
portspec: str,
|
||||
|
|
@ -787,23 +872,29 @@ class Pather(PortList):
|
|||
spacing: float | ArrayLike | None = None,
|
||||
**bounds: Any,
|
||||
) -> Self:
|
||||
"""
|
||||
Route one or more ports using straight segments or single 90-degree bends.
|
||||
|
||||
Provide exactly one routing mode:
|
||||
- `length` for a single port,
|
||||
- `each` to extend each selected port independently by the same amount, or
|
||||
- one bundle bound such as `xmin`, `emax`, or `min_past_furthest`.
|
||||
|
||||
`spacing` and `set_rotation` are only valid when using a bundle bound.
|
||||
"""
|
||||
with self._logger.log_operation(self, 'trace', portspec, ccw=ccw, length=length, spacing=spacing, **bounds):
|
||||
if isinstance(portspec, str):
|
||||
portspec = [portspec]
|
||||
self._validate_trace_args(portspec, length=length, spacing=spacing, bounds=bounds)
|
||||
if length is not None:
|
||||
if len(portspec) > 1:
|
||||
raise BuildError('length only allowed with a single port')
|
||||
return self._traceL(portspec[0], ccw, length, **bounds)
|
||||
if 'each' in bounds:
|
||||
if bounds.get('each') is not None:
|
||||
each = bounds.pop('each')
|
||||
for p in portspec:
|
||||
self._traceL(p, ccw, each, **bounds)
|
||||
return self
|
||||
# Bundle routing
|
||||
bt_keys = {'emin', 'emax', 'pmin', 'pmax', 'xmin', 'xmax', 'ymin', 'ymax', 'min_past_furthest'}
|
||||
bt = next((k for k in bounds if k in bt_keys), None)
|
||||
if not bt:
|
||||
raise BuildError('No bound type specified for trace()')
|
||||
bt = self._present_bundle_bounds(bounds)[0]
|
||||
bval = bounds.pop(bt)
|
||||
set_rot = bounds.pop('set_rotation', None)
|
||||
exts = ell(self.pattern[tuple(portspec)], ccw, spacing=spacing, bound=bval, bound_type=bt, set_rotation=set_rot)
|
||||
|
|
@ -824,7 +915,7 @@ class Pather(PortList):
|
|||
|
||||
Exactly one of `p`, `pos`, `position`, `x`, or `y` may be used as a positional
|
||||
bound. Positional bounds are only valid for a single port and may not be combined
|
||||
with `length`.
|
||||
with `length`, `spacing`, `each`, or bundle-bound keywords such as `xmin`/`emax`.
|
||||
"""
|
||||
with self._logger.log_operation(self, 'trace_to', portspec, ccw=ccw, spacing=spacing, **bounds):
|
||||
if isinstance(portspec, str):
|
||||
|
|
@ -839,6 +930,7 @@ class Pather(PortList):
|
|||
if resolved is not None:
|
||||
if len(portspec) > 1:
|
||||
raise BuildError('Position bounds only allowed with a single port')
|
||||
self._validate_trace_to_positional_args(spacing=spacing, bounds=bounds)
|
||||
_key, _value, length = resolved
|
||||
other_bounds = {bk: bv for bk, bv in bounds.items() if bk not in self._POSITION_KEYS and bk != 'length'}
|
||||
return self._traceL(portspec[0], ccw, length, **other_bounds)
|
||||
|
|
@ -862,11 +954,13 @@ class Pather(PortList):
|
|||
|
||||
`length` is the along-travel displacement. If omitted, exactly one positional
|
||||
bound (`p`, `pos`, `position`, `x`, or `y`) must be provided for a single port,
|
||||
and the required travel distance is derived from that bound.
|
||||
and the required travel distance is derived from that bound. When `length` is
|
||||
provided, no other routing-bound keywords are accepted.
|
||||
"""
|
||||
with self._logger.log_operation(self, 'jog', portspec, offset=offset, length=length, **bounds):
|
||||
if isinstance(portspec, str):
|
||||
portspec = [portspec]
|
||||
self._validate_jog_args(length=length, bounds=bounds)
|
||||
other_bounds = dict(bounds)
|
||||
if length is None:
|
||||
if len(portspec) != 1:
|
||||
|
|
@ -884,11 +978,13 @@ class Pather(PortList):
|
|||
"""
|
||||
Route a U-turn.
|
||||
|
||||
`length` is the along-travel displacement to the final port. If omitted, it defaults to 0.
|
||||
`length` is the along-travel displacement to the final port. If omitted, it defaults
|
||||
to 0. Positional and bundle-bound keywords are not supported for this operation.
|
||||
"""
|
||||
with self._logger.log_operation(self, 'uturn', portspec, offset=offset, length=length, **bounds):
|
||||
if isinstance(portspec, str):
|
||||
portspec = [portspec]
|
||||
self._validate_uturn_args(bounds)
|
||||
for p in portspec:
|
||||
self._traceU(p, offset, length=length if length else 0, **bounds)
|
||||
return self
|
||||
|
|
|
|||
|
|
@ -351,6 +351,41 @@ def test_pather_trace_to_rejects_conflicting_position_bounds() -> None:
|
|||
p.trace_to('A', None, x=-5, length=3)
|
||||
|
||||
|
||||
def test_pather_trace_rejects_length_with_bundle_bound() -> None:
|
||||
p = Pather(Library(), tools=PathTool(layer='M1', width=1, ptype='wire'))
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
with pytest.raises(BuildError, match='length cannot be combined'):
|
||||
p.trace('A', None, length=5, xmin=-100)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('kwargs', ({'xmin': -10, 'xmax': -20}, {'xmax': -20, 'xmin': -10}))
|
||||
def test_pather_trace_rejects_multiple_bundle_bounds(kwargs: dict[str, int]) -> None:
|
||||
p = Pather(Library(), tools=PathTool(layer='M1', width=1, ptype='wire'))
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
p.pattern.ports['B'] = Port((0, 5), rotation=0, ptype='wire')
|
||||
|
||||
with pytest.raises(BuildError, match='exactly one bundle bound'):
|
||||
p.trace(['A', 'B'], None, **kwargs)
|
||||
|
||||
|
||||
def test_pather_jog_rejects_length_with_position_bound() -> None:
|
||||
p = Pather(Library(), tools=PathTool(layer='M1', width=1, ptype='wire'))
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
with pytest.raises(BuildError, match='length cannot be combined'):
|
||||
p.jog('A', 2, length=5, x=-999)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('kwargs', ({'x': -999}, {'xmin': -10}))
|
||||
def test_pather_uturn_rejects_routing_bounds(kwargs: dict[str, int]) -> None:
|
||||
p = Pather(Library(), tools=PathTool(layer='M1', width=1, ptype='wire'))
|
||||
p.pattern.ports['A'] = Port((0, 0), rotation=0, ptype='wire')
|
||||
|
||||
with pytest.raises(BuildError, match='Unsupported routing bounds for uturn'):
|
||||
p.uturn('A', 4, **kwargs)
|
||||
|
||||
|
||||
def test_pather_uturn_none_length_defaults_to_zero() -> None:
|
||||
lib = Library()
|
||||
tool = PathTool(layer='M1', width=1, ptype='wire')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue