Source code for mink_warp.solvers.base
"""Common solver interface shared by every batched IK backend.
Every solver minimises the same per-world weighted least-squares cost
C(q) = 1/2 * sum_tasks || cost_i * error_i(q) ||^2
and exposes the identical entry point :meth:`Solver.solve_and_integrate`, so the
backend (:class:`DLSSolver`, :class:`LMSolver`, :class:`LBFGSSolver`) is
interchangeable in a control loop or benchmark.
"""
from __future__ import annotations
import abc
from collections.abc import Sequence
import warp as wp
from ..configuration import Configuration
from ..tasks.task import Task
[docs]
class Solver(abc.ABC):
"""Batched IK solver operating on a shared :class:`Configuration`.
Contract: :meth:`solve_and_integrate` advances the configuration toward the
task targets and returns a representative tangent velocity ``(nworld, nv)``.
For multi-iteration backends (LM / L-BFGS) the returned ``v`` is the sum of
the per-iteration tangent steps divided by ``dt``. Re-integrating ``v`` over
``dt`` reproduces the optimized configuration exactly for Euclidean joints
(hinge / slide); for free / ball joints it agrees to second order (the true
update is a composition of manifold exponentials, ``v`` their tangent sum).
The configuration itself is always left at the exact optimized state.
A solver is bound to its configuration's ``nworld`` / ``nv`` and cannot be
reused with a differently sized one.
"""
#: Registry key / human label.
name: str = "solver"
#: Whether this backend enforces hard limits. Only :class:`ConstrainedSolver`
#: sets this True; the cost-only backends (DLS / LM / L-BFGS) cannot honour a
#: ``limits=`` argument and callers must not silently assume they do.
supports_limits: bool = False
def __init__(self, configuration: Configuration):
self.configuration = configuration
[docs]
@abc.abstractmethod
def solve_and_integrate(
self,
tasks: Sequence[Task],
dt: float,
*,
iterations: int = 1,
use_graph: bool = False,
**kwargs,
) -> wp.array:
"""Advance ``configuration`` and return the tangent velocity."""
[docs]
def step(
self,
tasks: Sequence[Task],
dt: float,
*,
iterations: int = 1,
**kwargs,
) -> wp.array:
"""Alias for :meth:`solve_and_integrate` (no graph capture)."""
return self.solve_and_integrate(
tasks, dt, iterations=iterations, **kwargs
)
[docs]
def invalidate_graph(self) -> None:
"""Drop any captured CUDA graph. Backends without a graph no-op."""
@staticmethod
def _check_dt(dt: float) -> None:
if dt <= 0.0:
raise ValueError(f"dt must be > 0, got {dt}")