Source code for isaaclab_newton.physics.kamino_manager

# 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

"""Kamino Newton manager."""

from __future__ import annotations

import logging

import warp as wp
from newton import Model, eval_fk
from newton.solvers import SolverKamino

from isaaclab.physics import PhysicsManager

from .kamino_manager_cfg import KaminoSolverCfg
from .newton_manager import NewtonManager

logger = logging.getLogger(__name__)


def _model_has_loop_closing_joints(model: Model) -> bool:
    """Return whether ``model`` contains converted loop-closing articulation joints.

    Newton stores regular tree joints in ``[articulation_start[i], articulation_end[i])`` and
    loop-closing joints in ``[articulation_end[i], articulation_start[i + 1])``. Loop closures
    are present when the next articulation sentinel exceeds the tree joint end for any
    articulation.

    Args:
        model: Finalized Newton model to inspect.

    Returns:
        ``True`` if at least one articulation has loop-closing joints.
    """
    articulation_start = model.articulation_start
    articulation_end = model.articulation_end
    if articulation_start is None or articulation_end is None:
        return False
    articulation_start_np = articulation_start.numpy()
    articulation_end_np = articulation_end.numpy()
    if articulation_end_np.shape[0] == 0:
        return False
    return bool((articulation_start_np[1:] > articulation_end_np).any())


[docs] class NewtonKaminoManager(NewtonManager): """:class:`NewtonManager` specialization for the Kamino solver. Uses Newton's :class:`CollisionPipeline` unless :attr:`KaminoSolverCfg.use_collision_detector` is ``True``, in which case Kamino's internal collision detector handles contact generation. """ # Annotate the concrete solver type. _solver: SolverKamino @classmethod def _get_kamino_solver_cfg(cls) -> KaminoSolverCfg: cfg = PhysicsManager._cfg if cfg is None: raise RuntimeError("Physics manager is not initialized.") solver_cfg = getattr(cfg, "solver_cfg", None) if not isinstance(solver_cfg, KaminoSolverCfg): raise TypeError(f"Expected KaminoSolverCfg, got {type(solver_cfg).__name__}.") return solver_cfg @classmethod def _eval_fk_impl(cls, world_reset_mask: wp.array | None, fk_mask: wp.array | None) -> None: """Update body states from joint coordinates. For the Kamino (maximal-coordinate) solver, body poses/velocities are the authoritative simulation state. When :attr:`KaminoSolverCfg.use_fk_solver` is enabled, this calls :meth:`SolverKamino.reset`, which runs Kamino's loop-closure forward kinematics: it reads body poses/velocities from the joint coordinates (including the base body's pose/twist) and writes back a consistent full joint and body state. When ``use_fk_solver`` is disabled, falls back to Newton's articulated ``eval_fk`` over ``fk_mask``; the caller is then responsible for writing constraint-consistent joint values. Args: world_reset_mask: Per-world mask passed to :meth:`SolverKamino.reset` (``None`` means all). fk_mask: Per-articulation mask of articulations to update (``None`` means all). """ if cls._get_kamino_solver_cfg().use_fk_solver: cls._solver.reset( cls._state_0, world_mask=world_reset_mask, config=SolverKamino.ResetConfig.from_joints(), ) else: eval_fk(cls._model, cls._state_0.joint_q, cls._state_0.joint_qd, cls._state_0, fk_mask) # Reset solver internals without performing Kamino's FK. cls._solver.reset( cls._state_0, world_mask=world_reset_mask, config=SolverKamino.ResetConfig.preserve(), ) @classmethod def _build_solver(cls, model: Model, solver_cfg: KaminoSolverCfg) -> None: """Construct :class:`SolverKamino` and populate the base-class slots. Sets :attr:`NewtonManager._needs_collision_pipeline` to ``True`` only when ``use_collision_detector=False`` (Kamino's internal detector handles contacts otherwise). Sets :attr:`NewtonManager._needs_fk_before_step` because Kamino treats body state as authoritative: reset worlds (written via joint coordinates) must be reconciled before each step. The shared :attr:`NewtonManager._world_reset_mask` and :attr:`NewtonManager._fk_reset_mask` restrict that pre-step reconcile and :meth:`NewtonManager.forward` to reset worlds, so non-reset worlds keep their live, authoritative body state through Kamino's :meth:`_eval_fk_impl` overwrite. Raises: RuntimeError: If the model has more than one articulation per environment. The Kamino interface in IsaacLab currently only supports one articulation per environment. """ # Set the max contacts per world if specified. if solver_cfg.max_contacts_per_world is not None: model.rigid_contact_max = int(solver_cfg.max_contacts_per_world) * model.world_count logger.info( "[KAMINO] Capping rigid_contact_max to %d (%d/world * %d worlds)", model.rigid_contact_max, solver_cfg.max_contacts_per_world, model.world_count, ) # Set the use_fk_solver flag based on the model's articulation structure if not specified by user. if solver_cfg.use_fk_solver is None: solver_cfg.use_fk_solver = _model_has_loop_closing_joints(model) if solver_cfg.use_fk_solver and model.articulation_count != model.world_count: raise RuntimeError( "The Kamino FK solver requires exactly one articulation per environment, but the model" f" has {model.articulation_count} articulations across {model.world_count} environments." " Multiple articulations per environment are not yet supported in Kamino's FK solver." ) NewtonManager._solver = SolverKamino(model, solver_cfg.to_solver_config()) NewtonManager._use_single_state = False NewtonManager._needs_collision_pipeline = not solver_cfg.use_collision_detector NewtonManager._needs_fk_before_step = True