Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

This repository provides a set of reinforcement learning tasks for Booster robots using [Isaac Lab](https://isaac-sim.github.io/IsaacLab/main/index.html).
Currently it includes the fabulous [BeyondMimic motion tracking](https://github.com/HybridRobotics/whole_body_tracking) framework adapted to Booster K1 robots.
The motion conversion and replay utilities under `scripts/mimic/` support both Booster K1 and T1 robots.
This repository follows the standard Isaac Lab project structure, and is tested with IsaacLab 2.2 and Isaac Sim 5.0.

## Installation
Expand All @@ -30,9 +31,15 @@ This repository follows the standard Isaac Lab project structure, and is tested
- Prepare BeyondMimic motion data:
```bash
# use 'FULL_PATH_TO_isaaclab.sh|bat -p' instead of 'python' if Isaac Lab is not installed in Python venv or conda
python scripts/csv_to_npz.py --headless --input_file=<PATH_TO_BOOSTER_ASSETS>/motions/K1/<MOTION>.csv --input_fps=<FPS> --output_name=<PATH_TO_BOOSTER_ASSETS>/motions/K1/<MOTION>.npz
python scripts/mimic/csv_to_npz.py --headless --input_file=<PATH_TO_BOOSTER_ASSETS>/motions/<ROBOT>/<MOTION>.csv --input_fps=<FPS> --output_file=<PATH_TO_BOOSTER_ASSETS>/motions/<ROBOT>/<MOTION>.npz --output_fps=50 --robot=<k1|t1>
```

Optional arguments:

- `--robot=t1` converts T1 motion files. The default robot is `k1`.
- `--frame_range <START> <END>` converts only a subset of frames. Frame indices are 1-based and inclusive.
- `--output_fps` controls the interpolation rate of the exported `.npz` motion.

## Usage

- Listing the available tasks:
Expand All @@ -58,6 +65,17 @@ This repository follows the standard Isaac Lab project structure, and is tested

This script also exports the trained policy to a TorchScript/ONNX file for deployment on real robots in `logs/rsl_rl/<EXPERIMENT>/<RUN>/exported/`.

- Replay a converted motion file for inspection:

```bash
python scripts/mimic/replay_npz.py --motion=<PATH_TO_MOTION>.npz --robot=<k1|t1>

# or download and replay from Weights & Biases registry
python scripts/mimic/replay_npz.py --registry_name=<WANDB_REGISTRY_NAME> --robot=<k1|t1>
```

When `--registry_name` does not include an alias, the script automatically uses `:latest`.

## Deploy

After a model has been trained and exported, you can deploy the trained policy in MuJoCo or on real Booster robots using the [booster_deploy](https://github.com/BoosterRobotics/booster_deploy) repository. For more details, please refer to the instructions in the [booster_deploy](https://github.com/BoosterRobotics/booster_deploy) repository.
Expand Down
47 changes: 33 additions & 14 deletions scripts/csv_to_npz.py → scripts/mimic/csv_to_npz.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

# Usage
python csv_to_npz.py --input_file LAFAN/dance1_subject2.csv --input_fps 30 --frame_range 122 722 \
--output_file ./motions/dance1_subject2.npz --output_fps 50
--output_file ./motions/dance1_subject2.npz --output_fps 50 --robot k1
"""

"""Launch Isaac Sim Simulator first."""
Expand All @@ -29,8 +29,15 @@
" loaded."
),
)
parser.add_argument("--output_name", type=str, required=True, help="The name of the motion npz file.")
parser.add_argument("--output_file", type=str, required=True, help="The name of the motion npz file.")
parser.add_argument("--output_fps", type=int, default=50, help="The fps of the output motion.")
parser.add_argument(
"--robot",
type=str.lower,
default="k1",
choices=["k1", "t1"],
help="Target robot type. Defaults to k1. Use t1 to convert T1 motion format.",
)

# append AppLauncher cli args
AppLauncher.add_app_launcher_args(parser)
Expand All @@ -57,8 +64,14 @@
# Pre-defined configs
##
# Booster assets currently only provide retargeted motion data for Booster K1 robot
from booster_train.assets.robots.booster import BOOSTER_K1_CFG as ROBOT_CFG
from booster_assets.motions import K1_JOINT_NAMES as JOINT_NAMES
from booster_train.assets.robots.booster import BOOSTER_K1_CFG, BOOSTER_T1_CFG
from booster_assets.motions import K1_JOINT_NAMES, T1_JOINT_NAMES


ROBOT_CONFIGS = {
"k1": (BOOSTER_K1_CFG, K1_JOINT_NAMES),
"t1": (BOOSTER_T1_CFG, T1_JOINT_NAMES),
}


@configclass
Expand All @@ -78,7 +91,7 @@ class ReplayMotionsSceneCfg(InteractiveSceneCfg):
)

# articulation
robot: ArticulationCfg = ROBOT_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot")
robot: ArticulationCfg = BOOSTER_K1_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot")


class MotionLoader:
Expand Down Expand Up @@ -194,12 +207,15 @@ def _so3_derivative(self, rotations: torch.Tensor, dt: float) -> torch.Tensor:
def get_next_state(
self,
) -> tuple[
torch.Tensor,
torch.Tensor,
torch.Tensor,
torch.Tensor,
torch.Tensor,
torch.Tensor,
tuple[
torch.Tensor,
torch.Tensor,
torch.Tensor,
torch.Tensor,
torch.Tensor,
torch.Tensor,
],
bool,
]:
"""Gets the next state of the motion."""
state = (
Expand Down Expand Up @@ -303,19 +319,22 @@ def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene, joi
):
log[k] = np.stack(log[k], axis=0)

np.savez(f"{args_cli.output_name}", **log)
print(f"[INFO]: Motion saved to {args_cli.output_name}")
np.savez(f"{args_cli.output_file}", **log)
print(f"[INFO]: Motion saved to {args_cli.output_file}")
sys.exit(0)


def main():
"""Main function."""
robot_cfg, joint_names = ROBOT_CONFIGS[args_cli.robot]

# Load kit helper
sim_cfg = sim_utils.SimulationCfg(device=args_cli.device)
sim_cfg.dt = 1.0 / args_cli.output_fps
sim = SimulationContext(sim_cfg)
# Design scene
scene_cfg = ReplayMotionsSceneCfg(num_envs=1, env_spacing=2.0)
scene_cfg.robot = robot_cfg.replace(prim_path="{ENV_REGEX_NS}/Robot")
scene = InteractiveScene(scene_cfg)
# Play the simulator
sim.reset()
Expand All @@ -325,7 +344,7 @@ def main():
run_simulator(
sim,
scene,
joint_names=JOINT_NAMES,
joint_names=joint_names,
)


Expand Down
42 changes: 31 additions & 11 deletions scripts/replay_npz.py → scripts/mimic/replay_npz.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
"""This script demonstrates how to replay K1 robot motions from npz files.
"""This script demonstrates how to replay robot motions from npz files.

.. code-block:: bash

# Usage - Direct file path
python replay_npz.py --motion <path_to_motion.npz>
python replay_npz.py --motion <path_to_motion.npz> --robot k1

# Usage - From wandb registry
python replay_npz.py --registry_name <wandb_registry_name>
python replay_npz.py --registry_name <wandb_registry_name> --robot t1
"""

"""Launch Isaac Sim Simulator first."""
Expand All @@ -23,6 +23,13 @@
parser = argparse.ArgumentParser(description="Replay converted motions.")
parser.add_argument("--motion", type=str, default=None, help="Path to the motion npz file.")
parser.add_argument("--registry_name", type=str, default=None, help="The name of the wand registry.")
parser.add_argument(
"--robot",
type=str.lower,
default="k1",
choices=["k1", "t1"],
help="Target robot type. Defaults to k1. Use t1 to replay T1 motion format.",
)

# append AppLauncher cli args
AppLauncher.add_app_launcher_args(parser)
Expand All @@ -45,10 +52,16 @@
##
# Pre-defined configs
##
from booster_train.assets.robots.booster import BOOSTER_K1_CFG
from booster_train.assets.robots.booster import BOOSTER_K1_CFG, BOOSTER_T1_CFG
from booster_train.tasks.manager_based.beyond_mimic.mdp.commands import MotionLoader


ROBOT_CONFIGS = {
"k1": BOOSTER_K1_CFG,
"t1": BOOSTER_T1_CFG,
}


@configclass
class ReplayMotionsSceneCfg(InteractiveSceneCfg):
"""Configuration for a replay motions scene."""
Expand Down Expand Up @@ -95,15 +108,18 @@ def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene):
else:
raise ValueError("Either --motion or --registry_name must be provided.")

# Load npz file to get body names and determine body_indexes
# For K1, we typically use Trunk as anchor body (index 0)
# body_indexes should be a list of indices corresponding to the bodies we want to use
# For replay, we only need the anchor body (Trunk), which is typically at index 0
body_indexes = [0] # Default to index 0 for anchor body (Trunk)
with np.load(motion_file) as motion_data:
motion_body_names = motion_data["body_names"].tolist() if "body_names" in motion_data else robot.body_names

# For both K1/T1 converted motions, the anchor body is stored at index 0.
anchor_body_name = motion_body_names[0]

motion = MotionLoader(
motion_file,
body_indexes,
[anchor_body_name],
robot.joint_names,
default_motion_body_names=robot.body_names,
default_motion_joint_names=robot.joint_names,
tail_len=0,
device=str(sim.device),
)
Expand All @@ -116,7 +132,8 @@ def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene):
time_steps[reset_ids] = 0

root_states = robot.data.default_root_state.clone()
root_states[:, :3] = motion.body_pos_w[time_steps][:, 0] + scene.env_origins[:, None, :]
root_states[:, :3] = motion.body_pos_w[time_steps][:, 0]
root_states[:, :2] += scene.env_origins[:, :2]
root_states[:, 3:7] = motion.body_quat_w[time_steps][:, 0]
root_states[:, 7:10] = motion.body_lin_vel_w[time_steps][:, 0]
root_states[:, 10:] = motion.body_ang_vel_w[time_steps][:, 0]
Expand All @@ -132,11 +149,14 @@ def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene):


def main():
robot_cfg = ROBOT_CONFIGS[args_cli.robot]

sim_cfg = sim_utils.SimulationCfg(device=args_cli.device)
sim_cfg.dt = 0.02
sim = SimulationContext(sim_cfg)

scene_cfg = ReplayMotionsSceneCfg(num_envs=1, env_spacing=2.0)
scene_cfg.robot = robot_cfg.replace(prim_path="{ENV_REGEX_NS}/Robot")
scene = InteractiveScene(scene_cfg)
sim.reset()
# Run the simulator
Expand Down
16 changes: 16 additions & 0 deletions source/booster_train/booster_train/assets/robots/booster.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,3 +329,19 @@
),
},
)

T1_ACTION_SCALE = {}
for a in BOOSTER_T1_CFG.actuators.values():
e = a.effort_limit_sim
s = a.stiffness
names = a.joint_names_expr
if not isinstance(e, dict):
e = {n: e for n in names}
if not isinstance(s, dict):
s = {n: s for n in names}
for n in names:
if n in e and n in s and s[n]:
T1_ACTION_SCALE[n] = 0.25 * e[n] / s[n]

print(f'{BOOSTER_T1_CFG.actuators=}')
print(f'{T1_ACTION_SCALE=}')
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# 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 gymnasium as gym

##
# Register Gym environments.
##

gym.register(
id="Booster-T1-Dance-v0",
entry_point="isaaclab.envs:ManagerBasedRLEnv",
disable_env_checker=True,
kwargs={
"env_cfg_entry_point": f"{__name__}.env_cfg:RoughWoStateEstimationEnvCfg",
"rsl_rl_cfg_entry_point": f"{__name__}.ppo_cfg:PPORunnerCfg",
},
)

gym.register(
id="Booster-T1-Dance-v0-Play",
entry_point="isaaclab.envs:ManagerBasedRLEnv",
disable_env_checker=True,
kwargs={
"env_cfg_entry_point": f"{__name__}.env_cfg:PlayFlatWoStateEstimationEnvCfg",
"rsl_rl_cfg_entry_point": f"{__name__}.ppo_cfg:PPORunnerCfg",
},
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from isaaclab.utils import configclass
from isaaclab.terrains import TerrainGeneratorCfg
import isaaclab.terrains as terrain_gen
from booster_assets import BOOSTER_ASSETS_DIR
from booster_train.assets.robots.booster import BOOSTER_T1_CFG as ROBOT_CFG, T1_ACTION_SCALE
from booster_train.tasks.manager_based.beyond_mimic.agents.rsl_rl_ppo_cfg import LOW_FREQ_SCALE
from .tracking_env_cfg import TrackingEnvCfg


@configclass
class FlatEnvCfg(TrackingEnvCfg):
def __post_init__(self):
super().__post_init__()

self.scene.robot = ROBOT_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot")
self.actions.joint_pos.scale = T1_ACTION_SCALE
self.commands.motion.motion_file = f"{BOOSTER_ASSETS_DIR}/motions/T1/CLIO_Dimitroula_1_stageii.npz"
self.commands.motion.anchor_body_name = "Trunk"
self.commands.motion.body_names = [
'Trunk',
'H2',
'Hip_Roll_Left',
'Shank_Left',
'left_foot_link',
'Hip_Roll_Right',
'Shank_Right',
'right_foot_link',
'Waist',
'AL2',
'AL3',
'left_hand_link',
'AR2',
'AR3',
'right_hand_link',
]


@configclass
class FlatWoStateEstimationEnvCfg(FlatEnvCfg):
def __post_init__(self):
super().__post_init__()
self.observations.policy.motion_anchor_pos_b = None
self.observations.policy.base_lin_vel = None


@configclass
class RoughWoStateEstimationEnvCfg(FlatWoStateEstimationEnvCfg):
def __post_init__(self):
super().__post_init__()

self.scene.terrain.terrain_type = "generator"
self.scene.terrain.debug_vis = False # 设为True可视化地形分布
self.scene.terrain.terrain_generator = TerrainGeneratorCfg(
size=(10.0, 10.0), # 每个地形块尺寸(米)
border_width=20.0, # 边界宽度(米)
num_rows=5, # 地形网格行数
num_cols=10, # 地形网格列数
horizontal_scale=0.1, # 水平分辨率
vertical_scale=0.005, # 垂直分辨率
slope_threshold=0.75, # 网格简化阈值
use_cache=False, # 每次重新生成地形
curriculum=False, # 启用课程学习
sub_terrains={
# 80%接近平面的地形(非常平滑)
"nearly_flat": terrain_gen.HfRandomUniformTerrainCfg(
proportion=0.8,
noise_range=(0.0, 0.005), # 高度波动0-0.5cm(几乎平坦)
noise_step=0.005, # 噪声步长0.5cm
border_width=0.25,
),
# 20%随机粗糙地形
"random_rough": terrain_gen.HfRandomUniformTerrainCfg(
proportion=0.2,
noise_range=(-0.015, 0.015), # 高度波动±1.5cm
noise_step=0.005, # 噪声步长0.5cm
border_width=0.25,
),
},
)


@configclass
class PlayFlatWoStateEstimationEnvCfg(FlatWoStateEstimationEnvCfg):
def __post_init__(self):
super().__post_init__()
self.commands.motion.play = True
self.events.push_robot = None
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from isaaclab.utils import configclass
from booster_train.tasks.manager_based.beyond_mimic.agents.rsl_rl_ppo_cfg import BasePPORunnerCfg


@configclass
class PPORunnerCfg(BasePPORunnerCfg):
max_iterations = 30000
experiment_name = "t1_dance"
Loading