Source code for unilab.envs.mdp.commands.pose_command

# Derived from mujocolab/mjlab v1.6.0 (0fb8a681),
# src/mjlab_microduck/tasks/mdp.py command terms.
# Copyright 2025, The mjlab Developers.
# Modified by UniLab for NumPy and the public ManagerBasedRlEnv contract;
# licensed under Apache-2.0.
"""Generic pose and posture command terms for Manager-Based tasks."""

from __future__ import annotations

import math
from dataclasses import dataclass
from numbers import Real
from typing import TYPE_CHECKING

import numpy as np

from unilab.dtype_config import get_global_dtype
from unilab.managers.command_manager import CommandTerm, CommandTermCfg

if TYPE_CHECKING:
    from unilab.managers._types import ManagerBasedRlEnv


def _real(value: object, *, label: str, minimum: float | None = None) -> float:
    if isinstance(value, (bool, np.bool_)) or not isinstance(value, Real):
        raise TypeError(f"{label} must be a real number")
    result = float(value)
    if not math.isfinite(result):
        raise ValueError(f"{label} must be finite")
    if minimum is not None and result < minimum:
        raise ValueError(f"{label} must be at least {minimum}")
    return result


def _range_pair(value: object, *, label: str) -> tuple[float, float]:
    if not isinstance(value, (tuple, list)) or len(value) != 2:
        raise TypeError(f"{label} must be a two-value range")
    lower = _real(value[0], label=f"{label} lower")
    upper = _real(value[1], label=f"{label} upper")
    if lower > upper:
        raise ValueError(f"{label} lower {lower} exceeds upper {upper}")
    return lower, upper


[docs] @dataclass(kw_only=True) class UniformPoseCommandCfg(CommandTermCfg): """Sample a fixed-width pose vector independently per dimension.""" ranges: tuple[tuple[float, float], ...] | list[list[float]] zero_command_prob: float = 0.0
[docs] def build(self, env: ManagerBasedRlEnv) -> UniformPoseCommand: return UniformPoseCommand(self, env)
[docs] class UniformPoseCommand(CommandTerm): """Uniformly sampled vector command held until the next resample.""" cfg: UniformPoseCommandCfg
[docs] def __init__(self, cfg: UniformPoseCommandCfg, env: ManagerBasedRlEnv): ranges = self._validated_ranges(cfg.ranges) probability = _real( cfg.zero_command_prob, label="UniformPoseCommandCfg zero_command_prob", ) if probability > 1.0: raise ValueError("UniformPoseCommandCfg zero_command_prob must be within [0, 1]") self._zero_command_prob = probability super().__init__(cfg, env) self._command = np.zeros((self.num_envs, len(ranges)), dtype=get_global_dtype())
@staticmethod def _validated_ranges(ranges: object) -> tuple[tuple[float, float], ...]: if not isinstance(ranges, (tuple, list)) or not ranges: raise ValueError("UniformPoseCommandCfg ranges must not be empty") return tuple( _range_pair(item, label=f"UniformPoseCommandCfg ranges[{index}]") for index, item in enumerate(ranges) ) @property def command(self) -> np.ndarray: return self._command def _update_metrics(self, env_ids: np.ndarray | None = None) -> None: del env_ids def _resample_command(self, env_ids: np.ndarray) -> None: if len(env_ids) == 0: return ranges = self._validated_ranges(self.cfg.ranges) if len(ranges) != self._command.shape[1]: raise ValueError( "UniformPoseCommandCfg ranges width changed from " f"{self._command.shape[1]} to {len(ranges)}; curricula may only " "change per-axis bounds" ) for column, (lower, upper) in enumerate(ranges): self._command[env_ids, column] = self._env.rng.uniform( lower, upper, size=len(env_ids), ) if self._zero_command_prob > 0.0: zero = self._env.rng.uniform(0.0, 1.0, size=len(env_ids)) < self._zero_command_prob self._command[env_ids[zero]] = 0.0 def _update_command(self, env_ids: np.ndarray | None) -> None: del env_ids
__all__ = ["UniformPoseCommand", "UniformPoseCommandCfg"]