Source code for isaaclab_newton.ik.newton_ik_objectives

# 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

"""Runtime Newton IK objective implementations.

Each class is built by :class:`~isaaclab_newton.ik.NewtonIKSolver` from the
matching :class:`~isaaclab_newton.ik.newton_ik_objectives_cfg.NewtonIKObjectiveCfg`
and owns the concrete :class:`newton.ik.IKObjective` instances appended to the
solver. Pose objectives also describe their action contribution as Warp data:
an :attr:`~NewtonIKObjective.action_dim`, the coordinate names for that slice,
a numeric :attr:`~NewtonIKPoseObjective.command_code` / relative flag, a Warp
``scale`` array and a target-frame ``offset`` transform. The action term reads
these directly into a Warp kernel; nothing here touches Torch.

Importing this module pulls ``newton`` (and ``pxr``), so it is loaded lazily via
the package ``lazy_export`` only after Kit has launched. Custom objectives
integrate by subclassing :class:`NewtonIKObjective`, taking ``(cfg, ctx)`` in
``__init__`` -- pulling only the :class:`NewtonIKBuildContext` fields they need --
and populating :attr:`NewtonIKObjective.solver_objectives`.
"""

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass

import newton.ik as ik
import warp as wp

from .newton_ik_objectives_cfg import NewtonIKJointLimitObjectiveCfg, NewtonIKPoseObjectiveCfg

# Numeric command codes consumed by the action's Warp kernel.
COMMAND_POSITION = 0
COMMAND_POSE = 1


@dataclass(frozen=True)
class NewtonIKBuildContext:
    """Build-time inputs shared with every objective; each pulls what it needs."""

    model: object
    """Finalized Newton prototype model (e.g. for joint limits)."""

    num_envs: int
    """Number of parallel IK problems (target-array batch size)."""

    device: str
    """Warp device string for objective-owned arrays."""

    resolve_link: Callable[[str], int]
    """Maps a body name to its Newton link index in the prototype model."""


[docs] class NewtonIKObjective: """Base built IK objective. Owns the concrete :class:`newton.ik.IKObjective` instances in :attr:`solver_objectives`. Pose objectives also set a :attr:`name` and a non-zero :attr:`action_dim`; constraint objectives leave the defaults. """ name: str | None = None """Unique objective name, or ``None`` when the objective has no runtime target.""" action_dim: int = 0 """Number of action coordinates this objective consumes (0 for constraints).""" solver_objectives: list[ik.IKObjective] """Concrete Newton objectives appended to the solver's objective list."""
[docs] class NewtonIKPoseObjective(NewtonIKObjective): """Command-driven position + rotation objective tracking one end-effector body. Exposes its command convention to the action's Warp kernel as data: :attr:`command_code` / :attr:`use_relative`, the per-coordinate :attr:`scale` (``wp.float32``), the target-frame :attr:`offset` (``wp.transformf``), and the position/rotation target arrays the kernel writes into. """
[docs] def __init__(self, cfg: NewtonIKPoseObjectiveCfg, ctx: NewtonIKBuildContext): self.name = cfg.name if cfg.name is not None else cfg.body_name self.command_type = cfg.command_type self.use_relative_mode = cfg.use_relative_mode self.link_index = ctx.resolve_link(cfg.body_name) self.action_dim = len(self.command_coordinate_names()) self.command_code = COMMAND_POSITION if cfg.command_type == "position" else COMMAND_POSE self.use_relative = int(cfg.use_relative_mode) scale_values = [float(cfg.scale)] * self.action_dim if _is_scalar(cfg.scale) else [float(s) for s in cfg.scale] if len(scale_values) != self.action_dim: raise ValueError( f"Newton IK pose objective '{self.name}' scale must be a float or length-{self.action_dim} " f"sequence, got {len(scale_values)} values." ) self.scale = wp.array(scale_values, dtype=wp.float32, device=ctx.device) self.offset = wp.transformf(wp.vec3f(*cfg.body_offset_pos), wp.quatf(*cfg.body_offset_rot)) target_positions = wp.zeros((ctx.num_envs,), dtype=wp.vec3, device=ctx.device) target_rotations = wp.array([(0.0, 0.0, 0.0, 1.0)] * ctx.num_envs, dtype=wp.vec4, device=ctx.device) self.position_objective = ik.IKObjectivePosition( link_index=self.link_index, link_offset=wp.vec3(*cfg.body_offset_pos), target_positions=target_positions, weight=cfg.position_weight, ) self.rotation_objective = ik.IKObjectiveRotation( link_index=self.link_index, link_offset_rotation=wp.quat(*cfg.body_offset_rot), target_rotations=target_rotations, weight=cfg.rotation_weight, ) self.solver_objectives = [self.position_objective, self.rotation_objective]
def command_coordinate_names(self) -> list[str]: if self.command_type == "position": return ["x", "y", "z"] if self.command_type == "pose": if self.use_relative_mode: return ["x", "y", "z", "roll", "pitch", "yaw"] return ["x", "y", "z", "qx", "qy", "qz", "qw"] raise ValueError(f"Unsupported Newton IK command type: {self.command_type}")
[docs] class NewtonIKJointLimitObjective(NewtonIKObjective): """Soft joint-limit constraint reading the model's coordinate limits."""
[docs] def __init__(self, cfg: NewtonIKJointLimitObjectiveCfg, ctx: NewtonIKBuildContext): self.objective = ik.IKObjectiveJointLimit( joint_limit_lower=ctx.model.joint_limit_lower, joint_limit_upper=ctx.model.joint_limit_upper, weight=cfg.weight, ) self.solver_objectives = [self.objective]
def _is_scalar(value) -> bool: return isinstance(value, (int, float))