Repository: linden713/humanoid_amp Branch: master Commit: 99efa6d60e35 Files: 77 Total size: 52.2 MB Directory structure: gitextract_zeak4x_i/ ├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── agents/ │ ├── __init__.py │ ├── skrl_dance_amp_cfg.yaml │ ├── skrl_g1_dance_amp_cfg.yaml │ ├── skrl_g1_walk_amp_cfg.yaml │ ├── skrl_run_amp_cfg.yaml │ └── skrl_walk_amp_cfg.yaml ├── g1_amp_env.py ├── g1_amp_env_cfg.py ├── g1_cfg.py ├── humanoid_amp_env.py ├── humanoid_amp_env_cfg.py ├── motions/ │ ├── G1_dance.npz │ ├── G1_dance_old.npz │ ├── G1_walk.npz │ ├── README.md │ ├── __init__.py │ ├── data_convert.py │ ├── humanoid_dance.npz │ ├── humanoid_run.npz │ ├── humanoid_walk.npz │ ├── motion_loader.py │ ├── motion_replayer.py │ ├── motion_viewer.py │ ├── record_data.py │ ├── test/ │ │ └── get_joint_name.py │ ├── update_pelvis_data.py │ ├── verify_motion.py │ └── visualize_motion.py ├── play.py ├── pyproject.toml ├── sync_skrl_scripts.sh ├── train.py └── usd/ ├── g1_29dof_rev_1_0.urdf ├── g1_29dof_rev_1_0.usd ├── instanceable_meshes.usd └── meshes/ ├── head_link.STL ├── left_ankle_pitch_link.STL ├── left_ankle_roll_link.STL ├── left_elbow_link.STL ├── left_hip_pitch_link.STL ├── left_hip_roll_link.STL ├── left_hip_yaw_link.STL ├── left_knee_link.STL ├── left_rubber_hand.STL ├── left_shoulder_pitch_link.STL ├── left_shoulder_roll_link.STL ├── left_shoulder_yaw_link.STL ├── left_wrist_pitch_link.STL ├── left_wrist_roll_link.STL ├── left_wrist_roll_rubber_hand.STL ├── left_wrist_yaw_link.STL ├── logo_link.STL ├── pelvis.STL ├── pelvis_contour_link.STL ├── right_ankle_pitch_link.STL ├── right_ankle_roll_link.STL ├── right_elbow_link.STL ├── right_hip_pitch_link.STL ├── right_hip_roll_link.STL ├── right_hip_yaw_link.STL ├── right_knee_link.STL ├── right_rubber_hand.STL ├── right_shoulder_pitch_link.STL ├── right_shoulder_roll_link.STL ├── right_shoulder_yaw_link.STL ├── right_wrist_pitch_link.STL ├── right_wrist_roll_link.STL ├── right_wrist_roll_rubber_hand.STL ├── right_wrist_yaw_link.STL ├── torso_link_rev_1_0.STL ├── waist_roll_link_rev_1_0.STL ├── waist_support_link.STL └── waist_yaw_link_rev_1_0.STL ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ **/__pycache__/ *.pyc *.html logs/ outputs/* *.egg-info ================================================ FILE: LICENSE ================================================ BSD 3-Clause License Copyright (c) 2025, Linden Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================ ![Humanoid AMP](docs/human_amp.png) --- # Humanoid AMP [![IsaacSim](https://img.shields.io/badge/IsaacSim-5.0.0-silver.svg)](https://docs.isaacsim.omniverse.nvidia.com/latest/index.html) [![IsaacLab](https://img.shields.io/badge/IsaacLab-2.2.0-silver.svg)](https://isaac-sim.github.io/IsaacLab/main/index.html) [![Python](https://img.shields.io/badge/python-3.10-blue.svg)](https://docs.python.org/3/whatsnew/3.10.html) [![License](https://img.shields.io/badge/license-BSD--3-yellow.svg)](https://opensource.org/licenses/BSD-3-Clause) ## Prerequisites This project requires Isaac Sim and Isaac Lab installed via pip. If you haven't installed them yet, pleanse follow the instructions in [Isaac Lab Installation Guide](https://isaac-sim.github.io/IsaacLab/main/source/setup/installation/pip_installation.html) ## Setup No symbolic links or special setup required! This project works as a standalone Isaac Lab extension. Install the repo in editable mode (mirrors the workflow used in unitree_rl_lab): ```bash pip install -e . ``` We copy the train and play script from isaaclab, note you do not need to do it yourself. ```bash bash ./sync_skrl_scripts.sh ``` ## Train ```bash python -m humanoid_amp.train --task Isaac-G1-AMP-Walk-Direct-v0 --headless --num_envs 4096 ``` or for dance training: ```bash python -m humanoid_amp.train --task Isaac-G1-AMP-Dance-Direct-v0 --headless --num_envs 4096 ``` Additional training options: ```bash # Resume from checkpoint python -m humanoid_amp.train --task Isaac-G1-AMP-Walk-Direct-v0 --checkpoint logs/skrl/path/to/checkpoint ``` ## Eval ```bash python -m humanoid_amp.play --task Isaac-G1-AMP-Walk-Direct-v0 --num_envs 32 --checkpoint logs/skrl//checkpoints/Latest.ckpt ``` ## TensorBoard ```bash python -m tensorboard.main --logdir logs/skrl/ ``` Then open your browser to http://localhost:6006 Walk training: `master` branch. Dance training: **`dance`** branch. The usage of some helper script functions is explained at the beginning of the file. ## Motions Scripts - `motion_loader.py` - Load motion data from npz files and provide sampling functionality - `motion_viewer.py` - 3D visualization player for motion data - `data_convert.py` - Convert CSV motion data to npz format with interpolation and forward kinematics - `motion_replayer.py` - Replay motion data in Isaac Sim with optional recording - `record_data.py` - Recording and managing motion data utility classes - `verify_motion.py` - Verify and display npz file contents - `visualize_motion.py` - Generate interactive HTML charts to visualize motion data - `update_pelvis_data.py` - Fix pelvis posture and body center issues in replay motion by replacing pelvis data from source file ## Dataset & URDF **Note**: The original dataset and URDF files from [Unitree Robotics](https://huggingface.co/datasets/unitreerobotics/LAFAN1_Retargeting_Dataset) have been removed by the official source. If you're still looking for the dataset, a third-party mirror is currently available here: [lvhaidong/LAFAN1_Retargeting_Dataset](https://huggingface.co/datasets/lvhaidong/LAFAN1_Retargeting_Dataset) Or you can also use the data from [AMASS](https://huggingface.co/datasets/ember-lab-berkeley/AMASS_Retargeted_for_G1) ## TODO - ✅ Current: Dancing motion is working - ✅ Test the `test` branch thoroughly and merge it into `dance` as soon as possible - ✅ Write a more detailed README to cover the new features and usage - [ ] Add clearer comments and explanations in all Python scripts ## Resources [![Demo - Walk](https://img.shields.io/badge/Demo-Walk-ff69b4?style=for-the-badge&logo=bilibili)](https://www.bilibili.com/video/BV19cRvYhEL8/?vd_source=5159ce41348cd4fd3d83ef9169dc8dbc) [![Demo - Dance (Bilibili)](https://img.shields.io/badge/Demo-Dance-ff69b4?style=for-the-badge&logo=bilibili)](https://www.bilibili.com/video/BV1vW36zEEG8/?share_source=copy_web&vd_source=0de36dd681c4f7ffab776ec97939e21f) [![Demo - Dance (YouTube)](https://img.shields.io/badge/Demo-Dance-FF0000?style=for-the-badge&logo=youtube)](https://youtu.be/_ItIFkp-Xi4) [![Documentation](https://img.shields.io/badge/Documentation-DeepWiki-blue?style=for-the-badge&logo=gitbook)](https://deepwiki.com/linden713/humanoid_amp) **Contributions**, **discussions**, and **stars** are all welcome! ❥(^_-) ================================================ FILE: __init__.py ================================================ # Copyright (c) 2022-2025, The Isaac Lab Project Developers. # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause """ AMP Humanoid locomotion environment. """ import gymnasium as gym from . import agents ## # Register Gym environments. ## gym.register( id="Isaac-Humanoid-AMP-Dance-Direct-v0", entry_point=f"{__name__}.humanoid_amp_env:HumanoidAmpEnv", disable_env_checker=True, kwargs={ "env_cfg_entry_point": f"{__name__}.humanoid_amp_env_cfg:HumanoidAmpDanceEnvCfg", "skrl_amp_cfg_entry_point": f"{agents.__name__}:skrl_dance_amp_cfg.yaml", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_dance_amp_cfg.yaml", }, ) gym.register( id="Isaac-Humanoid-AMP-Run-Direct-v0", entry_point=f"{__name__}.humanoid_amp_env:HumanoidAmpEnv", disable_env_checker=True, kwargs={ "env_cfg_entry_point": f"{__name__}.humanoid_amp_env_cfg:HumanoidAmpRunEnvCfg", "skrl_amp_cfg_entry_point": f"{agents.__name__}:skrl_run_amp_cfg.yaml", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_run_amp_cfg.yaml", }, ) gym.register( id="Isaac-Humanoid-AMP-Walk-Direct-v0", entry_point=f"{__name__}.humanoid_amp_env:HumanoidAmpEnv", disable_env_checker=True, kwargs={ "env_cfg_entry_point": f"{__name__}.humanoid_amp_env_cfg:HumanoidAmpWalkEnvCfg", "skrl_amp_cfg_entry_point": f"{agents.__name__}:skrl_walk_amp_cfg.yaml", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_walk_amp_cfg.yaml", }, ) gym.register( id="Isaac-G1-AMP-Dance-Direct-v0", entry_point=f"{__name__}.g1_amp_env:G1AmpEnv", disable_env_checker=True, kwargs={ "env_cfg_entry_point": f"{__name__}.g1_amp_env_cfg:G1AmpDanceEnvCfg", "skrl_amp_cfg_entry_point": f"{agents.__name__}:skrl_g1_dance_amp_cfg.yaml", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_g1_dance_amp_cfg.yaml", }, ) gym.register( id="Isaac-G1-AMP-Walk-Direct-v0", entry_point=f"{__name__}.g1_amp_env:G1AmpEnv", disable_env_checker=True, kwargs={ "env_cfg_entry_point": f"{__name__}.g1_amp_env_cfg:G1AmpWalkEnvCfg", "skrl_amp_cfg_entry_point": f"{agents.__name__}:skrl_g1_walk_amp_cfg.yaml", "skrl_cfg_entry_point": f"{agents.__name__}:skrl_g1_walk_amp_cfg.yaml", }, ) ================================================ FILE: agents/__init__.py ================================================ # Copyright (c) 2022-2025, The Isaac Lab Project Developers. # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause ================================================ FILE: agents/skrl_dance_amp_cfg.yaml ================================================ seed: 42 # Models are instantiated using skrl's model instantiator utility # https://skrl.readthedocs.io/en/latest/api/utils/model_instantiators.html models: separate: True policy: # see gaussian_model parameters class: GaussianMixin clip_actions: False clip_log_std: True min_log_std: -20.0 max_log_std: 2.0 initial_log_std: -2.9 fixed_log_std: True network: - name: net input: STATES layers: [1024, 512] activations: relu output: ACTIONS value: # see deterministic_model parameters class: DeterministicMixin clip_actions: False network: - name: net input: STATES layers: [1024, 512] activations: relu output: ONE discriminator: # see deterministic_model parameters class: DeterministicMixin clip_actions: False network: - name: net input: STATES layers: [1024, 512] activations: relu output: ONE # Rollout memory # https://skrl.readthedocs.io/en/latest/api/memories/random.html memory: class: RandomMemory memory_size: -1 # automatically determined (same as agent:rollouts) # AMP memory (reference motion dataset) # https://skrl.readthedocs.io/en/latest/api/memories/random.html motion_dataset: class: RandomMemory memory_size: 200000 # AMP memory (preventing discriminator overfitting) # https://skrl.readthedocs.io/en/latest/api/memories/random.html reply_buffer: class: RandomMemory memory_size: 1000000 # AMP agent configuration (field names are from AMP_DEFAULT_CONFIG) # https://skrl.readthedocs.io/en/latest/api/agents/amp.html agent: class: AMP rollouts: 16 learning_epochs: 6 mini_batches: 2 discount_factor: 0.99 lambda: 0.95 learning_rate: 5.0e-05 learning_rate_scheduler: null learning_rate_scheduler_kwargs: null state_preprocessor: RunningStandardScaler state_preprocessor_kwargs: null value_preprocessor: RunningStandardScaler value_preprocessor_kwargs: null amp_state_preprocessor: RunningStandardScaler amp_state_preprocessor_kwargs: null random_timesteps: 0 learning_starts: 0 grad_norm_clip: 0.0 ratio_clip: 0.2 value_clip: 0.2 clip_predicted_values: True entropy_loss_scale: 0.0 value_loss_scale: 2.5 discriminator_loss_scale: 5.0 amp_batch_size: 512 task_reward_weight: 0.0 style_reward_weight: 1.0 discriminator_batch_size: 4096 discriminator_reward_scale: 2.0 discriminator_logit_regularization_scale: 0.05 discriminator_gradient_penalty_scale: 5.0 discriminator_weight_decay_scale: 1.0e-04 # rewards_shaper_scale: 1.0 time_limit_bootstrap: False # logging and checkpoint experiment: directory: "humanoid_amp_dance" experiment_name: "" write_interval: auto checkpoint_interval: auto # Sequential trainer # https://skrl.readthedocs.io/en/latest/api/trainers/sequential.html trainer: class: SequentialTrainer timesteps: 80000 environment_info: log ================================================ FILE: agents/skrl_g1_dance_amp_cfg.yaml ================================================ seed: 42 # Models are instantiated using skrl's model instantiator utility # https://skrl.readthedocs.io/en/latest/api/utils/model_instantiators.html models: separate: True policy: # see gaussian_model parameters class: GaussianMixin clip_actions: False clip_log_std: True min_log_std: -20.0 max_log_std: 2.0 initial_log_std: -2.9 fixed_log_std: True network: - name: net input: STATES layers: [1024, 512] activations: relu output: ACTIONS value: # see deterministic_model parameters class: DeterministicMixin clip_actions: False network: - name: net input: STATES layers: [1024, 512] activations: relu output: ONE discriminator: # see deterministic_model parameters class: DeterministicMixin clip_actions: False network: - name: net input: STATES layers: [1024, 512] activations: relu output: ONE # Rollout memory # https://skrl.readthedocs.io/en/latest/api/memories/random.html memory: class: RandomMemory memory_size: -1 # automatically determined (same as agent:rollouts) # AMP memory (reference motion dataset) # https://skrl.readthedocs.io/en/latest/api/memories/random.html motion_dataset: class: RandomMemory memory_size: 200000 # AMP memory (preventing discriminator overfitting) # https://skrl.readthedocs.io/en/latest/api/memories/random.html reply_buffer: class: RandomMemory memory_size: 1000000 # AMP agent configuration (field names are from AMP_DEFAULT_CONFIG) # https://skrl.readthedocs.io/en/latest/api/agents/amp.html agent: class: AMP rollouts: 16 learning_epochs: 6 mini_batches: 2 discount_factor: 0.99 lambda: 0.95 learning_rate: 5.0e-05 # learning_rate_scheduler: null # learning_rate_scheduler_kwargs: null learning_rate_scheduler: KLAdaptiveLR learning_rate_scheduler_kwargs: kl_threshold: 0.008 state_preprocessor: RunningStandardScaler state_preprocessor_kwargs: null value_preprocessor: RunningStandardScaler value_preprocessor_kwargs: null amp_state_preprocessor: RunningStandardScaler amp_state_preprocessor_kwargs: null random_timesteps: 0 learning_starts: 0 grad_norm_clip: 0.0 ratio_clip: 0.2 value_clip: 0.2 clip_predicted_values: True entropy_loss_scale: 0.02 value_loss_scale: 2.5 discriminator_loss_scale: 5.0 amp_batch_size: 512 task_reward_weight: 0 style_reward_weight: 1.0 discriminator_batch_size: 4096 discriminator_reward_scale: 2.0 discriminator_logit_regularization_scale: 0.05 discriminator_gradient_penalty_scale: 5.0 discriminator_weight_decay_scale: 1.0e-04 # rewards_shaper_scale: 1.0 time_limit_bootstrap: False # logging and checkpoint experiment: directory: "g1_amp_dance" experiment_name: "" write_interval: auto checkpoint_interval: auto # Sequential trainer # https://skrl.readthedocs.io/en/latest/api/trainers/sequential.html trainer: class: SequentialTrainer timesteps: 50000 environment_info: log ================================================ FILE: agents/skrl_g1_walk_amp_cfg.yaml ================================================ seed: 42 # Models are instantiated using skrl's model instantiator utility # https://skrl.readthedocs.io/en/latest/api/utils/model_instantiators.html models: separate: True policy: # see gaussian_model parameters class: GaussianMixin clip_actions: False clip_log_std: True min_log_std: -20.0 max_log_std: 2.0 initial_log_std: -2.9 fixed_log_std: True network: - name: net input: STATES layers: [1024, 512] activations: relu output: ACTIONS value: # see deterministic_model parameters class: DeterministicMixin clip_actions: False network: - name: net input: STATES layers: [1024, 512] activations: relu output: ONE discriminator: # see deterministic_model parameters class: DeterministicMixin clip_actions: False network: - name: net input: STATES layers: [1024, 512] activations: relu output: ONE # Rollout memory # https://skrl.readthedocs.io/en/latest/api/memories/random.html memory: class: RandomMemory memory_size: -1 # automatically determined (same as agent:rollouts) # AMP memory (reference motion dataset) # https://skrl.readthedocs.io/en/latest/api/memories/random.html motion_dataset: class: RandomMemory memory_size: 200000 # AMP memory (preventing discriminator overfitting) # https://skrl.readthedocs.io/en/latest/api/memories/random.html reply_buffer: class: RandomMemory memory_size: 1000000 # AMP agent configuration (field names are from AMP_DEFAULT_CONFIG) # https://skrl.readthedocs.io/en/latest/api/agents/amp.html agent: class: AMP rollouts: 16 learning_epochs: 6 mini_batches: 2 discount_factor: 0.99 lambda: 0.95 learning_rate: 5.0e-05 learning_rate_scheduler: null learning_rate_scheduler_kwargs: null state_preprocessor: RunningStandardScaler state_preprocessor_kwargs: null value_preprocessor: RunningStandardScaler value_preprocessor_kwargs: null amp_state_preprocessor: RunningStandardScaler amp_state_preprocessor_kwargs: null random_timesteps: 0 learning_starts: 0 grad_norm_clip: 0.0 ratio_clip: 0.2 value_clip: 0.2 clip_predicted_values: True entropy_loss_scale: 0.0 value_loss_scale: 2.5 discriminator_loss_scale: 5.0 amp_batch_size: 512 task_reward_weight: 0 style_reward_weight: 1.0 discriminator_batch_size: 4096 discriminator_reward_scale: 2.0 discriminator_logit_regularization_scale: 0.05 discriminator_gradient_penalty_scale: 5.0 discriminator_weight_decay_scale: 1.0e-04 # rewards_shaper_scale: 1.0 time_limit_bootstrap: False # logging and checkpoint experiment: directory: "g1_amp_walk" experiment_name: "" write_interval: auto checkpoint_interval: auto # Sequential trainer # https://skrl.readthedocs.io/en/latest/api/trainers/sequential.html trainer: class: SequentialTrainer timesteps: 50000 environment_info: log ================================================ FILE: agents/skrl_run_amp_cfg.yaml ================================================ seed: 42 # Models are instantiated using skrl's model instantiator utility # https://skrl.readthedocs.io/en/latest/api/utils/model_instantiators.html models: separate: True policy: # see gaussian_model parameters class: GaussianMixin clip_actions: False clip_log_std: True min_log_std: -20.0 max_log_std: 2.0 initial_log_std: -2.9 fixed_log_std: True network: - name: net input: STATES layers: [1024, 512] activations: relu output: ACTIONS value: # see deterministic_model parameters class: DeterministicMixin clip_actions: False network: - name: net input: STATES layers: [1024, 512] activations: relu output: ONE discriminator: # see deterministic_model parameters class: DeterministicMixin clip_actions: False network: - name: net input: STATES layers: [1024, 512] activations: relu output: ONE # Rollout memory # https://skrl.readthedocs.io/en/latest/api/memories/random.html memory: class: RandomMemory memory_size: -1 # automatically determined (same as agent:rollouts) # AMP memory (reference motion dataset) # https://skrl.readthedocs.io/en/latest/api/memories/random.html motion_dataset: class: RandomMemory memory_size: 200000 # AMP memory (preventing discriminator overfitting) # https://skrl.readthedocs.io/en/latest/api/memories/random.html reply_buffer: class: RandomMemory memory_size: 1000000 # AMP agent configuration (field names are from AMP_DEFAULT_CONFIG) # https://skrl.readthedocs.io/en/latest/api/agents/amp.html agent: class: AMP rollouts: 16 learning_epochs: 6 mini_batches: 2 discount_factor: 0.99 lambda: 0.95 learning_rate: 5.0e-05 learning_rate_scheduler: null learning_rate_scheduler_kwargs: null state_preprocessor: RunningStandardScaler state_preprocessor_kwargs: null value_preprocessor: RunningStandardScaler value_preprocessor_kwargs: null amp_state_preprocessor: RunningStandardScaler amp_state_preprocessor_kwargs: null random_timesteps: 0 learning_starts: 0 grad_norm_clip: 0.0 ratio_clip: 0.2 value_clip: 0.2 clip_predicted_values: True entropy_loss_scale: 0.0 value_loss_scale: 2.5 discriminator_loss_scale: 5.0 amp_batch_size: 512 task_reward_weight: 0.0 style_reward_weight: 1.0 discriminator_batch_size: 4096 discriminator_reward_scale: 2.0 discriminator_logit_regularization_scale: 0.05 discriminator_gradient_penalty_scale: 5.0 discriminator_weight_decay_scale: 1.0e-04 # rewards_shaper_scale: 1.0 time_limit_bootstrap: False # logging and checkpoint experiment: directory: "humanoid_amp_run" experiment_name: "" write_interval: auto checkpoint_interval: auto # Sequential trainer # https://skrl.readthedocs.io/en/latest/api/trainers/sequential.html trainer: class: SequentialTrainer timesteps: 80000 environment_info: log ================================================ FILE: agents/skrl_walk_amp_cfg.yaml ================================================ seed: 42 # Models are instantiated using skrl's model instantiator utility # https://skrl.readthedocs.io/en/latest/api/utils/model_instantiators.html models: separate: True policy: # see gaussian_model parameters class: GaussianMixin clip_actions: False clip_log_std: True min_log_std: -20.0 max_log_std: 2.0 initial_log_std: -2.9 fixed_log_std: True network: - name: net input: STATES layers: [1024, 512] activations: relu output: ACTIONS value: # see deterministic_model parameters class: DeterministicMixin clip_actions: False network: - name: net input: STATES layers: [1024, 512] activations: relu output: ONE discriminator: # see deterministic_model parameters class: DeterministicMixin clip_actions: False network: - name: net input: STATES layers: [1024, 512] activations: relu output: ONE # Rollout memory # https://skrl.readthedocs.io/en/latest/api/memories/random.html memory: class: RandomMemory memory_size: -1 # automatically determined (same as agent:rollouts) # AMP memory (reference motion dataset) # https://skrl.readthedocs.io/en/latest/api/memories/random.html motion_dataset: class: RandomMemory memory_size: 200000 # AMP memory (preventing discriminator overfitting) # https://skrl.readthedocs.io/en/latest/api/memories/random.html reply_buffer: class: RandomMemory memory_size: 1000000 # AMP agent configuration (field names are from AMP_DEFAULT_CONFIG) # https://skrl.readthedocs.io/en/latest/api/agents/amp.html agent: class: AMP rollouts: 16 learning_epochs: 6 mini_batches: 2 discount_factor: 0.99 lambda: 0.95 learning_rate: 5.0e-05 learning_rate_scheduler: null learning_rate_scheduler_kwargs: null state_preprocessor: RunningStandardScaler state_preprocessor_kwargs: null value_preprocessor: RunningStandardScaler value_preprocessor_kwargs: null amp_state_preprocessor: RunningStandardScaler amp_state_preprocessor_kwargs: null random_timesteps: 0 learning_starts: 0 grad_norm_clip: 0.0 ratio_clip: 0.2 value_clip: 0.2 clip_predicted_values: True entropy_loss_scale: 0.0 value_loss_scale: 2.5 discriminator_loss_scale: 5.0 amp_batch_size: 512 task_reward_weight: 0.0 style_reward_weight: 1.0 discriminator_batch_size: 4096 discriminator_reward_scale: 2.0 discriminator_logit_regularization_scale: 0.05 discriminator_gradient_penalty_scale: 5.0 discriminator_weight_decay_scale: 1.0e-04 # rewards_shaper_scale: 1.0 time_limit_bootstrap: False # logging and checkpoint experiment: directory: "humanoid_amp_walk" experiment_name: "" write_interval: auto checkpoint_interval: auto # Sequential trainer # https://skrl.readthedocs.io/en/latest/api/trainers/sequential.html trainer: class: SequentialTrainer timesteps: 80000 environment_info: log ================================================ FILE: g1_amp_env.py ================================================ # Copyright (c) 2022-2025, The Isaac Lab Project Developers. # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations import gymnasium as gym import numpy as np import torch import isaaclab.sim as sim_utils from isaaclab.assets import Articulation from isaaclab.envs import DirectRLEnv from isaaclab.sim.spawners.from_files import GroundPlaneCfg, spawn_ground_plane from isaaclab.utils.math import quat_apply from .g1_amp_env_cfg import G1AmpEnvCfg from .motions import MotionLoader class G1AmpEnv(DirectRLEnv): cfg: G1AmpEnvCfg def __init__(self, cfg: G1AmpEnvCfg, render_mode: str | None = None, **kwargs): super().__init__(cfg, render_mode, **kwargs) # action offset and scale dof_lower_limits = self.robot.data.soft_joint_pos_limits[0, :, 0] dof_upper_limits = self.robot.data.soft_joint_pos_limits[0, :, 1] self.action_offset = 0.5 * (dof_upper_limits + dof_lower_limits) self.action_scale = dof_upper_limits - dof_lower_limits # load motion self._motion_loader = MotionLoader(motion_file=self.cfg.motion_file, device=self.device) # DOF and key body indexes key_body_names = [ "left_shoulder_pitch_link", "right_shoulder_pitch_link", "left_elbow_link", "right_elbow_link", "right_hip_yaw_link", "left_hip_yaw_link", "right_rubber_hand", "left_rubber_hand", "right_ankle_roll_link", "left_ankle_roll_link"] self.ref_body_index = self.robot.data.body_names.index(self.cfg.reference_body) self.key_body_indexes = [self.robot.data.body_names.index(name) for name in key_body_names] # Used to for reset strategy self.motion_dof_indexes = self._motion_loader.get_dof_index(self.robot.data.joint_names) self.motion_ref_body_index = self._motion_loader.get_body_index([self.cfg.reference_body])[0] self.motion_key_body_indexes = self._motion_loader.get_body_index(key_body_names) # reconfigure AMP observation space according to the number of observations and create the buffer self.amp_observation_size = self.cfg.num_amp_observations * self.cfg.amp_observation_space self.amp_observation_space = gym.spaces.Box(low=-np.inf, high=np.inf, shape=(self.amp_observation_size,)) self.amp_observation_buffer = torch.zeros( (self.num_envs, self.cfg.num_amp_observations, self.cfg.amp_observation_space), device=self.device ) def _setup_scene(self): self.robot = Articulation(self.cfg.robot) # add ground plane spawn_ground_plane( prim_path="/World/ground", cfg=GroundPlaneCfg( physics_material=sim_utils.RigidBodyMaterialCfg( static_friction=1.0, dynamic_friction=1.0, restitution=0.0, ), ), ) # clone and replicate self.scene.clone_environments(copy_from_source=False) # add articulation to scene self.scene.articulations["robot"] = self.robot # add lights light_cfg = sim_utils.DomeLightCfg(intensity=2000.0, color=(0.75, 0.75, 0.75)) light_cfg.func("/World/Light", light_cfg) def _pre_physics_step(self, actions: torch.Tensor): self.actions = actions.clone() # self.pre_actions = actions.clone() def _apply_action(self): # self.pre_actions = self.actions.clone() target = self.action_offset + self.action_scale * self.actions self.robot.set_joint_position_target(target) def _get_observations(self) -> dict: # build task observation obs = compute_obs( self.robot.data.joint_pos, self.robot.data.joint_vel, self.robot.data.body_pos_w[:, self.ref_body_index], self.robot.data.body_quat_w[:, self.ref_body_index], self.robot.data.body_lin_vel_w[:, self.ref_body_index], self.robot.data.body_ang_vel_w[:, self.ref_body_index], self.robot.data.body_pos_w[:, self.key_body_indexes], ) # update AMP observation history for i in reversed(range(self.cfg.num_amp_observations - 1)): self.amp_observation_buffer[:, i + 1] = self.amp_observation_buffer[:, i] # build AMP observation self.amp_observation_buffer[:, 0] = obs.clone() self.extras = {"amp_obs": self.amp_observation_buffer.view(-1, self.amp_observation_size)} return {"policy": obs} # def _get_rewards(self) -> torch.Tensor: # return torch.ones((self.num_envs,), dtype=torch.float32, device=self.sim.device) def _get_rewards(self) -> torch.Tensor: total_reward, reward_log = compute_rewards( self.cfg.rew_termination, self.cfg.rew_action_l2, self.cfg.rew_joint_pos_limits, self.cfg.rew_joint_acc_l2, self.cfg.rew_joint_vel_l2, self.reset_terminated, self.actions, self.robot.data.joint_pos, self.robot.data.soft_joint_pos_limits, self.robot.data.joint_acc, self.robot.data.joint_vel, ) self.extras["log"] = reward_log return total_reward def _get_dones(self) -> tuple[torch.Tensor, torch.Tensor]: time_out = self.episode_length_buf >= self.max_episode_length - 1 if self.cfg.early_termination: died = self.robot.data.body_pos_w[:, self.ref_body_index, 2] < self.cfg.termination_height else: died = torch.zeros_like(time_out) return died, time_out def _reset_idx(self, env_ids: torch.Tensor | None): if env_ids is None or len(env_ids) == self.num_envs: env_ids = self.robot._ALL_INDICES self.robot.reset(env_ids) super()._reset_idx(env_ids) if self.cfg.reset_strategy == "default": root_state, joint_pos, joint_vel = self._reset_strategy_default(env_ids) elif self.cfg.reset_strategy.startswith("random"): start = "start" in self.cfg.reset_strategy root_state, joint_pos, joint_vel = self._reset_strategy_random(env_ids, start) else: raise ValueError(f"Unknown reset strategy: {self.cfg.reset_strategy}") self.robot.write_root_link_pose_to_sim(root_state[:, :7], env_ids) self.robot.write_root_com_velocity_to_sim(root_state[:, 7:], env_ids) self.robot.write_joint_state_to_sim(joint_pos, joint_vel, None, env_ids) # reset strategies def _reset_strategy_default(self, env_ids: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: root_state = self.robot.data.default_root_state[env_ids].clone() root_state[:, :3] += self.scene.env_origins[env_ids] joint_pos = self.robot.data.default_joint_pos[env_ids].clone() joint_vel = self.robot.data.default_joint_vel[env_ids].clone() return root_state, joint_pos, joint_vel def _reset_strategy_random( self, env_ids: torch.Tensor, start: bool = False ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: # sample random motion times (or zeros if start is True) num_samples = env_ids.shape[0] times = np.zeros(num_samples) if start else self._motion_loader.sample_times(num_samples) # sample random motions ( dof_positions, dof_velocities, body_positions, body_rotations, body_linear_velocities, body_angular_velocities, ) = self._motion_loader.sample(num_samples=num_samples, times=times) # get root transforms (the humanoid torso) motion_torso_index = self._motion_loader.get_body_index(["pelvis"])[0] root_state = self.robot.data.default_root_state[env_ids].clone() root_state[:, 0:3] = body_positions[:, motion_torso_index] + self.scene.env_origins[env_ids] root_state[:, 2] += 0.05 # lift the humanoid slightly to avoid collisions with the ground root_state[:, 3:7] = body_rotations[:, motion_torso_index] root_state[:, 7:10] = body_linear_velocities[:, motion_torso_index] root_state[:, 10:13] = body_angular_velocities[:, motion_torso_index] # get DOFs state dof_pos = dof_positions[:, self.motion_dof_indexes] dof_vel = dof_velocities[:, self.motion_dof_indexes] # update AMP observation amp_observations = self.collect_reference_motions(num_samples, times) self.amp_observation_buffer[env_ids] = amp_observations.view(num_samples, self.cfg.num_amp_observations, -1) return root_state, dof_pos, dof_vel # env methods def collect_reference_motions(self, num_samples: int, current_times: np.ndarray | None = None) -> torch.Tensor: # sample random motion times (or use the one specified) if current_times is None: current_times = self._motion_loader.sample_times(num_samples) times = ( np.expand_dims(current_times, axis=-1) - self._motion_loader.dt * np.arange(0, self.cfg.num_amp_observations) ).flatten() # get motions ( dof_positions, dof_velocities, body_positions, body_rotations, body_linear_velocities, body_angular_velocities, ) = self._motion_loader.sample(num_samples=num_samples, times=times) # compute AMP observation amp_observation = compute_obs( dof_positions[:, self.motion_dof_indexes], dof_velocities[:, self.motion_dof_indexes], body_positions[:, self.motion_ref_body_index], body_rotations[:, self.motion_ref_body_index], body_linear_velocities[:, self.motion_ref_body_index], body_angular_velocities[:, self.motion_ref_body_index], body_positions[:, self.motion_key_body_indexes], ) return amp_observation.view(-1, self.amp_observation_size) @torch.jit.script def quaternion_to_tangent_and_normal(q: torch.Tensor) -> torch.Tensor: ref_tangent = torch.zeros_like(q[..., :3]) ref_normal = torch.zeros_like(q[..., :3]) ref_tangent[..., 0] = 1 ref_normal[..., -1] = 1 tangent = quat_apply(q, ref_tangent) normal = quat_apply(q, ref_normal) return torch.cat([tangent, normal], dim=len(tangent.shape) - 1) @torch.jit.script def compute_obs( dof_positions: torch.Tensor, dof_velocities: torch.Tensor, root_positions: torch.Tensor, root_rotations: torch.Tensor, root_linear_velocities: torch.Tensor, root_angular_velocities: torch.Tensor, key_body_positions: torch.Tensor, ) -> torch.Tensor: obs = torch.cat( ( dof_positions, dof_velocities, root_positions[:, 2:3], # root body height quaternion_to_tangent_and_normal(root_rotations), root_linear_velocities, root_angular_velocities, (key_body_positions - root_positions.unsqueeze(-2)).view(key_body_positions.shape[0], -1), ), dim=-1, ) return obs @torch.jit.script def compute_rewards( rew_scale_termination: float, rew_scale_action_l2: float, rew_scale_joint_pos_limits: float, rew_scale_joint_acc_l2: float, rew_scale_joint_vel_l2: float, reset_terminated: torch.Tensor, actions: torch.Tensor, joint_pos: torch.Tensor, soft_joint_pos_limits: torch.Tensor, joint_acc: torch.Tensor, joint_vel: torch.Tensor, ): rew_termination = rew_scale_termination * reset_terminated.float() rew_action_l2 = rew_scale_action_l2 * torch.sum(torch.square(actions), dim=1) out_of_limits = -(joint_pos - soft_joint_pos_limits[:,:,0]).clip(max=0.0) out_of_limits += (joint_pos - soft_joint_pos_limits[:,:,1]).clip(min=0.0) rew_joint_pos_limits = rew_scale_joint_pos_limits * torch.sum(out_of_limits, dim=1) rew_joint_acc_l2 = rew_scale_joint_acc_l2 * torch.sum(torch.square(joint_acc), dim=1) rew_joint_vel_l2 = rew_scale_joint_vel_l2 * torch.sum(torch.square(joint_vel), dim=1) total_reward = rew_termination + rew_action_l2 + rew_joint_pos_limits + rew_joint_acc_l2 + rew_joint_vel_l2 log = { "rew_termination": (rew_termination).mean(), "rew_action_l2": (rew_action_l2).mean(), "rew_joint_pos_limits": (rew_joint_pos_limits).mean(), "rew_joint_acc_l2": (rew_joint_acc_l2).mean(), "rew_joint_vel_l2": (rew_joint_vel_l2).mean(), } return total_reward, log ================================================ FILE: g1_amp_env_cfg.py ================================================ # Copyright (c) 2022-2025, The Isaac Lab Project Developers. # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations import os from dataclasses import MISSING from .g1_cfg import G1_CFG from isaaclab.envs import DirectRLEnvCfg from isaaclab.scene import InteractiveSceneCfg from isaaclab.sim import PhysxCfg, SimulationCfg from isaaclab.utils import configclass from isaaclab.assets import ArticulationCfg MOTIONS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "motions") @configclass class G1AmpEnvCfg(DirectRLEnvCfg): """Humanoid AMP environment config (base class).""" # reward rew_termination = -0 rew_action_l2 = -0.00 rew_joint_pos_limits = -0 rew_joint_acc_l2 =-0.00 rew_joint_vel_l2= -0.00 # env episode_length_s = 10.0 decimation = 2 # spaces observation_space = 71 + 3 * 10 #TODO action_space = 29 state_space = 0 num_amp_observations = 2 amp_observation_space = 71 + 3 * 10 early_termination = True termination_height = 0.5 motion_file: str = MISSING reference_body = "pelvis" reset_strategy = "random" # default, random, random-start """Strategy to be followed when resetting each environment (humanoid's pose and joint states). * default: pose and joint states are set to the initial state of the asset. * random: pose and joint states are set by sampling motions at random, uniform times. * random-start: pose and joint states are set by sampling motion at the start (time zero). """ # simulation sim: SimulationCfg = SimulationCfg( dt=1 / 60, render_interval=decimation, physx=PhysxCfg( gpu_found_lost_pairs_capacity=2**23, gpu_total_aggregate_pairs_capacity=2**23, ), ) # scene scene: InteractiveSceneCfg = InteractiveSceneCfg(num_envs=4096, env_spacing=4.0, replicate_physics=True) # robot robot: ArticulationCfg = G1_CFG.replace(prim_path="/World/envs/env_.*/Robot") @configclass class G1AmpDanceEnvCfg(G1AmpEnvCfg): motion_file = os.path.join(MOTIONS_DIR, "G1_dance.npz") @configclass class G1AmpWalkEnvCfg(G1AmpEnvCfg): motion_file = os.path.join(MOTIONS_DIR, "G1_walk.npz") ================================================ FILE: g1_cfg.py ================================================ import isaaclab.sim as sim_utils from isaaclab.actuators import ImplicitActuatorCfg from isaaclab.assets import ArticulationCfg import os CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) G1_CFG = ArticulationCfg( prim_path="{ENV_REGEX_NS}/Robot", spawn=sim_utils.UsdFileCfg( usd_path=os.path.join(CURRENT_DIR, "usd/g1_29dof_rev_1_0.usd"), activate_contact_sensors=True, rigid_props=sim_utils.RigidBodyPropertiesCfg( disable_gravity=False, retain_accelerations=False, linear_damping=0.0, angular_damping=0.0, max_linear_velocity=3.0, max_angular_velocity=3.0, max_depenetration_velocity=10.0, ), articulation_props=sim_utils.ArticulationRootPropertiesCfg( enabled_self_collisions=False, solver_position_iteration_count=4, solver_velocity_iteration_count=0 ), ), init_state=ArticulationCfg.InitialStateCfg( pos=(0.0, 0.0, 0.8), joint_pos={ ".*_hip_pitch_joint": -0.20, ".*_knee_joint": 0.42, ".*_ankle_pitch_joint": -0.23, # ".*_elbow_pitch_joint": 0.87, "left_shoulder_roll_joint": 0.16, "left_shoulder_pitch_joint": 0.35, "right_shoulder_roll_joint": -0.16, "right_shoulder_pitch_joint": 0.35, }, joint_vel={".*": 0.0}, ), soft_joint_pos_limit_factor=0.9, actuators={ "legs": ImplicitActuatorCfg( joint_names_expr=[ ".*_hip_yaw_joint", ".*_hip_roll_joint", ".*_hip_pitch_joint", ".*_knee_joint", "waist_yaw_joint", "waist_roll_joint", "waist_pitch_joint", ], effort_limit=300, velocity_limit=100.0, stiffness={ ".*_hip_yaw_joint": 150.0, ".*_hip_roll_joint": 150.0, ".*_hip_pitch_joint": 200.0, ".*_knee_joint": 200.0, "waist_yaw_joint": 200.0, "waist_roll_joint": 200.0, "waist_pitch_joint": 200.0, }, damping={ ".*_hip_yaw_joint": 5.0, ".*_hip_roll_joint": 5.0, ".*_hip_pitch_joint": 5.0, ".*_knee_joint": 5.0, "waist_yaw_joint": 5.0, "waist_roll_joint": 5.0, "waist_pitch_joint": 5.0, }, armature={ ".*_hip_.*": 0.01, ".*_knee_joint": 0.01, "waist_yaw_joint": 0.01, "waist_roll_joint": 0.01, "waist_pitch_joint": 0.01, }, ), "feet": ImplicitActuatorCfg( effort_limit=20, joint_names_expr=[".*_ankle_pitch_joint", ".*_ankle_roll_joint"], stiffness=20.0, damping=2.0, armature=0.01, ), "arms": ImplicitActuatorCfg( joint_names_expr=[ ".*_shoulder_pitch_joint", ".*_shoulder_roll_joint", ".*_shoulder_yaw_joint", ".*_elbow_joint", ".*_wrist_.*", ], effort_limit=300, velocity_limit=100.0, stiffness=40.0, damping=10.0, armature={ ".*_shoulder_.*": 0.01, ".*_elbow_.*": 0.01, ".*_wrist_.*": 0.01, }, ), }, ) ================================================ FILE: humanoid_amp_env.py ================================================ # Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations import gymnasium as gym import numpy as np import torch import isaaclab.sim as sim_utils from isaaclab.assets import Articulation from isaaclab.envs import DirectRLEnv from isaaclab.sim.spawners.from_files import GroundPlaneCfg, spawn_ground_plane from isaaclab.utils.math import quat_apply from .humanoid_amp_env_cfg import HumanoidAmpEnvCfg from .motions import MotionLoader class HumanoidAmpEnv(DirectRLEnv): cfg: HumanoidAmpEnvCfg def __init__(self, cfg: HumanoidAmpEnvCfg, render_mode: str | None = None, **kwargs): super().__init__(cfg, render_mode, **kwargs) # action offset and scale dof_lower_limits = self.robot.data.soft_joint_pos_limits[0, :, 0] dof_upper_limits = self.robot.data.soft_joint_pos_limits[0, :, 1] self.action_offset = 0.5 * (dof_upper_limits + dof_lower_limits) self.action_scale = dof_upper_limits - dof_lower_limits # load motion self._motion_loader = MotionLoader(motion_file=self.cfg.motion_file, device=self.device) # DOF and key body indexes key_body_names = ["right_hand", "left_hand", "right_foot", "left_foot"] self.ref_body_index = self.robot.data.body_names.index(self.cfg.reference_body) self.key_body_indexes = [self.robot.data.body_names.index(name) for name in key_body_names] self.motion_dof_indexes = self._motion_loader.get_dof_index(self.robot.data.joint_names) self.motion_ref_body_index = self._motion_loader.get_body_index([self.cfg.reference_body])[0] self.motion_key_body_indexes = self._motion_loader.get_body_index(key_body_names) # reconfigure AMP observation space according to the number of observations and create the buffer self.amp_observation_size = self.cfg.num_amp_observations * self.cfg.amp_observation_space self.amp_observation_space = gym.spaces.Box(low=-np.inf, high=np.inf, shape=(self.amp_observation_size,)) self.amp_observation_buffer = torch.zeros( (self.num_envs, self.cfg.num_amp_observations, self.cfg.amp_observation_space), device=self.device ) def _setup_scene(self): self.robot = Articulation(self.cfg.robot) # add ground plane spawn_ground_plane( prim_path="/World/ground", cfg=GroundPlaneCfg( physics_material=sim_utils.RigidBodyMaterialCfg( static_friction=1.0, dynamic_friction=1.0, restitution=0.0, ), ), ) # clone and replicate self.scene.clone_environments(copy_from_source=False) # we need to explicitly filter collisions for CPU simulation if self.device == "cpu": self.scene.filter_collisions(global_prim_paths=["/World/ground"]) # add articulation to scene self.scene.articulations["robot"] = self.robot # add lights light_cfg = sim_utils.DomeLightCfg(intensity=2000.0, color=(0.75, 0.75, 0.75)) light_cfg.func("/World/Light", light_cfg) def _pre_physics_step(self, actions: torch.Tensor): self.actions = actions.clone() def _apply_action(self): target = self.action_offset + self.action_scale * self.actions self.robot.set_joint_position_target(target) def _get_observations(self) -> dict: # build task observation obs = compute_obs( self.robot.data.joint_pos, self.robot.data.joint_vel, self.robot.data.body_pos_w[:, self.ref_body_index], self.robot.data.body_quat_w[:, self.ref_body_index], self.robot.data.body_lin_vel_w[:, self.ref_body_index], self.robot.data.body_ang_vel_w[:, self.ref_body_index], self.robot.data.body_pos_w[:, self.key_body_indexes], ) # update AMP observation history for i in reversed(range(self.cfg.num_amp_observations - 1)): self.amp_observation_buffer[:, i + 1] = self.amp_observation_buffer[:, i] # build AMP observation self.amp_observation_buffer[:, 0] = obs.clone() self.extras = {"amp_obs": self.amp_observation_buffer.view(-1, self.amp_observation_size)} return {"policy": obs} def _get_rewards(self) -> torch.Tensor: return torch.ones((self.num_envs,), dtype=torch.float32, device=self.sim.device) def _get_dones(self) -> tuple[torch.Tensor, torch.Tensor]: time_out = self.episode_length_buf >= self.max_episode_length - 1 if self.cfg.early_termination: died = self.robot.data.body_pos_w[:, self.ref_body_index, 2] < self.cfg.termination_height else: died = torch.zeros_like(time_out) return died, time_out def _reset_idx(self, env_ids: torch.Tensor | None): if env_ids is None or len(env_ids) == self.num_envs: env_ids = self.robot._ALL_INDICES self.robot.reset(env_ids) super()._reset_idx(env_ids) if self.cfg.reset_strategy == "default": root_state, joint_pos, joint_vel = self._reset_strategy_default(env_ids) elif self.cfg.reset_strategy.startswith("random"): start = "start" in self.cfg.reset_strategy root_state, joint_pos, joint_vel = self._reset_strategy_random(env_ids, start) else: raise ValueError(f"Unknown reset strategy: {self.cfg.reset_strategy}") self.robot.write_root_link_pose_to_sim(root_state[:, :7], env_ids) self.robot.write_root_com_velocity_to_sim(root_state[:, 7:], env_ids) self.robot.write_joint_state_to_sim(joint_pos, joint_vel, None, env_ids) # reset strategies def _reset_strategy_default(self, env_ids: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: root_state = self.robot.data.default_root_state[env_ids].clone() root_state[:, :3] += self.scene.env_origins[env_ids] joint_pos = self.robot.data.default_joint_pos[env_ids].clone() joint_vel = self.robot.data.default_joint_vel[env_ids].clone() return root_state, joint_pos, joint_vel def _reset_strategy_random( self, env_ids: torch.Tensor, start: bool = False ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: # sample random motion times (or zeros if start is True) num_samples = env_ids.shape[0] times = np.zeros(num_samples) if start else self._motion_loader.sample_times(num_samples) # sample random motions ( dof_positions, dof_velocities, body_positions, body_rotations, body_linear_velocities, body_angular_velocities, ) = self._motion_loader.sample(num_samples=num_samples, times=times) # get root transforms (the humanoid torso) motion_torso_index = self._motion_loader.get_body_index(["torso"])[0] root_state = self.robot.data.default_root_state[env_ids].clone() root_state[:, 0:3] = body_positions[:, motion_torso_index] + self.scene.env_origins[env_ids] root_state[:, 2] += 0.15 # lift the humanoid slightly to avoid collisions with the ground root_state[:, 3:7] = body_rotations[:, motion_torso_index] root_state[:, 7:10] = body_linear_velocities[:, motion_torso_index] root_state[:, 10:13] = body_angular_velocities[:, motion_torso_index] # get DOFs state dof_pos = dof_positions[:, self.motion_dof_indexes] dof_vel = dof_velocities[:, self.motion_dof_indexes] # update AMP observation amp_observations = self.collect_reference_motions(num_samples, times) self.amp_observation_buffer[env_ids] = amp_observations.view(num_samples, self.cfg.num_amp_observations, -1) return root_state, dof_pos, dof_vel # env methods def collect_reference_motions(self, num_samples: int, current_times: np.ndarray | None = None) -> torch.Tensor: # sample random motion times (or use the one specified) if current_times is None: current_times = self._motion_loader.sample_times(num_samples) times = ( np.expand_dims(current_times, axis=-1) - self._motion_loader.dt * np.arange(0, self.cfg.num_amp_observations) ).flatten() # get motions ( dof_positions, dof_velocities, body_positions, body_rotations, body_linear_velocities, body_angular_velocities, ) = self._motion_loader.sample(num_samples=num_samples, times=times) # compute AMP observation amp_observation = compute_obs( dof_positions[:, self.motion_dof_indexes], dof_velocities[:, self.motion_dof_indexes], body_positions[:, self.motion_ref_body_index], body_rotations[:, self.motion_ref_body_index], body_linear_velocities[:, self.motion_ref_body_index], body_angular_velocities[:, self.motion_ref_body_index], body_positions[:, self.motion_key_body_indexes], ) return amp_observation.view(-1, self.amp_observation_size) @torch.jit.script def quaternion_to_tangent_and_normal(q: torch.Tensor) -> torch.Tensor: ref_tangent = torch.zeros_like(q[..., :3]) ref_normal = torch.zeros_like(q[..., :3]) ref_tangent[..., 0] = 1 ref_normal[..., -1] = 1 tangent = quat_apply(q, ref_tangent) normal = quat_apply(q, ref_normal) return torch.cat([tangent, normal], dim=len(tangent.shape) - 1) @torch.jit.script def compute_obs( dof_positions: torch.Tensor, dof_velocities: torch.Tensor, root_positions: torch.Tensor, root_rotations: torch.Tensor, root_linear_velocities: torch.Tensor, root_angular_velocities: torch.Tensor, key_body_positions: torch.Tensor, ) -> torch.Tensor: obs = torch.cat( ( dof_positions, dof_velocities, root_positions[:, 2:3], # root body height quaternion_to_tangent_and_normal(root_rotations), root_linear_velocities, root_angular_velocities, (key_body_positions - root_positions.unsqueeze(-2)).view(key_body_positions.shape[0], -1), ), dim=-1, ) return obs ================================================ FILE: humanoid_amp_env_cfg.py ================================================ # Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause from __future__ import annotations import os from dataclasses import MISSING from isaaclab_assets import HUMANOID_28_CFG from isaaclab.actuators import ImplicitActuatorCfg from isaaclab.assets import ArticulationCfg from isaaclab.envs import DirectRLEnvCfg from isaaclab.scene import InteractiveSceneCfg from isaaclab.sim import PhysxCfg, SimulationCfg from isaaclab.utils import configclass MOTIONS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "motions") @configclass class HumanoidAmpEnvCfg(DirectRLEnvCfg): """Humanoid AMP environment config (base class).""" # env episode_length_s = 10.0 decimation = 2 # spaces observation_space = 81 action_space = 28 state_space = 0 num_amp_observations = 2 amp_observation_space = 81 early_termination = True termination_height = 0.5 motion_file: str = MISSING reference_body = "torso" reset_strategy = "random" # default, random, random-start """Strategy to be followed when resetting each environment (humanoid's pose and joint states). * default: pose and joint states are set to the initial state of the asset. * random: pose and joint states are set by sampling motions at random, uniform times. * random-start: pose and joint states are set by sampling motion at the start (time zero). """ # simulation sim: SimulationCfg = SimulationCfg( dt=1 / 60, render_interval=decimation, physx=PhysxCfg( gpu_found_lost_pairs_capacity=2**23, gpu_total_aggregate_pairs_capacity=2**23, ), ) # scene scene: InteractiveSceneCfg = InteractiveSceneCfg(num_envs=4096, env_spacing=10.0, replicate_physics=True) # robot robot: ArticulationCfg = HUMANOID_28_CFG.replace(prim_path="/World/envs/env_.*/Robot").replace( actuators={ "body": ImplicitActuatorCfg( joint_names_expr=[".*"], velocity_limit=100.0, stiffness=None, damping=None, ), }, ) @configclass class HumanoidAmpDanceEnvCfg(HumanoidAmpEnvCfg): motion_file = os.path.join(MOTIONS_DIR, "humanoid_dance.npz") @configclass class HumanoidAmpRunEnvCfg(HumanoidAmpEnvCfg): motion_file = os.path.join(MOTIONS_DIR, "humanoid_run.npz") @configclass class HumanoidAmpWalkEnvCfg(HumanoidAmpEnvCfg): motion_file = os.path.join(MOTIONS_DIR, "humanoid_walk.npz") ================================================ FILE: motions/README.md ================================================ # Motion files The motion files are in NumPy-file format that contains data from the skeleton DOFs and bodies that perform the motion. The data (accessed by key) is described in the following table, where: * `N` is the number of motion frames recorded * `D` is the number of skeleton DOFs * `B` is the number of skeleton bodies | Key | Dtype | Shape | Description | | --- | ---- | ----- | ----------- | | `fps` | int64 | () | FPS at which motion was sampled | | `dof_names` | unicode string | (D,) | Skeleton DOF names | | `body_names` | unicode string | (B,) | Skeleton body names | | `dof_positions` | float32 | (N, D) | Skeleton DOF positions | | `dof_velocities` | float32 | (N, D) | Skeleton DOF velocities | | `body_positions` | float32 | (N, B, 3) | Skeleton body positions | | `body_rotations` | float32 | (N, B, 4) | Skeleton body rotations (as `wxyz` quaternion) | | `body_linear_velocities` | float32 | (N, B, 3) | Skeleton body linear velocities | | `body_angular_velocities` | float32 | (N, B, 3) | Skeleton body angular velocities | ## Motion visualization The `motion_viewer.py` file allows to visualize the skeleton motion recorded in a motion file. Open an terminal in the `motions` folder and run the following command. ```bash python motion_viewer.py --file MOTION_FILE_NAME.npz ``` See `python motion_viewer.py --help` for available arguments. Humanoid: ``` dof_names: Data type: 1: angular_vels[0] = compute_angular_velocity(quats[0], quats[1], dt) angular_vels[-1] = compute_angular_velocity(quats[-2], quats[-1], dt) for k in range(1, N - 1): av1 = compute_angular_velocity(quats[k - 1], quats[k], dt) av2 = compute_angular_velocity(quats[k], quats[k + 1], dt) angular_vels[k] = 0.5 * (av1 + av2) # Smoothing body_angular_velocities[:, j, :] = gaussian_filter1d(angular_vels, sigma=1, axis=0) # 3.11 Package and save to NPZ data_dict = { "fps": fps, # int64 scalar, sampling rate "dof_names": dof_names, # unicode array (D,) "body_names": body_names, # unicode array (B,) "dof_positions": dof_positions, # float32 (N, D) "dof_velocities": dof_velocities_smoothed, # float32 (N, D) "body_positions": body_positions, # float32 (N, B, 3) "body_rotations": body_rotations, # float32 (N, B, 4) (w,x,y,z) "body_linear_velocities": body_linear_velocities, # float32 (N, B, 3) "body_angular_velocities": body_angular_velocities # float32 (N, B, 3) } out_filename = "g1.npz" np.savez(out_filename, **data_dict) print(f"Conversion completed, data saved to {out_filename}") print("fps:", fps) print("dof_names:", dof_names.shape) print("body_names:", body_names.shape) print("dof_positions:", dof_positions.shape) print("dof_velocities:", dof_velocities_smoothed.shape) print("body_positions:", body_positions.shape) print("body_rotations:", body_rotations.shape) print("body_linear_velocities:", body_linear_velocities.shape) print("body_angular_velocities:", body_angular_velocities.shape) if __name__ == "__main__": main() ================================================ FILE: motions/motion_loader.py ================================================ # Copyright (c) 2022-2025, The Isaac Lab Project Developers. # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause import numpy as np import os import torch from typing import Optional class MotionLoader: """ Helper class to load and sample motion data from NumPy-file format. """ def __init__(self, motion_file: str, device: torch.device) -> None: """Load a motion file and initialize the internal variables. Args: motion_file: Motion file path to load. device: The device to which to load the data. Raises: AssertionError: If the specified motion file doesn't exist. """ assert os.path.isfile(motion_file), f"Invalid file path: {motion_file}" data = np.load(motion_file) self.device = device self._dof_names = data["dof_names"].tolist() self._body_names = data["body_names"].tolist() self.dof_positions = torch.tensor(data["dof_positions"], dtype=torch.float32, device=self.device) self.dof_velocities = torch.tensor(data["dof_velocities"], dtype=torch.float32, device=self.device) self.body_positions = torch.tensor(data["body_positions"], dtype=torch.float32, device=self.device) self.body_rotations = torch.tensor(data["body_rotations"], dtype=torch.float32, device=self.device) self.body_linear_velocities = torch.tensor( data["body_linear_velocities"], dtype=torch.float32, device=self.device ) self.body_angular_velocities = torch.tensor( data["body_angular_velocities"], dtype=torch.float32, device=self.device ) self.dt = 1.0 / data["fps"] self.num_frames = self.dof_positions.shape[0] self.duration = self.dt * (self.num_frames - 1) print(f"Motion loaded ({motion_file}): duration: {self.duration} sec, frames: {self.num_frames}") @property def dof_names(self) -> list[str]: """Skeleton DOF names.""" return self._dof_names @property def body_names(self) -> list[str]: """Skeleton rigid body names.""" return self._body_names @property def num_dofs(self) -> int: """Number of skeleton's DOFs.""" return len(self._dof_names) @property def num_bodies(self) -> int: """Number of skeleton's rigid bodies.""" return len(self._body_names) def _interpolate( self, a: torch.Tensor, *, b: Optional[torch.Tensor] = None, blend: Optional[torch.Tensor] = None, start: Optional[np.ndarray] = None, end: Optional[np.ndarray] = None, ) -> torch.Tensor: """Linear interpolation between consecutive values. Args: a: The first value. Shape is (N, X) or (N, M, X). b: The second value. Shape is (N, X) or (N, M, X). blend: Interpolation coefficient between 0 (a) and 1 (b). start: Indexes to fetch the first value. If both, ``start`` and ``end` are specified, the first and second values will be fetches from the argument ``a`` (dimension 0). end: Indexes to fetch the second value. If both, ``start`` and ``end` are specified, the first and second values will be fetches from the argument ``a`` (dimension 0). Returns: Interpolated values. Shape is (N, X) or (N, M, X). """ if start is not None and end is not None: return self._interpolate(a=a[start], b=a[end], blend=blend) if a.ndim >= 2: blend = blend.unsqueeze(-1) if a.ndim >= 3: blend = blend.unsqueeze(-1) return (1.0 - blend) * a + blend * b def _slerp( self, q0: torch.Tensor, *, q1: Optional[torch.Tensor] = None, blend: Optional[torch.Tensor] = None, start: Optional[np.ndarray] = None, end: Optional[np.ndarray] = None, ) -> torch.Tensor: """Interpolation between consecutive rotations (Spherical Linear Interpolation). Args: q0: The first quaternion (wxyz). Shape is (N, 4) or (N, M, 4). q1: The second quaternion (wxyz). Shape is (N, 4) or (N, M, 4). blend: Interpolation coefficient between 0 (q0) and 1 (q1). start: Indexes to fetch the first quaternion. If both, ``start`` and ``end` are specified, the first and second quaternions will be fetches from the argument ``q0`` (dimension 0). end: Indexes to fetch the second quaternion. If both, ``start`` and ``end` are specified, the first and second quaternions will be fetches from the argument ``q0`` (dimension 0). Returns: Interpolated quaternions. Shape is (N, 4) or (N, M, 4). """ if start is not None and end is not None: return self._slerp(q0=q0[start], q1=q0[end], blend=blend) if q0.ndim >= 2: blend = blend.unsqueeze(-1) if q0.ndim >= 3: blend = blend.unsqueeze(-1) qw, qx, qy, qz = 0, 1, 2, 3 # wxyz cos_half_theta = ( q0[..., qw] * q1[..., qw] + q0[..., qx] * q1[..., qx] + q0[..., qy] * q1[..., qy] + q0[..., qz] * q1[..., qz] ) neg_mask = cos_half_theta < 0 q1 = q1.clone() q1[neg_mask] = -q1[neg_mask] cos_half_theta = torch.abs(cos_half_theta) cos_half_theta = torch.unsqueeze(cos_half_theta, dim=-1) half_theta = torch.acos(cos_half_theta) sin_half_theta = torch.sqrt(1.0 - cos_half_theta * cos_half_theta) ratio_a = torch.sin((1 - blend) * half_theta) / sin_half_theta ratio_b = torch.sin(blend * half_theta) / sin_half_theta new_q_x = ratio_a * q0[..., qx : qx + 1] + ratio_b * q1[..., qx : qx + 1] new_q_y = ratio_a * q0[..., qy : qy + 1] + ratio_b * q1[..., qy : qy + 1] new_q_z = ratio_a * q0[..., qz : qz + 1] + ratio_b * q1[..., qz : qz + 1] new_q_w = ratio_a * q0[..., qw : qw + 1] + ratio_b * q1[..., qw : qw + 1] new_q = torch.cat([new_q_w, new_q_x, new_q_y, new_q_z], dim=len(new_q_w.shape) - 1) new_q = torch.where(torch.abs(sin_half_theta) < 0.001, 0.5 * q0 + 0.5 * q1, new_q) new_q = torch.where(torch.abs(cos_half_theta) >= 1, q0, new_q) return new_q def _compute_frame_blend(self, times: np.ndarray) -> tuple[np.ndarray, np.ndarray, np.ndarray]: """Compute the indexes of the first and second values, as well as the blending time to interpolate between them and the given times. Args: times: Times, between 0 and motion duration, to sample motion values. Specified times will be clipped to fall within the range of the motion duration. Returns: First value indexes, Second value indexes, and blending time between 0 (first value) and 1 (second value). """ phase = np.clip(times / self.duration, 0.0, 1.0) index_0 = (phase * (self.num_frames - 1)).round(decimals=0).astype(int) index_1 = np.minimum(index_0 + 1, self.num_frames - 1) blend = ((times - index_0 * self.dt) / self.dt).round(decimals=5) return index_0, index_1, blend def sample_times(self, num_samples: int, duration: float | None = None) -> np.ndarray: """Sample random motion times uniformly. Args: num_samples: Number of time samples to generate. duration: Maximum motion duration to sample. If not defined samples will be within the range of the motion duration. Raises: AssertionError: If the specified duration is longer than the motion duration. Returns: Time samples, between 0 and the specified/motion duration. """ duration = self.duration if duration is None else duration assert ( duration <= self.duration ), f"The specified duration ({duration}) is longer than the motion duration ({self.duration})" return duration * np.random.uniform(low=0.0, high=1.0, size=num_samples) def sample( self, num_samples: int, times: Optional[np.ndarray] = None, duration: float | None = None ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: """Sample motion data. Args: num_samples: Number of time samples to generate. If ``times`` is defined, this parameter is ignored. times: Motion time used for sampling. If not defined, motion data will be random sampled uniformly in time. duration: Maximum motion duration to sample. If not defined, samples will be within the range of the motion duration. If ``times`` is defined, this parameter is ignored. Returns: Sampled motion DOF positions (with shape (N, num_dofs)), DOF velocities (with shape (N, num_dofs)), body positions (with shape (N, num_bodies, 3)), body rotations (with shape (N, num_bodies, 4), as wxyz quaternion), body linear velocities (with shape (N, num_bodies, 3)) and body angular velocities (with shape (N, num_bodies, 3)). """ times = self.sample_times(num_samples, duration) if times is None else times index_0, index_1, blend = self._compute_frame_blend(times) blend = torch.tensor(blend, dtype=torch.float32, device=self.device) return ( self._interpolate(self.dof_positions, blend=blend, start=index_0, end=index_1), self._interpolate(self.dof_velocities, blend=blend, start=index_0, end=index_1), self._interpolate(self.body_positions, blend=blend, start=index_0, end=index_1), self._slerp(self.body_rotations, blend=blend, start=index_0, end=index_1), self._interpolate(self.body_linear_velocities, blend=blend, start=index_0, end=index_1), self._interpolate(self.body_angular_velocities, blend=blend, start=index_0, end=index_1), ) def get_dof_index(self, dof_names: list[str]) -> list[int]: """Get skeleton DOFs indexes by DOFs names. Args: dof_names: List of DOFs names. Raises: AssertionError: If the specified DOFs name doesn't exist. Returns: List of DOFs indexes. """ indexes = [] for name in dof_names: assert name in self._dof_names, f"The specified DOF name ({name}) doesn't exist: {self._dof_names}" indexes.append(self._dof_names.index(name)) return indexes def get_body_index(self, body_names: list[str]) -> list[int]: """Get skeleton body indexes by body names. Args: dof_names: List of body names. Raises: AssertionError: If the specified body name doesn't exist. Returns: List of body indexes. """ indexes = [] for name in body_names: assert name in self._body_names, f"The specified body name ({name}) doesn't exist: {self._body_names}" indexes.append(self._body_names.index(name)) return indexes if __name__ == "__main__": import argparse parser = argparse.ArgumentParser() parser.add_argument("--file", type=str, required=True, help="Motion file") args, _ = parser.parse_known_args() motion = MotionLoader(args.file, "cpu") print("- number of frames:", motion.num_frames) print("- number of DOFs:", motion.num_dofs) print("- dt:", motion.dt) print("- fps:", 1.0 / motion.dt) print("- number of bodies:", motion.num_bodies) ================================================ FILE: motions/motion_replayer.py ================================================ """ Motion Replayer for G1 Humanoid Robot This script replays motion data for the G1 humanoid robot in Isaac Sim. It can also record the simulation data for further analysis. Usage: python motion_replayer.py [options] Options: --motion MOTION_FILE Motion data file to replay (default: G1_dance.npz) --record Enable recording of simulation data --output OUTPUT_FILE Output file name for recorded data (default: recorded_motion.npz) --device DEVICE Device to run simulation on (default: cuda:0) Example: # Replay a motion file python motion_replayer.py --motion G1_dance.npz # Record simulation data while replaying python motion_replayer.py --motion G1_dance.npz --record --output my_recording.npz """ import sys import os sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) import argparse from isaaclab.app import AppLauncher from record_data import MotionRecorder # Command line arguments parser = argparse.ArgumentParser() AppLauncher.add_app_launcher_args(parser) parser.add_argument("--motion", type=str, default="G1_dance.npz") parser.add_argument("--record", action="store_true", help="Enable recording of simulation data") parser.add_argument("--output", type=str, default="recorded_motion.npz", help="Output filename for recorded data") args_cli = parser.parse_args() # Launch Isaac Sim app_launcher = AppLauncher(args_cli) simulation_app = app_launcher.app import torch import isaaclab.sim as sim_utils from isaaclab.scene import InteractiveScene, InteractiveSceneCfg from motion_loader import MotionLoader from g1_cfg import G1_CFG from record_data import MotionRecorder # Load motion data and get dt motion = MotionLoader(args_cli.motion, device=args_cli.device) num_frames = motion.num_frames print(f"motion.dt: {motion.dt}") # Find the index for the root body, typically 'pelvis' try: print(f"Searching for 'pelvis' in the following list of body names: {motion.body_names}") root_idx = motion.body_names.index('pelvis') print(f"Found root body 'pelvis' at index: {root_idx}") except (ValueError, AttributeError): print("\nError: Could not find 'pelvis' in the motion file's body_names.") print("The motion replayer requires 'pelvis' to be defined as the root body.") simulation_app.close() sys.exit(1) # Configure simulation with dt matching motion.dt sim_cfg = sim_utils.SimulationCfg( dt=motion.dt, device=args_cli.device, gravity=(0.0, 0.0, -9.81), # Explicitly set gravity render_interval=1, # Render every physics step enable_scene_query_support=True, use_fabric=True, physx=sim_utils.PhysxCfg( solver_type=1, # TGS solver min_position_iteration_count=8, # Increase solver iterations max_position_iteration_count=8, min_velocity_iteration_count=4, # Add velocity iterations max_velocity_iteration_count=4, enable_ccd=True, # Enable continuous collision detection enable_stabilization=True, # Enable additional stabilization bounce_threshold_velocity=0.2, # Lower threshold for more stable contacts friction_offset_threshold=0.04, # Increase friction threshold friction_correlation_distance=0.025 # Increase correlation distance ), ) sim = sim_utils.SimulationContext(sim_cfg) sim.set_camera_view([3.0, 3.0, 3.0], [0.0, 0.0, 0.0]) # Configure scene scene_cfg = InteractiveSceneCfg( num_envs=1, env_spacing=2.0 ) scene_cfg.robot = G1_CFG.replace(prim_path="/World/Robot") scene = InteractiveScene(scene_cfg) # Add DomeLight for illumination light_cfg = sim_utils.DomeLightCfg(intensity=2000.0, color=(0.75, 0.75, 0.75)) light_cfg.func("/World/Light", light_cfg) # Add black ground plane at -0.5m ground_cfg = sim_utils.GroundPlaneCfg(color=(0.0, 0.0,-0.5)) # ground_cfg.func("/World/ground", ground_cfg) ground_cfg.func( "/World/ground", ground_cfg, translation=(0.0, 0.0, -0.5), # 这里是 (x, y, z) ) # Reset simulation sim.reset() scene.reset() robot = scene["robot"] # Align joint order from motion file to robot's joint order motion_dof_indices = motion.get_dof_index(robot.joint_names) # Initialize data recorder # We want to record the robot's state, so we pass the robot's joint names recorder = MotionRecorder( robot, dof_names_to_record=robot.joint_names, fps=int(round(1.0 / motion.dt)), device=args_cli.device ) if args_cli.record: recorder.start_recording() print(f"\nStarting data recording, will save to {args_cli.output}") print(f"Will record one complete cycle, total {num_frames} frames") print("\nStarting G1 motion replay loop...") print("Tip: Close window or press Ctrl+C to exit") try: # First pass: Record data if args_cli.record: for i in range(num_frames): if not simulation_app.is_running(): break # Get current frame's joint and root states (aligned order!) joint_pos = motion.dof_positions[i, motion_dof_indices].unsqueeze(0) joint_vel = motion.dof_velocities[i, motion_dof_indices].unsqueeze(0) root_pos = motion.body_positions[i, root_idx].unsqueeze(0) root_rot = motion.body_rotations[i, root_idx].unsqueeze(0) root_vel = motion.body_linear_velocities[i, root_idx].unsqueeze(0) root_ang_vel = motion.body_angular_velocities[i, root_idx].unsqueeze(0) root_state = torch.cat([root_pos, root_rot, root_vel, root_ang_vel], dim=-1) # Write to simulation robot.write_root_link_pose_to_sim(root_state[:, :7], torch.tensor([0], device=args_cli.device)) robot.write_root_com_velocity_to_sim(root_state[:, 7:], torch.tensor([0], device=args_cli.device)) robot.write_joint_state_to_sim(joint_pos, joint_vel, None, torch.tensor([0], device=args_cli.device)) # Step simulation (strict dt synchronization) scene.update(dt=sim.get_physics_dt()) scene.write_data_to_sim() sim.step(render=True) # Record data recorder.record_frame(i) # Show progress if i % 10 == 0: # Show progress every 10 frames print(f"\rRecording progress: {i}/{num_frames} frames", end="", flush=True) # Save data after completing one cycle print("\n\nCompleted one cycle of recording, saving data...") recorder.stop_recording() recorder.save_data(args_cli.output) print(f"Data saved to {args_cli.output}") print("\nContinuing motion replay...") # Continue motion replay loop while simulation_app.is_running(): for i in range(num_frames): if not simulation_app.is_running(): break # Get current frame's joint and root states (aligned order!) joint_pos = motion.dof_positions[i, motion_dof_indices].unsqueeze(0) joint_vel = motion.dof_velocities[i, motion_dof_indices].unsqueeze(0) root_pos = motion.body_positions[i, root_idx].unsqueeze(0) root_rot = motion.body_rotations[i, root_idx].unsqueeze(0) root_vel = motion.body_linear_velocities[i, root_idx].unsqueeze(0) root_ang_vel = motion.body_angular_velocities[i, root_idx].unsqueeze(0) root_state = torch.cat([root_pos, root_rot, root_vel, root_ang_vel], dim=-1) # Write to simulation robot.write_root_link_pose_to_sim(root_state[:, :7], torch.tensor([0], device=args_cli.device)) robot.write_root_com_velocity_to_sim(root_state[:, 7:], torch.tensor([0], device=args_cli.device)) robot.write_joint_state_to_sim(joint_pos, joint_vel, None, torch.tensor([0], device=args_cli.device)) # Step simulation (strict dt synchronization) scene.update(dt=sim.get_physics_dt()) scene.write_data_to_sim() sim.step(render=True) except KeyboardInterrupt: print("\nProgram interrupted by user") finally: simulation_app.close() ================================================ FILE: motions/motion_viewer.py ================================================ # Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause import matplotlib import matplotlib.animation import matplotlib.pyplot as plt import numpy as np import torch import mpl_toolkits.mplot3d # noqa: F401 try: from .motion_loader import MotionLoader except ImportError: from motion_loader import MotionLoader class MotionViewer: """ Helper class to visualize motion data from NumPy-file format. """ def __init__(self, motion_file: str, device: torch.device | str = "cpu", render_scene: bool = False) -> None: """Load a motion file and initialize the internal variables. Args: motion_file: Motion file path to load. device: The device to which to load the data. render_scene: Whether the scene (space occupied by the skeleton during movement) is rendered instead of a reduced view of the skeleton. Raises: AssertionError: If the specified motion file doesn't exist. """ self._figure = None self._figure_axes = None self._render_scene = render_scene # load motions self._motion_loader = MotionLoader(motion_file=motion_file, device=device) self._num_frames = self._motion_loader.num_frames self._current_frame = 0 self._body_positions = self._motion_loader.body_positions.cpu().numpy() print("\nBody") for i, name in enumerate(self._motion_loader.body_names): minimum = np.min(self._body_positions[:, i], axis=0).round(decimals=2) maximum = np.max(self._body_positions[:, i], axis=0).round(decimals=2) print(f" |-- [{name}] minimum position: {minimum}, maximum position: {maximum}") def _drawing_callback(self, frame: int) -> None: """Drawing callback called each frame""" # get current motion frame # get data vertices = self._body_positions[self._current_frame] # draw skeleton state self._figure_axes.clear() self._figure_axes.scatter(*vertices.T, color="black", depthshade=False) # adjust exes according to motion view # - scene if self._render_scene: # compute axes limits minimum = np.min(self._body_positions.reshape(-1, 3), axis=0) maximum = np.max(self._body_positions.reshape(-1, 3), axis=0) center = 0.5 * (maximum + minimum) diff = 0.75 * (maximum - minimum) # - skeleton else: # compute axes limits minimum = np.min(vertices, axis=0) maximum = np.max(vertices, axis=0) center = 0.5 * (maximum + minimum) diff = np.array([0.75 * np.max(maximum - minimum).item()] * 3) # scale view self._figure_axes.set_xlim((center[0] - diff[0], center[0] + diff[0])) self._figure_axes.set_ylim((center[1] - diff[1], center[1] + diff[1])) self._figure_axes.set_zlim((center[2] - diff[2], center[2] + diff[2])) self._figure_axes.set_box_aspect(aspect=diff / diff[0]) # plot ground plane x, y = np.meshgrid([center[0] - diff[0], center[0] + diff[0]], [center[1] - diff[1], center[1] + diff[1]]) self._figure_axes.plot_surface(x, y, np.zeros_like(x), color="green", alpha=0.2) # print metadata self._figure_axes.set_xlabel("X") self._figure_axes.set_ylabel("Y") self._figure_axes.set_zlabel("Z") self._figure_axes.set_title(f"frame: {self._current_frame}/{self._num_frames}") # increase frame counter self._current_frame += 1 if self._current_frame >= self._num_frames: self._current_frame = 0 def show(self) -> None: """Show motion""" # create a 3D figure self._figure = plt.figure() self._figure_axes = self._figure.add_subplot(projection="3d") # matplotlib animation (the instance must live as long as the animation will run) self._animation = matplotlib.animation.FuncAnimation( fig=self._figure, func=self._drawing_callback, frames=self._num_frames, interval=1000 * self._motion_loader.dt, ) plt.show() if __name__ == "__main__": import argparse parser = argparse.ArgumentParser() parser.add_argument("--file", type=str, required=True, help="Motion file") parser.add_argument( "--render-scene", action="store_true", default=False, help=( "Whether the scene (space occupied by the skeleton during movement) is rendered instead of a reduced view" " of the skeleton." ), ) parser.add_argument("--matplotlib-backend", type=str, default="TkAgg", help="Matplotlib interactive backend") args, _ = parser.parse_known_args() # https://matplotlib.org/stable/users/explain/figure/backends.html#interactive-backends matplotlib.use(args.matplotlib_backend) viewer = MotionViewer(args.file, render_scene=args.render_scene) viewer.show() ================================================ FILE: motions/record_data.py ================================================ """ Motion Data Recording Tool This script provides functionality for recording and managing motion data from robots. It includes classes and functions for: - Recording motion data in real-time - Saving motion data to npz files - Loading motion data from npz files - Managing motion data structure - Converting joint orders between different formats Usage: As a module: from record_data import MotionRecorder, MotionData recorder = MotionRecorder(robot, motion_dof_indices, fps=60) recorder.start_recording() # ... record frames ... recorder.stop_recording() recorder.save_data("output.npz") """ #!/usr/bin/env python3 # -*- coding: utf-8 -*- import numpy as np import torch from typing import List, Dict, Optional import os from dataclasses import dataclass from datetime import datetime @dataclass class MotionData: fps: int dof_names: np.ndarray body_names: np.ndarray dof_positions: np.ndarray dof_velocities: np.ndarray body_positions: np.ndarray body_rotations: np.ndarray body_linear_velocities: np.ndarray body_angular_velocities: np.ndarray def smooth_motion_data(motion_data: MotionData, window_size: int = 3) -> MotionData: """Apply smoothing to motion data to reduce jitter. Args: motion_data: Original motion data window_size: Size of smoothing window Returns: Smoothed motion data """ # Create a copy of the data smoothed = MotionData( fps=motion_data.fps, dof_names=motion_data.dof_names, body_names=motion_data.body_names, dof_positions=motion_data.dof_positions.copy(), dof_velocities=motion_data.dof_velocities.copy(), body_positions=motion_data.body_positions.copy(), body_rotations=motion_data.body_rotations.copy(), body_linear_velocities=motion_data.body_linear_velocities.copy(), body_angular_velocities=motion_data.body_angular_velocities.copy() ) # Apply smoothing to positions and velocities for i in range(window_size, len(smoothed.dof_positions) - window_size): # Smooth joint positions smoothed.dof_positions[i] = np.mean( motion_data.dof_positions[i-window_size:i+window_size+1], axis=0 ) # Smooth joint velocities smoothed.dof_velocities[i] = np.mean( motion_data.dof_velocities[i-window_size:i+window_size+1], axis=0 ) # Smooth body positions smoothed.body_positions[i] = np.mean( motion_data.body_positions[i-window_size:i+window_size+1], axis=0 ) # Smooth body rotations (using quaternion averaging) smoothed.body_rotations[i] = np.mean( motion_data.body_rotations[i-window_size:i+window_size+1], axis=0 ) # Normalize quaternions smoothed.body_rotations[i] = smoothed.body_rotations[i] / np.linalg.norm( smoothed.body_rotations[i], axis=-1, keepdims=True ) return smoothed class MotionRecorder: """Motion data recorder for robot movements""" def __init__(self, robot, dof_names_to_record: List[str], fps: int = 60, device: str = "cuda", smoothing_window: int = 3): """ Args: robot: Robot object for metadata dof_names_to_record: List of joint names to record. fps: Target frame rate (used to calculate dt) device: Device to use (cuda/cpu) smoothing_window: Window size for motion smoothing """ self.robot = robot self.fps = int(fps) self.dt = 1.0 / self.fps self.device = device self.smoothing_window = smoothing_window # Get names directly from robot to avoid empty arrays self.dof_names = np.asarray(dof_names_to_record, dtype=np.str_) self.body_names = np.asarray(robot.body_names, dtype=np.str_) try: self.root_body_idx = list(robot.body_names).index('pelvis') except ValueError: raise ValueError("The robot asset must have a body named 'pelvis' to be used with MotionRecorder.") # Create a mapping from the robot's full joint list to the desired recording list robot_dof_map = {name: i for i, name in enumerate(robot.joint_names)} self.dof_indices = np.array([robot_dof_map[name] for name in dof_names_to_record], dtype=np.int32) self.recorded_frames: list[dict] = [] self.is_recording = False # 其余接口保持不变 -------------------------- def start_recording(self): self.recorded_frames.clear() self.is_recording = True def stop_recording(self): self.is_recording = False def record_frame(self, frame_idx: int): """Record single frame data (no need to pass robot/motion_dof_indices externally)""" if not self.is_recording: return None # --- Joints --- dof_pos = self.robot.data.joint_pos[0].cpu().numpy() dof_vel = self.robot.data.joint_vel[0].cpu().numpy() # --- Root --- root_pos = self.robot.data.body_pos_w[0, self.root_body_idx].cpu().numpy() root_rot = self.robot.data.body_quat_w[0, self.root_body_idx].cpu().numpy() root_lin_vel = self.robot.data.body_lin_vel_w[0, self.root_body_idx].cpu().numpy() root_ang_vel = self.robot.data.body_ang_vel_w[0, self.root_body_idx].cpu().numpy() # --- Full body --- body_pos = self.robot.data.body_pos_w[0].cpu().numpy() body_rot = self.robot.data.body_quat_w[0].cpu().numpy() body_lin_vel = self.robot.data.body_lin_vel_w[0].cpu().numpy() body_ang_vel = self.robot.data.body_ang_vel_w[0].cpu().numpy() self.recorded_frames.append( dict( frame_idx=frame_idx, dof_positions=dof_pos[self.dof_indices], dof_velocities=dof_vel[self.dof_indices], root_position=root_pos, root_rotation=root_rot, root_linear_velocity=root_lin_vel, root_angular_velocity=root_ang_vel, body_positions=body_pos, body_rotations=body_rot, body_linear_velocities=body_lin_vel, body_angular_velocities=body_ang_vel, ) ) return self.recorded_frames[-1] def get_recorded_data(self) -> MotionData | None: if not self.recorded_frames: return None n = len(self.recorded_frames) d = len(self.dof_indices) b = len(self.body_names) # Pre-allocate arrays dof_pos = np.zeros((n, d), np.float32) dof_vel = np.zeros_like(dof_pos) body_pos = np.zeros((n, b, 3), np.float32) body_rot = np.zeros((n, b, 4), np.float32) body_lin = np.zeros_like(body_pos) body_ang = np.zeros_like(body_pos) for i, f in enumerate(self.recorded_frames): dof_pos[i] = f["dof_positions"] dof_vel[i] = f["dof_velocities"] body_pos[i] = f["body_positions"] body_rot[i] = f["body_rotations"] body_lin[i] = f["body_linear_velocities"] body_ang[i] = f["body_angular_velocities"] motion_data = MotionData( fps=self.fps, dof_names=self.dof_names, body_names=self.body_names, dof_positions=dof_pos, dof_velocities=dof_vel, body_positions=body_pos, body_rotations=body_rot, body_linear_velocities=body_lin, body_angular_velocities=body_ang, ) # Apply smoothing if window size > 1 if self.smoothing_window > 1: motion_data = smooth_motion_data(motion_data, self.smoothing_window) return motion_data def save_data(self, out_file: str, data: MotionData | None = None) -> bool: if data is None: data = self.get_recorded_data() if data is None: print("No data to save") return False os.makedirs(os.path.dirname(os.path.abspath(out_file)), exist_ok=True) try: np.savez( out_file, fps=data.fps, dof_names=data.dof_names, body_names=data.body_names, dof_positions=data.dof_positions, dof_velocities=data.dof_velocities, body_positions=data.body_positions, body_rotations=data.body_rotations, body_linear_velocities=data.body_linear_velocities, body_angular_velocities=data.body_angular_velocities, record_time=datetime.now().strftime("%Y-%m-%d %H:%M:%S"), ) print(f"Data saved to {out_file}") return True except Exception as e: print("Error saving data:", e) return False def load_motion_data(file_path: str) -> Optional[MotionData]: """Load motion data file Args: file_path: Path to .npz file Returns: MotionData object if successful, None otherwise """ try: data = np.load(file_path) return MotionData( fps=int(data['fps']), dof_names=data['dof_names'], body_names=data['body_names'], dof_positions=data['dof_positions'], dof_velocities=data['dof_velocities'], body_positions=data['body_positions'], body_rotations=data['body_rotations'], body_linear_velocities=data['body_linear_velocities'], body_angular_velocities=data['body_angular_velocities'] ) except Exception as e: print(f"Error loading data file: {e}") return None ================================================ FILE: motions/test/get_joint_name.py ================================================ #!/usr/bin/env python import argparse import os import time import numpy as np import torch # ========= 解析命令行参数 ========== parser = argparse.ArgumentParser( description="Potential Field Controller Demo in Isaac Lab (Repulsive Only)." ) parser.add_argument("--robot", type=str, default="franka_panda", help="Name of the robot.") parser.add_argument("--num_envs", type=int, default=1, help="Number of environments to spawn.") # 这里添加 AppLauncher 相关的命令行参数 from isaaclab.app import AppLauncher AppLauncher.add_app_launcher_args(parser) args_cli = parser.parse_args() # ========= 启动 Omniverse 应用 ========== app_launcher = AppLauncher(args_cli) simulation_app = app_launcher.app # ========= 导入 Isaac Lab 相关模块 ========== import isaaclab.sim as sim_utils from isaaclab.assets import AssetBaseCfg, RigidObject, RigidObjectCfg from isaaclab.managers import SceneEntityCfg from isaaclab.markers import VisualizationMarkers from isaaclab.markers.config import FRAME_MARKER_CFG from isaaclab.scene import InteractiveScene, InteractiveSceneCfg from isaaclab.utils import configclass from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR from isaaclab_assets import FRANKA_PANDA_HIGH_PD_CFG # isort:skip G1_MINIMAL_CFG from isaaclab_assets import G1_MINIMAL_CFG from isaaclab.actuators import ImplicitActuatorCfg from isaaclab.assets import ArticulationCfg # ========= 定义桌面场景配置 ========== @configclass class TableTopSceneCfg(InteractiveSceneCfg): """桌面场景的配置""" ground = AssetBaseCfg( prim_path="/World/defaultGroundPlane", spawn=sim_utils.GroundPlaneCfg(), init_state=AssetBaseCfg.InitialStateCfg(pos=(0.0, 0.0, 0.0)), ) dome_light = AssetBaseCfg( prim_path="/World/Light", spawn=sim_utils.DomeLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75)), ) robot: ArticulationCfg = ArticulationCfg( prim_path="/World/Robot", spawn=sim_utils.UsdFileCfg( usd_path=os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "usd/g1_29dof_rev_1_0.usd"), activate_contact_sensors=True, rigid_props=sim_utils.RigidBodyPropertiesCfg( disable_gravity=False, retain_accelerations=False, linear_damping=0.0, angular_damping=0.0, max_linear_velocity=1000.0, max_angular_velocity=1000.0, max_depenetration_velocity=1.0, ), articulation_props=sim_utils.ArticulationRootPropertiesCfg( enabled_self_collisions=False, solver_position_iteration_count=8, solver_velocity_iteration_count=4 ), ), init_state=ArticulationCfg.InitialStateCfg( pos=(0.0, 1.0, 0.8), joint_pos={ ".*_hip_pitch_joint": -0.20, ".*_knee_joint": 0.42, ".*_ankle_pitch_joint": -0.23, # ".*_elbow_pitch_joint": 0.87, "left_shoulder_roll_joint": 0.16, "left_shoulder_pitch_joint": 0.35, "right_shoulder_roll_joint": -0.16, "right_shoulder_pitch_joint": 0.35, }, joint_vel={".*": 0.0}, ), soft_joint_pos_limit_factor=0.9, actuators={ "legs": ImplicitActuatorCfg( joint_names_expr=[ ".*_hip_yaw_joint", ".*_hip_roll_joint", ".*_hip_pitch_joint", ".*_knee_joint", "waist_yaw_joint", "waist_roll_joint", "waist_pitch_joint", ], effort_limit=300, velocity_limit=100.0, stiffness={ ".*_hip_yaw_joint": 150.0, ".*_hip_roll_joint": 150.0, ".*_hip_pitch_joint": 200.0, ".*_knee_joint": 200.0, "waist_yaw_joint": 200.0, "waist_roll_joint": 200.0, "waist_pitch_joint": 200.0, }, damping={ ".*_hip_yaw_joint": 5.0, ".*_hip_roll_joint": 5.0, ".*_hip_pitch_joint": 5.0, ".*_knee_joint": 5.0, "waist_yaw_joint": 5.0, "waist_roll_joint": 5.0, "waist_pitch_joint": 5.0, }, armature={ ".*_hip_.*": 0.01, ".*_knee_joint": 0.01, "waist_yaw_joint": 0.01, "waist_roll_joint": 0.01, "waist_pitch_joint": 0.01, }, ), "feet": ImplicitActuatorCfg( effort_limit=20, joint_names_expr=[".*_ankle_pitch_joint", ".*_ankle_roll_joint"], stiffness=20.0, damping=2.0, armature=0.01, ), "arms": ImplicitActuatorCfg( joint_names_expr=[ ".*_shoulder_pitch_joint", ".*_shoulder_roll_joint", ".*_shoulder_yaw_joint", ".*_elbow_joint", ".*_wrist_.*", ], effort_limit=300, velocity_limit=100.0, stiffness=40.0, damping=10.0, armature={ ".*_shoulder_.*": 0.01, ".*_elbow_.*": 0.01, ".*_wrist_.*": 0.01, }, ), }, ) # ========= 辅助函数:打印机器人资产下所有 prim 的路径 ========== def list_robot_prims(): try: # 使用 USD Python API 遍历 "/World/Robot" 下的所有子节点 from pxr import Usd stage = Usd.Stage.GetCurrent() robot_prim = stage.GetPrimAtPath("/World/Robot") if robot_prim: print("在 '/World/Robot' 下发现的 prim 路径:") for prim in robot_prim.GetChildren(): print(prim.GetPath()) else: print("在 '/World/Robot' 未找到任何 prim,请检查机器人是否正确加载。") except Exception as e: print("获取 USD Stage 时出错:", e) # ========= 运行仿真 ========== def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene): """主循环:运行仿真并打印出机器人关节与刚体信息(请先观察打印结果确定名称规则)""" # 先打印出机器人资产下所有 prim 路径,帮助你确认实际的命名规则 list_robot_prims() # 如果你在上面的输出中找到了关节和身体对应的命名,可以在下面设置相应的正则表达式。 # 例如,假设你观察后发现关节名称中包含 "joint" 而刚体名称中包含 "body",可以这样设置: robot = scene["robot"] print("解析到的关节名称列表:", robot.joint_names) print("解析到的刚体名称列表:", robot.body_names) print(robot.data.joint_pos.shape) print(robot.data.joint_vel.shape) # self.robot.data.body_pos_w[:, self.ref_body_index], # self.robot.data.body_quat_w[:, self.ref_body_index], # self.robot.data.body_lin_vel_w[:, self.ref_body_index], # self.robot.data.body_ang_vel_w[:, self.ref_body_index], # self.robot.data.body_pos_w[:, self.key_body_indexes], step_count = 0 while simulation_app.is_running(): # 更新场景并执行一步物理仿真 scene.update(dt=sim.get_physics_dt()) scene.write_data_to_sim() sim.step() step_count += 1 # ========= 主函数 ========== def main(): # 创建仿真配置,指定仿真步长和设备(例如 "cuda:0" 或 "cpu") sim_cfg = sim_utils.SimulationCfg(dt=0.01, device=args_cli.device) sim = sim_utils.SimulationContext(sim_cfg) sim.set_camera_view([2.5, 2.5, 2.5], [0.0, 0.0, 0.0]) # 初始化场景(由 InteractiveScene 管理所有实体) scene_cfg = TableTopSceneCfg(num_envs=args_cli.num_envs, env_spacing=2.0) scene = InteractiveScene(scene_cfg) # 重置仿真环境 sim.reset() print("[INFO]: Setup complete. Starting simulation...") # 进入主仿真循环 run_simulator(sim, scene) if __name__ == "__main__": main() simulation_app.close() # 解析到的关节名称列表: ['left_hip_pitch_joint', 'right_hip_pitch_joint', 'waist_yaw_joint', 'left_hip_roll_joint', 'right_hip_roll_joint', 'waist_roll_joint', 'left_hip_yaw_joint', 'right_hip_yaw_joint', 'waist_pitch_joint', 'left_knee_joint', 'right_knee_joint', 'left_shoulder_pitch_joint', 'right_shoulder_pitch_joint', 'left_ankle_pitch_joint', 'right_ankle_pitch_joint', 'left_shoulder_roll_joint', 'right_shoulder_roll_joint', 'left_ankle_roll_joint', 'right_ankle_roll_joint', 'left_shoulder_yaw_joint', 'right_shoulder_yaw_joint', 'left_elbow_joint', 'right_elbow_joint', 'left_wrist_roll_joint', 'right_wrist_roll_joint', 'left_wrist_pitch_joint', 'right_wrist_pitch_joint', 'left_wrist_yaw_joint', 'right_wrist_yaw_joint'] # 解析到的刚体名称列表: ['pelvis', 'imu_in_pelvis', 'left_hip_pitch_link', 'pelvis_contour_link', 'right_hip_pitch_link', 'waist_yaw_link', 'left_hip_roll_link', 'right_hip_roll_link', 'waist_roll_link', 'left_hip_yaw_link', 'right_hip_yaw_link', 'torso_link', 'left_knee_link', 'right_knee_link', 'd435_link', 'head_link', 'imu_in_torso', 'left_shoulder_pitch_link', 'logo_link', 'mid360_link', 'right_shoulder_pitch_link', 'left_ankle_pitch_link', 'right_ankle_pitch_link', 'left_shoulder_roll_link', 'right_shoulder_roll_link', 'left_ankle_roll_link', 'right_ankle_roll_link', 'left_shoulder_yaw_link', 'right_shoulder_yaw_link', 'left_elbow_link', 'right_elbow_link', 'left_wrist_roll_link', 'right_wrist_roll_link', 'left_wrist_pitch_link', 'right_wrist_pitch_link', 'left_wrist_yaw_link', 'right_wrist_yaw_link', 'left_rubber_hand', 'right_rubber_hand'] # torch.Size([1, 29]) # torch.Size([1, 29]) ================================================ FILE: motions/update_pelvis_data.py ================================================ import numpy as np import os import argparse import plotly.graph_objects as go from plotly.subplots import make_subplots from scipy.spatial.transform import Rotation as R def plot_pelvis_data_2d(source_data, target_data, source_pelvis_idx, target_pelvis_idx): """ Generates 2D plots comparing pelvis data (positions, velocities, rotations) from source and target files over time. """ print("Generating 2D comparison plots...") plots_to_create = [ ('body_positions', 'Pelvis Position Comparison', 'Position (m)'), ('body_linear_velocities', 'Pelvis Linear Velocity Comparison', 'Velocity (m/s)'), ('body_angular_velocities', 'Pelvis Angular Velocity Comparison', 'Angular Velocity (rad/s)'), ('body_rotations', 'Pelvis Rotation (Quaternion) Comparison', 'Value') ] fig = make_subplots( rows=len(plots_to_create), cols=1, subplot_titles=[p[1] for p in plots_to_create], vertical_spacing=0.08 ) fps = source_data.get('fps', 60) for i, (key, title, y_title) in enumerate(plots_to_create, 1): for data, idx, name in [(source_data, source_pelvis_idx, 'Source'), (target_data, target_pelvis_idx, 'Target')]: if key not in data: continue motion_data = data[key][:, idx] num_frames = motion_data.shape[0] time = np.arange(num_frames) / fps dims = motion_data.shape[1] labels = ['X', 'Y', 'Z', 'W'] if dims == 4 else ['X', 'Y', 'Z'] for d in range(dims): fig.add_trace( go.Scatter( x=time, y=motion_data[:, d], name=f'{name} {labels[d]}', legendgroup=f'group{i}', line=dict(dash='solid' if name == 'Source' else 'dash', width=1.5), showlegend=(d==0) # Show legend only for the first component to avoid clutter ), row=i, col=1 ) fig.update_yaxes(title_text=y_title, row=i, col=1) fig.update_xaxes(title_text="Time (s)", row=i, col=1) fig.update_layout( title_text="Pelvis Data 2D Comparison", height=350 * len(plots_to_create), legend_tracegroupgap = 250 ) output_html_file = "pelvis_data_2d_comparison.html" fig.write_html(output_html_file) print(f"2D plot visualization saved to: {os.path.abspath(output_html_file)}") def visualize_pelvis_trajectories(source_data, target_data, source_pelvis_idx, target_pelvis_idx): """ Visualizes and compares the 3D trajectories and orientations of the pelvis from two motion data sources. """ print("Generating 3D trajectory visualization...") fig = go.Figure() for data, idx, name in [(source_data, source_pelvis_idx, 'Source'), (target_data, target_pelvis_idx, 'Target')]: positions = data['body_positions'][:, idx, :] rotations_quat = data['body_rotations'][:, idx, :] # Add trajectory line fig.add_trace(go.Scatter3d( x=positions[:, 0], y=positions[:, 1], z=positions[:, 2], mode='lines', name=f'{name} Trajectory', line=dict(width=4) )) # Add orientation cones num_frames = positions.shape[0] step = max(1, num_frames // 20) # Show about 20 orientation markers rotations = R.from_quat(rotations_quat[:, [1, 2, 3, 0]]) # reorder to x,y,z,w for SciPy # Get the forward vector (e.g., x-axis) for each orientation forward_vectors = rotations.apply([0.1, 0, 0]) fig.add_trace(go.Cone( x=positions[::step, 0], y=positions[::step, 1], z=positions[::step, 2], u=forward_vectors[::step, 0], v=forward_vectors[::step, 1], w=forward_vectors[::step, 2], sizemode="absolute", sizeref=0.1, showscale=False, name=f'{name} Orientation', anchor="tip" )) fig.update_layout( title='Pelvis 3D Trajectory and Orientation Comparison', scene=dict( xaxis_title='X (m)', yaxis_title='Y (m)', zaxis_title='Z (m)', aspectmode='data' ), legend_title_text='Data Source' ) output_html_file = "pelvis_trajectory_comparison.html" fig.write_html(output_html_file) print(f"Visualization saved to: {os.path.abspath(output_html_file)}") def update_pelvis_data(source_file, target_file, dry_run=False): """ Copies the pelvis data from a source motion file to a target motion file. This function loads two .npz motion files, finds the 'pelvis' body index in both, and then overwrites the pelvis trajectory data (position, rotation, velocities) in the target file with the data from the source file. Args: source_file (str): Path to the .npz file with the source pelvis data. target_file (str): Path to the .npz file to be updated. dry_run (bool): If True, print data for comparison without saving. """ try: # Load the source and target .npz files source_data = np.load(source_file) target_data = np.load(target_file) print(f"Loaded source file: {source_file}") print(f"Loaded target file: {target_file}") # Find the index of 'pelvis' in the body_names array for both files source_body_names = list(source_data['body_names']) target_body_names = list(target_data['body_names']) source_pelvis_idx = source_body_names.index('pelvis') target_pelvis_idx = target_body_names.index('pelvis') print(f"Found 'pelvis' at index {source_pelvis_idx} in source file.") print(f"Found 'pelvis' at index {target_pelvis_idx} in target file.") if dry_run: print("\n--- DRY RUN MODE ---") print("Displaying pelvis data for the first 5 frames. No files will be changed.") print("\n--- Source Pelvis Data (first 5 frames) ---") print("Body Positions:") print(source_data['body_positions'][:5, source_pelvis_idx]) print("\nBody Rotations:") print(source_data['body_rotations'][:5, source_pelvis_idx]) print("\n--- Target Pelvis Data (first 5 frames) ---") print("Body Positions:") print(target_data['body_positions'][:5, target_pelvis_idx]) print("\nBody Rotations:") print(target_data['body_rotations'][:5, target_pelvis_idx]) visualize_pelvis_trajectories(source_data, target_data, source_pelvis_idx, target_pelvis_idx) plot_pelvis_data_2d(source_data, target_data, source_pelvis_idx, target_pelvis_idx) print("\n--- END DRY RUN ---") return # Convert the loaded target data into a mutable dictionary target_data_dict = {key: target_data[key] for key in target_data.files} # Check if the number of frames is consistent num_frames_source = source_data['body_positions'].shape[0] num_frames_target = target_data_dict['body_positions'].shape[0] if num_frames_source < num_frames_target: print(f"Warning: Source file has fewer frames ({num_frames_source}) than target file ({num_frames_target}). Target will be truncated.") num_frames = num_frames_source # Truncate all relevant arrays in the target data for key in ['dof_positions', 'dof_velocities', 'body_positions', 'body_rotations', 'body_linear_velocities', 'body_angular_velocities']: if key in target_data_dict: target_data_dict[key] = target_data_dict[key][:num_frames] else: num_frames = num_frames_target if num_frames_source > num_frames_target: print(f"Warning: Source file has more frames ({num_frames_source}) than target file ({num_frames_target}). Source data will be truncated.") # Copy the pelvis data from source to target print("Copying pelvis data...") target_data_dict['body_positions'][:, target_pelvis_idx] = source_data['body_positions'][:num_frames, source_pelvis_idx] target_data_dict['body_rotations'][:, target_pelvis_idx] = source_data['body_rotations'][:num_frames, source_pelvis_idx] # target_data_dict['body_linear_velocities'][:, target_pelvis_idx] = source_data['body_linear_velocities'][:num_frames, source_pelvis_idx] # target_data_dict['body_angular_velocities'][:, target_pelvis_idx] = source_data['body_angular_velocities'][:num_frames, source_pelvis_idx] # Save the updated data back to the target file np.savez( target_file, **target_data_dict ) print(f"Successfully updated pelvis data in {target_file}") except FileNotFoundError as e: print(f"Error: {e}. Please ensure the file paths are correct.") except ValueError: print("Error: 'pelvis' not found in one of the files. Cannot proceed.") except Exception as e: print(f"An unexpected error occurred: {e}") if __name__ == "__main__": # Set to True to print data without modifying files DRY_RUN = True # Define the file paths relative to the script's location script_dir = os.path.dirname(__file__) source_motion_file = os.path.join(script_dir, "G1_dance_old.npz") target_motion_file = os.path.join(script_dir, "G1_dance.npz") update_pelvis_data(source_motion_file, target_motion_file, dry_run=DRY_RUN) ================================================ FILE: motions/verify_motion.py ================================================ """ Motion Data Verification Tool This script verifies and displays detailed information about motion data stored in npz files. It shows information about: - FPS and duration - File size and name - Data structure and content - Sample data for each type of motion data Usage: python verify_motion.py --file Arguments: --file Path to the npz file containing motion data Example: python verify_motion.py --file G1_dance.npz Output: Prints detailed information about the motion data file, including: - Basic file information (size, name) - FPS and duration - Data structure overview - Sample data for each type of motion data """ import numpy as np import argparse import os np.set_printoptions(threshold=100, precision=3, suppress=True) def verify_motion_file(npz_file): # Load npz file data = np.load(npz_file, allow_pickle=True) # First display FPS information if 'fps' in data: fps = data['fps'].item() print(f"\nFPS: {fps}") if 'dof_positions' in data: duration = data['dof_positions'].shape[0] / fps print(f"Motion duration: {duration:.2f} seconds") else: print("\nWarning: 'fps' key not found!") # Get file size file_size = os.path.getsize(npz_file) / (1024 * 1024) # Convert to MB print(f"\nFile Information:") print(f"Filename: {os.path.basename(npz_file)}") print(f"File size: {file_size:.2f} MB") print("\nData Content Overview:") print("-" * 50) for key in data: arr = data[key] print(f"\n{key}:") print(f" Data type: {arr.dtype}") print(f" Data shape: {arr.shape}") # Display content based on different data types if key == 'dof_names': print(f" Joint names:") for i, name in enumerate(arr): print(f" {i+1}. {name}") elif key == 'body_names': print(f" Body part names:") for i, name in enumerate(arr): print(f" {i+1}. {name}") elif key == "body_positions": print(f" Body positions sample (first 2 frames, first 3 parts):") print(arr[:2, :3, :]) # Show first 2 frames, first 3 parts positions elif key == "body_rotations": print(f" Body rotations sample (first 2 frames, first 3 parts, quaternions):") print(arr[:2, :3, :]) # Show first 2 frames, first 3 parts quaternion rotations elif key == "body_linear_velocities": print(f" Body linear velocities sample (first 2 frames, first 3 parts):") print(arr[:2, :3, :]) # Show first 2 frames, first 3 parts linear velocities elif key == "body_angular_velocities": print(f" Body angular velocities sample (first 2 frames, first 3 parts):") print(arr[:2, :3, :]) # Show first 2 frames, first 3 parts angular velocities elif key == "dof_positions": print(f" Joint positions sample (first 2 frames, first 5 joints):") print(arr[:2, :5]) # Show first 2 frames, first 5 joints positions elif key == "dof_velocities": print(f" Joint velocities sample (first 2 frames, first 5 joints):") print(arr[:2, :5]) # Show first 2 frames, first 5 joints velocities if __name__ == "__main__": parser = argparse.ArgumentParser(description="Verify and display motion data file contents") parser.add_argument("--file", type=str, required=True, help="Path to the npz file") args = parser.parse_args() verify_motion_file(args.file) ================================================ FILE: motions/visualize_motion.py ================================================ """ Motion Data Visualization Tool This script visualizes motion data from npz files, creating interactive plots for: - Joint positions - Joint velocities - Body linear velocities - Body angular velocities Usage: python visualize_motion.py --file Arguments: --file Path to the npz file containing motion data Example: python visualize_motion.py --file G1_dance.npz Output: Generates an HTML file with interactive plots in the same directory as the input file. The output filename will be _visualization.html """ import numpy as np import plotly.graph_objects as go from plotly.subplots import make_subplots import argparse import os def visualize_motion(npz_file): # Load data data = np.load(npz_file, allow_pickle=True) # Get basic information fps = data['fps'].item() if 'fps' in data else 60 dof_names = data['dof_names'] if 'dof_names' in data else [] body_names = data['body_names'] if 'body_names' in data else [] # Create subplot layout fig = make_subplots( rows=5, cols=1, subplot_titles=('Root Link 3D Position (pelvis)', 'Joint Positions (DOF Positions)', 'Joint Velocities (DOF Velocities)', 'Body Linear Velocities', 'Body Angular Velocities'), vertical_spacing=0.05, specs=[[{"type": "scatter"}], [{"type": "scatter"}], [{"type": "scatter"}], [{"type": "scatter"}], [{"type": "scatter"}]], row_heights=[0.2, 0.2, 0.2, 0.2, 0.2] ) # Time axis time = np.arange(len(data['dof_positions'])) / fps if 'dof_positions' in data else np.arange(len(data['body_linear_velocities'])) / fps # 1. Plot root link 3D position if 'body_positions' in data and 'pelvis' in body_names: root_idx = body_names.tolist().index('pelvis') root_positions = data['body_positions'][:, root_idx, :] # X component fig.add_trace( go.Scatter( x=time, y=root_positions[:, 0], name="Pelvis Position (X)", mode='lines', line=dict(width=1) ), row=1, col=1 ) # Y component fig.add_trace( go.Scatter( x=time, y=root_positions[:, 1], name="Pelvis Position (Y)", mode='lines', line=dict(width=1) ), row=1, col=1 ) # Z component fig.add_trace( go.Scatter( x=time, y=root_positions[:, 2], name="Pelvis Position (Z)", mode='lines', line=dict(width=1) ), row=1, col=1 ) # 2. Plot all joint positions if 'dof_positions' in data: for i in range(len(dof_names)): fig.add_trace( go.Scatter( x=time, y=data['dof_positions'][:, i], name=dof_names[i], mode='lines', line=dict(width=1) ), row=2, col=1 ) # 3. Plot all joint velocities if 'dof_velocities' in data: for i in range(len(dof_names)): fig.add_trace( go.Scatter( x=time, y=data['dof_velocities'][:, i], name=dof_names[i], mode='lines', line=dict(width=1) ), row=3, col=1 ) # 4. Plot all body linear velocities (X, Y, Z components) if 'body_linear_velocities' in data: for i in range(len(body_names)): velocities = data['body_linear_velocities'][:, i, :] # X component fig.add_trace( go.Scatter( x=time, y=velocities[:, 0], name=f"{body_names[i]} (X)", mode='lines', line=dict(width=1) ), row=4, col=1 ) # Y component fig.add_trace( go.Scatter( x=time, y=velocities[:, 1], name=f"{body_names[i]} (Y)", mode='lines', line=dict(width=1) ), row=4, col=1 ) # Z component fig.add_trace( go.Scatter( x=time, y=velocities[:, 2], name=f"{body_names[i]} (Z)", mode='lines', line=dict(width=1) ), row=4, col=1 ) # 5. Plot all body angular velocities (X, Y, Z components) if 'body_angular_velocities' in data: for i in range(len(body_names)): velocities = data['body_angular_velocities'][:, i, :] # X component fig.add_trace( go.Scatter( x=time, y=velocities[:, 0], name=f"{body_names[i]} (X)", mode='lines', line=dict(width=1) ), row=5, col=1 ) # Y component fig.add_trace( go.Scatter( x=time, y=velocities[:, 1], name=f"{body_names[i]} (Y)", mode='lines', line=dict(width=1) ), row=5, col=1 ) # Z component fig.add_trace( go.Scatter( x=time, y=velocities[:, 2], name=f"{body_names[i]} (Z)", mode='lines', line=dict(width=1) ), row=5, col=1 ) # Update layout fig.update_layout( title_text="Motion Data Visualization (Complete Data)", height=2000, showlegend=True, legend=dict( yanchor="top", y=0.99, xanchor="left", x=1.05, font=dict(size=8) ) ) # Update axis labels for i in range(1, 6): fig.update_xaxes(title_text="Time (seconds)", row=i, col=1) fig.update_yaxes(title_text="Position (m)", row=1, col=1) fig.update_yaxes(title_text="Position (radians)", row=2, col=1) fig.update_yaxes(title_text="Velocity (rad/s)", row=3, col=1) fig.update_yaxes(title_text="Linear Velocity (m/s)", row=4, col=1) fig.update_yaxes(title_text="Angular Velocity (rad/s)", row=5, col=1) # Generate output file path automatically input_dir = os.path.dirname(os.path.abspath(npz_file)) input_filename = os.path.splitext(os.path.basename(npz_file))[0] output_html = os.path.join(input_dir, f"{input_filename}_visualization.html") # Save visualization results fig.write_html(output_html) print(f"Visualization results saved to: {output_html}") def quaternion_to_euler(q): """Convert quaternion to Euler angles (roll, pitch, yaw)""" # Normalize quaternion q = q / np.linalg.norm(q) w, x, y, z = q # Calculate Euler angles roll = np.arctan2(2 * (w*x + y*z), 1 - 2 * (x*x + y*y)) pitch = np.arcsin(2 * (w*y - z*x)) yaw = np.arctan2(2 * (w*z + x*y), 1 - 2 * (y*y + z*z)) return np.array([roll, pitch, yaw]) if __name__ == "__main__": parser = argparse.ArgumentParser(description="Visualize motion data from npz file") parser.add_argument("--file", type=str, required=True, help="Path to the npz file") args = parser.parse_args() visualize_motion(args.file) ================================================ FILE: play.py ================================================ # Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause """ Script to play a checkpoint of an RL agent from skrl. Visit the skrl documentation (https://skrl.readthedocs.io) to see the examples structured in a more user-friendly way. """ """Launch Isaac Sim Simulator first.""" import argparse import sys from isaaclab.app import AppLauncher # add argparse arguments parser = argparse.ArgumentParser(description="Play a checkpoint of an RL agent from skrl.") parser.add_argument("--video", action="store_true", default=False, help="Record videos during training.") parser.add_argument("--video_length", type=int, default=200, help="Length of the recorded video (in steps).") parser.add_argument( "--disable_fabric", action="store_true", default=False, help="Disable fabric and use USD I/O operations." ) parser.add_argument("--num_envs", type=int, default=None, help="Number of environments to simulate.") parser.add_argument("--task", type=str, default=None, help="Name of the task.") parser.add_argument( "--agent", type=str, default=None, help=( "Name of the RL agent configuration entry point. Defaults to None, in which case the argument " "--algorithm is used to determine the default agent configuration entry point." ), ) parser.add_argument("--checkpoint", type=str, default=None, help="Path to model checkpoint.") parser.add_argument("--seed", type=int, default=None, help="Seed used for the environment") parser.add_argument( "--use_pretrained_checkpoint", action="store_true", help="Use the pre-trained checkpoint from Nucleus.", ) parser.add_argument( "--ml_framework", type=str, default="torch", choices=["torch", "jax", "jax-numpy"], help="The ML framework used for training the skrl agent.", ) parser.add_argument( "--algorithm", type=str, default="PPO", choices=["AMP", "PPO", "IPPO", "MAPPO"], help="The RL algorithm used for training the skrl agent.", ) parser.add_argument("--real-time", action="store_true", default=False, help="Run in real-time, if possible.") # append AppLauncher cli args AppLauncher.add_app_launcher_args(parser) # parse the arguments args_cli, hydra_args = parser.parse_known_args() # always enable cameras to record video if args_cli.video: args_cli.enable_cameras = True # clear out sys.argv for Hydra sys.argv = [sys.argv[0]] + hydra_args # launch omniverse app app_launcher = AppLauncher(args_cli) simulation_app = app_launcher.app """Rest everything follows.""" import gymnasium as gym import os import random import time import torch import skrl from packaging import version # check for minimum supported skrl version SKRL_VERSION = "1.4.3" if version.parse(skrl.__version__) < version.parse(SKRL_VERSION): skrl.logger.error( f"Unsupported skrl version: {skrl.__version__}. " f"Install supported version using 'pip install skrl>={SKRL_VERSION}'" ) exit() if args_cli.ml_framework.startswith("torch"): from skrl.utils.runner.torch import Runner elif args_cli.ml_framework.startswith("jax"): from skrl.utils.runner.jax import Runner from isaaclab.envs import ( DirectMARLEnv, DirectMARLEnvCfg, DirectRLEnvCfg, ManagerBasedRLEnvCfg, multi_agent_to_single_agent, ) from isaaclab.utils.dict import print_dict from isaaclab.utils.pretrained_checkpoint import get_published_pretrained_checkpoint from isaaclab_rl.skrl import SkrlVecEnvWrapper import isaaclab_tasks # noqa: F401 from isaaclab_tasks.utils import get_checkpoint_path from isaaclab_tasks.utils.hydra import hydra_task_config # PLACEHOLDER: Extension template (do not remove this comment) # config shortcuts if args_cli.agent is None: algorithm = args_cli.algorithm.lower() agent_cfg_entry_point = "skrl_cfg_entry_point" if algorithm in ["ppo"] else f"skrl_{algorithm}_cfg_entry_point" else: agent_cfg_entry_point = args_cli.agent algorithm = agent_cfg_entry_point.split("_cfg")[0].split("skrl_")[-1].lower() @hydra_task_config(args_cli.task, agent_cfg_entry_point) def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, experiment_cfg: dict): """Play with skrl agent.""" # grab task name for checkpoint path task_name = args_cli.task.split(":")[-1] train_task_name = task_name.replace("-Play", "") # override configurations with non-hydra CLI arguments env_cfg.scene.num_envs = args_cli.num_envs if args_cli.num_envs is not None else env_cfg.scene.num_envs env_cfg.sim.device = args_cli.device if args_cli.device is not None else env_cfg.sim.device # configure the ML framework into the global skrl variable if args_cli.ml_framework.startswith("jax"): skrl.config.jax.backend = "jax" if args_cli.ml_framework == "jax" else "numpy" # randomly sample a seed if seed = -1 if args_cli.seed == -1: args_cli.seed = random.randint(0, 10000) # set the agent and environment seed from command line # note: certain randomization occur in the environment initialization so we set the seed here experiment_cfg["seed"] = args_cli.seed if args_cli.seed is not None else experiment_cfg["seed"] env_cfg.seed = experiment_cfg["seed"] # specify directory for logging experiments (load checkpoint) log_root_path = os.path.join("logs", "skrl", experiment_cfg["agent"]["experiment"]["directory"]) log_root_path = os.path.abspath(log_root_path) print(f"[INFO] Loading experiment from directory: {log_root_path}") # get checkpoint path if args_cli.use_pretrained_checkpoint: resume_path = get_published_pretrained_checkpoint("skrl", train_task_name) if not resume_path: print("[INFO] Unfortunately a pre-trained checkpoint is currently unavailable for this task.") return elif args_cli.checkpoint: resume_path = os.path.abspath(args_cli.checkpoint) else: resume_path = get_checkpoint_path( log_root_path, run_dir=f".*_{algorithm}_{args_cli.ml_framework}", other_dirs=["checkpoints"] ) log_dir = os.path.dirname(os.path.dirname(resume_path)) # set the log directory for the environment (works for all environment types) env_cfg.log_dir = log_dir # create isaac environment env = gym.make(args_cli.task, cfg=env_cfg, render_mode="rgb_array" if args_cli.video else None) # convert to single-agent instance if required by the RL algorithm if isinstance(env.unwrapped, DirectMARLEnv) and algorithm in ["ppo"]: env = multi_agent_to_single_agent(env) # get environment (step) dt for real-time evaluation try: dt = env.step_dt except AttributeError: dt = env.unwrapped.step_dt # wrap for video recording if args_cli.video: video_kwargs = { "video_folder": os.path.join(log_dir, "videos", "play"), "step_trigger": lambda step: step == 0, "video_length": args_cli.video_length, "disable_logger": True, } print("[INFO] Recording videos during training.") print_dict(video_kwargs, nesting=4) env = gym.wrappers.RecordVideo(env, **video_kwargs) # wrap around environment for skrl env = SkrlVecEnvWrapper(env, ml_framework=args_cli.ml_framework) # same as: `wrap_env(env, wrapper="auto")` # configure and instantiate the skrl runner # https://skrl.readthedocs.io/en/latest/api/utils/runner.html experiment_cfg["trainer"]["close_environment_at_exit"] = False experiment_cfg["agent"]["experiment"]["write_interval"] = 0 # don't log to TensorBoard experiment_cfg["agent"]["experiment"]["checkpoint_interval"] = 0 # don't generate checkpoints runner = Runner(env, experiment_cfg) print(f"[INFO] Loading model checkpoint from: {resume_path}") runner.agent.load(resume_path) # set agent to evaluation mode runner.agent.set_running_mode("eval") # reset environment obs, _ = env.reset() timestep = 0 # simulate environment while simulation_app.is_running(): start_time = time.time() # run everything in inference mode with torch.inference_mode(): # agent stepping outputs = runner.agent.act(obs, timestep=0, timesteps=0) # - multi-agent (deterministic) actions if hasattr(env, "possible_agents"): actions = {a: outputs[-1][a].get("mean_actions", outputs[0][a]) for a in env.possible_agents} # - single-agent (deterministic) actions else: actions = outputs[-1].get("mean_actions", outputs[0]) # env stepping obs, _, _, _, _ = env.step(actions) if args_cli.video: timestep += 1 # exit the play loop after recording one video if timestep == args_cli.video_length: break # time delay for real-time evaluation sleep_time = dt - (time.time() - start_time) if args_cli.real_time and sleep_time > 0: time.sleep(sleep_time) # close the simulator env.close() if __name__ == "__main__": # run the main function main() # close sim app simulation_app.close() ================================================ FILE: pyproject.toml ================================================ [build-system] requires = ["setuptools>=65.0"] build-backend = "setuptools.build_meta" [project] name = "humanoid-amp" version = "0.1.0" description = "Humanoid AMP tasks and utilities for Isaac Lab" readme = "README.md" requires-python = ">=3.10" authors = [ { name = "Unitree Robotics Hobbyist" } ] license = "BSD-3-Clause" classifiers = [ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Operating System :: POSIX :: Linux" ] [tool.setuptools] packages = ["humanoid_amp"] [tool.setuptools.package-dir] humanoid_amp = "." [tool.setuptools.package-data] humanoid_amp = [ "motions/**/*", "usd/**/*" ] ================================================ FILE: sync_skrl_scripts.sh ================================================ #!/usr/bin/env bash # Sync skrl train/play scripts from Isaac Lab releases and reapply local tweaks. set -euo pipefail BASE_URL="https://raw.githubusercontent.com/isaac-sim/IsaacLab/main/scripts/reinforcement_learning/skrl" FILES=(train.py play.py) fetch() { local url="$1" local dest="$2" curl -fsSL "${url}" -o "${dest}" } for name in "${FILES[@]}"; do url="${BASE_URL}/${name}" echo "Fetching ${url}" fetch "${url}" "${name}" done echo "Done." ================================================ FILE: train.py ================================================ # Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause """ Script to train RL agent with skrl. Visit the skrl documentation (https://skrl.readthedocs.io) to see the examples structured in a more user-friendly way. """ """Launch Isaac Sim Simulator first.""" import argparse import sys from isaaclab.app import AppLauncher # add argparse arguments parser = argparse.ArgumentParser(description="Train an RL agent with skrl.") parser.add_argument("--video", action="store_true", default=False, help="Record videos during training.") parser.add_argument("--video_length", type=int, default=200, help="Length of the recorded video (in steps).") parser.add_argument("--video_interval", type=int, default=2000, help="Interval between video recordings (in steps).") parser.add_argument("--num_envs", type=int, default=None, help="Number of environments to simulate.") parser.add_argument("--task", type=str, default=None, help="Name of the task.") parser.add_argument( "--agent", type=str, default=None, help=( "Name of the RL agent configuration entry point. Defaults to None, in which case the argument " "--algorithm is used to determine the default agent configuration entry point." ), ) parser.add_argument("--seed", type=int, default=None, help="Seed used for the environment") parser.add_argument( "--distributed", action="store_true", default=False, help="Run training with multiple GPUs or nodes." ) parser.add_argument("--checkpoint", type=str, default=None, help="Path to model checkpoint to resume training.") parser.add_argument("--max_iterations", type=int, default=None, help="RL Policy training iterations.") parser.add_argument("--export_io_descriptors", action="store_true", default=False, help="Export IO descriptors.") parser.add_argument( "--ml_framework", type=str, default="torch", choices=["torch", "jax", "jax-numpy"], help="The ML framework used for training the skrl agent.", ) parser.add_argument( "--algorithm", type=str, default="PPO", choices=["AMP", "PPO", "IPPO", "MAPPO"], help="The RL algorithm used for training the skrl agent.", ) # append AppLauncher cli args AppLauncher.add_app_launcher_args(parser) # parse the arguments args_cli, hydra_args = parser.parse_known_args() # always enable cameras to record video if args_cli.video: args_cli.enable_cameras = True # clear out sys.argv for Hydra sys.argv = [sys.argv[0]] + hydra_args # launch omniverse app app_launcher = AppLauncher(args_cli) simulation_app = app_launcher.app """Rest everything follows.""" import gymnasium as gym import os import random from datetime import datetime import omni import skrl from packaging import version # check for minimum supported skrl version SKRL_VERSION = "1.4.3" if version.parse(skrl.__version__) < version.parse(SKRL_VERSION): skrl.logger.error( f"Unsupported skrl version: {skrl.__version__}. " f"Install supported version using 'pip install skrl>={SKRL_VERSION}'" ) exit() if args_cli.ml_framework.startswith("torch"): from skrl.utils.runner.torch import Runner elif args_cli.ml_framework.startswith("jax"): from skrl.utils.runner.jax import Runner from isaaclab.envs import ( DirectMARLEnv, DirectMARLEnvCfg, DirectRLEnvCfg, ManagerBasedRLEnvCfg, multi_agent_to_single_agent, ) from isaaclab.utils.assets import retrieve_file_path from isaaclab.utils.dict import print_dict from isaaclab.utils.io import dump_yaml from isaaclab_rl.skrl import SkrlVecEnvWrapper import isaaclab_tasks # noqa: F401 from isaaclab_tasks.utils.hydra import hydra_task_config # PLACEHOLDER: Extension template (do not remove this comment) # config shortcuts if args_cli.agent is None: algorithm = args_cli.algorithm.lower() agent_cfg_entry_point = "skrl_cfg_entry_point" if algorithm in ["ppo"] else f"skrl_{algorithm}_cfg_entry_point" else: agent_cfg_entry_point = args_cli.agent algorithm = agent_cfg_entry_point.split("_cfg")[0].split("skrl_")[-1].lower() @hydra_task_config(args_cli.task, agent_cfg_entry_point) def main(env_cfg: ManagerBasedRLEnvCfg | DirectRLEnvCfg | DirectMARLEnvCfg, agent_cfg: dict): """Train with skrl agent.""" # override configurations with non-hydra CLI arguments env_cfg.scene.num_envs = args_cli.num_envs if args_cli.num_envs is not None else env_cfg.scene.num_envs env_cfg.sim.device = args_cli.device if args_cli.device is not None else env_cfg.sim.device # multi-gpu training config if args_cli.distributed: env_cfg.sim.device = f"cuda:{app_launcher.local_rank}" # max iterations for training if args_cli.max_iterations: agent_cfg["trainer"]["timesteps"] = args_cli.max_iterations * agent_cfg["agent"]["rollouts"] agent_cfg["trainer"]["close_environment_at_exit"] = False # configure the ML framework into the global skrl variable if args_cli.ml_framework.startswith("jax"): skrl.config.jax.backend = "jax" if args_cli.ml_framework == "jax" else "numpy" # randomly sample a seed if seed = -1 if args_cli.seed == -1: args_cli.seed = random.randint(0, 10000) # set the agent and environment seed from command line # note: certain randomization occur in the environment initialization so we set the seed here agent_cfg["seed"] = args_cli.seed if args_cli.seed is not None else agent_cfg["seed"] env_cfg.seed = agent_cfg["seed"] # specify directory for logging experiments log_root_path = os.path.join("logs", "skrl", agent_cfg["agent"]["experiment"]["directory"]) log_root_path = os.path.abspath(log_root_path) print(f"[INFO] Logging experiment in directory: {log_root_path}") # specify directory for logging runs: {time-stamp}_{run_name} log_dir = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + f"_{algorithm}_{args_cli.ml_framework}" # The Ray Tune workflow extracts experiment name using the logging line below, hence, do not change it (see PR #2346, comment-2819298849) print(f"Exact experiment name requested from command line: {log_dir}") if agent_cfg["agent"]["experiment"]["experiment_name"]: log_dir += f'_{agent_cfg["agent"]["experiment"]["experiment_name"]}' # set directory into agent config agent_cfg["agent"]["experiment"]["directory"] = log_root_path agent_cfg["agent"]["experiment"]["experiment_name"] = log_dir # update log_dir log_dir = os.path.join(log_root_path, log_dir) # dump the configuration into log-directory dump_yaml(os.path.join(log_dir, "params", "env.yaml"), env_cfg) dump_yaml(os.path.join(log_dir, "params", "agent.yaml"), agent_cfg) # get checkpoint path (to resume training) resume_path = retrieve_file_path(args_cli.checkpoint) if args_cli.checkpoint else None # set the IO descriptors export flag if requested if isinstance(env_cfg, ManagerBasedRLEnvCfg): env_cfg.export_io_descriptors = args_cli.export_io_descriptors else: omni.log.warn( "IO descriptors are only supported for manager based RL environments. No IO descriptors will be exported." ) # set the log directory for the environment (works for all environment types) env_cfg.log_dir = log_dir # create isaac environment env = gym.make(args_cli.task, cfg=env_cfg, render_mode="rgb_array" if args_cli.video else None) # convert to single-agent instance if required by the RL algorithm if isinstance(env.unwrapped, DirectMARLEnv) and algorithm in ["ppo"]: env = multi_agent_to_single_agent(env) # wrap for video recording if args_cli.video: video_kwargs = { "video_folder": os.path.join(log_dir, "videos", "train"), "step_trigger": lambda step: step % args_cli.video_interval == 0, "video_length": args_cli.video_length, "disable_logger": True, } print("[INFO] Recording videos during training.") print_dict(video_kwargs, nesting=4) env = gym.wrappers.RecordVideo(env, **video_kwargs) # wrap around environment for skrl env = SkrlVecEnvWrapper(env, ml_framework=args_cli.ml_framework) # same as: `wrap_env(env, wrapper="auto")` # configure and instantiate the skrl runner # https://skrl.readthedocs.io/en/latest/api/utils/runner.html runner = Runner(env, agent_cfg) # load checkpoint (if specified) if resume_path: print(f"[INFO] Loading model checkpoint from: {resume_path}") runner.agent.load(resume_path) # run training runner.run() # close the simulator env.close() if __name__ == "__main__": # run the main function main() # close sim app simulation_app.close() ================================================ FILE: usd/g1_29dof_rev_1_0.urdf ================================================ ================================================ FILE: usd/g1_29dof_rev_1_0.usd ================================================ [File too large to display: 26.0 MB] ================================================ FILE: usd/instanceable_meshes.usd ================================================ [File too large to display: 26.0 MB]