Source code for isaaclab.utils.leapp.leapp_semantics

# 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

"""LEAPP semantic metadata helpers for raw tensor-producing functions."""

from __future__ import annotations

from collections.abc import Callable
from contextlib import suppress
from dataclasses import dataclass
from typing import Any

import warp as wp

from isaaclab.utils.array import convert_to_torch

try:
    from leapp import InputKindEnum, OutputKindEnum
except ImportError:

    class _LeappEnumSentinel:
        """Stand-in when leapp is not installed.

        Any attribute access returns ``None`` so that
        ``@leapp_tensor_semantics(kind=InputKindEnum.BODY_POSE)``
        silently stores ``kind=None`` instead of crashing at import time.
        The real enum values are only needed at export time, when leapp
        *is* guaranteed to be available.
        """

        def __getattr__(self, name: str):
            return None

    InputKindEnum = _LeappEnumSentinel()  # type: ignore[assignment,misc]
    OutputKindEnum = _LeappEnumSentinel()  # type: ignore[assignment,misc]


[docs] @dataclass(frozen=True) class LeappTensorSemantics: """Semantic metadata attached directly to a raw tensor-producing function.""" kind: Any = None element_names: list[str] | list[list[str]] | None = None element_names_resolver: Callable | None = None const: bool = False
XYZ_ELEMENT_NAMES: list[str] = ["x", "y", "z"] QUAT_XYZW_ELEMENT_NAMES: list[str] = ["qx", "qy", "qz", "qw"] POSE7_ELEMENT_NAMES: list[str] = ["x", "y", "z", "qx", "qy", "qz", "qw"] POSE6_ELEMENT_NAMES: list[str] = ["x", "y", "z", "angular_x", "angular_y", "angular_z"] WRENCH6_ELEMENT_NAMES: list[str] = ["fx", "fy", "fz", "tx", "ty", "tz"] def select_element_names(names: list[str] | None, indices: Any = None) -> list[str] | None: """Select element names using optional runtime indices.""" if names is None: return None if indices is None or indices == slice(None): return list(names) if isinstance(indices, slice): return list(names[indices]) if isinstance(indices, wp.array): indices = convert_to_torch(indices) with suppress(AttributeError): indices = indices.tolist() if isinstance(indices, (list, tuple)): return [names[int(index)] for index in indices] if isinstance(indices, int): return [names[indices]] return None def leapp_tensor_semantics( *, kind: Any = None, element_names: list[str] | list[list[str]] | None = None, element_names_resolver: Callable | None = None, const: bool = False, ) -> Callable: """Attach LEAPP semantic metadata to a raw tensor-producing function.""" semantics = LeappTensorSemantics( kind=kind, element_names=element_names, element_names_resolver=element_names_resolver, const=const, ) def _apply(func: Callable) -> Callable: func._leapp_semantics = semantics return func return _apply def resolve_leapp_element_names(semantics: LeappTensorSemantics | None, data_self) -> list | None: """Resolve element names from attached semantics and a tensor-producing object.""" if semantics is None: return None if semantics.element_names is not None: return semantics.element_names if semantics.element_names_resolver is not None: return semantics.element_names_resolver(data_self) return None # ── Predefined element-name resolvers ───────────────────────────── def joint_names_resolver(data_self) -> list[str] | None: """Resolve joint element names from the data object at trace time.""" return select_element_names( getattr(data_self, "joint_names", getattr(data_self, "_joint_names", None)), getattr(data_self, "_joint_ids", None), ) def body_names_resolver(data_self) -> list[str] | None: """Resolve body element names from the data object at trace time.""" return select_element_names( getattr(data_self, "body_names", getattr(data_self, "_body_names", None)), getattr(data_self, "_body_ids", None), ) def _compound_resolver(outer_fn: Callable, inner_names: list[str]) -> Callable: """Build a 2D resolver: ``[outer_names, inner_constant_names]``.""" def resolver(data_self) -> list | None: outer = outer_fn(data_self) return [outer, inner_names] if outer else None return resolver def _target_frame_names(data_self) -> list[str] | None: names = getattr(data_self, "target_frame_names", None) return list(names) if names is not None else None body_xyz_resolver = _compound_resolver(body_names_resolver, XYZ_ELEMENT_NAMES) body_pose_resolver = _compound_resolver(body_names_resolver, POSE7_ELEMENT_NAMES) body_pose6_resolver = _compound_resolver(body_names_resolver, POSE6_ELEMENT_NAMES) body_quat_resolver = _compound_resolver(body_names_resolver, QUAT_XYZW_ELEMENT_NAMES) body_wrench_resolver = _compound_resolver(body_names_resolver, WRENCH6_ELEMENT_NAMES) target_frame_xyz_resolver = _compound_resolver(_target_frame_names, XYZ_ELEMENT_NAMES) target_frame_quat_resolver = _compound_resolver(_target_frame_names, QUAT_XYZW_ELEMENT_NAMES) target_frame_pose_resolver = _compound_resolver(_target_frame_names, POSE7_ELEMENT_NAMES)