Source code for isaaclab_newton.ik.newton_ik_solver
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
from __future__ import annotations
from collections.abc import Callable, Sequence
import newton.ik as ik
import warp as wp
from .newton_ik_objectives import NewtonIKBuildContext, NewtonIKObjective
from .newton_ik_objectives_cfg import NewtonIKObjectiveCfg
from .newton_ik_solver_cfg import NewtonIKSolverCfg
[docs]
class NewtonIKSolver:
"""Batched wrapper around Newton's inverse-kinematics solver.
The solver is configured by an ordered list of
:class:`~isaaclab_newton.ik.newton_ik_objectives_cfg.NewtonIKObjectiveCfg`.
Each cfg is resolved to its runtime
:class:`~isaaclab_newton.ik.newton_ik_objectives.NewtonIKObjective` and its
concrete Newton objectives are appended to the underlying
:class:`newton.ik.IKSolver`. The built objectives are exposed via
:attr:`objectives` / :attr:`objectives_by_name`; callers update a pose
objective's target by calling :meth:`~..newton_ik_objectives.NewtonIKPoseObjective.set_target_pose`
on it directly.
The solver solves ``num_envs`` independent problems and is agnostic to how
targets are produced -- the prototype-broadcast policy used by the Newton IK
action term lives in the action, not here. ``link_resolver`` maps an
objective's body name to a Newton link index; the caller owns it because the
name-to-index mapping depends on the model layout (e.g. cloned env prefixes).
"""
cfg: NewtonIKSolverCfg
[docs]
def __init__(
self,
cfg: NewtonIKSolverCfg,
*,
model,
num_envs: int,
device: str,
objectives: Sequence[NewtonIKObjectiveCfg],
link_resolver: Callable[[str], int],
):
if not objectives:
raise ValueError("NewtonIKSolver requires at least one objective cfg.")
self.cfg = cfg
ctx = NewtonIKBuildContext(model=model, num_envs=num_envs, device=device, resolve_link=link_resolver)
self.objectives: list[NewtonIKObjective] = []
self.objectives_by_name: dict[str, NewtonIKObjective] = {}
solver_objectives: list[ik.IKObjective] = []
for objective_cfg in objectives:
objective = objective_cfg.class_type(objective_cfg, ctx)
if objective.name is not None:
if objective.name in self.objectives_by_name:
raise ValueError(f"Newton IK objective names must be unique: duplicate '{objective.name}'.")
self.objectives_by_name[objective.name] = objective
self.objectives.append(objective)
solver_objectives.extend(objective.solver_objectives)
self.joint_q_out = wp.zeros((num_envs, model.joint_coord_count), dtype=wp.float32, device=device)
self.solver = ik.IKSolver(
model=model,
n_problems=num_envs,
objectives=solver_objectives,
optimizer=ik.IKOptimizer(cfg.optimizer),
jacobian_mode=ik.IKJacobianType(cfg.jacobian_mode),
sampler=ik.IKSampler(cfg.sampler),
n_seeds=cfg.n_seeds,
noise_std=cfg.noise_std,
rng_seed=cfg.rng_seed,
lambda_initial=cfg.lambda_initial,
)
@property
def costs(self) -> wp.array:
"""Expanded per-seed costs from the most recent Newton solve."""
return self.solver.costs
@property
def joint_q(self) -> wp.array:
"""Expanded joint-coordinate buffer storing all sampled seeds."""
return self.solver.joint_q
def solve(self, joint_pos: wp.array) -> wp.array:
"""Solve IK from the Warp seed ``joint_pos``, shape ``[num_envs, joint_coord_count]``.
Returns the solver's output buffer, overwritten on the next solve -- consume
or copy it before solving again.
"""
self.solver.step(
joint_pos,
self.joint_q_out,
iterations=self.cfg.iterations,
step_size=self.cfg.step_size,
)
return self.joint_q_out