[Pather / Tool] more unification work and fixes

This commit is contained in:
Jan Petykiewicz 2026-07-13 14:44:39 -07:00
commit f9611933ac
19 changed files with 1022 additions and 137 deletions

View file

@ -32,7 +32,7 @@ from __future__ import annotations
from collections.abc import Iterable, Mapping, Sequence
from dataclasses import dataclass, replace
from itertools import combinations
from math import cos, isclose as math_isclose, sin
from math import cos, sin
from typing import Any, Literal
import numpy
@ -50,6 +50,7 @@ from ..tools import (
SOffer,
StraightOffer,
Tool,
UOffer,
)
from ..error import (
MinimumStatus,
@ -57,7 +58,9 @@ from ..error import (
RouteFailureDetails,
RouteFailurePolicy,
RouteOperation,
ToolContractError,
)
from .._tolerances import scalar_close
from ..utils import ell
from . import bounds as planner_bounds
from .interface import (
@ -84,7 +87,7 @@ def validate_strategy(strategy: RouteTieBreakStrategy | str) -> RouteTieBreakStr
def is_close(a: float, b: float) -> bool:
"""Compare route-solver scalars with the planner tolerance."""
return math_isclose(float(a), float(b), rel_tol=1e-5, abs_tol=1e-8)
return scalar_close(a, b)
def clean_parameter(value: float) -> float:
@ -225,8 +228,6 @@ class SelectedPrimitive:
"""Finite additive planning cost reported by the offer."""
role: Literal['main', 'adapter'] = 'main'
"""Whether this step satisfies route geometry or adapts ptype."""
route_kind: PrimitiveKind | None = None
"""Primitive kind used when querying the Tool for this step."""
@dataclass(frozen=True, slots=True)
@ -318,7 +319,7 @@ class Solver:
def __init__(self, request: SolverRequest) -> None:
self.request = request
self.eval_cache: dict[tuple[int, float, str | None, str | None, str, str, PrimitiveKind | None], SelectedPrimitive] = {}
self.eval_cache: dict[tuple[int, float, str | None, str | None, str, str], SelectedPrimitive] = {}
self.offer_cache: dict[
tuple[PrimitiveKind, str | None, str | None, tuple[tuple[str, Any], ...]],
tuple[PrimitiveOffer, ...],
@ -333,9 +334,9 @@ class Solver:
for step in steps:
if step.role == 'adapter':
continue
if step.route_kind == 'bend':
if step.offer.kind == 'bend':
count += 1
elif step.route_kind in ('s', 'u'):
elif step.offer.kind in ('s', 'u'):
count += 2
return count
@ -371,7 +372,6 @@ class Solver:
tuple((
offer_key(step.offer),
step.role,
step.route_kind,
round(float(step.parameter), 9),
endpoint_key(step.out_port),
) for step in candidate.steps),
@ -448,16 +448,54 @@ class Solver:
kwargs = dict(self.request.tool_options)
if extra:
kwargs.update(extra)
def query_tool() -> tuple[PrimitiveOffer, ...]:
expected_offer_type = {
'straight': StraightOffer,
'bend': BendOffer,
's': SOffer,
'u': UOffer,
}[kind]
offers = self.request.tool.primitive_offers(
kind,
in_ptype=in_ptype,
out_ptype=out_ptype,
**kwargs,
)
if not isinstance(offers, tuple):
raise ToolContractError(
f'Tool.primitive_offers({kind!r}) must return a tuple, '
f'got {type(offers).__name__}'
)
for index, offer in enumerate(offers):
if not isinstance(offer, PrimitiveOffer):
raise ToolContractError(
f'Tool.primitive_offers({kind!r}) item {index} must be a PrimitiveOffer, '
f'got {type(offer).__name__}'
)
if offer.kind != kind:
raise ToolContractError(
f'Tool.primitive_offers({kind!r}) item {index} returned '
f'{offer.kind!r} offer'
)
if not isinstance(offer, expected_offer_type):
raise ToolContractError(
f'Tool.primitive_offers({kind!r}) item {index} must be '
f'{expected_offer_type.__name__}, got {type(offer).__name__}'
)
return offers
extra_items = tuple(sorted((extra or {}).items()))
try:
cache_key = (kind, in_ptype, out_ptype, tuple(sorted(kwargs.items())))
cache_key = (kind, in_ptype, out_ptype, extra_items)
hash(cache_key)
except TypeError:
return self.request.tool.primitive_offers(kind, in_ptype=in_ptype, out_ptype=out_ptype, **kwargs)
return query_tool()
cached = self.offer_cache.get(cache_key)
if cached is not None:
return cached
offers = self.request.tool.primitive_offers(kind, in_ptype=in_ptype, out_ptype=out_ptype, **kwargs)
offers = query_tool()
self.offer_cache[cache_key] = offers
return offers
@ -469,7 +507,6 @@ class Solver:
*,
out_ptype: str | None,
role: Literal['main', 'adapter'],
route_kind: PrimitiveKind | None,
route_name: str | None = None,
) -> SelectedPrimitive:
"""
@ -481,7 +518,7 @@ class Solver:
"""
route_name = self.request.route_name if route_name is None else route_name
selected = offer.canonicalize_parameter(clean_parameter(parameter))
key = (id(offer), round(float(selected), 12), in_ptype, out_ptype, role, route_name, route_kind)
key = (id(offer), round(float(selected), 12), in_ptype, out_ptype, role, route_name)
cached = self.eval_cache.get(key)
if cached is not None:
return cached
@ -491,28 +528,37 @@ class Solver:
if isinstance(offer, SOffer) and is_close(selected, 0):
raise BuildError('zero-jog S primitive candidates are not allowed')
out_port = offer.endpoint_at(selected)
if not isinstance(out_port, Port):
raise ToolContractError(
f'{route_name} primitive endpoint_at() must return a Port, '
f'got {type(out_port).__name__}'
)
if not ptypes_compatible(out_port.ptype, offer.out_ptype):
raise RoutePlanningError(
raise ToolContractError(
f'{route_name} primitive endpoint ptype does not match declared offer out_ptype',
policy=RouteFailurePolicy.FATAL,
)
if out_ptype is not None and not ptypes_compatible(out_port.ptype, out_ptype):
raise RoutePlanningError(
'Requested out_ptype does not match primitive endpoint ptype',
policy=RouteFailurePolicy.FATAL,
)
cost = float(offer.cost_at(selected))
try:
if type(offer).cost_at is PrimitiveOffer.cost_at:
cost = float(PrimitiveOffer._cost_for_endpoint(offer, selected, out_port))
else:
cost = float(offer.cost_at(selected))
except (TypeError, ValueError, OverflowError) as err:
raise ToolContractError(f'{route_name} primitive returned a non-numeric cost') from err
if not numpy.isfinite(cost):
raise BuildError(f'{route_name} primitive returned non-finite cost')
raise ToolContractError(f'{route_name} primitive returned non-finite cost')
if cost < 0:
raise BuildError(f'{route_name} primitive returned negative cost')
raise ToolContractError(f'{route_name} primitive returned negative cost')
primitive = SelectedPrimitive(
offer,
selected,
out_port,
cost,
role=role,
route_kind=route_kind,
)
self.eval_cache[key] = primitive
return primitive
@ -532,7 +578,7 @@ class Solver:
for step in steps:
out_port = step.out_port
if out_port.rotation is None:
raise BuildError('Primitive endpoints must have rotation')
raise ToolContractError('Primitive endpoints must have rotation')
angle_cos = cos(angle)
angle_sin = sin(angle)
x += angle_cos * float(out_port.x) - angle_sin * float(out_port.y)
@ -573,7 +619,6 @@ class Solver:
current_ptype,
out_ptype=None,
role='adapter',
route_kind=kind,
route_name=f'{kind} adapter',
)
except BuildError as err:
@ -606,7 +651,6 @@ class Solver:
current_ptype,
out_ptype=None,
role='main',
route_kind='straight',
route_name='trace',
)
except BuildError as err:
@ -642,7 +686,6 @@ class Solver:
current_ptype,
out_ptype=None,
role='main',
route_kind='bend',
route_name='trace',
)
except BuildError as err:
@ -678,7 +721,6 @@ class Solver:
current_ptype,
out_ptype=None,
role='main',
route_kind=kind,
route_name=route_name,
)
except BuildError as err:
@ -814,7 +856,6 @@ class Solver:
current_ptype,
out_ptype=None,
role=step.role,
route_kind=step.route_kind,
)
selected.append(selected_step)
current_ptype = selected_step.out_port.ptype
@ -1183,7 +1224,7 @@ class RoutingPlanner:
out_port.rotate_around((0, 0), pi + port_rot)
out_port.translate(current.offset)
render_steps.append(RenderStep(
selected.offer.opcode,
selected.offer.kind,
leg.tool,
current.copy(),
out_port.copy(),