[Pather/Planner] Improve error reporting
This commit is contained in:
parent
09b131026f
commit
3a3cd854a5
9 changed files with 777 additions and 58 deletions
|
|
@ -22,7 +22,7 @@ from __future__ import annotations
|
|||
# ruff: noqa: ANN401,PLR0912,PLR0913,PLR0915,TC001,TC002,TC003
|
||||
|
||||
from collections.abc import Iterable, Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, replace
|
||||
from itertools import combinations
|
||||
from math import cos, isclose as math_isclose, sin
|
||||
from typing import Any, Literal
|
||||
|
|
@ -43,6 +43,7 @@ from ..tools import (
|
|||
StraightOffer,
|
||||
Tool,
|
||||
)
|
||||
from ..error import RouteError, RouteFailureDetails, RouteOperation
|
||||
from ..utils import ell
|
||||
from . import bounds as planner_bounds
|
||||
from .interface import (
|
||||
|
|
@ -56,6 +57,10 @@ from .interface import (
|
|||
RouteTieBreakStrategy = Literal['straight_first', 'turn_first']
|
||||
|
||||
|
||||
class NoLegalRouteError(BuildError):
|
||||
"""Internal marker for exhaustive candidate-selection failure."""
|
||||
|
||||
|
||||
def validate_strategy(strategy: RouteTieBreakStrategy | str) -> RouteTieBreakStrategy:
|
||||
"""Return a supported route tie-break strategy or raise a routing error."""
|
||||
if strategy in ('straight_first', 'turn_first'):
|
||||
|
|
@ -396,9 +401,11 @@ class Solver:
|
|||
if errors:
|
||||
last_error = errors[-1]
|
||||
if self.request.route_name in str(last_error):
|
||||
raise last_error
|
||||
raise BuildError(f'{self.request.route_name} route is unsupported: {last_error}') from last_error
|
||||
raise BuildError(f'No legal primitive offer for {self.request.route_name}')
|
||||
raise NoLegalRouteError(str(last_error)) from last_error
|
||||
raise NoLegalRouteError(
|
||||
f'{self.request.route_name} route is unsupported: {last_error}'
|
||||
) from last_error
|
||||
raise NoLegalRouteError(f'No legal primitive offer for {self.request.route_name}')
|
||||
|
||||
return min(
|
||||
candidates,
|
||||
|
|
@ -1038,6 +1045,9 @@ class RoutingPlanner:
|
|||
self,
|
||||
family: PrimitiveKind,
|
||||
context: RoutePortContext,
|
||||
operation: RouteOperation | None = None,
|
||||
diagnostic_request: Mapping[str, Any] | None = None,
|
||||
/,
|
||||
*,
|
||||
length: float | None = None,
|
||||
jog: float | None = None,
|
||||
|
|
@ -1049,6 +1059,32 @@ class RoutingPlanner:
|
|||
**kwargs: Any,
|
||||
) -> RouteLeg:
|
||||
"""Solve one route leg and attach it to its source Pather context."""
|
||||
if operation is None:
|
||||
if family in ('straight', 'bend'):
|
||||
operation = 'trace'
|
||||
elif family == 's':
|
||||
operation = 'jog'
|
||||
else:
|
||||
operation = 'uturn'
|
||||
if diagnostic_request is None:
|
||||
diagnostic_request = {}
|
||||
|
||||
if length is not None and (not numpy.isfinite(length) or length < 0):
|
||||
details = RouteFailureDetails(
|
||||
operation=operation,
|
||||
portspec=context.portspec,
|
||||
in_ptype=context.port.ptype,
|
||||
out_ptype=kwargs.get('out_ptype'),
|
||||
request=diagnostic_request,
|
||||
resolved_length=float(length),
|
||||
resolved_jog=None if jog is None else float(jog),
|
||||
minimum_length=None,
|
||||
minimum_attempted=False,
|
||||
minimum_exhausted=False,
|
||||
cause='Resolved route length must be finite and nonnegative',
|
||||
)
|
||||
raise RouteError(details, fatal=True)
|
||||
|
||||
request = self.route_request(
|
||||
family=family,
|
||||
context=context,
|
||||
|
|
@ -1062,10 +1098,47 @@ class RoutingPlanner:
|
|||
)
|
||||
try:
|
||||
candidate = self.solver_for_request(request).solve()
|
||||
except BuildError as err:
|
||||
if family == 'u' and length is None and not getattr(err, 'fatal', False):
|
||||
raise BuildError('No legal primitive offer for omitted-length U-turn') from err
|
||||
raise
|
||||
except NoLegalRouteError as err:
|
||||
cause = (
|
||||
'No legal primitive offer for omitted-length U-turn'
|
||||
if family == 'u' and length is None
|
||||
else str(err)
|
||||
)
|
||||
|
||||
minimum_length: float | None = None
|
||||
minimum_cause: str | None = None
|
||||
minimum_exhausted = False
|
||||
if length is None:
|
||||
minimum_cause = cause
|
||||
minimum_exhausted = True
|
||||
else:
|
||||
minimum_request = replace(request, length=None)
|
||||
try:
|
||||
minimum_candidate = self.solver_for_request(minimum_request).solve()
|
||||
minimum_length = minimum_candidate.public_length
|
||||
except NoLegalRouteError as minimum_err:
|
||||
minimum_cause = str(minimum_err)
|
||||
minimum_exhausted = True
|
||||
except Exception as minimum_err:
|
||||
if route_error_is_fatal(minimum_err):
|
||||
raise
|
||||
minimum_cause = str(minimum_err)
|
||||
|
||||
details = RouteFailureDetails(
|
||||
operation=operation,
|
||||
portspec=context.portspec,
|
||||
in_ptype=context.port.ptype,
|
||||
out_ptype=request.out_ptype,
|
||||
request=diagnostic_request,
|
||||
resolved_length=None if length is None else float(length),
|
||||
resolved_jog=None if jog is None else float(jog),
|
||||
minimum_length=minimum_length,
|
||||
minimum_attempted=True,
|
||||
minimum_exhausted=minimum_exhausted,
|
||||
cause=cause,
|
||||
minimum_cause=minimum_cause,
|
||||
)
|
||||
raise RouteError(details) from err
|
||||
return self.route_leg_from_candidate(context, candidate, plug_into=plug_into)
|
||||
|
||||
def prepared_route_action_from_leg(
|
||||
|
|
@ -1129,25 +1202,87 @@ class RoutingPlanner:
|
|||
) -> PreparedRouteResult:
|
||||
"""Plan straight or single-bend traces, including `each` and bundle-bound modes."""
|
||||
route_bounds = dict(bounds)
|
||||
request_details = {'ccw': ccw, **{
|
||||
key: value for key, value in route_bounds.items() if value is not None
|
||||
}}
|
||||
if length is not None:
|
||||
request_details['length'] = length
|
||||
if spacing is not None:
|
||||
request_details['spacing'] = spacing
|
||||
if strategy is not None:
|
||||
request_details['strategy'] = strategy
|
||||
operation: RouteOperation = 'trace'
|
||||
diagnostic_request = request_details
|
||||
return self._plan_trace_route(
|
||||
contexts,
|
||||
ccw,
|
||||
length,
|
||||
operation,
|
||||
diagnostic_request,
|
||||
spacing=spacing,
|
||||
strategy=strategy,
|
||||
**route_bounds,
|
||||
)
|
||||
|
||||
def _plan_trace_route(
|
||||
self,
|
||||
contexts: Sequence[RoutePortContext],
|
||||
ccw: SupportsBool | None,
|
||||
length: float | None,
|
||||
operation: RouteOperation,
|
||||
diagnostic_request: Mapping[str, Any],
|
||||
/,
|
||||
*,
|
||||
spacing: float | ArrayLike | None,
|
||||
strategy: RouteTieBreakStrategy | str | None,
|
||||
**route_bounds: Any,
|
||||
) -> PreparedRouteResult:
|
||||
portspec = tuple(context.portspec for context in contexts)
|
||||
planner_bounds.validate_trace_args(portspec, length=length, spacing=spacing, bounds=route_bounds)
|
||||
family: Literal['straight', 'bend'] = 'straight' if ccw is None else 'bend'
|
||||
if length is not None:
|
||||
leg = self.plan_leg(family, contexts[0], length=length, ccw=ccw, strategy=strategy, **route_bounds)
|
||||
leg = self.plan_leg(
|
||||
family,
|
||||
contexts[0],
|
||||
operation,
|
||||
diagnostic_request,
|
||||
length=length,
|
||||
ccw=ccw,
|
||||
strategy=strategy,
|
||||
**route_bounds,
|
||||
)
|
||||
return self.prepared_result_from_legs((leg,))
|
||||
|
||||
if route_bounds.get('each') is not None:
|
||||
each = route_bounds.pop('each')
|
||||
return PreparedRouteResult(tuple(
|
||||
self.prepared_route_action_from_leg(
|
||||
self.plan_leg(family, context, length=each, ccw=ccw, strategy=strategy, **route_bounds),
|
||||
self.plan_leg(
|
||||
family,
|
||||
context,
|
||||
operation,
|
||||
diagnostic_request,
|
||||
length=each,
|
||||
ccw=ccw,
|
||||
strategy=strategy,
|
||||
**route_bounds,
|
||||
),
|
||||
)
|
||||
for context in contexts
|
||||
))
|
||||
|
||||
bundle_bounds = planner_bounds.present_bundle_bounds(route_bounds)
|
||||
if not bundle_bounds:
|
||||
leg = self.plan_leg(family, contexts[0], length=None, ccw=ccw, strategy=strategy, **route_bounds)
|
||||
leg = self.plan_leg(
|
||||
family,
|
||||
contexts[0],
|
||||
operation,
|
||||
diagnostic_request,
|
||||
length=None,
|
||||
ccw=ccw,
|
||||
strategy=strategy,
|
||||
**route_bounds,
|
||||
)
|
||||
return self.prepared_result_from_legs((leg,))
|
||||
|
||||
bound_type = bundle_bounds[0]
|
||||
|
|
@ -1164,7 +1299,16 @@ class RoutingPlanner:
|
|||
actions = []
|
||||
for port_name, route_length in extensions.items():
|
||||
context = next(context for context in contexts if context.portspec == port_name)
|
||||
leg = self.plan_leg(family, context, length=route_length, ccw=ccw, strategy=strategy, **route_bounds)
|
||||
leg = self.plan_leg(
|
||||
family,
|
||||
context,
|
||||
operation,
|
||||
diagnostic_request,
|
||||
length=route_length,
|
||||
ccw=ccw,
|
||||
strategy=strategy,
|
||||
**route_bounds,
|
||||
)
|
||||
actions.append(self.prepared_route_action_from_leg(leg))
|
||||
return PreparedRouteResult(tuple(actions))
|
||||
|
||||
|
|
@ -1179,6 +1323,37 @@ class RoutingPlanner:
|
|||
) -> PreparedRouteResult:
|
||||
"""Plan `trace_to()` by resolving positional targets or delegating to `trace()` modes."""
|
||||
route_bounds = dict(bounds)
|
||||
request_details = {'ccw': ccw, **{
|
||||
key: value for key, value in route_bounds.items() if value is not None
|
||||
}}
|
||||
if spacing is not None:
|
||||
request_details['spacing'] = spacing
|
||||
if strategy is not None:
|
||||
request_details['strategy'] = strategy
|
||||
operation: RouteOperation = 'trace_to'
|
||||
diagnostic_request = request_details
|
||||
return self._plan_trace_to_route(
|
||||
contexts,
|
||||
ccw,
|
||||
operation,
|
||||
diagnostic_request,
|
||||
spacing=spacing,
|
||||
strategy=strategy,
|
||||
**route_bounds,
|
||||
)
|
||||
|
||||
def _plan_trace_to_route(
|
||||
self,
|
||||
contexts: Sequence[RoutePortContext],
|
||||
ccw: SupportsBool | None,
|
||||
operation: RouteOperation,
|
||||
diagnostic_request: Mapping[str, Any],
|
||||
/,
|
||||
*,
|
||||
spacing: float | ArrayLike | None,
|
||||
strategy: RouteTieBreakStrategy | str | None,
|
||||
**route_bounds: Any,
|
||||
) -> PreparedRouteResult:
|
||||
if len(contexts) == 1:
|
||||
resolved = planner_bounds.resolved_position_bound(contexts[0].port, route_bounds, allow_length=False)
|
||||
else:
|
||||
|
|
@ -1186,7 +1361,16 @@ class RoutingPlanner:
|
|||
if any(route_bounds.get(key) is not None for key in planner_bounds.POSITION_KEYS):
|
||||
raise BuildError('Position bounds only allowed with a single port')
|
||||
if resolved is None:
|
||||
return self.plan_trace_route(contexts, ccw, spacing=spacing, strategy=strategy, **route_bounds)
|
||||
return self._plan_trace_route(
|
||||
contexts,
|
||||
ccw,
|
||||
route_bounds.pop('length', None),
|
||||
operation,
|
||||
diagnostic_request,
|
||||
spacing=spacing,
|
||||
strategy=strategy,
|
||||
**route_bounds,
|
||||
)
|
||||
|
||||
planner_bounds.validate_trace_to_positional_args(spacing=spacing, bounds=route_bounds)
|
||||
_key, _value, length = resolved
|
||||
|
|
@ -1196,7 +1380,16 @@ class RoutingPlanner:
|
|||
if key not in planner_bounds.POSITION_KEYS and key != 'length'
|
||||
}
|
||||
family: Literal['straight', 'bend'] = 'straight' if ccw is None else 'bend'
|
||||
leg = self.plan_leg(family, contexts[0], length=length, ccw=ccw, strategy=strategy, **other_bounds)
|
||||
leg = self.plan_leg(
|
||||
family,
|
||||
contexts[0],
|
||||
operation,
|
||||
diagnostic_request,
|
||||
length=length,
|
||||
ccw=ccw,
|
||||
strategy=strategy,
|
||||
**other_bounds,
|
||||
)
|
||||
return self.prepared_result_from_legs((leg,))
|
||||
|
||||
def plan_jog_route(
|
||||
|
|
@ -1210,8 +1403,28 @@ class RoutingPlanner:
|
|||
**bounds: Any,
|
||||
) -> PreparedRouteResult:
|
||||
"""Plan S-bend routes for single ports or spaced bundles."""
|
||||
request_details = {'offset': offset, **{
|
||||
key: value for key, value in bounds.items() if value is not None
|
||||
}}
|
||||
if length is not None:
|
||||
request_details['length'] = length
|
||||
if spacing is not None:
|
||||
request_details['spacing'] = spacing
|
||||
if strategy is not None:
|
||||
request_details['strategy'] = strategy
|
||||
operation: RouteOperation = 'jog'
|
||||
diagnostic_request = request_details
|
||||
if numpy.isclose(offset, 0):
|
||||
return self.plan_trace_to_route(contexts, None, length=length, spacing=spacing, strategy=strategy, **bounds)
|
||||
return self._plan_trace_to_route(
|
||||
contexts,
|
||||
None,
|
||||
operation,
|
||||
diagnostic_request,
|
||||
length=length,
|
||||
spacing=spacing,
|
||||
strategy=strategy,
|
||||
**bounds,
|
||||
)
|
||||
route_bounds = dict(bounds)
|
||||
portspec = tuple(context.portspec for context in contexts)
|
||||
planner_bounds.validate_jog_args(portspec, length=length, spacing=spacing, bounds=route_bounds)
|
||||
|
|
@ -1224,9 +1437,28 @@ class RoutingPlanner:
|
|||
if len(contexts) > 1:
|
||||
return PreparedRouteResult(tuple(
|
||||
self.prepared_route_action_from_leg(leg)
|
||||
for leg in self.plan_su_bundle_routes('s', contexts, offset, length, spacing, strategy=strategy, **other_bounds)
|
||||
for leg in self.plan_su_bundle_routes(
|
||||
's',
|
||||
contexts,
|
||||
offset,
|
||||
length,
|
||||
spacing,
|
||||
operation,
|
||||
diagnostic_request,
|
||||
strategy=strategy,
|
||||
**other_bounds,
|
||||
)
|
||||
))
|
||||
leg = self.plan_leg('s', contexts[0], length=length, jog=offset, strategy=strategy, **other_bounds)
|
||||
leg = self.plan_leg(
|
||||
's',
|
||||
contexts[0],
|
||||
operation,
|
||||
diagnostic_request,
|
||||
length=length,
|
||||
jog=offset,
|
||||
strategy=strategy,
|
||||
**other_bounds,
|
||||
)
|
||||
return self.prepared_result_from_legs((leg,))
|
||||
|
||||
def plan_uturn_route(
|
||||
|
|
@ -1241,14 +1473,44 @@ class RoutingPlanner:
|
|||
) -> PreparedRouteResult:
|
||||
"""Plan U-turn routes for single ports or spaced bundles."""
|
||||
route_bounds = dict(bounds)
|
||||
request_details = {'offset': offset, **{
|
||||
key: value for key, value in route_bounds.items() if value is not None
|
||||
}}
|
||||
if length is not None:
|
||||
request_details['length'] = length
|
||||
if spacing is not None:
|
||||
request_details['spacing'] = spacing
|
||||
if strategy is not None:
|
||||
request_details['strategy'] = strategy
|
||||
operation: RouteOperation = 'uturn'
|
||||
diagnostic_request = request_details
|
||||
portspec = tuple(context.portspec for context in contexts)
|
||||
planner_bounds.validate_uturn_args(portspec, spacing=spacing, bounds=route_bounds)
|
||||
if len(contexts) > 1:
|
||||
return PreparedRouteResult(tuple(
|
||||
self.prepared_route_action_from_leg(leg)
|
||||
for leg in self.plan_su_bundle_routes('u', contexts, offset, length, spacing, strategy=strategy, **route_bounds)
|
||||
for leg in self.plan_su_bundle_routes(
|
||||
'u',
|
||||
contexts,
|
||||
offset,
|
||||
length,
|
||||
spacing,
|
||||
operation,
|
||||
diagnostic_request,
|
||||
strategy=strategy,
|
||||
**route_bounds,
|
||||
)
|
||||
))
|
||||
leg = self.plan_leg('u', contexts[0], length=length, jog=offset, strategy=strategy, **route_bounds)
|
||||
leg = self.plan_leg(
|
||||
'u',
|
||||
contexts[0],
|
||||
operation,
|
||||
diagnostic_request,
|
||||
length=length,
|
||||
jog=offset,
|
||||
strategy=strategy,
|
||||
**route_bounds,
|
||||
)
|
||||
return self.prepared_result_from_legs((leg,))
|
||||
|
||||
def plan_su_bundle_routes(
|
||||
|
|
@ -1258,6 +1520,9 @@ class RoutingPlanner:
|
|||
offset: float,
|
||||
length: float | None,
|
||||
spacing: float | ArrayLike | None,
|
||||
operation: RouteOperation,
|
||||
diagnostic_request: Mapping[str, Any],
|
||||
/,
|
||||
*,
|
||||
strategy: RouteTieBreakStrategy | str | None = None,
|
||||
**kwargs: Any,
|
||||
|
|
@ -1270,14 +1535,32 @@ class RoutingPlanner:
|
|||
and offset so all legs can be planned independently.
|
||||
"""
|
||||
if len(contexts) == 1:
|
||||
return (self.plan_leg(kind, contexts[0], length=length, jog=offset, strategy=strategy, **kwargs),)
|
||||
return (self.plan_leg(
|
||||
kind,
|
||||
contexts[0],
|
||||
operation,
|
||||
diagnostic_request,
|
||||
length=length,
|
||||
jog=offset,
|
||||
strategy=strategy,
|
||||
**kwargs,
|
||||
),)
|
||||
route_name = 'jog' if kind == 's' else 'uturn'
|
||||
if kind == 'u' and is_close(offset, 0):
|
||||
raise BuildError('multi-port uturn() requires nonzero offset to determine bundle ordering')
|
||||
contexts_by_name = {context.portspec: context for context in contexts}
|
||||
initial_specs = planner_bounds.su_bundle_specs(contexts, offset, 0, spacing, route_name=route_name)
|
||||
anchor_portspec, _anchor_length, _anchor_offset = initial_specs[0]
|
||||
anchor = self.plan_leg(kind, contexts_by_name[anchor_portspec], length=length, jog=offset, strategy=strategy, **kwargs)
|
||||
anchor = self.plan_leg(
|
||||
kind,
|
||||
contexts_by_name[anchor_portspec],
|
||||
operation,
|
||||
diagnostic_request,
|
||||
length=length,
|
||||
jog=offset,
|
||||
strategy=strategy,
|
||||
**kwargs,
|
||||
)
|
||||
base_length = anchor.candidate.public_length
|
||||
specs = planner_bounds.su_bundle_specs(contexts, offset, base_length, spacing, route_name=route_name)
|
||||
first_portspec, _first_length, _first_offset = specs[0]
|
||||
|
|
@ -1287,7 +1570,10 @@ class RoutingPlanner:
|
|||
routes_by_name[spec_portspec] = self.plan_leg(
|
||||
'straight',
|
||||
contexts_by_name[spec_portspec],
|
||||
operation,
|
||||
diagnostic_request,
|
||||
length=spec_length,
|
||||
jog=spec_offset,
|
||||
strategy=strategy,
|
||||
**kwargs,
|
||||
)
|
||||
|
|
@ -1295,6 +1581,8 @@ class RoutingPlanner:
|
|||
routes_by_name[spec_portspec] = self.plan_leg(
|
||||
kind,
|
||||
contexts_by_name[spec_portspec],
|
||||
operation,
|
||||
diagnostic_request,
|
||||
length=spec_length,
|
||||
jog=spec_offset,
|
||||
strategy=strategy,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue