Source code for isaaclab.assets.articulation.ordering

# 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

"""Public articulation ordering name-map type and its validation.

This module owns the public ordering surface: the
:class:`ArticulationOrderingConvention` enum, the frozen
:class:`ArticulationNameMap` permutation type (built only by the module-level
:func:`build_articulation_name_map` factory, never constructed directly), and
the helpers that parse conventions and build validated maps between backend and
public order.
Discovering a convention's concrete names lives in
:mod:`isaaclab.assets.articulation.ordering_resolvers`, and device-side axis
conversion lives in :mod:`isaaclab.assets.articulation.ordering_kernels`.
"""

from __future__ import annotations

from collections.abc import Sequence
from dataclasses import dataclass
from enum import Enum
from typing import TYPE_CHECKING, Literal

import numpy as np
import warp as wp

if TYPE_CHECKING:
    from .articulation_cfg import ArticulationCfg


def _validate_articulation_names(names: list[str] | tuple[str, ...], *, parameter_name: str) -> tuple[str, ...]:
    """Return the names as a tuple after a hard list-or-tuple-of-str check.

    Ingestion boundaries accept both plain sequence types because config
    sources such as Hydra produce lists; the canonical stored form is always
    a tuple.
    """
    if type(names) not in (list, tuple):
        raise TypeError(f"{parameter_name} must be a list or tuple of strings; got {type(names).__name__}.")
    for index, name in enumerate(names):
        if type(name) is not str:
            raise TypeError(f"{parameter_name} element {index} must be str; got {name!r} ({type(name).__name__}).")
    return tuple(names)


[docs] class ArticulationOrderingConvention(str, Enum): """Built-in non-default public articulation name-ordering conventions. Attributes: PHYSX: Active PhysX or OVPhysX tensor-view order. MJWARP: Newton or MJWarp articulation-view order. ROBOT_SCHEMA: Authored target order of the ``isaac:physics:robotJoints`` and ``isaac:physics:robotLinks`` relationships. ``None`` selects the active backend order by default and is not a member of this enum. """ PHYSX = "physx" MJWARP = "mjwarp" ROBOT_SCHEMA = "robot_schema"
[docs] @dataclass(frozen=True) class ArticulationNameMap: """Frozen permutation between backend and public articulation order. ``user`` in the field names means the order exposed by the public API. ``user_to_backend`` maps a public index to its backend index, while ``backend_to_user`` maps a backend index to its public index. The CPU tuples and device arrays are complete inverse permutations of the same length; both device maps are one-dimensional ``wp.int32`` arrays on the articulation's device. The frozen dataclass prevents field reassignment, but the Warp arrays remain mutable objects and callers must treat both device maps as read-only. Instances are built by the owning articulation during initialization via :func:`build_articulation_name_map`; the class is not intended for direct construction. Identity orderings are represented as ``None`` rather than a map — the :attr:`~isaaclab.assets.Articulation.joint_ordering` and :attr:`~isaaclab.assets.Articulation.body_ordering` properties are ``None`` whenever public and backend orders coincide, so a non-``None`` map always denotes an actual permutation. """ user_to_backend_indices: tuple[int, ...] """One-dimensional CPU map from public index to backend index.""" backend_to_user_indices: tuple[int, ...] """One-dimensional CPU map from backend index to public index.""" user_to_backend: wp.array(dtype=wp.int32) """Read-only public-to-backend device map, shape ``(num_names,)``, dtype ``wp.int32``.""" backend_to_user: wp.array(dtype=wp.int32) """Read-only backend-to-public device map, shape ``(num_names,)``, dtype ``wp.int32``."""
[docs] def parse_articulation_ordering_convention( ordering: str | ArticulationOrderingConvention | None, ) -> ArticulationOrderingConvention | None: """Parse a symbolic public articulation ordering convention. Accepted aliases are ``"physx"``, ``"mjwarp"``, and ``"robot_schema"``. String aliases are matched case-insensitively. ``None`` keeps the active backend's default order and is not an enum member. Args: ordering: Convention alias, :class:`ArticulationOrderingConvention` member, or ``None``. Returns: The matching :class:`ArticulationOrderingConvention` member, or ``None`` when no non-default convention is requested. Raises: TypeError: If :paramref:`ordering` is not a supported type. ValueError: If :paramref:`ordering` is an unsupported string alias. """ if ordering is None: return None if isinstance(ordering, ArticulationOrderingConvention): return ordering if isinstance(ordering, str): try: return ArticulationOrderingConvention(ordering.lower()) except ValueError as exc: valid_values = ", ".join(convention.value for convention in ArticulationOrderingConvention) raise ValueError( f"Unsupported articulation ordering convention '{ordering}'. Expected one of: {valid_values}." ) from exc raise TypeError( "Articulation ordering convention must be a string, " f"{ArticulationOrderingConvention.__name__}, or None. Got {type(ordering).__name__}." )
[docs] def apply_articulation_ordering_preset( cfg: ArticulationCfg, ordering: str | ArticulationOrderingConvention | None, ) -> ArticulationCfg: """Apply one public ordering preset to both joints and bodies. Args: cfg: Articulation configuration to copy when a preset is requested. ordering: Convention alias, :class:`ArticulationOrderingConvention` member, or ``None``. Returns: A copy of :paramref:`cfg` whose :attr:`ArticulationCfg.joint_ordering` and :attr:`ArticulationCfg.body_ordering` use the parsed convention. When :paramref:`ordering` is ``None``, returns the original :paramref:`cfg` object unchanged. Raises: TypeError: If :paramref:`ordering` is not a supported type. ValueError: If :paramref:`ordering` is an unsupported string alias. """ parsed_ordering = parse_articulation_ordering_convention(ordering) if parsed_ordering is None: return cfg return cfg.replace(joint_ordering=parsed_ordering, body_ordering=parsed_ordering)
def build_articulation_name_map( *, kind: Literal["joint", "body"], backend_names: Sequence[str], user_names: Sequence[str] | None, device: str, ) -> ArticulationNameMap | None: """Build a validated map between backend and public articulation order. Args: kind: Mapped element kind, either ``"joint"`` or ``"body"``; used in error messages. backend_names: Names in active backend solver-view order. user_names: Complete permutation in public API order, or ``None`` to keep the backend order. device: Warp device on which both device index maps are allocated. Returns: An :class:`ArticulationNameMap` containing CPU index tuples and one-dimensional ``wp.int32`` device maps on :paramref:`device`, or ``None`` when :paramref:`user_names` is ``None`` or already in backend order (identity). Raises: ValueError: If either name sequence has duplicates or :paramref:`user_names` is not a complete permutation of :paramref:`backend_names`. TypeError: If either name input is not a list or tuple of strings. """ backend_names = _validate_articulation_names(backend_names, parameter_name="backend_names") if user_names is None: return None user_names = _validate_articulation_names(user_names, parameter_name="user_names") if len(set(backend_names)) != len(backend_names): raise ValueError(f"Duplicate backend {kind} names are not supported: {backend_names}.") if len(set(user_names)) != len(user_names): raise ValueError(f"Duplicate requested {kind} names are not supported: {user_names}.") backend_name_set = set(backend_names) user_name_set = set(user_names) if user_name_set != backend_name_set: missing = sorted(backend_name_set - user_name_set) extra = sorted(user_name_set - backend_name_set) raise ValueError( f"Requested {kind} names must be a complete permutation of backend names. Missing={missing}, extra={extra}." ) if user_names == backend_names: return None backend_index_by_name = {name: index for index, name in enumerate(backend_names)} user_to_backend_np = np.asarray([backend_index_by_name[name] for name in user_names], dtype=np.int32) backend_to_user_np = np.empty_like(user_to_backend_np) backend_to_user_np[user_to_backend_np] = np.arange(len(user_names), dtype=np.int32) return ArticulationNameMap( user_to_backend_indices=tuple(int(index) for index in user_to_backend_np), backend_to_user_indices=tuple(int(index) for index in backend_to_user_np), user_to_backend=wp.array(user_to_backend_np, dtype=wp.int32, device=device), backend_to_user=wp.array(backend_to_user_np, dtype=wp.int32, device=device), )