From e0643905501d99392f451c27d90e5bb1a08cfdd2 Mon Sep 17 00:00:00 2001 From: Boshi An <41010290+boshi-an@users.noreply.github.com> Date: Sat, 29 Nov 2025 15:45:07 +0100 Subject: [PATCH 01/50] Added several test cases for robots (#702) * added several test cases for robots * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- metasim/sim/mujoco/mujoco.py | 4 + .../test_robot_cfg/conftest.py | 67 ++++++ .../test_robot_cfg/test_collision.py | 196 ++++++++++++++++++ .../test_robot_cfg/test_default_pos.py | 46 ++++ .../test_robot_cfg/test_default_qpos.py | 2 +- .../test_robot_cfg/test_qpos_limit.py | 2 +- metasim/test/test_utils.py | 3 + 7 files changed, 318 insertions(+), 2 deletions(-) create mode 100644 metasim/test/test_scenario_cfg/test_robot_cfg/test_collision.py create mode 100644 metasim/test/test_scenario_cfg/test_robot_cfg/test_default_pos.py diff --git a/metasim/sim/mujoco/mujoco.py b/metasim/sim/mujoco/mujoco.py index 7e74ff73a..647f6349c 100644 --- a/metasim/sim/mujoco/mujoco.py +++ b/metasim/sim/mujoco/mujoco.py @@ -466,6 +466,10 @@ def _add_robots_to_model(self, mjcf_model: mjcf.RootElement) -> None: child_body.pos = "0 0 0" # Reset child body position to origin with respect to the attached robot robot_attached.quat = child_body.quat if child_body.quat is not None else "1 0 0 0" robot_attached.add("inertial", mass="1e-9", diaginertia="1e-9 1e-9 1e-9", pos=pos) + if hasattr(robot, "default_position") and robot.default_position is not None: + robot_attached.pos = list(robot.default_position) + if hasattr(robot, "default_orientation") and robot.default_orientation is not None: + robot_attached.quat = robot.default_orientation self.mj_objects[robot.name] = robot_xml self._mujoco_robot_names.append(robot_xml.full_identifier) diff --git a/metasim/test/test_scenario_cfg/test_robot_cfg/conftest.py b/metasim/test/test_scenario_cfg/test_robot_cfg/conftest.py index 59f909ef3..968863693 100644 --- a/metasim/test/test_scenario_cfg/test_robot_cfg/conftest.py +++ b/metasim/test/test_scenario_cfg/test_robot_cfg/conftest.py @@ -62,6 +62,73 @@ def get_qpos_limit_scenario(sim: str, num_envs: int) -> ScenarioCfg: ) +def get_default_pos_scenario(sim: str, num_envs: int) -> ScenarioCfg: + """Create scenario configuration for default position tests.""" + if sim not in _SUPPORTED_SIMS: + raise ValueError(f"Unsupported simulator '{sim}' for default position tests") + + return ScenarioCfg( + robots=[ + FrankaCfg( + name="franka", + default_position=(0, 0, 1.0), + default_orientation=(0.707107, 0, 0, 0.707107), + ) + ], + headless=True, + num_envs=num_envs, + simulator=sim, + ) + + +def get_collision_scenario(sim: str, num_envs: int) -> ScenarioCfg: + """Create scenario configuration for self collision tests.""" + if sim not in _SUPPORTED_SIMS: + raise ValueError(f"Unsupported simulator '{sim}' for self collision tests") + + return ScenarioCfg( + robots=[ + FrankaCfg( + name="franka1", + default_joint_positions={ + "panda_joint1": 0.0 - 0.1, + "panda_joint2": -0.785398 - 0.1, + "panda_joint3": 0.0 - 0.1, + "panda_joint4": -2.356194 - 0.1, + "panda_joint5": 0.0 - 0.1, + "panda_joint6": 1.570796 + 0.1, + "panda_joint7": 0.785398 + 0.1, + "panda_finger_joint1": 0.0, + "panda_finger_joint2": 0.0, + }, + default_position=(0, 0, 1.0), + enabled_self_collisions=False, + ), + FrankaCfg( + name="franka2", + default_joint_positions={ + "panda_joint1": 0.0 - 0.1, + "panda_joint2": -0.785398 - 0.1, + "panda_joint3": 0.0 - 0.1, + "panda_joint4": -2.356194 - 0.1, + "panda_joint5": 0.0 - 0.1, + "panda_joint6": 1.570796 + 0.1, + "panda_joint7": 0.785398 + 0.1, + "panda_finger_joint1": 0.0, + "panda_finger_joint2": 0.0, + }, + default_position=(0, 1, 1.0), + enabled_self_collisions=True, + ), + ], + headless=True, + num_envs=num_envs, + simulator=sim, + ) + + # Register scenarios with file-specific prefixes register_shared_suite("metasim.test.test_scenario_cfg.test_robot_cfg.test_default_qpos", get_default_qpos_scenario) register_shared_suite("metasim.test.test_scenario_cfg.test_robot_cfg.test_qpos_limit", get_qpos_limit_scenario) +register_shared_suite("metasim.test.test_scenario_cfg.test_robot_cfg.test_collision", get_collision_scenario) +register_shared_suite("metasim.test.test_scenario_cfg.test_robot_cfg.test_default_pos", get_default_pos_scenario) diff --git a/metasim/test/test_scenario_cfg/test_robot_cfg/test_collision.py b/metasim/test/test_scenario_cfg/test_robot_cfg/test_collision.py new file mode 100644 index 000000000..f1ba8a4b6 --- /dev/null +++ b/metasim/test/test_scenario_cfg/test_robot_cfg/test_collision.py @@ -0,0 +1,196 @@ +"""Integration tests for joint position limits.""" + +from __future__ import annotations + +import math + +import pytest +import rootutils +import torch + +rootutils.setup_root(__file__, pythonpath=True) + +from metasim.sim.base import BaseSimHandler + + +@pytest.mark.sim("isaacsim", "mujoco", "isaacgym", "mjx", "sapien2", "sapien3") +def test_self_collision(handler: BaseSimHandler): + """Test that joint limits are respected during simulation.""" + dof_targets = [ + { + "franka1": { + "dof_pos_target": { + "panda_joint1": 0.0, + "panda_joint2": 1.3, # Self collision + "panda_joint3": 0.0, + "panda_joint4": -2.356194, + "panda_joint5": 0.0, + "panda_joint6": 1.0, # Self collision + "panda_joint7": 0.785398, + "panda_finger_joint1": 0.04, + "panda_finger_joint2": 0.04, + } + }, + "franka2": { + "dof_pos_target": { + "panda_joint1": 0.0, + "panda_joint2": 1.3, # Self collision + "panda_joint3": 0.0, + "panda_joint4": -2.356194, + "panda_joint5": 0.0, + "panda_joint6": 1.0, # Self collision + "panda_joint7": 0.785398, + "panda_finger_joint1": 0.04, + "panda_finger_joint2": 0.04, + } + }, + } + ] * handler.scenario.num_envs + handler.set_dof_targets(dof_targets) + + # Simulate to let joints reach their targets (clamped by limits) + for _ in range(10): + handler.simulate() + + states_after = handler.get_states(mode="dict") + + assert math.isclose(states_after[0]["robots"]["franka1"]["dof_pos"]["panda_joint2"], 1.3, abs_tol=1e-3), ( + "franka1 (without self collisions) joint 2 should be close to 1.3" + ) + assert not math.isclose(states_after[0]["robots"]["franka2"]["dof_pos"]["panda_joint2"], 1.3, abs_tol=1e-3), ( + "franka2 (with self collisions) joint 2 should not be close to 1.3" + ) + + +@pytest.mark.sim("isaacsim", "mujoco", "isaacgym", "mjx", "sapien2", "sapien3") +def test_mutual_collision(handler: BaseSimHandler): + """Test that joint limits are respected during simulation.""" + + dof_resets = [ + { + "robots": { + "franka1": { + "dof_pos_target": { + "panda_joint1": 0.0 - 0.1, + "panda_joint2": -0.785398 - 0.1, + "panda_joint3": 0.0 - 0.1, + "panda_joint4": -2.356194 - 0.1, + "panda_joint5": 0.0 - 0.1, + "panda_joint6": 1.570796 + 0.1, + "panda_joint7": 0.785398 + 0.1, + "panda_finger_joint1": 0.0, + "panda_finger_joint2": 0.0, + } + }, + "franka2": { + "dof_pos_target": { + "panda_joint1": 0.0 - 0.1, + "panda_joint2": -0.785398 - 0.1, + "panda_joint3": 0.0 - 0.1, + "panda_joint4": -2.356194 - 0.1, + "panda_joint5": 0.0 - 0.1, + "panda_joint6": 1.570796 + 0.1, + "panda_joint7": 0.785398 + 0.1, + "panda_finger_joint1": 0.0, + "panda_finger_joint2": 0.0, + } + }, + }, + "objects": {}, + } + ] * handler.scenario.num_envs + + dof_targets = [ + { + "franka1": { + "dof_pos_target": { + "panda_joint1": 0.65, + "panda_joint2": 0.564, + "panda_joint3": 0.25, + "panda_joint4": -1.27, + "panda_joint5": -0.08, + "panda_joint6": 2.13, + "panda_joint7": 0.785, + "panda_finger_joint1": 0.04, + "panda_finger_joint2": 0.04, + } + }, + "franka2": { + "dof_pos_target": { + "panda_joint1": -0.82, + "panda_joint2": 0.05, + "panda_joint3": 0.01, + "panda_joint4": -1.71, + "panda_joint5": 0.04, + "panda_joint6": 2.98, + "panda_joint7": 0.785, + "panda_finger_joint1": 0.04, + "panda_finger_joint2": 0.04, + } + }, + } + ] * handler.scenario.num_envs + + # Reset the simulation to the default positions + handler.set_states(dof_resets) + handler.set_dof_targets(dof_targets) + + # Simulate to let joints reach their targets (clamped by limits) + for _ in range(10): + handler.simulate() + + states_after = handler.get_states(mode="dict") + + franka1_dof_state_list = [ + states_after[0]["robots"]["franka1"]["dof_pos"]["panda_joint1"], + states_after[0]["robots"]["franka1"]["dof_pos"]["panda_joint2"], + states_after[0]["robots"]["franka1"]["dof_pos"]["panda_joint3"], + states_after[0]["robots"]["franka1"]["dof_pos"]["panda_joint4"], + states_after[0]["robots"]["franka1"]["dof_pos"]["panda_joint5"], + states_after[0]["robots"]["franka1"]["dof_pos"]["panda_joint6"], + states_after[0]["robots"]["franka1"]["dof_pos"]["panda_joint7"], + states_after[0]["robots"]["franka1"]["dof_pos"]["panda_finger_joint1"], + states_after[0]["robots"]["franka1"]["dof_pos"]["panda_finger_joint2"], + ] + franka2_dof_state_list = [ + states_after[0]["robots"]["franka2"]["dof_pos"]["panda_joint1"], + states_after[0]["robots"]["franka2"]["dof_pos"]["panda_joint2"], + states_after[0]["robots"]["franka2"]["dof_pos"]["panda_joint3"], + states_after[0]["robots"]["franka2"]["dof_pos"]["panda_joint4"], + states_after[0]["robots"]["franka2"]["dof_pos"]["panda_joint5"], + states_after[0]["robots"]["franka2"]["dof_pos"]["panda_joint6"], + states_after[0]["robots"]["franka2"]["dof_pos"]["panda_joint7"], + states_after[0]["robots"]["franka2"]["dof_pos"]["panda_finger_joint1"], + states_after[0]["robots"]["franka2"]["dof_pos"]["panda_finger_joint2"], + ] + franka1_dof_state_tensor = torch.tensor(franka1_dof_state_list) + franka2_dof_state_tensor = torch.tensor(franka2_dof_state_list) + concated_states = torch.cat([franka1_dof_state_tensor, franka2_dof_state_tensor], dim=0) + franka1_dof_target_list = [ + dof_targets[0]["franka1"]["dof_pos_target"]["panda_joint1"], + dof_targets[0]["franka1"]["dof_pos_target"]["panda_joint2"], + dof_targets[0]["franka1"]["dof_pos_target"]["panda_joint3"], + dof_targets[0]["franka1"]["dof_pos_target"]["panda_joint4"], + dof_targets[0]["franka1"]["dof_pos_target"]["panda_joint5"], + dof_targets[0]["franka1"]["dof_pos_target"]["panda_joint6"], + dof_targets[0]["franka1"]["dof_pos_target"]["panda_joint7"], + dof_targets[0]["franka1"]["dof_pos_target"]["panda_finger_joint1"], + dof_targets[0]["franka1"]["dof_pos_target"]["panda_finger_joint2"], + ] + franka2_dof_target_list = [ + dof_targets[0]["franka2"]["dof_pos_target"]["panda_joint1"], + dof_targets[0]["franka2"]["dof_pos_target"]["panda_joint2"], + dof_targets[0]["franka2"]["dof_pos_target"]["panda_joint3"], + dof_targets[0]["franka2"]["dof_pos_target"]["panda_joint4"], + dof_targets[0]["franka2"]["dof_pos_target"]["panda_joint5"], + dof_targets[0]["franka2"]["dof_pos_target"]["panda_joint6"], + dof_targets[0]["franka2"]["dof_pos_target"]["panda_joint7"], + dof_targets[0]["franka2"]["dof_pos_target"]["panda_finger_joint1"], + dof_targets[0]["franka2"]["dof_pos_target"]["panda_finger_joint2"], + ] + franka1_dof_target_tensor = torch.tensor(franka1_dof_target_list) + franka2_dof_target_tensor = torch.tensor(franka2_dof_target_list) + concated_targets = torch.cat([franka1_dof_target_tensor, franka2_dof_target_tensor], dim=0) + assert not torch.allclose(concated_states, concated_targets, atol=1e-3), ( + "franka1 and franka2 should be in collision, thus the states should not be close to the targets" + ) diff --git a/metasim/test/test_scenario_cfg/test_robot_cfg/test_default_pos.py b/metasim/test/test_scenario_cfg/test_robot_cfg/test_default_pos.py new file mode 100644 index 000000000..dbfc306f6 --- /dev/null +++ b/metasim/test/test_scenario_cfg/test_robot_cfg/test_default_pos.py @@ -0,0 +1,46 @@ +"""Integration tests for default joint position configuration.""" + +from __future__ import annotations + +import pytest +import rootutils +from loguru import logger as log + +rootutils.setup_root(__file__, pythonpath=True) + +from metasim.test.test_utils import assert_close + + +@pytest.mark.sim("isaacsim", "mujoco", "isaacgym", "mjx", "sapien2", "sapien3") +def test_default_pos(handler): + """Test that default robot positions are correctly applied.""" + handler.set_dof_targets( + [ + { + "franka": { + "dof_pos_target": { + "panda_joint1": 0.0, + "panda_joint2": -0.785398, + "panda_joint3": 0.0, + "panda_joint4": -2.356194, + "panda_joint5": 0.0, + "panda_joint6": 1.570796, + "panda_joint7": 0.785398, + "panda_finger_joint1": 0.04, + "panda_finger_joint2": 0.04, + } + } + } + ] + * handler.scenario.num_envs + ) + + # Check initial state matches the default positions from scenario + states_default = handler.get_states(mode="dict") + + default_pos = handler.scenario.robots[0].default_position + default_rot = handler.scenario.robots[0].default_orientation + assert_close(states_default[0]["robots"]["franka"]["pos"], default_pos, atol=1e-3, message="franka pos error") + assert_close(states_default[0]["robots"]["franka"]["rot"], default_rot, atol=1e-3, message="franka rot error") + + log.info(f"Default pos test passed for {handler.scenario.simulator}") diff --git a/metasim/test/test_scenario_cfg/test_robot_cfg/test_default_qpos.py b/metasim/test/test_scenario_cfg/test_robot_cfg/test_default_qpos.py index c279017e1..5d1a93ab8 100644 --- a/metasim/test/test_scenario_cfg/test_robot_cfg/test_default_qpos.py +++ b/metasim/test/test_scenario_cfg/test_robot_cfg/test_default_qpos.py @@ -11,7 +11,7 @@ from metasim.test.test_utils import assert_close -@pytest.mark.mujoco +@pytest.mark.sim("isaacsim", "mujoco", "isaacgym", "mjx", "sapien2", "sapien3") def test_default_qpos(handler): """Test that default joint positions are correctly applied.""" handler.set_dof_targets( diff --git a/metasim/test/test_scenario_cfg/test_robot_cfg/test_qpos_limit.py b/metasim/test/test_scenario_cfg/test_robot_cfg/test_qpos_limit.py index c71468c99..deb56cc6e 100644 --- a/metasim/test/test_scenario_cfg/test_robot_cfg/test_qpos_limit.py +++ b/metasim/test/test_scenario_cfg/test_robot_cfg/test_qpos_limit.py @@ -11,7 +11,7 @@ from roboverse_pack.robots.franka_cfg import FrankaCfg -@pytest.mark.sim("isaacsim", "mujoco", "isaacgym", "mjx") +@pytest.mark.sim("isaacsim", "mujoco", "isaacgym", "mjx", "sapien2", "sapien3") def test_qpos_limit(handler): """Test that joint limits are respected during simulation.""" handler.set_dof_targets( diff --git a/metasim/test/test_utils.py b/metasim/test/test_utils.py index 11efe389c..b8e370a61 100644 --- a/metasim/test/test_utils.py +++ b/metasim/test/test_utils.py @@ -7,11 +7,14 @@ def assert_close(a, b, atol=1e-3, message="Consistency Error"): if isinstance(a, torch.Tensor): + b = torch.tensor(b) assert torch.allclose(a, b, atol=atol), f"a: {a} != b: {b} " + message elif isinstance(a, float): + b = float(b) assert math.isclose(a, b, abs_tol=atol), f"a: {a} != b: {b} " + message else: raise ValueError(f"Unsupported type: {type(a)}") + return True def get_test_parameters(): From dd2a033b260803f9d938f575481ea9ef9a5ff476 Mon Sep 17 00:00:00 2001 From: Mingyuan Sun <64095400+myuansun@users.noreply.github.com> Date: Mon, 1 Dec 2025 12:30:09 +0800 Subject: [PATCH 02/50] [Feature] Add Terrain Configuration and Lidar Sensor (#696) * [feature] ground and lidar * [update] doc update and drop redundant modifications --- .../reinforcement_learning/unitree_rl.md | 195 ++++++- .../scenario/grounds.py | 93 ++-- metasim/scenario/scenario.py | 7 +- metasim/sim/isaacgym/isaacgym.py | 109 ++-- metasim/sim/isaacsim/isaacsim.py | 169 +++++- metasim/sim/mujoco/mujoco.py | 151 +++++- metasim/utils/setup_util.py | 38 ++ metasim/utils/terrain_utils.py | 506 ++++++++++++++++++ .../configs/callback_funcs/reset_funcs.py | 139 +++++ .../rl/unitree_rl/configs/cfg_queries.py | 290 ++++++---- .../configs/locomotion/walk_g1_dof12.py | 5 +- .../configs/locomotion/walk_g1_dof29.py | 10 +- .../rl/unitree_rl/helper/__init__.py | 1 - .../rl/unitree_rl/helper/terrain_generator.py | 178 ------ roboverse_learn/rl/unitree_rl/helper/utils.py | 8 +- roboverse_learn/rl/unitree_rl/main.py | 5 + roboverse_pack/grounds/__init__.py | 9 + roboverse_pack/grounds/gap_cfg.py | 27 + roboverse_pack/grounds/obstacle_cfg.py | 35 ++ roboverse_pack/grounds/pit_cfg.py | 27 + roboverse_pack/grounds/slope_cfg.py | 27 + roboverse_pack/grounds/stair_cfg.py | 28 + roboverse_pack/grounds/stone_cfg.py | 30 ++ roboverse_pack/robots/g1_cfg.py | 81 +-- 24 files changed, 1682 insertions(+), 486 deletions(-) rename roboverse_learn/rl/unitree_rl/configs/cfg_terrain.py => metasim/scenario/grounds.py (75%) create mode 100644 metasim/utils/terrain_utils.py delete mode 100644 roboverse_learn/rl/unitree_rl/helper/terrain_generator.py create mode 100644 roboverse_pack/grounds/__init__.py create mode 100644 roboverse_pack/grounds/gap_cfg.py create mode 100644 roboverse_pack/grounds/obstacle_cfg.py create mode 100644 roboverse_pack/grounds/pit_cfg.py create mode 100644 roboverse_pack/grounds/slope_cfg.py create mode 100644 roboverse_pack/grounds/stair_cfg.py create mode 100644 roboverse_pack/grounds/stone_cfg.py diff --git a/docs/source/roboverse_learn/reinforcement_learning/unitree_rl.md b/docs/source/roboverse_learn/reinforcement_learning/unitree_rl.md index 94c2b81a0..264ef068d 100644 --- a/docs/source/roboverse_learn/reinforcement_learning/unitree_rl.md +++ b/docs/source/roboverse_learn/reinforcement_learning/unitree_rl.md @@ -8,18 +8,76 @@ Train and deploy locomotion policies for Unitree robots across three stages: Well Supported robots: `g1_dof29` (full-body with without hands) and `g1_dof12` (lower-body). -## Environment setup +## Environment Setup -Install the RL library dependency (rsl_rl v3.1.1) from source: +### Core Dependencies + +#### For Python > 3.8 (IsaacSim/Mujoco) + +You can install rsl-rl-lib directly via pip: +```bash +pip install rsl-rl-lib ``` -git clone https://github.com/leggedrobotics/rsl_rl -cd rsl_rl -git checkout v3.1.1 + +#### For Python 3.8 (IsaacGym) +Due to compatibility requirements with IsaacGym's Python 3.8 environment, you'll need to install from source with modified dependencies: +```bash +# Clone the repository and checkout v3.1.0 +git clone https://github.com/leggedrobotics/rsl_rl && \ +cd rsl_rl && \ +git checkout v3.1.0 + +# Apply compatibility patches +sed -i 's/"torch>=2\.6\.0"/"torch>=2.4.1"/' pyproject.toml && \ +sed -i 's/"torchvision>=0\.5\.0"/"torchvision>=0.19.1"/' pyproject.toml && \ +sed -i 's/"tensordict>=0\.7\.0"/"tensordict>=0.5.0"/' pyproject.toml && \ +sed -i '/^# SPDX-License-Identifier: BSD-3-Clause$/a from __future__ import annotations' rsl_rl/algorithms/distillation.py + +# Install in editable mode pip install -e . ``` +### Optional Dependencies + +#### LiDAR Sensor Support (OmniPerception) + +The LiDAR implementation uses the [OmniPerception](https://github.com/aCodeDog/OmniPerception) package for GPU-accelerated ray tracing. + +**For IsaacGym and MuJoCo:** + +Install the LidarSensor package: +```bash +cd /path/to/OmniPerception/LidarSensor +pip install -e . +``` + +For complete IsaacGym/MuJoCo integration details, see the [OmniPerception IsaacGym example](https://github.com/aCodeDog/OmniPerception/tree/main/LidarSensor/LidarSensor/example/isaacgym). + +**For IsaacSim (IsaacLab):** + +First you need to install IsaacLab from source. Follow the official [IsaacLab installation guide](https://isaac-sim.github.io/IsaacLab/main/source/setup/installation/index.html). + +Then install the LiDAR sensor extension: + +```bash +cd /path/to/OmniPerception/LidarSensor/LidarSensor/example/isaaclab +./install_lidar_sensor.sh /path/to/your/IsaacLab +``` -## Training (IsaacGym) +For complete IsaacSim integration details, see the [OmniPerception IsaacLab example](https://github.com/aCodeDog/OmniPerception/tree/main/LidarSensor/LidarSensor/example/isaaclab/isaaclab). + +#### Real-World Deployment (unitree_sdk2_python) + +For real-world deployment, install the `unitree_sdk2_python` package: +```bash +cd third_party +git clone https://github.com/unitreerobotics/unitree_sdk2_python.git +cd unitree_sdk2_python +pip install -e . +``` + + +## Training General form: ``` @@ -39,6 +97,10 @@ python roboverse_learn/rl/unitree_rl/main.py --task walk_g1_dof29 --sim isaacsim ``` python roboverse_learn/rl/unitree_rl/main.py --task walk_g1_dof12 --sim isaacgym --num_envs 8192 --robot g1_dof12 ``` +- G1 humanoid walking with terrain (slope): +``` +python roboverse_learn/rl/unitree_rl/main.py --task walk_g1_dof29 --sim isaacgym --num_envs 8192 --robot g1_dof29 --ground slope_cfg +``` Outputs and checkpoints are saved to: ``` @@ -74,15 +136,7 @@ python roboverse_learn/rl/unitree_rl/main.py \ ``` the `--resume` and `--checkpoint` option can also be used during training for checkpoint resume. -## Real-World deployment - -First please install the `unitree_sdk2_python` package: -``` -cd third_party -git clone https://github.com/unitreerobotics/unitree_sdk2_python.git -cd unitree_sdk2_python -pip install -e . -``` +## Real-World Deployment Real-world deployment entry point: ``` @@ -95,13 +149,120 @@ python roboverse_learn/rl/unitree_rl/deploy/deploy_real.py eno1 g1_dof29_dex3.ya where you should modify the corresponding `yaml` file in `roboverse_learn/rl/unitree_rl/deploy/configs`, setting the `policy_path` to the exported jit policy. This will initialize the real controller and stream commands to the robot. Ensure your networking and safety interlocks are correctly configured. -## Command-line arguments +## Advanced Features + +### Terrain Configuration + +The framework now supports customizable terrain generation for training locomotion policies on varied ground conditions. Terrain can be configured via the `ground` parameter in your scenario configuration. + +#### Supported Terrain Types + +The `GroundCfg` class supports multiple terrain primitives: +- **Slope**: Planar inclined surfaces +- **Stair**: Staircase features +- **Obstacle**: Random rectangular obstacle fields +- **Stone**: Stone-like protrusions +- **Gap**: Gaps that robots must traverse +- **Pit**: Rectangular pits + +#### Terrain Parameters + +Key configuration parameters: +- `width`, `length`: Terrain dimensions in meters +- `horizontal_scale`, `vertical_scale`: Resolution and height scaling +- `margin`: Border margin around terrain +- `static_friction`, `dynamic_friction`, `restitution`: Physics material properties +- `elements`: Dictionary of terrain primitives to include +- `difficulty`: Difficulty progression settings + +#### Example Usage + +```python +from metasim.scenario.grounds import GroundCfg, SlopeCfg, StairCfg + +ground_cfg = GroundCfg( + width=20.0, + length=20.0, + horizontal_scale=0.1, + vertical_scale=0.005, + static_friction=1.0, + dynamic_friction=1.0, + elements={ + "slope": [SlopeCfg(origin=[0, 0], size=[2.0, 2.0], slope=0.3)], + "stair": [StairCfg(origin=[5, 0], size=[2.0, 2.0], step_height=0.1)] + } +) +``` + +Terrain configuration is supported across all simulators (IsaacGym, IsaacSim, MuJoCo). + +### LiDAR Sensor Support + +The framework now includes LiDAR point cloud sensing capabilities for enhanced perception-based locomotion policies. + +#### Overview + +The `LidarPointCloud` query provides 3D point cloud data from a simulated LiDAR sensor: +- Supports IsaacGym, IsaacSim, and MuJoCo simulators +- Returns point clouds in both local (sensor frame) and world frames +- Configurable sensor mounting location and type +- Raycasts against terrain, ground, and scenario objects + +#### Configuration + +Add LiDAR to your environment via the `callbacks_query` parameter: + +```python +from roboverse_learn.rl.unitree_rl.configs.cfg_queries import LidarPointCloud + +callbacks_query = { + "lidar_point_cloud": LidarPointCloud( + link_name="mid360_link", # Robot link to attach sensor + sensor_type="mid360", # Sensor type (e.g., mid360, mid70) + apply_optical_center_offset=True, + optical_center_offset_z=0.03503, + enabled=True + ) +} +``` + +#### Sensor Parameters + +- `link_name` (str): Name of the robot link where LiDAR is mounted (default: "mid360_link") +- `sensor_type` (str): Type of LiDAR sensor pattern (default: "mid360") +- `apply_optical_center_offset` (bool): Apply optical center offset correction +- `optical_center_offset_z` (float): Z-axis offset for optical center +- `enabled` (bool): Enable/disable LiDAR query + +#### Output Format + +The query returns a dictionary with: +- `points_local`: Point cloud in sensor frame (E, N, 3) +- `points_world`: Point cloud in world frame (E, N, 3) +- `dist`: Distance measurements (when available) +- `link`: Name of the link the sensor is attached to + +where E is the number of environments and N is the number of points per scan. + +#### Example Task Configuration + +See [walk_g1_dof29.py](roboverse_learn/rl/unitree_rl/configs/locomotion/walk_g1_dof29.py) for a complete example: + +```python +callbacks_query = { + "contact_forces": ContactForces(history_length=3), + "lidar_point_cloud": LidarPointCloud(enabled=True) +} +``` + +## Command-line Arguments The most relevant flags (see `helper/utils.py`): - `--task` (str): Task name. CamelCase or snake_case accepted. Examples: `walk_g1_dof29`, `walk_g1_dof12`. - `--robot` (str): Robot identifier. Common: `g1_dof29`, `g1_dof12`. - `--num_envs` (int): Number of parallel environments. - `--sim` (str): Simulator. Supported: `isaacgym` (training), `mujoco` (evaluation). +- `--ground` (str): Ground/terrain configuration to load. References predefined configurations in `roboverse_pack/grounds/`. Examples: `slope_cfg`, `stair_cfg`, `obstacle_cfg`, `stone_cfg`, `gap_cfg`, `pit_cfg`. If not specified, uses default flat ground. - `--run_name` (str): Required run tag for training logs/checkpoints. - `--learning_iterations` (int): Number of learning iterations (default 15000). - `--resume` (flag): Resume training from a checkpoint dir (datetime) in the specified run. @@ -111,4 +272,4 @@ The most relevant flags (see `helper/utils.py`): Notes: - Checkpoints: `outputs/unitree_rl///model_.pt` -- Exported JIT model (when used): `outputs/unitree_rl///exported/model_exported_jit.pt` +- Exported JIT model (when used): `outputs/unitree_rl///exported/model_exported_jit.pt` \ No newline at end of file diff --git a/roboverse_learn/rl/unitree_rl/configs/cfg_terrain.py b/metasim/scenario/grounds.py similarity index 75% rename from roboverse_learn/rl/unitree_rl/configs/cfg_terrain.py rename to metasim/scenario/grounds.py index 8a3e9c692..77a337d4e 100644 --- a/roboverse_learn/rl/unitree_rl/configs/cfg_terrain.py +++ b/metasim/scenario/grounds.py @@ -1,12 +1,48 @@ from __future__ import annotations -from typing import Literal -import yaml +from typing import Literal from metasim.utils import configclass + +@configclass +class GroundCfg: + """Global ground description used to assemble terrain tiles.""" + + width: float = 20.0 # m + length: float = 20.0 # m + horizontal_scale: float = 0.1 # m + vertical_scale: float = 0.1 # m + margin: float = 10 # m + max_mesh_triangles: int = 2000 # cap IsaacSim mesh complexity to avoid GPU OOM + elements: dict[str, list[BaseTerrainCfg]] = None + repeat_direction_gap: list[int, Literal["row", "column"], float] = (0, "row", 0.1) # (repeat, repeat_direction) + difficulty: list[float, float, Literal["linear"]] = [1.0, 4.0, "linear"] # (difficulty, type) + # For Isaacgym + static_friction = 1.0 + dynamic_friction = 1.0 + restitution = 1.0 + + def __post_init__(self): + self.num_rows: int = int(self.width / self.horizontal_scale) + self.margin_num_rows: int = int(self.margin / self.horizontal_scale) + self.num_cols: int = int(self.length / self.horizontal_scale) + self.margin_num_cols: int = int(self.margin / self.horizontal_scale) + if self.elements is None: + self.elements = { + "slope": [], + "stair": [], + "obstacle": [], + "stone": [], + "gap": [], + "pit": [], + } + + @configclass class BaseTerrainCfg: + """Base parameters shared by all terrain primitives.""" + type: str = "base" origin: list[float] = [0, 0] # [row, col] OR [width, length] size: list[float] = [1.0, 1.0] # [width, length] OR [row, col] @@ -15,6 +51,8 @@ class BaseTerrainCfg: @configclass class SlopeCfg(BaseTerrainCfg): + """Config for a planar slope feature.""" + type: str = "slope" origin: list[float] = [0, 0] size: list[float] = [1.0, 1.0] @@ -25,6 +63,8 @@ class SlopeCfg(BaseTerrainCfg): @configclass class StairCfg(BaseTerrainCfg): + """Config for staircase features.""" + type: str = "stair" origin: list[float] = [0, 0] size: list[float] = [1.0, 1.0] @@ -34,6 +74,8 @@ class StairCfg(BaseTerrainCfg): @configclass class ObstacleCfg(BaseTerrainCfg): + """Config for obstacle fields composed of random rectangles.""" + type: str = "obstacle" origin: list[float] = [0, 0] size: list[float] = [1.0, 1.0] @@ -44,6 +86,8 @@ class ObstacleCfg(BaseTerrainCfg): @configclass class StoneCfg(BaseTerrainCfg): + """Config for stone-like protrusions.""" + type: str = "stone" origin: list[float] = [0, 0] size: list[float] = [1.0, 1.0] @@ -54,6 +98,8 @@ class StoneCfg(BaseTerrainCfg): @configclass class GapCfg(BaseTerrainCfg): + """Config for gaps that robots must traverse.""" + type: str = "gap" origin: list[float] = [0, 0] size: list[float] = [1.0, 1.0] @@ -63,49 +109,10 @@ class GapCfg(BaseTerrainCfg): @configclass class PitCfg(BaseTerrainCfg): + """Config for rectangular pits.""" + type: str = "pit" position: list[float] = [0, 0] size: list[float] = [1.0, 1.0] depth: float = 1.0 platform_size: float = 1.0 - - -@configclass -class GroundCfg: - width: float = 20.0 # m - length: float = 20.0 # m - horizontal_scale: float = 0.1 # m - vertical_scale: float = 0.005 # m - margin: float = 10 # m - elements: dict[str, SlopeCfg | StairCfg | ObstacleCfg | StoneCfg | GapCfg | PitCfg] = None - repeat_direction_gap: list[int, Literal["row", "column"], float] = (0, "row", 0.1) # (repeat, repeat_direction) - difficulty: list[float, float, Literal["linear"]] = [1.0, 4.0, "linear"] # (difficulty, type) - # For Isaacgym - static_friction = 1.0 - dynamic_friction = 1.0 - restitution = 1.0 - - def __post_init__(self): - self.num_rows: int = int(self.width / self.horizontal_scale) - self.margin_num_rows: int = int(self.margin / self.horizontal_scale) - self.num_cols: int = int(self.length / self.horizontal_scale) - self.margin_num_cols: int = int(self.margin / self.horizontal_scale) - - @classmethod - def from_yaml(cls, yaml_file: str) -> GroundCfg: - with open(yaml_file) as f: - raw_data = yaml.safe_load(f)["terrain"] - elements = {t: [] for t in ["slope", "stair", "obstacle", "stone", "gap", "pit"]} - for elem in raw_data["elements"]: - t = elem["type"] - class_wrapper = globals().get(f"{t.capitalize()}Cfg") - if class_wrapper is None: - raise ValueError(f"Unknown terrain type: {t}") - elements[t].append(class_wrapper(**elem)) - - raw_data["elements"] = elements - return cls(**raw_data) - - -if __name__ == "__main__": - cfg = BaseTerrainCfg.from_yaml("terrain.yaml") diff --git a/metasim/scenario/scenario.py b/metasim/scenario/scenario.py index 3ae89b4ff..f92d5ff28 100644 --- a/metasim/scenario/scenario.py +++ b/metasim/scenario/scenario.py @@ -6,9 +6,10 @@ from metasim.utils.configclass import configclass from metasim.utils.hf_util import FileDownloader -from metasim.utils.setup_util import get_robot, get_scene +from metasim.utils.setup_util import get_ground, get_robot, get_scene from .cameras import BaseCameraCfg +from .grounds import GroundCfg from .lights import BaseLightCfg, DistantLightCfg from .objects import BaseObjCfg from .render import RenderCfg @@ -28,6 +29,7 @@ class ScenarioCfg: objects: list[BaseObjCfg] = [] cameras: list[BaseCameraCfg] = [] gs_scene: GSSceneCfg | None = None + ground: GroundCfg | None = None # runtime render: RenderCfg = RenderCfg() @@ -64,6 +66,9 @@ def __post_init__(self) -> None: if isinstance(self.scene, str): self.scene = get_scene(self.scene) + if isinstance(self.ground, str): + self.ground = get_ground(self.ground) + # FileDownloader(self).do_it() # download any external assets def check_assets(self): diff --git a/metasim/sim/isaacgym/isaacgym.py b/metasim/sim/isaacgym/isaacgym.py index 6e0ee3f61..b3ea14bc8 100644 --- a/metasim/sim/isaacgym/isaacgym.py +++ b/metasim/sim/isaacgym/isaacgym.py @@ -35,8 +35,8 @@ from metasim.scenario.scenario import ScenarioCfg from metasim.sim import BaseSimHandler from metasim.types import Action, DictEnvState -from metasim.utils.dict import class_to_dict from metasim.utils.state import CameraState, ObjectState, RobotState, TensorState +from metasim.utils.terrain_utils import TerrainGenerator class IsaacgymHandler(BaseSimHandler): @@ -326,9 +326,13 @@ def _set_up_camera(self) -> None: self.gym.set_camera_location(camera_handle, self._envs[i_env], camera_eye, camera_lookat) if cam_cfg.mount_to is not None: if isinstance(cam_cfg.mount_link, str): - mount_handle = self._robot_link_dict[cam_cfg.mount_link] + mount_handle = self._robot_link_dict[ + cam_cfg.mount_link.split("/")[-1] + ] # isaacgym requires the leaf prim elif isinstance(cam_cfg.mount_link, tuple): - mount_handle = self._robot_link_dict[cam_cfg.mount_link[1]] + mount_handle = self._robot_link_dict[ + cam_cfg.mount_link[1].split("/")[-1] + ] # isaacgym requires the leaf prim camera_pose = gymapi.Transform( gymapi.Vec3(*cam_cfg.mount_pos), gymapi.Quat(*cam_cfg.mount_quat[1:], cam_cfg.mount_quat[0]) ) @@ -470,8 +474,6 @@ def _load_robot_assets(self) -> None: # FIXME: hard code for 0-1 action space, should remove all the scale stuff later robot_dof_props["driveMode"][i] = gymapi.DOF_MODE_EFFORT - # robot_dof_props["stiffness"][i] = i_actuator_cfg.stiffness - # robot_dof_props["damping"][i] = i_actuator_cfg.damping robot_dof_props["stiffness"][i] = 0.0 robot_dof_props["damping"][i] = 0.0 robot_dof_props["armature"][i] = getattr(self.robots[0], "armature", 0.01) @@ -524,9 +526,7 @@ def _make_envs( ) # x, y, z, w order for gymapi.Quat # add ground plane - plane_params = gymapi.PlaneParams() - plane_params.normal = gymapi.Vec3(0, 0, 1) - self.gym.add_ground(self.sim, plane_params) + self._add_ground() # get object and robot asset obj_assets_list = [self._load_object_asset(obj) for obj in self.objects] @@ -685,12 +685,6 @@ def _make_envs( self._env_rigid_body_global_indices[-1]["robot"] = robot_rigid_body_indices - # domain randomization for robots - # FIXME: add domain randomization with new API - # self.rand_rigid_body_fric(self.scenario.random.friction, i, robot_rigid_shape_props_asset) - # robot_body_props = self.gym.get_actor_rigid_body_properties(env, robot_handle) - # self.rand_rigid_body_mass(self.scenario.random.mass, i, robot_body_props) - # GET initial state, copy for reset later self._initial_state = np.copy(self.gym.get_sim_rigid_body_states(self.sim, gymapi.STATE_ALL)) @@ -1165,34 +1159,65 @@ def _get_body_ids_reindex(self, obj_name: str) -> list[int]: def _get_joint_ids_reindex(self, obj_name: str) -> list[int]: return [self._joint_info[obj_name]["global_indices"][jn] for jn in self._get_joint_names(obj_name)] - def rand_rigid_body_fric(self, cfg, env_id: int, props: list[gymapi.RigidShapeProperties]): - """Randomize the friction of the rigid bodies.""" - if not cfg.enabled: - return - if not hasattr(self, "_rand_fric_dist"): - params_dict = class_to_dict(cfg) - params_dict["num_envs"] = self.num_envs - params_dict["device"] = self.device - dist_fn = cfg.dist_fn - self._rand_fric_dist = dist_fn(params_dict) - # TODO: add rigid body id index - for s in range(len(props)): - props[s].friction = self._rand_fric_dist[env_id] - return props - - def rand_rigid_body_mass(self, cfg, env_id: int, props: list[gymapi.RigidBodyProperties]): - """Randomize the base mass.""" - if not cfg.enabled: - return - if not hasattr(self, "_rand_mass_dist"): - params_dict = class_to_dict(cfg) - params_dict["num_envs"] = self.num_envs - params_dict["device"] = self.device - dist_fn = cfg.dist_fn - self._rand_mass_dist = dist_fn(params_dict) - # TODO: add rigid body id index - props[0].mass += self._rand_mass_dist[env_id] - return props + def _add_ground(self): + if self.scenario.ground is not None: + tg = TerrainGenerator(self.scenario.ground) + vertices, triangles, height_mat = tg.generate_terrain(self.scenario.ground, type="both") + tm_params = gymapi.TriangleMeshParams() + tm_params.nb_vertices = vertices.shape[0] + tm_params.nb_triangles = triangles.shape[0] + + # Center the terrain at the origin + half_width = (vertices[:, 0].max() - vertices[:, 0].min()) / 2.0 + half_height = (vertices[:, 1].max() - vertices[:, 1].min()) / 2.0 + tm_params.transform.p.x = -half_width + tm_params.transform.p.y = -half_height + tm_params.transform.p.z = 0.0 + tm_params.static_friction = getattr(self.scenario.ground, "static_friction", 1.0) + tm_params.dynamic_friction = getattr(self.scenario.ground, "dynamic_friction", 1.0) + tm_params.restitution = getattr(self.scenario.ground, "restitution", 0.0) + self.gym.add_triangle_mesh( + self.sim, vertices.flatten(order="C"), triangles.flatten(order="C"), tm_params + ) # add terrain to sim + self._ground_mesh_vertices = vertices + self._ground_mesh_triangles = triangles + self._height_mat = height_mat + else: + plane_params = gymapi.PlaneParams() + plane_params.normal = gymapi.Vec3(0, 0, 1) + plane_params.static_friction = 1.0 + plane_params.dynamic_friction = 1.0 + plane_params.restitution = 0.0 + self.gym.add_ground(self.sim, plane_params) + + # Generate a flat grid mesh for Warp registration based on env grid layout. + step = float(self.scenario.env_spacing) + num_per_row = math.sqrt(self.num_envs) if self.num_envs > 0 else 1 + num_rows = math.ceil(self.num_envs / max(num_per_row, 1)) if self.num_envs > 0 else 1 + width = max(1, num_per_row) * step + height = max(1, num_rows) * step + border_offset = 20.0 # extend the ground a bit + hw, hh = width * 0.5 + border_offset, height * 0.5 + border_offset + + # 4 corner vertices (x, y, z=0) + self._ground_mesh_vertices = np.array( + [ + [-hw, -hh, 0.0], # 0 + [hw, -hh, 0.0], # 1 + [-hw, hh, 0.0], # 2 + [hw, hh, 0.0], # 3 + ], + dtype=np.float32, + ) + + # two triangles covering the quad (CCW winding, normal +Z) + self._ground_mesh_triangles = np.array( + [ + [0, 2, 1], + [1, 2, 3], + ], + dtype=np.int32, + ) @property def num_envs(self) -> int: diff --git a/metasim/sim/isaacsim/isaacsim.py b/metasim/sim/isaacsim/isaacsim.py index ca61078ed..6b8a5a683 100644 --- a/metasim/sim/isaacsim/isaacsim.py +++ b/metasim/sim/isaacsim/isaacsim.py @@ -3,12 +3,14 @@ from __future__ import annotations import argparse +import math import os from copy import deepcopy import numpy as np import torch from loguru import logger as log +from scipy.interpolate import RegularGridInterpolator from metasim.queries.base import BaseQueryType from metasim.scenario.cameras import PinholeCameraCfg @@ -29,6 +31,7 @@ from metasim.types import DictEnvState from metasim.utils.dict import deep_get from metasim.utils.state import CameraState, ObjectState, RobotState, TensorState +from metasim.utils.terrain_utils import TerrainGenerator # Optional: RoboSplatter imports for GS background rendering try: @@ -198,8 +201,8 @@ def launch(self, simulation_app=None, simulation_args=None) -> None: # Initialize GS background if enabled self._build_gs_background() - - return super().launch() + super().launch() + self.sim.reset() # crucial for calling _initialize_callbacks in binded sensors def close(self) -> None: log.info("close Isaacsim Handler") @@ -486,7 +489,6 @@ def _on_keyboard_event(self, event, *args, **kwargs): self.sim.set_render_mode(SimulationContext.RenderMode.FULL_RENDERING) def set_dof_targets(self, actions: torch.Tensor) -> None: - # TODO: support set torque if isinstance(actions, torch.Tensor): actions_tensor = actions else: @@ -497,12 +499,15 @@ def set_dof_targets(self, actions: torch.Tensor) -> None: for env_id in range(self.num_envs): joint_targets = actions[env_id][robot.name]["dof_pos_target"] for j, joint_name in enumerate(sorted_joint_names): - robot_tensor[env_id, j] = torch.tensor(joint_targets[joint_name], device=self.device) + robot_tensor[env_id, j] = torch.tensor( + joint_targets[joint_name], + device=self.device, + ) per_robot_tensors.append(robot_tensor) actions_tensor = torch.cat(per_robot_tensors, dim=-1) offset = 0 - for robot in self.robots: + for i, robot in enumerate(self.robots): robot_inst = self.scene.articulations[robot.name] sorted_joint_names = self.get_joint_names(robot.name, sort=True) joint_count = len(sorted_joint_names) @@ -526,7 +531,14 @@ def set_dof_targets(self, actions: torch.Tensor) -> None: continue joint_targets = robot_actions_sorted[:, action_indices] - robot_inst.set_joint_position_target(joint_targets, joint_ids=joint_ids) + + if self._manual_pd_on[i]: + # torque / effort control + robot_inst.set_joint_effort_target(joint_targets, joint_ids=joint_ids) + else: + # position control + robot_inst.set_joint_position_target(joint_targets, joint_ids=joint_ids) + robot_inst.write_data_to_sim() def _simulate(self): @@ -791,6 +803,11 @@ def _load_terrain(self) -> None: }, ) + ground_cfg = getattr(self.scenario, "ground", None) + static_friction = getattr(ground_cfg, "static_friction", 1.0) if ground_cfg is not None else 1.0 + dynamic_friction = getattr(ground_cfg, "dynamic_friction", 1.0) if ground_cfg is not None else 1.0 + restitution = getattr(ground_cfg, "restitution", 0.0) if ground_cfg is not None else 0.0 + terrain_config = TerrainImporterCfg( prim_path="/World/ground", terrain_type="generator", @@ -799,9 +816,9 @@ def _load_terrain(self) -> None: physics_material=sim_utils.RigidBodyMaterialCfg( friction_combine_mode="multiply", restitution_combine_mode="multiply", - static_friction=1.0, - dynamic_friction=1.0, - restitution=0.0, + static_friction=static_friction, + dynamic_friction=dynamic_friction, + restitution=restitution, ), debug_vis=False, visual_material=sim_utils.MdlFileCfg( @@ -816,6 +833,140 @@ def _load_terrain(self) -> None: self.terrain = terrain_config.class_type(terrain_config) self.terrain.env_origins = self.terrain.terrain_origins + if ground_cfg is not None: + self._build_custom_terrain_mesh(ground_cfg) + + def _build_custom_terrain_mesh(self, ground_cfg) -> None: + """Procedurally author a USD mesh using TerrainGenerator.""" + tg = TerrainGenerator(ground_cfg) + stage_vertices, stage_triangles, height_mat = tg.generate_terrain(ground_cfg, type="both") + + max_triangles = getattr(ground_cfg, "max_mesh_triangles", 20000) + raw_heights = (height_mat / tg.vertical_scale).astype(np.float32) + ds_heights, scale_x, scale_y = self._downsample_height_field(raw_heights, tg.horizontal_scale, max_triangles) + if not math.isclose(scale_x, scale_y, rel_tol=1e-4): + stage_vertices = stage_vertices.copy() + stage_vertices[:, 1] *= scale_y / max(scale_x, 1e-6) + + stage_height_mat = ds_heights * tg.vertical_scale + if stage_triangles.shape[0] != stage_triangles.shape[0]: + log.info( + "Downsampled IsaacSim terrain mesh from %d to %d triangles (limit=%d).", + len(stage_triangles), + len(stage_triangles), + max_triangles, + ) + + self._ground_mesh_vertices = stage_vertices + self._ground_mesh_triangles = stage_triangles.astype(np.int32) + self._height_mat = stage_height_mat + + # Center the terrain at the origin + terrain_vertices = self._ground_mesh_vertices.copy() + half_width = (terrain_vertices[:, 0].max() - terrain_vertices[:, 0].min()) / 2.0 + half_height = (terrain_vertices[:, 1].max() - terrain_vertices[:, 1].min()) / 2.0 + terrain_vertices[:, 0] -= half_width + terrain_vertices[:, 1] -= half_height + + from pxr import Gf, PhysxSchema, UsdGeom, UsdPhysics, UsdShade + + try: + import omni.isaac.core.utils.prims as prim_utils + except ModuleNotFoundError: + import isaacsim.core.utils.prims as prim_utils + + stage = prim_utils.get_current_stage() + if stage is None: + log.error("IsaacSim stage is not available; cannot create terrain mesh.") + return + + ground_root_path = "/World/ground" + ground_root = stage.GetPrimAtPath(ground_root_path) + if not ground_root or not ground_root.IsValid(): + ground_root = stage.DefinePrim(ground_root_path, "Xform") + else: + for child in list(ground_root.GetChildren()): + stage.RemovePrim(child.GetPath()) + + mesh_path = f"{ground_root_path}/generated_mesh" + mesh = UsdGeom.Mesh.Define(stage, mesh_path) + mesh.CreateSubdivisionSchemeAttr().Set("none") + mesh.CreateDoubleSidedAttr(True) + mesh.CreatePointsAttr([Gf.Vec3f(float(x), float(y), float(z)) for x, y, z in terrain_vertices]) + mesh.CreateFaceVertexCountsAttr([3] * len(self._ground_mesh_triangles)) + mesh.CreateFaceVertexIndicesAttr(self._ground_mesh_triangles.flatten().tolist()) + mesh.CreateExtentAttr(self._compute_mesh_extent(terrain_vertices)) + mesh.CreateDisplayColorAttr([Gf.Vec3f(0.6, 0.6, 0.6)]) + + # Enable physics collisions on the generated mesh + collision_api = UsdPhysics.CollisionAPI.Apply(mesh.GetPrim()) + collision_api.CreateCollisionEnabledAttr(True) + UsdPhysics.MeshCollisionAPI.Apply(mesh.GetPrim()) + rigid_api = UsdPhysics.RigidBodyAPI.Apply(mesh.GetPrim()) + rigid_api.CreateRigidBodyEnabledAttr(False) + + physx_collision_api = PhysxSchema.PhysxCollisionAPI.Apply(mesh.GetPrim()) + physx_collision_api.CreateRestOffsetAttr(0.0) + physx_collision_api.CreateContactOffsetAttr(0.02) + PhysxSchema.PhysxTriangleMeshCollisionAPI.Apply(mesh.GetPrim()) + + static_friction = getattr(ground_cfg, "static_friction", 1.0) + dynamic_friction = getattr(ground_cfg, "dynamic_friction", 1.0) + restitution = getattr(ground_cfg, "restitution", 0.0) + + material_path = "/World/Materials/TerrainMaterial" + usd_material = UsdShade.Material.Define(stage, material_path) + material_api = UsdPhysics.MaterialAPI.Apply(usd_material.GetPrim()) + material_api.CreateStaticFrictionAttr(static_friction) + material_api.CreateDynamicFrictionAttr(dynamic_friction) + material_api.CreateRestitutionAttr(restitution) + mat_binding = UsdShade.MaterialBindingAPI(mesh.GetPrim()) + mat_binding.Bind(usd_material, materialPurpose="physics") + + self._ground_mesh_vertices = terrain_vertices + self._terrain_margin = tg.margin + + log.info( + "Generated IsaacSim terrain mesh with %d vertices and %d triangles.", + len(self._ground_mesh_vertices), + len(self._ground_mesh_triangles), + ) + + def _downsample_height_field( + self, height_field_raw: np.ndarray, horizontal_scale: float, max_triangles: int + ) -> tuple[np.ndarray, float, float]: + """Reduce height-field resolution to keep triangle count manageable for PhysX GPU buffers.""" + rows, cols = height_field_raw.shape + total_triangles = 2 * max(rows - 1, 0) * max(cols - 1, 0) + if max_triangles is None or max_triangles <= 0 or total_triangles <= max_triangles or total_triangles == 0: + return height_field_raw, horizontal_scale, horizontal_scale + + reduction = math.sqrt(total_triangles / max_triangles) + new_rows = max(2, math.floor((rows - 1) / reduction) + 1) + new_cols = max(2, math.floor((cols - 1) / reduction) + 1) + + src_x = np.linspace(0.0, rows - 1, rows, dtype=np.float32) + src_y = np.linspace(0.0, cols - 1, cols, dtype=np.float32) + interpolator = RegularGridInterpolator((src_x, src_y), height_field_raw, bounds_error=False, fill_value=None) + dst_x = np.linspace(0.0, rows - 1, new_rows, dtype=np.float32) + dst_y = np.linspace(0.0, cols - 1, new_cols, dtype=np.float32) + grid = np.stack(np.meshgrid(dst_x, dst_y, indexing="ij"), axis=-1) + downsampled = interpolator(grid) + + total_width_x = (rows - 1) * horizontal_scale + total_width_y = (cols - 1) * horizontal_scale + scale_x = total_width_x / max(new_rows - 1, 1) + scale_y = total_width_y / max(new_cols - 1, 1) + + return downsampled.astype(np.float32), scale_x, scale_y + + @staticmethod + def _compute_mesh_extent(vertices: np.ndarray): + from pxr import Gf + + min_corner = vertices.min(axis=0) + max_corner = vertices.max(axis=0) + return [Gf.Vec3f(*min_corner.tolist()), Gf.Vec3f(*max_corner.tolist())] def _load_scene(self) -> None: """Load scene from SceneCfg configuration. diff --git a/metasim/sim/mujoco/mujoco.py b/metasim/sim/mujoco/mujoco.py index 647f6349c..cebba7b7e 100644 --- a/metasim/sim/mujoco/mujoco.py +++ b/metasim/sim/mujoco/mujoco.py @@ -54,6 +54,7 @@ def _safe_glctx_del(self): from metasim.sim import BaseSimHandler from metasim.types import Action from metasim.utils.state import CameraState, ObjectState, RobotState, TensorState, state_tensor_to_nested +from metasim.utils.terrain_utils import TerrainGenerator try: import mujoco.viewer @@ -173,6 +174,17 @@ def launch(self) -> None: self._mj_model = mujoco.MjModel.from_xml_path(xml_path) self._mj_data = mujoco.MjData(self._mj_model) + # load the ground + if hasattr(self, "_height_mat"): + if self._height_mat is not None: + # normalize height field to [0, 1] range for mujoco hfield + height_mat = self._height_mat + z_min = float(height_mat.min()) + z_max = float(height_mat.max()) + z_span = max(z_max - z_min, 1e-6) + normalized_height_mat = (height_mat - z_min) / z_span + self.physics.model.hfield_data[:] = normalized_height_mat.flatten(order="C") + # Create a default-sized renderer (camera sizes can be applied on demand) self.renderer = mujoco.Renderer(self._mj_model, width=640, height=480) @@ -312,26 +324,36 @@ def _create_primitive_xml(self, obj): def _init_mujoco(self) -> mjcf.RootElement: """Initialize MuJoCo model with optional scene support.""" - if self.scenario.scene is not None: - mjcf_model = mjcf.from_path(self.scenario.scene.mjcf_path) - log.info(f"Loaded scene from: {self.scenario.scene.mjcf_path}") - else: - mjcf_model = mjcf.RootElement() - self._add_default_ground(mjcf_model) + mjcf_model = self._init_scene() if self.scenario.sim_params.dt is not None: mjcf_model.option.timestep = self.scenario.sim_params.dt else: mjcf_model.option.timestep = 0.001 - - self._add_cameras_to_model(mjcf_model) + self._add_ground(mjcf_model) self._add_objects_to_model(mjcf_model) self._add_robots_to_model(mjcf_model) + self._add_cameras_to_model(mjcf_model) if self.scenario.sim_params.dt is not None: mjcf_model.option.timestep = self.scenario.sim_params.dt return mjcf_model + def _init_scene(self) -> mjcf.RootElement: + """Initialize scene elements.""" + if self.scenario.scene is not None: + mjcf_model = mjcf.from_path(self.scenario.scene.mjcf_path) + log.info(f"Loaded scene from: {self.scenario.scene.mjcf_path}") + else: + mjcf_model = mjcf.RootElement() + return mjcf_model + + def _add_ground(self, mjcf_model: mjcf.RootElement) -> None: + if self.scenario.ground is not None: + self._add_custom_ground(mjcf_model) + else: + self._add_default_ground(mjcf_model) + def _apply_default_joint_positions(self) -> None: """Set initial joint positions from robot/object configs if provided.""" @@ -383,12 +405,66 @@ def _add_default_ground(self, mjcf_model: mjcf.RootElement) -> None: material="matplane", ) + # Expose a simple quad mesh centered at origin, similar to IsaacGym handler. + step = float(self.scenario.env_spacing) + num_per_row = 1 # MujocoHandler supports single env + num_rows = 1 + width = max(1, num_per_row) * step + height = max(1, num_rows) * step + border_offset = 20.0 + hw, hh = width * 0.5 + border_offset, height * 0.5 + border_offset + + self._ground_mesh_vertices = np.array( + [ + [-hw, -hh, 0.0], + [hw, -hh, 0.0], + [-hw, hh, 0.0], + [hw, hh, 0.0], + ], + dtype=np.float32, + ) + self._ground_mesh_triangles = np.array( + [ + [0, 2, 1], + [1, 2, 3], + ], + dtype=np.int32, + ) + def _add_cameras_to_model(self, mjcf_model: mjcf.RootElement) -> None: - """Add cameras to the model.""" + """Add cameras to the model. If mount_to is set, attach camera under that body so it follows motion.""" camera_max_width = 640 camera_max_height = 480 for camera in self.cameras: + camera_name = f"{camera.name}_custom" + camera_max_width = max(camera_max_width, camera.width) + camera_max_height = max(camera_max_height, camera.height) + + mount_to = camera.mount_to + if mount_to is not None: + mount_link = camera.mount_link.split("/")[-1] # mujoco requires the leaf prim + + model_prefix = self.mj_objects[mount_to].model + full_body_name = f"{model_prefix}/{mount_link}" + + body_elem = mjcf_model.find("body", full_body_name) + + # local mount pos/quat + mpos = camera.mount_pos + mquat = camera.mount_quat + + body_elem.add( + "camera", + name=camera_name, + mode="fixed", + pos=f"{mpos[0]} {mpos[1]} {mpos[2]}", + quat=f"{mquat[0]} {mquat[1]} {mquat[2]} {mquat[3]}", + fovy=camera.vertical_fov, + ) + continue + + # attach camera to world direction = np.array([ camera.look_at[0] - camera.pos[0], camera.look_at[1] - camera.pos[1], @@ -406,9 +482,7 @@ def _add_cameras_to_model(self, mjcf_model: mjcf.RootElement) -> None: "fovy": camera.vertical_fov, "xyaxes": f"{right[0]} {right[1]} {right[2]} {up[0]} {up[1]} {up[2]}", } - mjcf_model.worldbody.add("camera", name=f"{camera.name}_custom", **camera_params) - camera_max_width = max(camera_max_width, camera.width) - camera_max_height = max(camera_max_height, camera.height) + mjcf_model.worldbody.add("camera", name=camera_name, **camera_params) if camera_max_width > 640 or camera_max_height > 480: self._set_framebuffer_size(mjcf_model, camera_max_width, camera_max_height) @@ -603,7 +677,10 @@ def _get_states(self, env_ids: list[int] | None = None) -> list[dict]: camera_states = {} for camera in self.cameras: - camera_id = f"{camera.name}_custom" # XXX: hard code camera id for now + if camera.mount_to is not None: + camera_id = f"{self.mj_objects[camera.mount_to].model}/{camera.name}_custom" + else: + camera_id = f"{camera.name}_custom" depth = None @@ -1021,6 +1098,54 @@ def _get_body_ids_reindex(self, obj_name: str) -> list[int]: return self._body_ids_reindex_cache[obj_name] + def _add_custom_ground(self, mjcf_model): + tg = TerrainGenerator(self.scenario.ground) + static_friction = getattr(self.scenario.ground, "static_friction", 1.0) + dynamic_friction = getattr(self.scenario.ground, "dynamic_friction", 1.0) + restitution = getattr(self.scenario.ground, "restitution", 0.0) + + vertices, triangles, height_mat = tg.generate_terrain(self.scenario.ground, type="both") + # Also create a triangular mesh representation for queries (e.g., LiDAR warp raycasts), + # consistent with IsaacGym handler's ground mesh exposure. + # Store mesh for external consumers (e.g., LidarPointCloud) + self._ground_mesh_vertices = vertices + self._ground_mesh_triangles = triangles.astype(np.int32) + z_min = float(height_mat.min()) + z_max = float(height_mat.max()) + z_span = max(z_max - z_min, 1e-6) + hfield_name = "terrain" + + self._terrain_hfield_name = hfield_name + + ## Position hfield centered at origin + half_width = (vertices[:, 0].max() - vertices[:, 0].min()) / 2.0 + half_height = (vertices[:, 1].max() - vertices[:, 1].min()) / 2.0 + mjcf_model.asset.add( + "hfield", + name=hfield_name, + nrow=height_mat.shape[0], + ncol=height_mat.shape[1], + size=[ + half_height, + half_width, + z_span, + 0.001, + ], + ) + mjcf_model.worldbody.add( + "geom", + name="terrain_geom", + type="hfield", + hfield=hfield_name, + pos=[0.0, 0.0, z_min], + rgba="0.8 0.8 0.8 1", + friction=[static_friction, dynamic_friction, 0.001], + solimp=[0.9, 0.95, 0.001, 0.5, 2.0], + contype="1", + conaffinity="15", + ) + self._height_mat = height_mat + ############################################################ ## Misc ############################################################ diff --git a/metasim/utils/setup_util.py b/metasim/utils/setup_util.py index 986149e97..08df7a282 100644 --- a/metasim/utils/setup_util.py +++ b/metasim/utils/setup_util.py @@ -9,6 +9,7 @@ from loguru import logger as log from metasim.constants import SimType +from metasim.scenario.grounds import GroundCfg from metasim.scenario.robot import RobotCfg from metasim.scenario.scene import SceneCfg from metasim.sim.parallel import ParallelSimWrapper @@ -243,3 +244,40 @@ def get_scene(scene_name: str) -> SceneCfg: continue raise ValueError(f"Scene config class '{attr_name}' not found in {candidate_packages}. Errors: {errors}") + + +def get_ground(ground_name: str) -> GroundCfg: + """Resolve a ground configuration by name.""" + if is_snake_case(ground_name): + GroundName = to_camel_case(ground_name) + elif is_camel_case(ground_name): + GroundName = ground_name + else: + raise ValueError(f"Invalid ground name: {ground_name}") + + candidate_packages = [ + "roboverse_pack.grounds", + ] + + cwd = os.getcwd() + if cwd not in sys.path: + sys.path.insert(0, cwd) + + for fname in os.listdir(cwd): + if fname.endswith(".py") and not fname.startswith("_"): + candidate_packages.append(os.path.splitext(fname)[0]) + + attr_name = f"{GroundName}Cfg" + errors: list[str] = [] + + for pkg_name in candidate_packages: + try: + pkg = importlib.import_module(pkg_name) + ground_cls = getattr(pkg, attr_name) + return ground_cls() + except AttributeError: + continue + except Exception as e: + errors.append(f"{pkg_name}: {e}") + + raise ValueError(f"Ground config class '{attr_name}' not found in {candidate_packages}. Errors: {errors}") diff --git a/metasim/utils/terrain_utils.py b/metasim/utils/terrain_utils.py new file mode 100644 index 000000000..14e52087a --- /dev/null +++ b/metasim/utils/terrain_utils.py @@ -0,0 +1,506 @@ +from __future__ import annotations + +import math +from dataclasses import dataclass, field +from typing import Sequence + +import numpy as np +from scipy.interpolate import RegularGridInterpolator + + +def _discretize(value: float, scale: float) -> int: + """Project a metric value into height-field units.""" + return int(value / scale) + + +def _discretize_many(values: Sequence[float], scale: float) -> tuple[int, ...]: + return tuple(_discretize(v, scale) for v in values) + + +def _downsample_shape(terrain: SubTerrain, spacing: float) -> tuple[int, int]: + width = max(1, int(terrain.width * terrain.horizontal_scale / spacing)) + length = max(1, int(terrain.length * terrain.horizontal_scale / spacing)) + return width, length + + +def _center_slice(size: int, span: int) -> slice: + span = min(span, size) + offset = max((size - span) // 2, 0) + return slice(offset, offset + span) + + +def random_uniform_terrain( + terrain, + min_height, + max_height, + step=1, + downsampled_scale=None, +): + """Fill the terrain with uniformly sampled noise and bilinear upsampling.""" + downsampled_scale = downsampled_scale or terrain.horizontal_scale + min_h, max_h, step = _discretize_many((min_height, max_height, step), terrain.vertical_scale) + heights = np.arange(min_h, max_h + step, step, dtype=np.int32) + + coarse_shape = _downsample_shape(terrain, downsampled_scale) + coarse_height = np.random.choice(heights, size=coarse_shape) + + x_coarse = np.linspace(0.0, terrain.width * terrain.horizontal_scale, coarse_shape[0]) + y_coarse = np.linspace(0.0, terrain.length * terrain.horizontal_scale, coarse_shape[1]) + interpolator = RegularGridInterpolator((x_coarse, y_coarse), coarse_height, method="linear") + + x_fine = np.linspace(0.0, terrain.width * terrain.horizontal_scale, terrain.width) + y_fine = np.linspace(0.0, terrain.length * terrain.horizontal_scale, terrain.length) + grid_x, grid_y = np.meshgrid(x_fine, y_fine, indexing="ij") + samples = np.column_stack((grid_x.ravel(), grid_y.ravel())) + upsampled = np.rint(interpolator(samples)).reshape(terrain.width, terrain.length) + + terrain.height_field_raw += upsampled.astype(terrain.height_field_raw.dtype) + return terrain + + +def sloped_terrain(terrain, slope=1): + """Apply a constant slope along the x-axis.""" + gradient = np.arange(terrain.width, dtype=np.float32).reshape(terrain.width, 1) + max_height = slope * (terrain.horizontal_scale / terrain.vertical_scale) * terrain.width + scaled = (gradient * max_height / terrain.width).astype(terrain.height_field_raw.dtype) + terrain.height_field_raw += scaled + return terrain + + +def pyramid_sloped_terrain(terrain, slope=1, platform_size=1.0): + """Create a pyramid-like profile with an optional plateau.""" + center_x = max(terrain.width // 2, 1) + center_y = max(terrain.length // 2, 1) + x_profile = 1.0 - np.abs(np.arange(terrain.width) - center_x) / center_x + y_profile = 1.0 - np.abs(np.arange(terrain.length) - center_y) / center_y + x_profile = np.clip(x_profile, 0.0, 1.0).reshape(terrain.width, 1) + y_profile = np.clip(y_profile, 0.0, 1.0).reshape(1, terrain.length) + + max_height = slope * (terrain.horizontal_scale / terrain.vertical_scale) * (terrain.width / 2) + terrain.height_field_raw += (max_height * x_profile * y_profile).astype(terrain.height_field_raw.dtype) + + platform_radius = _discretize(platform_size / 2.0, terrain.horizontal_scale) + x1 = terrain.width // 2 - platform_radius + x2 = terrain.width // 2 + platform_radius + y1 = terrain.length // 2 - platform_radius + y2 = terrain.length // 2 + platform_radius + min_h = min(terrain.height_field_raw[x1, y1], 0) + max_h = max(terrain.height_field_raw[x1, y1], 0) + terrain.height_field_raw = np.clip(terrain.height_field_raw, min_h, max_h) + return terrain + + +def discrete_obstacles_terrain(terrain, max_height, min_size, max_size, num_rects, platform_size=1.0): + """Add randomly sized rectangular blocks or pits across the terrain.""" + max_height = _discretize(max_height, terrain.vertical_scale) + min_size, max_size, platform_cells = _discretize_many((min_size, max_size, platform_size), terrain.horizontal_scale) + + height_range = np.array([-max_height, -max_height // 2, max_height // 2, max_height], dtype=np.int32) + width_options = np.arange(min_size, max_size, 4, dtype=int) + length_options = np.arange(min_size, max_size, 4, dtype=int) + + i_max, j_max = terrain.height_field_raw.shape + for _ in range(num_rects): + if len(width_options) == 0 or len(length_options) == 0: + break + width = int(np.random.choice(width_options)) + length = int(np.random.choice(length_options)) + if width >= i_max or length >= j_max: + continue + valid_i = np.arange(0, i_max - width, 4, dtype=int) + valid_j = np.arange(0, j_max - length, 4, dtype=int) + if len(valid_i) == 0 or len(valid_j) == 0: + continue + start_i = int(np.random.choice(valid_i)) + start_j = int(np.random.choice(valid_j)) + terrain.height_field_raw[start_i : start_i + width, start_j : start_j + length] = np.random.choice(height_range) + + platform_x = _center_slice(terrain.width, platform_cells) + platform_y = _center_slice(terrain.length, platform_cells) + terrain.height_field_raw[platform_x, platform_y] = 0 + return terrain + + +def wave_terrain(terrain, num_waves=1, amplitude=1.0): + """Compose sinusoidal waves along both axes.""" + amplitude = _discretize(0.5 * amplitude, terrain.vertical_scale) + if num_waves <= 0: + return terrain + + freq = num_waves * np.pi * 2.0 / terrain.length + x = np.arange(terrain.width, dtype=np.float32).reshape(terrain.width, 1) + y = np.arange(terrain.length, dtype=np.float32).reshape(1, terrain.length) + undulation = amplitude * (np.cos(y * freq) + np.sin(x * freq)) + terrain.height_field_raw += undulation.astype(terrain.height_field_raw.dtype) + return terrain + + +def stairs_terrain(terrain, step_width, step_height): + """Generate a staircase that increases height in the x direction.""" + step_width = _discretize(step_width, terrain.horizontal_scale) + step_height = _discretize(step_height, terrain.vertical_scale) + if step_width <= 0 or step_height == 0: + return terrain + + num_steps = terrain.width // step_width + if num_steps <= 0: + return terrain + + heights = np.arange(1, num_steps + 1, dtype=np.int32) * step_height + profile = np.repeat(heights, step_width) + rows = min(profile.size, terrain.width) + terrain.height_field_raw[:rows, :] += profile[:rows].reshape(rows, 1).astype(terrain.height_field_raw.dtype) + return terrain + + +def pyramid_stairs_terrain(terrain, step_width, step_height, platform_size=1.0): + """Inset the terrain step by step while increasing height.""" + step_width = _discretize(step_width, terrain.horizontal_scale) + step_height = _discretize(step_height, terrain.vertical_scale) + platform_cells = _discretize(platform_size, terrain.horizontal_scale) + if step_width <= 0: + return terrain + + x_min, x_max = 0, terrain.width + y_min, y_max = 0, terrain.length + height = 0 + while (x_max - x_min) > platform_cells and (y_max - y_min) > platform_cells: + x_min += step_width + y_min += step_width + x_max -= step_width + y_max -= step_width + if x_min >= x_max or y_min >= y_max: + break + height += step_height + terrain.height_field_raw[x_min:x_max, y_min:y_max] = height + return terrain + + +def _fill_stepping_region(field: np.ndarray, stone_size: int, stone_distance: int, height_choices: np.ndarray): + """Populate a 2D array with stepping stones along the second axis.""" + primary = 0 + while primary < field.shape[1]: + primary_stop = min(field.shape[1], primary + stone_size) + offset = np.random.randint(0, max(stone_size, 1)) if stone_size > 0 else 0 + first_stop = max(0, offset - stone_distance) + if first_stop > 0: + field[:first_stop, primary:primary_stop] = np.random.choice(height_choices) + secondary = offset + while secondary < field.shape[0]: + secondary_stop = min(field.shape[0], secondary + stone_size) + field[secondary:secondary_stop, primary:primary_stop] = np.random.choice(height_choices) + secondary += stone_size + stone_distance + primary += stone_size + stone_distance + + +def stepping_stones_terrain(terrain, stone_size, stone_distance, max_height, platform_size=1.0, depth=-10): + """Scatter stepping stones separated by holes of uniform depth.""" + stone_size = _discretize(stone_size, terrain.horizontal_scale) + stone_distance = _discretize(stone_distance, terrain.horizontal_scale) + max_height = _discretize(max_height, terrain.vertical_scale) + platform_cells = _discretize(platform_size, terrain.horizontal_scale) + depth_cells = _discretize(depth, terrain.vertical_scale) + + height_choices = np.arange(-max_height - 1, max_height, 1, dtype=np.int32) + terrain.height_field_raw[:, :] = depth_cells + + if stone_size <= 0: + stone_size = 1 + if stone_distance < 0: + stone_distance = 0 + + if terrain.length >= terrain.width: + _fill_stepping_region(terrain.height_field_raw, stone_size, stone_distance, height_choices) + else: + _fill_stepping_region(terrain.height_field_raw.swapaxes(0, 1), stone_size, stone_distance, height_choices) + + platform_x = _center_slice(terrain.width, platform_cells) + platform_y = _center_slice(terrain.length, platform_cells) + terrain.height_field_raw[platform_x, platform_y] = 0 + return terrain + + +def convert_heightfield_to_trimesh(height_field_raw, horizontal_scale, vertical_scale, slope_threshold=None): + """Convert a regular heightfield to triangle mesh vertices and faces.""" + hf = height_field_raw + num_rows, num_cols = hf.shape + + y_coords = np.linspace(0.0, (num_cols - 1) * horizontal_scale, num_cols, dtype=np.float32) + x_coords = np.linspace(0.0, (num_rows - 1) * horizontal_scale, num_rows, dtype=np.float32) + yy, xx = np.meshgrid(y_coords, x_coords) + + if slope_threshold is not None: + scaled = slope_threshold * horizontal_scale / vertical_scale + move_x = np.zeros_like(hf, dtype=np.float32) + move_y = np.zeros_like(hf, dtype=np.float32) + move_corners = np.zeros_like(hf, dtype=np.float32) + + delta_x = hf[1:, :] - hf[:-1, :] + move_x[:-1, :] += delta_x > scaled + move_x[1:, :] -= (-delta_x) > scaled + + delta_y = hf[:, 1:] - hf[:, :-1] + move_y[:, :-1] += delta_y > scaled + move_y[:, 1:] -= (-delta_y) > scaled + + delta_diag = hf[1:, 1:] - hf[:-1, :-1] + move_corners[:-1, :-1] += delta_diag > scaled + move_corners[1:, 1:] -= (-delta_diag) > scaled + + xx += (move_x + np.where(move_x == 0, move_corners, 0)) * horizontal_scale + yy += (move_y + np.where(move_y == 0, move_corners, 0)) * horizontal_scale + + vertices = np.zeros((num_rows * num_cols, 3), dtype=np.float32) + vertices[:, 0] = xx.reshape(-1) + vertices[:, 1] = yy.reshape(-1) + vertices[:, 2] = hf.reshape(-1) * vertical_scale + + indices = np.arange(num_rows * num_cols, dtype=np.uint32).reshape(num_rows, num_cols) + v00 = indices[:-1, :-1].reshape(-1) + v01 = indices[:-1, 1:].reshape(-1) + v10 = indices[1:, :-1].reshape(-1) + v11 = indices[1:, 1:].reshape(-1) + + triangles = np.empty((2 * v00.size, 3), dtype=np.uint32) + triangles[0::2] = np.stack((v00, v11, v01), axis=1) + triangles[1::2] = np.stack((v00, v10, v11), axis=1) + + return vertices, triangles + + +@dataclass +class SubTerrain: + """Container representing a patch of terrain before tiling.""" + + terrain_name: str = "terrain" + width: int = 256 + length: int = 256 + vertical_scale: float = 1.0 + horizontal_scale: float = 1.0 + height_field_raw: np.ndarray = field(init=False) + + def __post_init__(self): + # Use float storage to allow sub-vertical-scale precision (smoother slopes without tiny scales) + self.height_field_raw = np.zeros((self.width, self.length), dtype=np.float32) + + +def gap_terrain(terrain, gap_size, platform_size=1.0): + """Create a square pit surrounded by a flat platform.""" + gap_cells = _discretize(gap_size, terrain.horizontal_scale) + platform_cells = _discretize(platform_size, terrain.horizontal_scale) + + center_x = terrain.length // 2 + center_y = terrain.width // 2 + x1 = (terrain.length - platform_cells) // 2 + x2 = x1 + gap_cells + y1 = (terrain.width - platform_cells) // 2 + y2 = y1 + gap_cells + + terrain.height_field_raw[center_x - x2 : center_x + x2, center_y - y2 : center_y + y2] = -1000 + terrain.height_field_raw[center_x - x1 : center_x + x1, center_y - y1 : center_y + y1] = 0 + + +def pit_terrain(terrain, depth, platform_size=1.0): + """Excavate a rectangular pit centered in the terrain.""" + depth_cells = _discretize(depth, terrain.vertical_scale) + half_platform = max(1, _discretize(platform_size, terrain.horizontal_scale) // 2) + + center_x = terrain.length // 2 + center_y = terrain.width // 2 + x_slice = slice(max(center_x - half_platform, 0), min(center_x + half_platform, terrain.length)) + y_slice = slice(max(center_y - half_platform, 0), min(center_y + half_platform, terrain.width)) + terrain.height_field_raw[x_slice, y_slice] = -depth_cells + + +class TerrainGenerator: + """Generate composite terrains from a declarative configuration.""" + + def __init__(self, config=None): + self.config = None + self.height_mat: np.ndarray | None = None + self.height_mat_pad: np.ndarray | None = None + self.horizontal_scale = 1.0 + self.vertical_scale = 1.0 + self.margin = 0.0 + if config is not None: + self._parse_cfg(config) + + def _parse_cfg(self, config): + self.config = config + self.horizontal_scale = config.horizontal_scale + self.vertical_scale = config.vertical_scale + self.margin = config.margin + self.height_mat = np.zeros((config.num_rows, config.num_cols), dtype=np.float32) + return self.config + + def _make_sub_terrain(self, config): + width = max(1, math.ceil(config.size[0] / self.horizontal_scale)) + length = max(1, math.ceil(config.size[1] / self.horizontal_scale)) + return SubTerrain( + config.type, + width=width, + length=length, + vertical_scale=self.vertical_scale, + horizontal_scale=self.horizontal_scale, + ) + + def _make_slope(self, config, difficulty: float = 1.0): + terrain = self._make_sub_terrain(config) + pyramid_sloped_terrain(terrain, slope=config.slope * difficulty, platform_size=config.platform_size) + if getattr(config, "random", False): + random_uniform_terrain( + terrain, + min_height=-0.05, + max_height=0.05, + step=0.005, + downsampled_scale=2.0 * self.horizontal_scale, + ) + return config.origin, terrain + + def _make_stair(self, config, difficulty: float = 1.0): + terrain = self._make_sub_terrain(config) + pyramid_stairs_terrain( + terrain, + step_width=config.step[0], + step_height=config.step[1] * difficulty, + platform_size=config.platform_size, + ) + return config.origin, terrain + + def _make_obstacle(self, config, difficulty: float = 1.0): + terrain = self._make_sub_terrain(config) + discrete_obstacles_terrain( + terrain, + max_height=config.max_height * difficulty, + min_size=config.rectangle_params[0], + max_size=config.rectangle_params[1], + num_rects=config.rectangle_params[2], + platform_size=config.platform_size, + ) + return config.origin, terrain + + def _make_stone(self, config, difficulty: float = 1.0): + terrain = self._make_sub_terrain(config) + stone_scale = max(np.log1p(difficulty), 1e-6) + stepping_stones_terrain( + terrain, + stone_size=config.stone_params[0] / stone_scale, + stone_distance=config.stone_params[1], + max_height=config.max_height, + platform_size=config.platform_size, + ) + return config.origin, terrain + + def _make_gap(self, config, difficulty: float = 1.0): + terrain = self._make_sub_terrain(config) + gap_terrain(terrain, gap_size=config.gap_size * difficulty, platform_size=config.platform_size) + return config.origin, terrain + + def _make_pit(self, config, difficulty: float = 1.0): + terrain = self._make_sub_terrain(config) + pit_terrain(terrain, depth=config.depth * difficulty, platform_size=config.platform_size) + return config.origin, terrain + + def _add_terrain_to_map(self, origin, terrain, matrix: np.ndarray): + start_row = math.floor(origin[0] / self.horizontal_scale) + start_col = math.floor(origin[1] / self.horizontal_scale) + end_row = start_row + terrain.width + end_col = start_col + terrain.length + matrix[start_row:end_row, start_col:end_col] = terrain.height_field_raw + return matrix + + def _repeat_terrain(self, repeat: int, direction: str, gap: float, difficulty_list: list[float]): + if repeat <= 0: + return self.height_mat + assert len(difficulty_list) == repeat, "Difficulty schedule must match repeat count." + + base_rows, base_cols = self.config.num_rows, self.config.num_cols + gap_cells = _discretize(gap, self.horizontal_scale) + axis = 1 if direction == "column" else 0 + gap_block = None + if gap_cells > 0: + gap_shape = (base_rows, gap_cells) if axis == 1 else (gap_cells, base_cols) + gap_block = np.zeros(gap_shape, dtype=np.float32) + + blocks = [self.height_mat] + for diff in difficulty_list: + if gap_block is not None: + blocks.append(gap_block.copy()) + blocks.append(self.generate_matrix(diff)) + + self.height_mat = np.concatenate(blocks, axis=axis) + self.config.num_rows = self.height_mat.shape[0] + self.config.num_cols = self.height_mat.shape[1] + return self.height_mat + + def generate_matrix(self, difficulty: float = 1.0) -> np.ndarray: + """Assemble a single tiled height matrix for the given difficulty value.""" + assert self.config is not None, "Call _parse_cfg or generate_terrain with a config before use." + composite = np.zeros((self.config.num_rows, self.config.num_cols), dtype=np.float32) + for terrain_type, terrains in self.config.elements.items(): + builder = getattr(self, f"_make_{terrain_type}", None) + if builder is None: + raise NotImplementedError(f"Terrain type '{terrain_type}' is not supported.") + for cfg in terrains: + origin, sub = builder(cfg, difficulty) + self._add_terrain_to_map(origin, sub, composite) + return composite + + def generate_terrain(self, config=None, type: str = "trimesh"): + """Generate terrain data (heightfield and/or mesh) for the configured ground.""" + if config is not None: + self._parse_cfg(config) + assert self.config is not None, "Terrain configuration must be set before generating terrain." + + repeats, direction, gap = self.config.repeat_direction_gap + if self.config.difficulty[2] == "linear": + schedule = np.linspace(self.config.difficulty[0], self.config.difficulty[1], repeats + 1).tolist() + else: + schedule = [self.config.difficulty[0]] * (repeats + 1) + + self.height_mat = self.generate_matrix(schedule.pop(0)) + if repeats: + self._repeat_terrain(repeats, direction, gap, schedule) + + pad_rows = self.config.margin_num_rows + pad_cols = self.config.margin_num_cols + self.height_mat_pad = np.pad( + self.height_mat, + ((pad_rows, pad_rows), (pad_cols, pad_cols)), + mode="constant", + constant_values=0, + ) + + if type == "trimesh": + vertices, triangles = convert_heightfield_to_trimesh( + height_field_raw=self.height_mat_pad, + horizontal_scale=self.horizontal_scale, + vertical_scale=self.vertical_scale, + slope_threshold=None, + ) + return vertices, triangles + if type == "heightfield": + return self.height_mat_pad * self.vertical_scale + if type == "both": + vertices, triangles = convert_heightfield_to_trimesh( + height_field_raw=self.height_mat_pad, + horizontal_scale=self.horizontal_scale, + vertical_scale=self.vertical_scale, + slope_threshold=None, + ) + return vertices, triangles, self.height_mat_pad * self.vertical_scale + raise ValueError(f"Unknown terrain export type '{type}'.") + + @property + def height_measure(self): + """Return the current unpadded heightfield in metric units.""" + if self.height_mat is None: + return None + return self.height_mat * self.vertical_scale + + @property + def height_measure_pad(self): + """Return the padded heightfield in metric units used for mesh export.""" + if self.height_mat_pad is None: + return None + return self.height_mat_pad * self.vertical_scale diff --git a/roboverse_learn/rl/unitree_rl/configs/callback_funcs/reset_funcs.py b/roboverse_learn/rl/unitree_rl/configs/callback_funcs/reset_funcs.py index b9b9cdf03..697ecb544 100644 --- a/roboverse_learn/rl/unitree_rl/configs/callback_funcs/reset_funcs.py +++ b/roboverse_learn/rl/unitree_rl/configs/callback_funcs/reset_funcs.py @@ -1,5 +1,6 @@ from __future__ import annotations +import numpy as np import torch from metasim.types import TensorState @@ -57,3 +58,141 @@ def reset_joints_by_scale(env: EnvTypes, env_ids: torch.Tensor | list, position_ env.setup_initial_env_states.robots[env.name].joint_vel[env_ids] = joint_vel # # set into the physics simulation # asset.write_joint_state_to_sim(joint_pos, joint_vel, joint_ids=asset_cfg.joint_ids, env_ids=env_ids) + + +def get_terrain_height_at_position(env: EnvTypes, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor: + """ + Query the terrain height at given (x, y) positions. + + All simulators now center the terrain at world origin (0,0). + The terrain center is at heightfield ((rows-1)/2, (cols-1)/2) which maps to world (0,0). + + Args: + env: The environment + x: X coordinates in world frame (shape: [num_envs]) + y: Y coordinates in world frame (shape: [num_envs]) + + Returns: + Height values at those positions (shape: [num_envs]) + """ + # Access the terrain data from the simulator + handler = env.handler + if not hasattr(handler, '_height_mat') or handler._height_mat is None: + # No terrain, return zero height + return torch.zeros_like(x) + + height_mat = handler._height_mat # numpy array + vertices = handler._ground_mesh_vertices + width = vertices[:, 0].max() - vertices[:, 0].min() + height = vertices[:, 1].max() - vertices[:, 1].min() + + # Convert world coordinates to heightfield indices + # Terrain is centered: world (0,0) = heightfield center + # heightfield (i,j) → world ((i - (rows-1)/2) * h_scale, (j - (cols-1)/2) * h_scale) + # Inverse: world (x,y) → heightfield (x/h_scale + (rows-1)/2, y/h_scale + (cols-1)/2) + x_np = x.cpu().numpy() + y_np = y.cpu().numpy() + + rows, cols = height_mat.shape + half_width = (rows - 1) / 2.0 + half_height = (cols - 1) / 2.0 + + x_idx = x_np / width * (rows - 1) + half_width + y_idx = y_np / height * (cols - 1) + half_height + + # Clamp to valid range + x_idx = np.clip(x_idx, 0, rows - 1) + y_idx = np.clip(y_idx, 0, cols - 1) + + # Use bilinear interpolation for smooth height queries + from scipy.interpolate import RegularGridInterpolator + x_coords = np.arange(rows) + y_coords = np.arange(cols) + interpolator = RegularGridInterpolator((x_coords, y_coords), height_mat, + method='linear', bounds_error=False, fill_value=0.0) + + points = np.column_stack([x_idx, y_idx]) + heights = interpolator(points) + + return torch.from_numpy(heights).to(x.device, dtype=x.dtype) + + +def random_root_state_terrain_aware( + env: EnvTypes, + env_ids: torch.Tensor | list, + pose_range: list[list]=[[0]*6, [0]*6], + velocity_range: list[list]=[[0]*6, [0]*6], + base_height_offset: float | None = None +) -> torch.Tensor: + """ + Reset robot root state with terrain-aware height adjustment. + + The robot will be spawned at base_height_offset above the terrain surface + at the randomly sampled (x, y) position. + + Args: + env: The environment + env_ids: Environment IDs to reset + pose_range: Range for random pose sampling [min, max] for [x, y, z, roll, pitch, yaw] + Note: z here represents additional offset on top of terrain + base_height_offset + velocity_range: Range for random velocity sampling + base_height_offset: Height above terrain to spawn the robot. If None, uses the robot's + default z-position from the configuration. + """ + if len(env_ids) == 0: + return + + root_states = env.default_env_states.robots[env.name].root_state[env_ids].clone() + + # Get per-env world-frame origins (if available) so we can convert + # local robot positions to world coordinates for terrain queries. + env_ids_idx = ( + env_ids + if isinstance(env_ids, torch.Tensor) + else torch.as_tensor(env_ids, device=env.device, dtype=torch.long) + ) + env_origins_xy = torch.zeros((len(env_ids_idx), 2), device=env.device, dtype=root_states.dtype) + if hasattr(env.handler, "scene") and hasattr(env.handler.scene, "env_origins"): + # IsaacSim exposes per-env origins via scene.env_origins + env_origins_xy = env.handler.scene.env_origins[env_ids_idx, :2] + elif hasattr(env.handler, "_env_origin") and len(getattr(env.handler, "_env_origin", [])) > 0: + # IsaacGym: env origins queried from gym.get_env_origin and cached on the handler + env_origins = torch.as_tensor(env.handler._env_origin, device=env.device, dtype=root_states.dtype) + env_origins_xy = env_origins[env_ids_idx, :2] + + # If base_height_offset not provided, use the robot's default z position + if base_height_offset is None: + base_height_offset = root_states[0, 2].item() + + # Sample random poses + pose_range = torch.tensor(pose_range, device=env.device) + rand_samples = sample_uniform(pose_range[0], pose_range[1], (len(env_ids), 6), device=env.device) + + # Calculate local x, y positions (env frame) + x_positions = root_states[:, 0] + rand_samples[:, 0] + y_positions = root_states[:, 1] + rand_samples[:, 1] + + # Convert to world-frame coordinates for terrain height query + x_world = x_positions + env_origins_xy[:, 0] + y_world = y_positions + env_origins_xy[:, 1] + + # Query terrain height at these x, y positions + terrain_heights = get_terrain_height_at_position(env, x_world, y_world) + + # Set z position = terrain height + base offset + random z offset + z_positions = terrain_heights + base_height_offset + + positions = torch.stack([x_positions, y_positions, z_positions], dim=1) + + # Handle orientations + orientations_delta = quat_from_euler_xyz(rand_samples[:, 3], rand_samples[:, 4], rand_samples[:, 5]) + orientations = quat_mul(root_states[:, 3:7], orientations_delta) + + # Handle velocities + velocity_range = torch.tensor(velocity_range, device=env.device) + rand_samples = sample_uniform(velocity_range[0], velocity_range[1], (len(env_ids), 6), device=env.device) + velocities = root_states[:, 7:13] + rand_samples + + env.setup_initial_env_states.robots[env.name].root_state[env_ids, 0:3] = positions + env.setup_initial_env_states.robots[env.name].root_state[env_ids, 3:7] = orientations + env.setup_initial_env_states.robots[env.name].root_state[env_ids, 7:13] = velocities diff --git a/roboverse_learn/rl/unitree_rl/configs/cfg_queries.py b/roboverse_learn/rl/unitree_rl/configs/cfg_queries.py index f24cfd5e0..d9e0a6655 100644 --- a/roboverse_learn/rl/unitree_rl/configs/cfg_queries.py +++ b/roboverse_learn/rl/unitree_rl/configs/cfg_queries.py @@ -1,5 +1,6 @@ from __future__ import annotations +import os import torch import numpy as np import warnings @@ -7,6 +8,14 @@ from metasim.sim.base import BaseSimHandler, BaseQueryType from metasim.utils.math import quat_apply, convert_quat +from metasim.scenario.objects import ( + BaseObjCfg, + PrimitiveCubeCfg, + PrimitiveSphereCfg, + PrimitiveCylinderCfg, + RigidObjCfg, + ArticulationObjCfg, +) try: import isaacgym # noqa: F401 @@ -19,96 +28,19 @@ pass -class ContactForces(BaseQueryType): - """Optional query to fetch per-body net contact forces for each robot. - - - For IsaacGym: uses the native net-contact tensor and maps it per-robot in handler indexing order. - - For IsaacSim: returns a zero tensor fallback per-robot (hook is in place; replace with real source when available). - """ - def __init__(self, history_length: int = 3): - super().__init__() - self.history_length = history_length - self._current_contact_force = None - self._contact_forces_queue = deque(maxlen=history_length) - - def bind_handler(self, handler:BaseSimHandler, *args, **kwargs): - super().bind_handler(handler, *args, **kwargs) - self.simulator = handler.scenario.simulator - self.num_envs = handler.scenario.num_envs - self.robots = handler.robots - if self.simulator in ["isaacgym", "mujoco"]: - self.body_ids_reindex = handler._get_body_ids_reindex(self.robots[0].name) - elif self.simulator == "isaacsim": - sorted_body_names = self.handler.get_body_names(self.robots[0].name, True) - self.body_ids_reindex = torch.tensor([self.handler.contact_sensor.body_names.index(name) for name in sorted_body_names], dtype=torch.int, device=self.handler.device) - else: - raise NotImplementedError - self.initialize() - self.__call__() - - def initialize(self): - for _ in range(self.history_length): - if self.simulator == "isaacgym": - self._current_contact_force = isaacgym.gymtorch.wrap_tensor(self.handler.gym.acquire_net_contact_force_tensor(self.handler.sim)) - elif self.simulator == "isaacsim": - self._current_contact_force = self.handler.contact_sensor.data.net_forces_w - elif self.simulator == "mujoco": - self._current_contact_force = self._get_contact_forces_mujoco() - else: - raise NotImplementedError - self._contact_forces_queue.append(self._current_contact_force.clone().view(self.num_envs, -1, 3)[:, self.body_ids_reindex, :]) - - def _get_contact_forces_mujoco(self) -> torch.Tensor: - """ - Compute net contact forces on each body. - Returns: - torch.Tensor: shape (nbody, 3), contact forces for each body - """ - nbody = self.handler.physics.model.nbody - contact_forces = torch.zeros((nbody, 3), device=self.handler.device) - - for i in range(self.handler.physics.data.ncon): - contact = self.handler.physics.data.contact[i] - force = np.zeros(6, dtype=np.float64) - mujoco.mj_contactForce(self.handler.physics.model.ptr, self.handler.physics.data.ptr, i, force) - f_contact = torch.from_numpy(force[:3]).to(device=self.handler.device) - - body1 = self.handler.physics.model.geom_bodyid[contact.geom1] - body2 = self.handler.physics.model.geom_bodyid[contact.geom2] - - contact_forces[body1] += f_contact - contact_forces[body2] -= f_contact - - return contact_forces - - def __call__(self): - if self.simulator == "isaacgym": - self.handler.gym.refresh_net_contact_force_tensor(self.handler.sim) - elif self.simulator == "isaacsim": - self._current_contact_force = self.handler.contact_sensor.data.net_forces_w - elif self.simulator == "mujoco": - self._current_contact_force = self._get_contact_forces_mujoco() - else: - raise NotImplementedError - self._contact_forces_queue.append(self._current_contact_force.view(self.num_envs, -1, 3)[:, self.body_ids_reindex, :]) - return {self.robots[0].name: self} - - @property - def contact_forces_history(self) -> torch.Tensor: - return torch.stack(list(self._contact_forces_queue), dim=1) # (num_envs, history_length, num_bodies, 3) - - @property - def contact_forces(self) -> torch.Tensor: - return self._contact_forces_queue[-1] - class LidarPointCloud(BaseQueryType): """Optional query that produces a LiDAR point cloud using LidarSensor + Warp. Notes - - Supports IsaacGym and MuJoCo via common state interface; raycasting is done against a generated terrain mesh. - - Robot self-geometry is not included in the mesh to keep this query generic and lightweight. - - Requires packages: LidarSensor, warp, trimesh. If unavailable, returns None payload when enabled. - - Quaternions: handler states use (w,x,y,z). LidarSensor expects (x,y,z,w). Conversion is handled internally. + - Supports IsaacGym and MuJoCo via common state interface; raycasting is done + against a generated mesh that includes the terrain and static scenario + objects (primitives) replicated across environments. + - Robot self-geometry is not included in the mesh to keep this query generic + and lightweight. + - Requires packages: LidarSensor, warp, trimesh. If unavailable, returns + None payload when enabled. + - Quaternions: handler states use (w,x,y,z). LidarSensor expects (x,y,z,w). + Conversion is handled internally. """ def __init__( @@ -132,7 +64,7 @@ def bind_handler(self, handler: BaseSimHandler, *args, **kwargs): self.handler = handler self.num_envs = handler.scenario.num_envs self.robots = handler.robots - self.device = str(handler.device) # warp only accepts str device + self.device = str(handler.device) # warp only accepts str device self._init_backend() def _init_backend(self): @@ -142,6 +74,17 @@ def _init_backend(self): self._init_backend_isaacsim() def _init_backend_mujoco_isaacgym(self): + """Initialize Warp-based LiDAR backend for IsaacGym / MuJoCo. + + Builds a static triangle mesh that includes: + - Ground/terrain (from handler._ground_mesh_vertices/_ground_mesh_triangles) + - Scenario objects: + * Primitive cubes / spheres / cylinders. + * RigidObjCfg / ArticulationObjCfg approximated from their mesh files + when available. + Objects are replicated across envs for IsaacGym, or instantiated once + for MuJoCo. + """ import warp as wp import trimesh from LidarSensor.lidar_sensor import LidarSensor @@ -152,11 +95,132 @@ def _init_backend_mujoco_isaacgym(self): self.LidarSensor = LidarSensor self.LidarConfig = LidarConfig self.wp.init() - vertices = self.handler._ground_mesh_vertices - triangles = self.handler._ground_mesh_triangles - import torch + # ------------------------------------------------------------------ # + # Build combined static scene mesh: terrain + scenario primitives + # ------------------------------------------------------------------ # + scene_vertices = getattr(self.handler, "_ground_mesh_vertices", None) + scene_triangles = getattr(self.handler, "_ground_mesh_triangles", None) + if scene_vertices is None or scene_triangles is None: + warnings.warn( + "LidarPointCloud: ground mesh not available on handler; " + "LiDAR will not include terrain/objects." + ) + self._backend_ready = False + return + + base_mesh = self.trimesh.Trimesh(vertices=scene_vertices, faces=scene_triangles, process=False) + meshes = [base_mesh] + + # Helper to convert quaternion (w,x,y,z) -> 3x3 rotation matrix + def _quat_wxyz_to_matrix(quat_wxyz: tuple[float, float, float, float]) -> np.ndarray: + w, x, y, z = quat_wxyz + ww, xx, yy, zz = w * w, x * x, y * y, z * z + xy, xz, yz = x * y, x * z, y * z + wx, wy, wz = w * x, w * y, w * z + return np.array( + [ + [ww + xx - yy - zz, 2 * (xy - wz), 2 * (xz + wy)], + [2 * (xy + wz), ww - xx + yy - zz, 2 * (yz - wx)], + [2 * (xz - wy), 2 * (yz + wx), ww - xx - yy + zz], + ], + dtype=np.float32, + ) + + # Helper: build a unit mesh in local object frame for supported objects + def _object_to_trimesh(obj: BaseObjCfg): + try: + # Primitive objects + if isinstance(obj, PrimitiveCubeCfg): + size = np.asarray(obj.size, dtype=np.float32) + return self.trimesh.creation.box(extents=size) + if isinstance(obj, PrimitiveSphereCfg): + return self.trimesh.creation.icosphere(radius=float(obj.radius), subdivisions=2) + if isinstance(obj, PrimitiveCylinderCfg): + return self.trimesh.creation.cylinder(radius=float(obj.radius), height=float(obj.height)) + + # File-based rigid / articulated objects: approximate via mesh file when possible. + if isinstance(obj, (RigidObjCfg, ArticulationObjCfg)): + candidates = [] + for attr in ("mesh_path", "usd_path", "urdf_path", "mjcf_path", "mjx_mjcf_path"): + path = getattr(obj, attr, None) + if path: + candidates.append(path) + mesh_path = None + for path in candidates: + ext = os.path.splitext(path)[1].lower() + if ext in (".stl", ".obj", ".ply", ".off", ".gltf", ".glb"): + mesh_path = path + break + if mesh_path is None: + return None + try: + loaded = self.trimesh.load(mesh_path) + # Handle both single meshes and scene graphs + if isinstance(loaded, self.trimesh.Scene): + # Concatenate all meshes in the scene + meshes_in_scene = list(loaded.geometry.values()) + if meshes_in_scene: + return self.trimesh.util.concatenate(meshes_in_scene) + return None + return loaded + except Exception as e: + warnings.warn( + f"LidarPointCloud: failed to load mesh for object '{obj.name}' " + f"from '{mesh_path}': {e}" + ) + return None + except Exception as e: # pragma: no cover - defensive + warnings.warn(f"LidarPointCloud: failed to build mesh for object '{obj.name}': {e}") + return None + return None + # Environment origins (IsaacGym has a grid of envs; MuJoCo uses a single env) + if self.simulator == "isaacgym" and hasattr(self.handler, "_env_origin"): + env_origins = [np.asarray(o, dtype=np.float32) for o in self.handler._env_origin] + else: + env_origins = [np.zeros(3, dtype=np.float32)] + + # Add scenario objects replicated across envs (IsaacGym) or once (MuJoCo) + for obj in getattr(self.handler, "objects", []): + obj_mesh_local = _object_to_trimesh(obj) + if obj_mesh_local is None: + continue + + # Default pose in local env frame + default_pos = np.asarray(getattr(obj, "default_position", (0.0, 0.0, 0.0)), dtype=np.float32) + default_quat = getattr(obj, "default_orientation", (1.0, 0.0, 0.0, 0.0)) + # Validate quaternion format + if not (isinstance(default_quat, (tuple, list, np.ndarray)) and len(default_quat) == 4): + warnings.warn(f"Invalid orientation for object '{obj.name}', using identity quaternion") + default_quat = (1.0, 0.0, 0.0, 0.0) + rot = _quat_wxyz_to_matrix(default_quat) + + base_T = np.eye(4, dtype=np.float32) + # Apply per-object scale for file-based objects if provided + if isinstance(obj, (RigidObjCfg, ArticulationObjCfg)): + scale = getattr(obj, "scale", (1.0, 1.0, 1.0)) + if isinstance(scale, (float, int)): + scale = (scale, scale, scale) + scale = np.asarray(scale, dtype=np.float32) + base_T[:3, :3] = rot @ np.diag(scale) + else: + base_T[:3, :3] = rot + + for origin in env_origins: + T = base_T.copy() + T[:3, 3] = default_pos + origin + obj_mesh = obj_mesh_local.copy() + obj_mesh.apply_transform(T) + meshes.append(obj_mesh) + + combined_mesh = self.trimesh.util.concatenate(meshes) if len(meshes) > 1 else meshes[0] + vertices = combined_mesh.vertices.astype(np.float32) + triangles = combined_mesh.faces.astype(np.int32) + + # ------------------------------------------------------------------ # + # Warp mesh + sensor buffers + # ------------------------------------------------------------------ # vertex_tensor = torch.tensor(vertices, device=self.device, dtype=torch.float32) faces_wp_int32_array = self.wp.from_numpy(triangles.reshape(-1), dtype=self.wp.int32, device=self.device) vertex_vec3_array = self.wp.from_torch(vertex_tensor, dtype=self.wp.vec3) @@ -197,6 +261,10 @@ def _init_backend_isaacsim(self): This attaches a LiDAR sensor (Livox/MID-360 pattern by default) to the specified robot link across all environments using the env-regex prim path. Point cloud is requested in the sensor (local) frame to allow consistent downstream transforms. + The raycaster is configured to hit: + - Ground mesh. + - Static scene geometry under /World/static. + - Robot bodies and all scenario objects under /World/envs/env_*/. """ # Isaac Lab LiDAR imports (as in the user-provided code snippet) from isaaclab.sensors import LidarSensor, LidarSensorCfg @@ -260,17 +328,49 @@ def _init_backend_isaacsim(self): # Attach LiDAR to the resolved link across envs via env-regex path prim_path = f"/World/envs/env_.*/{robot_name}/{self._resolved_link_name}" + # Collect dynamic mesh prim paths for robot + scenario objects + # Note: Paths must point to actual geometry prims using {ENV_REGEX_NS} placeholder. + # For ray casting performance, we prioritize collision geometry (simple primitives) + # over high-poly visual meshes. + dynamic_env_mesh_paths = [] + + # Query the stage to find collision/simple geometry prims + import omni + from pxr import Usd + + stage = omni.usd.get_context().get_stage() + env0_path = "/World/envs/env_0" + env_prim = stage.GetPrimAtPath(env0_path) + + if env_prim and env_prim.IsValid(): + # Prefer simple collision primitives over high-poly meshes for performance + collision_geom_types = {"Sphere", "Cube", "Cylinder", "Capsule", "Cone", "Plane"} + + # Find all geometry prims under env_0 + for prim in Usd.PrimRange(env_prim): + geom_type = prim.GetTypeName() + + # Only add collision primitives, skip Mesh types to avoid high-poly visual meshes + if geom_type in collision_geom_types: + # Convert env_0 path to use {ENV_REGEX_NS} placeholder + object_prim_path = prim.GetPath().pathString + # Replace /World/envs/env_0 with {ENV_REGEX_NS} + pattern_path = object_prim_path.replace("/World/envs/env_0", "{ENV_REGEX_NS}") + dynamic_env_mesh_paths.append(pattern_path) + # Configure a Livox/MID-360-like sensor, aligned in world with point cloud in local frame # Keep params conservative for performance; adjust as needed by caller + # Fix: Rotate -90° around X to point sensor forward (was pointing up, causing inf values) + # Quaternion (0.707107, -0.707107, 0.0, 0.0) in wxyz format rotates sensor Z-axis from UP to FORWARD lidar_cfg = LidarSensorCfg( prim_path=prim_path, - offset=LidarSensorCfg.OffsetCfg(pos=(0.0, 0.0, 0.0), rot=(1.0, 0.0, 0.0, 0.0)), + offset=LidarSensorCfg.OffsetCfg(pos=(0.0, 0.0, 0.0), rot=(0.707107, -0.707107, 0.0, 0.0)), attach_yaw_only=False, - ray_alignment="world", + ray_alignment="base", # rays rotate with robot pattern_cfg=LivoxPatternCfg(sensor_type=self.sensor_type, samples=24000), mesh_prim_paths=["/World/ground", "/World/static"], - # optionally include dynamic scene meshes: robot and default scene under each env - dynamic_env_mesh_prim_paths=[f"/World/envs/env_.*/{robot_name}/.*", "/World/envs/env_.*/.*/geometry/mesh"], + # include dynamic scene meshes: robot + per-env objects + dynamic_env_mesh_prim_paths=dynamic_env_mesh_paths, max_distance=20.0, min_range=0.2, return_pointcloud=True, # request point cloud output @@ -525,7 +625,7 @@ def _compute_lidar_points(self, robot_name: str, pos_w: torch.Tensor, quat_wxyz: } def __call__(self): - if not self.enabled: + if not self.enabled or not getattr(self, "_backend_ready", False): return {self.robots[0].name: None} robot_name = self.robots[0].name diff --git a/roboverse_learn/rl/unitree_rl/configs/locomotion/walk_g1_dof12.py b/roboverse_learn/rl/unitree_rl/configs/locomotion/walk_g1_dof12.py index 4797977e4..acddf350b 100644 --- a/roboverse_learn/rl/unitree_rl/configs/locomotion/walk_g1_dof12.py +++ b/roboverse_learn/rl/unitree_rl/configs/locomotion/walk_g1_dof12.py @@ -116,13 +116,14 @@ class RewardsScales: } callbacks_reset = { "random_root_state": ( - reset_funcs.random_root_state, + reset_funcs.random_root_state_terrain_aware, { "pose_range": [ - [0., 0., 0, 0, 0, 0], + [0., 0., 0, 0, 0, 0], # x, y, z_offset, roll, pitch, yaw [0., 0., 0, 0, 0, 0], ], "velocity_range": [[-0.5] * 6, [0.5] * 6], + # base_height_offset is None by default, uses robot's default z position (0.8m from cfg_base.py) }, ), "reset_joints_by_scale": ( diff --git a/roboverse_learn/rl/unitree_rl/configs/locomotion/walk_g1_dof29.py b/roboverse_learn/rl/unitree_rl/configs/locomotion/walk_g1_dof29.py index 9876229d0..f6bfeb9e2 100644 --- a/roboverse_learn/rl/unitree_rl/configs/locomotion/walk_g1_dof29.py +++ b/roboverse_learn/rl/unitree_rl/configs/locomotion/walk_g1_dof29.py @@ -10,6 +10,7 @@ ) import roboverse_learn.rl.unitree_rl.helper.curriculum_utils as curr_funs from metasim.queries import ContactForces +from roboverse_learn.rl.unitree_rl.configs.cfg_queries import LidarPointCloud from roboverse_learn.rl.unitree_rl.configs.cfg_randomizers import ( MaterialRandomizer, MassRandomizer, @@ -111,7 +112,7 @@ class RewardsScales: }, ) - callbacks_query = {"contact_forces": ContactForces(history_length=3)} + callbacks_query = {"contact_forces": ContactForces(history_length=3), "lidar_point_cloud": LidarPointCloud(enabled=True)} callbacks_setup = { "material_randomizer": MaterialRandomizer( obj_name="g1_dof29", @@ -129,13 +130,14 @@ class RewardsScales: } callbacks_reset = { "random_root_state": ( - reset_funcs.random_root_state, + reset_funcs.random_root_state_terrain_aware, { "pose_range": [ - [-0.5, -0.5, 0.0, 0, 0, -3.14], # x,y,z roll,pitch,yaw - [0.5, 0.5, 0.0, 0, 0, 3.14], + [-0.5, -0.5, 0.0, 0, 0, -3.14], # x, y, z_offset, roll, pitch, yaw + [0.5, 0.5, 0.05, 0, 0, 3.14], # z_offset can vary slightly ], "velocity_range": [[0] * 6, [0] * 6], + # base_height_offset is None by default, uses robot's default z position (0.8m from cfg_base.py) }, ), "reset_joints_by_scale": ( diff --git a/roboverse_learn/rl/unitree_rl/helper/__init__.py b/roboverse_learn/rl/unitree_rl/helper/__init__.py index 5eea6f668..16281fe0b 100644 --- a/roboverse_learn/rl/unitree_rl/helper/__init__.py +++ b/roboverse_learn/rl/unitree_rl/helper/__init__.py @@ -1,2 +1 @@ from .utils import * -from .terrain_generator import TerrainGenerator diff --git a/roboverse_learn/rl/unitree_rl/helper/terrain_generator.py b/roboverse_learn/rl/unitree_rl/helper/terrain_generator.py deleted file mode 100644 index f6e52b9f2..000000000 --- a/roboverse_learn/rl/unitree_rl/helper/terrain_generator.py +++ /dev/null @@ -1,178 +0,0 @@ -from __future__ import annotations - -import math -import numpy as np - -from roboverse_learn.rl.unitree_rl.helper import terrain_utils -from roboverse_learn.rl.unitree_rl.configs.cfg_terrain import * - -class TerrainGenerator: - """Abstract base class for backend-specific terrain implementation.""" - - def __init__(self, config: GroundCfg = None): - if config is not None: - self._parse_cfg(config) - - def _parse_cfg(self, config: GroundCfg): - """Parse the terrain configuration.""" - self.config = config - self.height_mat = np.zeros((config.num_rows, config.num_cols), dtype=np.int16) - self.horizontal_scale = config.horizontal_scale - self.vertical_scale = config.vertical_scale - self.margin = config.margin - - def _make_sub_terrain(self, config: BaseTerrainCfg): - terrain = terrain_utils.SubTerrain( - config.type, - width=math.ceil(config.size[0] / self.horizontal_scale), - length=math.ceil(config.size[1] / self.horizontal_scale), - vertical_scale=self.vertical_scale, - horizontal_scale=self.horizontal_scale, - ) - return terrain - - def _make_slope(self, config: SlopeCfg, difficulty: float = 1.0): - terrain = self._make_sub_terrain(config) - terrain_utils.pyramid_sloped_terrain( - terrain, - slope=config.slope * difficulty, - platform_size=config.platform_size, - ) - if config.random: - terrain_utils.random_uniform_terrain( - terrain, min_height=-0.05, max_height=0.05, step=0.005, downsampled_scale=2.0 * self.horizontal_scale - ) - return config.origin, terrain - - def _make_stair(self, config: StairCfg, difficulty: float = 1.0): - terrain = self._make_sub_terrain(config) - terrain_utils.pyramid_stairs_terrain( - terrain, - step_width=config.step[0], - step_height=config.step[1] * difficulty, - platform_size=config.platform_size, - ) - return config.origin, terrain - - def _make_obstacle(self, config: ObstacleCfg, difficulty: float = 1.0): - terrain = self._make_sub_terrain(config) - terrain_utils.discrete_obstacles_terrain( - terrain, - max_height=config.max_height * difficulty, - min_size=config.rectangle_params[0], - max_size=config.rectangle_params[1], - num_rects=config.rectangle_params[2], - platform_size=config.platform_size, - ) - return config.origin, terrain - - def _make_stone(self, config: StoneCfg, difficulty: float = 1.0): - terrain = self._make_sub_terrain(config) - terrain_utils.stepping_stones_terrain( - terrain, - stone_size=config.stone_params[0] / np.log(1 + difficulty), - stone_distance=config.stone_params[1], - max_height=config.max_height, - platform_size=config.platform_size, - ) - return config.origin, terrain - - def _make_gap(self, config: GapCfg, difficulty: float = 1.0): - terrain = self._make_sub_terrain(config) - terrain_utils.gap_terrain(terrain, gap_size=config.gap_size * difficulty, platform_size=config.platform_size) - return config.origin, terrain - - def _make_pit(self, config: PitCfg, difficulty: float = 1.0): - terrain = self._make_sub_terrain(config) - terrain_utils.pit_terrain(terrain, depth=config.depth * difficulty, platform_size=config.platform_size) - return config.origin, terrain - - def _add_terrain_to_map(self, origin, terrain: terrain_utils.SubTerrain, matrix: np.ndarray = None): - start_row = math.floor(origin[0] / self.horizontal_scale) - start_col = math.floor(origin[1] / self.horizontal_scale) - end_row = start_row + terrain.width - end_col = start_col + terrain.length - matrix[start_row:end_row, start_col:end_col] = terrain.height_field_raw - return matrix - - def _repeat_terrain( - self, repeat: int = 0, direction: str = "column", gap: float = 0.0, difficulty_list: list[float] | None = None - ): - """Repeat the terrain in the specified direction.""" - assert len(difficulty_list) == repeat, "Length of difficulty_list must match the number of repeats." - if direction == "column": - padding = np.zeros((self.config.num_rows, int(gap / self.horizontal_scale)), dtype=np.int16) - for i in range(0, repeat): - mat = self.generate_matrix(difficulty_list[i]) - extend_mat = np.concatenate((padding, mat), axis=1) - self.height_mat = np.concatenate((self.height_mat, extend_mat), axis=1) - elif direction == "row": - padding = np.zeros((int(gap / self.horizontal_scale), self.config.num_cols), dtype=np.int16) - extend_mat = np.concatenate((self.height_mat, padding), axis=0) - for i in range(0, repeat): - mat = self.generate_matrix(difficulty_list[i]) - extend_mat = np.concatenate((padding, mat), axis=0) - self.height_mat = np.concatenate((self.height_mat, extend_mat), axis=0) - else: - raise ValueError("Direction must be either 'column' or 'row'.") - self.config.num_rows = self.height_mat.shape[0] - self.config.num_cols = self.height_mat.shape[1] - return self.height_mat - - def generate_matrix(self, difficulty: float = 1.0) -> np.ndarray: - matrix = np.zeros((self.config.num_rows, self.config.num_cols), dtype=np.int16) - for t in self.config.elements.keys(): - func_name = f"_make_{t}" - if hasattr(self, func_name): - func = getattr(self, func_name) - for cfg in self.config.elements[t]: - origin, terrain = func(cfg, difficulty) - self._add_terrain_to_map(origin, terrain, matrix) - else: - raise NotImplementedError(f"Terrain type '{t}' is not implemented in {self.__class__.__name__}") - return matrix - - def generate_terrain(self, config: TerrainConfig = None, type: str = "trimesh"): - """Generate terrain based on the specified type and parameters.""" - if config is not None: - self._parse_cfg(config) - - assert hasattr(self, "config"), "Terrain configuration must be set before generating terrain." - difficulty_list = ( - np.linspace( - self.config.difficulty[0], self.config.difficulty[1], num=self.config.repeat_direction_gap[0] + 1 - ).tolist() - if self.config.difficulty[2] == "linear" - else [self.config.difficulty[0]] * (self.config.repeat_direction_gap[0] + 1) - ) - self.height_mat = self.generate_matrix(difficulty_list.pop(0)) - self.height_mat = self._repeat_terrain(*self.config.repeat_direction_gap, difficulty_list) - row_padding_size = self.config.margin_num_rows - col_padding_size = self.config.margin_num_cols - self.height_mat_pad = np.pad( - self.height_mat, - ((row_padding_size, row_padding_size), (col_padding_size, col_padding_size)), - mode="constant", - constant_values=0, - ) - if type == "trimesh": - vertices, triangles = terrain_utils.convert_heightfield_to_trimesh( - height_field_raw=self.height_mat_pad, - horizontal_scale=self.horizontal_scale, - vertical_scale=self.vertical_scale, - slope_threshold=0.1, - ) - - return vertices, triangles - elif type == "heightfield": - return self.height_mat_pad * self.vertical_scale - - @property - def height_measure(self): - """Get the height map of the generated terrain.""" - return self.height_mat * self.vertical_scale - - @property - def height_measure_pad(self): - """Get the padded height map of the generated terrain.""" - return self.height_mat_pad * self.vertical_scale diff --git a/roboverse_learn/rl/unitree_rl/helper/utils.py b/roboverse_learn/rl/unitree_rl/helper/utils.py index 3efe1da33..acbb871bb 100644 --- a/roboverse_learn/rl/unitree_rl/helper/utils.py +++ b/roboverse_learn/rl/unitree_rl/helper/utils.py @@ -90,6 +90,12 @@ def get_args(test=False): "default": "isaacgym", "help": "simulator type, currently only isaacgym is supported", }, + { + "name": "--ground", + "type": str, + "default": None, + "help": "The ground to load.", + }, { "name": "--headless", "action": "store_true", @@ -125,7 +131,7 @@ def get_args(test=False): "action": "store_true", "default": False, "help": "Whether to load the JIT model", - }, + } # {"name": "--run_name", "type": str, "required": True if not test else False, "help": "Name of the run. Overrides config file if provided."}, # {"name": "--load_run", "type": str, "default": None, "help": "Path to the config file. If provided, will override command line arguments."}, # {"name": "--use_wandb", "action": "store_true", "default": True, "help": "Use wandb for logging"}, diff --git a/roboverse_learn/rl/unitree_rl/main.py b/roboverse_learn/rl/unitree_rl/main.py index fba6f9fbc..0af475830 100644 --- a/roboverse_learn/rl/unitree_rl/main.py +++ b/roboverse_learn/rl/unitree_rl/main.py @@ -46,6 +46,11 @@ def prepare(args): if args.objects: overrides["objects"] = make_objects(args.objects) + ## Set + if args.ground: + overrides["ground"] = args.ground + + scenario.update(**overrides) device = "cpu" if args.sim == "mujoco" else ("cuda" if torch.cuda.is_available() else "cpu") diff --git a/roboverse_pack/grounds/__init__.py b/roboverse_pack/grounds/__init__.py new file mode 100644 index 000000000..c70660202 --- /dev/null +++ b/roboverse_pack/grounds/__init__.py @@ -0,0 +1,9 @@ +"""Ground configuration definitions for various custom environments.""" +# ruff: noqa: F401 + +from .gap_cfg import SingleGapCfg +from .obstacle_cfg import ObstacleFieldCfg +from .pit_cfg import SinglePitCfg +from .slope_cfg import SingleSlopeCfg +from .stair_cfg import SingleStairCfg +from .stone_cfg import SteppingStoneCfg diff --git a/roboverse_pack/grounds/gap_cfg.py b/roboverse_pack/grounds/gap_cfg.py new file mode 100644 index 000000000..20f5d5c06 --- /dev/null +++ b/roboverse_pack/grounds/gap_cfg.py @@ -0,0 +1,27 @@ +from metasim.scenario.grounds import GapCfg, GroundCfg +from metasim.utils import configclass + + +@configclass +class SingleGapCfg(GroundCfg): + """Ground with a single traversable gap.""" + + width: float = 10.0 + length: float = 10.0 + gap_size: float = 1.0 + platform_size: float = 2.0 + + def __post_init__(self): + super().__post_init__() + + for key in self.elements.keys(): + self.elements[key].clear() + + self.elements["gap"].append( + GapCfg( + origin=[0.0, 0.0], + size=[self.width, self.length], + gap_size=self.gap_size, + platform_size=self.platform_size, + ) + ) diff --git a/roboverse_pack/grounds/obstacle_cfg.py b/roboverse_pack/grounds/obstacle_cfg.py new file mode 100644 index 000000000..1bbc6d27f --- /dev/null +++ b/roboverse_pack/grounds/obstacle_cfg.py @@ -0,0 +1,35 @@ +from metasim.scenario.grounds import GroundCfg, ObstacleCfg +from metasim.utils import configclass + + +@configclass +class ObstacleFieldCfg(GroundCfg): + """Ground filled with rectangular obstacles.""" + + width: float = 12.0 + length: float = 12.0 + min_obstacle_size: float = 0.4 + max_obstacle_size: float = 1.0 + num_rectangles: int = 25 + max_height: float = 0.25 + platform_size: float = 1.5 + + def __post_init__(self): + super().__post_init__() + + for key in self.elements.keys(): + self.elements[key].clear() + + self.elements["obstacle"].append( + ObstacleCfg( + origin=[0.0, 0.0], + size=[self.width, self.length], + rectangle_params=[ + self.min_obstacle_size, + self.max_obstacle_size, + self.num_rectangles, + ], + max_height=self.max_height, + platform_size=self.platform_size, + ) + ) diff --git a/roboverse_pack/grounds/pit_cfg.py b/roboverse_pack/grounds/pit_cfg.py new file mode 100644 index 000000000..a773f6425 --- /dev/null +++ b/roboverse_pack/grounds/pit_cfg.py @@ -0,0 +1,27 @@ +from metasim.scenario.grounds import GroundCfg, PitCfg +from metasim.utils import configclass + + +@configclass +class SinglePitCfg(GroundCfg): + """Ground with a centered pit.""" + + width: float = 10.0 + length: float = 10.0 + depth: float = 0.8 + platform_size: float = 2.0 + + def __post_init__(self): + super().__post_init__() + + for key in self.elements.keys(): + self.elements[key].clear() + + self.elements["pit"].append( + PitCfg( + origin=[0.0, 0.0], + size=[self.width, self.length], + depth=self.depth, + platform_size=self.platform_size, + ) + ) diff --git a/roboverse_pack/grounds/slope_cfg.py b/roboverse_pack/grounds/slope_cfg.py new file mode 100644 index 000000000..dbad926a7 --- /dev/null +++ b/roboverse_pack/grounds/slope_cfg.py @@ -0,0 +1,27 @@ +from metasim.scenario.grounds import GroundCfg, SlopeCfg +from metasim.utils import configclass + + +@configclass +class SingleSlopeCfg(GroundCfg): + """Ground made of a single slope covering the full area.""" + + width: float = 10.0 + length: float = 10.0 + repeat_direction_gap = (1, "row", 5.0) + + def __post_init__(self): + super().__post_init__() + + for key in self.elements.keys(): + self.elements[key].clear() + + self.elements["slope"].append( + SlopeCfg( + origin=[0.0, 0.0], + size=[self.width, self.length], + slope=0.2, + random=False, + platform_size=2.0, + ) + ) diff --git a/roboverse_pack/grounds/stair_cfg.py b/roboverse_pack/grounds/stair_cfg.py new file mode 100644 index 000000000..a1649ce05 --- /dev/null +++ b/roboverse_pack/grounds/stair_cfg.py @@ -0,0 +1,28 @@ +from metasim.scenario.grounds import GroundCfg, StairCfg +from metasim.utils import configclass + + +@configclass +class SingleStairCfg(GroundCfg): + """Ground composed of a single staircase structure.""" + + width: float = 10.0 + length: float = 12.0 + step_width: float = 0.35 + step_height: float = 0.12 + platform_size: float = 2.0 + + def __post_init__(self): + super().__post_init__() + + for key in self.elements.keys(): + self.elements[key].clear() + + self.elements["stair"].append( + StairCfg( + origin=[0.0, 0.0], + size=[self.width, self.length], + step=[self.step_width, self.step_height], + platform_size=self.platform_size, + ) + ) diff --git a/roboverse_pack/grounds/stone_cfg.py b/roboverse_pack/grounds/stone_cfg.py new file mode 100644 index 000000000..18a5cd6b8 --- /dev/null +++ b/roboverse_pack/grounds/stone_cfg.py @@ -0,0 +1,30 @@ +from metasim.scenario.grounds import GroundCfg, StoneCfg +from metasim.utils import configclass + + +@configclass +class SteppingStoneCfg(GroundCfg): + """Ground with sparse stepping stones over a height field.""" + + width: float = 10.0 + length: float = 14.0 + stone_size: float = 0.7 + stone_spacing: float = 0.5 + max_height: float = 0.2 + platform_size: float = 1.5 + + def __post_init__(self): + super().__post_init__() + + for key in self.elements.keys(): + self.elements[key].clear() + + self.elements["stone"].append( + StoneCfg( + origin=[0.0, 0.0], + size=[self.width, self.length], + stone_params=[self.stone_size, self.stone_spacing], + max_height=self.max_height, + platform_size=self.platform_size, + ) + ) diff --git a/roboverse_pack/robots/g1_cfg.py b/roboverse_pack/robots/g1_cfg.py index 10c3f2bd6..82171d132 100644 --- a/roboverse_pack/robots/g1_cfg.py +++ b/roboverse_pack/robots/g1_cfg.py @@ -19,7 +19,7 @@ class G1Dof12Cfg(RobotCfg): enabled_gravity: bool = True fix_base_link: bool = False enabled_self_collisions: bool = True - # isaacgym_read_mjcf = False + isaacgym_read_mjcf = False isaacgym_flip_visual_attachments: bool = False collapse_fixed_joints: bool = False # True @@ -57,22 +57,6 @@ class G1Dof12Cfg(RobotCfg): "right_ankle_roll_joint": (-0.2618, 0.2618), } - # torque_limits: dict[str, float] = { - # # Hips & legs - # "left_hip_pitch_joint": 88, - # "left_hip_roll_joint": 139, - # "left_hip_yaw_joint": 88, - # "left_knee_joint": 139, - # "left_ankle_pitch_joint": 25, - # "left_ankle_roll_joint": 25, - # "right_hip_pitch_joint": 88, - # "right_hip_roll_joint": 139, - # "right_hip_yaw_joint": 88, - # "right_knee_joint": 139, - # "right_ankle_pitch_joint": 25, - # "right_ankle_roll_joint": 25, - # } - default_joint_positions: dict[str, float] = { # Hips & legs "left_hip_pitch_joint": -0.1, @@ -166,23 +150,6 @@ class G1Dof23Cfg(G1Dof12Cfg): "right_wrist_roll_joint": (-1.972222, 1.972222), } - # torque_limits = { - # **G1Dof12Cfg().torque_limits, - # # Waist - # "waist_yaw_joint": 88, - # # Shoulders & arms - # "left_shoulder_pitch_joint": 25, - # "left_shoulder_roll_joint": 25, - # "left_shoulder_yaw_joint": 25, - # "left_elbow_joint": 25, - # "left_wrist_roll_joint": 25, - # "right_shoulder_pitch_joint": 25, - # "right_shoulder_roll_joint": 25, - # "right_shoulder_yaw_joint": 25, - # "right_elbow_joint": 25, - # "right_wrist_roll_joint": 25, - # } - default_joint_positions = { **G1Dof12Cfg().default_joint_positions, # Waist @@ -291,12 +258,6 @@ class G1Dof29Cfg(G1Dof27Cfg): "waist_pitch_joint": (-0.52, 0.52), } - # torque_limits = { - # **G1Dof27Cfg().torque_limits, - # "waist_roll_joint": 25, - # "waist_pitch_joint": 25, - # } - default_joint_positions = { **G1Dof27Cfg().default_joint_positions, "waist_roll_joint": 0.0, @@ -309,27 +270,6 @@ class G1Dof29Cfg(G1Dof27Cfg): "waist_pitch_joint": "effort", } - # def __post_init__(self): - # self.cameras: list = [ - # PinholeCameraCfg( - # name="front_cam", - # data_types=["rgb"], - # height=480, - # width=640, - # focal_length=7.6, - # focus_distance=400.0, - # horizontal_aperture=20.0, - # clipping_range=(0.1, 1.0e5), - # mount_to=self.name, - # mount_link="d435_link", - # mount_pos=(0, 0.0, 0), - # # mount_quat=(0.5, -0.5, 0.5, -0.5), # ros convention - # mount_quat=(1, 0, 0, 0), # world convention - # # update_period: float = 0.02, - # ) - # ] - # return super().__post_init__() - @configclass class G1Dof29Dex3Cfg(G1Dof29Cfg): @@ -377,25 +317,6 @@ class G1Dof29Dex3Cfg(G1Dof29Cfg): "right_hand_index_1_joint": (0.0, 1.74532925), } - # torque_limits = { - # **G1Dof29Cfg().torque_limits, - # # Hands - # "left_hand_thumb_0_joint": 2.45, - # "left_hand_thumb_1_joint": 1.4, - # "left_hand_thumb_2_joint": 1.4, - # "left_hand_middle_0_joint": 1.4, - # "left_hand_middle_1_joint": 1.4, - # "left_hand_index_0_joint": 1.4, - # "left_hand_index_1_joint": 1.4, - # "right_hand_thumb_0_joint": 2.45, - # "right_hand_thumb_1_joint": 1.4, - # "right_hand_thumb_2_joint": 1.4, - # "right_hand_middle_0_joint": 1.4, - # "right_hand_middle_1_joint": 1.4, - # "right_hand_index_0_joint": 1.4, - # "right_hand_index_1_joint": 1.4, - # } - default_joint_positions = { **G1Dof29Cfg().default_joint_positions, # Hands From 792e9b2637e0a9b6c89104ecdc4a0466cc39a96a Mon Sep 17 00:00:00 2001 From: Mingyuan Sun <64095400+myuansun@users.noreply.github.com> Date: Wed, 3 Dec 2025 07:27:25 +0800 Subject: [PATCH 03/50] [add] add rsl-rl algorithm (#705) * [update] rslrl ppo extracted * [fix] change walk_g1 task to clean rl style * standarize algorithm configs * [update] Change RLTask to adapt to rsl-rl framework * [update] change saving directory * [update] doc * [fix] isaacgym import for sac * [update] refractor config system * [delete] redundant code removal --- .../reinforcement_learning/ppo.md | 21 +- metasim/task/rl_task.py | 57 +++- roboverse_learn/rl/clean_rl/ppo.py | 97 +------ roboverse_learn/rl/clean_rl/sac.py | 76 +---- roboverse_learn/rl/clean_rl/td3.py | 82 +----- roboverse_learn/rl/configs/__init__.py | 13 + .../rl/configs/clean_rl/__init__.py | 12 + roboverse_learn/rl/configs/clean_rl/base.py | 52 ++++ roboverse_learn/rl/configs/clean_rl/ppo.py | 51 ++++ roboverse_learn/rl/configs/clean_rl/sac.py | 43 +++ roboverse_learn/rl/configs/clean_rl/td3.py | 45 +++ roboverse_learn/rl/configs/fast_td3.py | 107 +++++++ .../rl_cfg.py => configs/rsl_rl/algorithm.py} | 54 ++-- roboverse_learn/rl/configs/rsl_rl/ppo.py | 129 +++++++++ roboverse_learn/rl/rsl_rl/README.md | 72 +++++ roboverse_learn/rl/rsl_rl/env_wrapper.py | 88 ++++++ roboverse_learn/rl/rsl_rl/eval.py | 138 +++++++++ roboverse_learn/rl/rsl_rl/ppo.py | 108 +++++++ .../unitree_rl/configs/algorithm/__init__.py | 1 - .../loco_manipulation/catch_humanoid.py | 6 +- .../configs/locomotion/walk_g1_dof12.py | 2 +- .../configs/locomotion/walk_g1_dof29.py | 8 +- roboverse_learn/rl/unitree_rl/helper/utils.py | 264 ------------------ roboverse_learn/rl/unitree_rl/main.py | 126 --------- .../rl/unitree_rl/runners/__init__.py | 6 - .../rl/unitree_rl/runners/master.py | 109 -------- .../rl/unitree_rl/runners/rsl_rl.py | 157 ----------- roboverse_learn/rl/unitree_rl/runners/sb3.py | 10 - .../tasks/unitree_rl/base/base_agent.py | 6 + .../unitree_rl/base/base_legged_robot.py | 78 +++++- .../unitree_rl/locomotion/walk_g1_dof12.py | 10 +- .../unitree_rl/locomotion/walk_g1_dof29.py | 10 +- 32 files changed, 1068 insertions(+), 970 deletions(-) create mode 100644 roboverse_learn/rl/configs/__init__.py create mode 100644 roboverse_learn/rl/configs/clean_rl/__init__.py create mode 100644 roboverse_learn/rl/configs/clean_rl/base.py create mode 100644 roboverse_learn/rl/configs/clean_rl/ppo.py create mode 100644 roboverse_learn/rl/configs/clean_rl/sac.py create mode 100644 roboverse_learn/rl/configs/clean_rl/td3.py create mode 100644 roboverse_learn/rl/configs/fast_td3.py rename roboverse_learn/rl/{unitree_rl/configs/algorithm/rsl_rl/rl_cfg.py => configs/rsl_rl/algorithm.py} (80%) create mode 100644 roboverse_learn/rl/configs/rsl_rl/ppo.py create mode 100644 roboverse_learn/rl/rsl_rl/README.md create mode 100644 roboverse_learn/rl/rsl_rl/env_wrapper.py create mode 100644 roboverse_learn/rl/rsl_rl/eval.py create mode 100644 roboverse_learn/rl/rsl_rl/ppo.py delete mode 100644 roboverse_learn/rl/unitree_rl/configs/algorithm/__init__.py delete mode 100644 roboverse_learn/rl/unitree_rl/main.py delete mode 100644 roboverse_learn/rl/unitree_rl/runners/__init__.py delete mode 100644 roboverse_learn/rl/unitree_rl/runners/master.py delete mode 100644 roboverse_learn/rl/unitree_rl/runners/rsl_rl.py delete mode 100644 roboverse_learn/rl/unitree_rl/runners/sb3.py diff --git a/docs/source/roboverse_learn/reinforcement_learning/ppo.md b/docs/source/roboverse_learn/reinforcement_learning/ppo.md index bd3747222..eae9b80c9 100644 --- a/docs/source/roboverse_learn/reinforcement_learning/ppo.md +++ b/docs/source/roboverse_learn/reinforcement_learning/ppo.md @@ -1,6 +1,6 @@ # PPO -RoboVerse provides two PPO implementations with different features and use cases: +RoboVerse provides three PPO implementations with different features and use cases: ## 1. Stable-Baselines3 PPO (Recommended for Beginners) @@ -37,12 +37,29 @@ python roboverse_learn/rl/clean_rl/ppo.py --task reach_origin --robot franka --s ### Configuration -Check the file header in `roboverse_learn/rl/clean_rl/ppo.py` for available configuration options including: +Configuration defaults live in `roboverse_learn/rl/configs/clean_rl/ppo.py` (parsed with `tyro`). Use `--help` for all options, including: - Task selection (`--task`) - Robot type (`--robot`) - Simulator backend (`--sim`) - Training hyperparameters (`--num_envs`, `--learning_rate`, etc.) +## 3. RSL-RL PPO (OnPolicyRunner) + +Based on [rsl_rl](https://github.com/leggedrobotics/rsl_rl) for high-throughput on-policy training with asymmetric observations. + +### Usage + +```bash +# RSL-RL PPO for Unitree G1 walking +python -m roboverse_learn.rl.rsl_rl.ppo --task walk_g1_dof29 --robot g1 --sim isaacgym --num-envs 4096 +``` + +### Configuration + +- Install dependency: `pip install rsl_rl` +- CLI defaults: `roboverse_learn/rl/configs/rsl_rl/ppo.py` (tyro). Run with `--help` to see environment, training, and PPO hyperparameters (`--num-steps-per-env`, `--max-iterations`, `--clip-param`, etc.). +- Outputs: checkpoints, TensorBoard logs, and final scripted policy are saved under `models/{exp_name}/{task}/` by default (override with `--model-dir`). + ## Quick Start Examples For detailed tutorials and infrastructure setup: diff --git a/metasim/task/rl_task.py b/metasim/task/rl_task.py index 10fd1bf41..8a38d5db0 100644 --- a/metasim/task/rl_task.py +++ b/metasim/task/rl_task.py @@ -36,6 +36,10 @@ def __init__( self.robot = scenario.robots[0] self._episode_steps = torch.zeros(self.num_envs, dtype=torch.int32, device=self.device) + # Observation buffers for RSL-RL compatibility + self._obs_buf = None + self._priv_obs_buf = None + # convert list state to tensor state for reset acceleration self._initial_states = list_state_to_tensor(self.handler, self._get_initial_states(), self.device) # first reset @@ -113,7 +117,16 @@ def reset(self, states=None, env_ids=None) -> tuple[torch.Tensor, dict]: states = self.handler.get_states() first_obs = self._observation(states).to(self.device) self._raw_observation_cache = first_obs.clone() - info = {"privileged_observation": self._privileged_observation(states)} + priv_obs = self._privileged_observation(states) + + # Update observation buffers for RSL-RL compatibility + self._obs_buf = first_obs + if isinstance(priv_obs, torch.Tensor): + self._priv_obs_buf = priv_obs.to(self.device) + else: + self._priv_obs_buf = first_obs + + info = {"privileged_observation": priv_obs} return first_obs, info def step( @@ -138,6 +151,13 @@ def step( terminated = self._terminated(states).bool().to(self.device) time_out = self._time_out(states).bool().to(self.device) + # Cache observations for RSL-RL compatibility + self._obs_buf = obs + if isinstance(priv_obs, torch.Tensor): + self._priv_obs_buf = priv_obs.to(self.device) + else: + self._priv_obs_buf = obs + episode_done = terminated | time_out info = { "privileged_observation": priv_obs, @@ -194,3 +214,38 @@ def _terminated(self, env_states) -> torch.Tensor: def _prepare_states(self, env_states, env_ids) -> torch.Tensor: """Prepare for the states before reset(do domain randomization).""" return env_states + + # ------------------------------------------------------------------------- + # RSL-RL compatibility properties + # ------------------------------------------------------------------------- + + @property + def obs_buf(self) -> torch.Tensor: + """Cached observation buffer for RSL-RL compatibility. + + This property enables RLTaskEnv-based environments to work with + RSL-RL's OnPolicyRunner without needing a wrapper. + """ + if self._obs_buf is None: + # Lazy initialization on first access + states = self.handler.get_states() + self._obs_buf = self._observation(states).to(self.device) + return self._obs_buf + + @property + def priv_obs_buf(self) -> torch.Tensor: + """Cached privileged observation buffer for RSL-RL compatibility. + + Returns privileged observations if available, otherwise returns + the same as obs_buf (symmetric actor-critic). + """ + if self._priv_obs_buf is None: + # Lazy initialization on first access + states = self.handler.get_states() + priv_obs = self._privileged_observation(states) + if isinstance(priv_obs, torch.Tensor): + self._priv_obs_buf = priv_obs.to(self.device) + else: + # Fallback to symmetric observations + self._priv_obs_buf = self.obs_buf + return self._priv_obs_buf diff --git a/roboverse_learn/rl/clean_rl/ppo.py b/roboverse_learn/rl/clean_rl/ppo.py index 1f07dcc34..9faa195d3 100644 --- a/roboverse_learn/rl/clean_rl/ppo.py +++ b/roboverse_learn/rl/clean_rl/ppo.py @@ -3,11 +3,13 @@ # This file is based on CleanRL's PPO implementation and has been adapted for RoboVerse. # Original CleanRL code is licensed under MIT License. -import os import random import time -from dataclasses import dataclass -from typing import Literal + +try: + import isaacgym # noqa: F401 +except ImportError: + pass import gymnasium as gym import numpy as np @@ -20,96 +22,13 @@ from torch.utils.tensorboard import SummaryWriter # RoboVerse imports -try: - import isaacgym # noqa: F401 -except ImportError: - pass rootutils.setup_root(__file__, pythonpath=True) from gymnasium import make_vec import metasim # noqa: F401 from roboverse_learn.rl.episode_tracker import EpisodeTracker - - -@dataclass -class Args: - exp_name: str = os.path.basename(__file__)[: -len(".py")] - """the name of this experiment""" - seed: int = 1 - """seed of the experiment""" - torch_deterministic: bool = True - """if toggled, `torch.backends.cudnn.deterministic=False`""" - cuda: bool = True - """if toggled, cuda will be enabled by default""" - track: bool = False - """if toggled, this experiment will be tracked with Weights and Biases""" - wandb_project_name: str = "cleanRL" - """the wandb's project name""" - wandb_entity: str = None - """the entity (team) of wandb's project""" - capture_video: bool = False - """whether to capture videos of the agent performances (check out `videos` folder)""" - save_model: bool = False - """whether to save model into the `runs/{run_name}` folder""" - upload_model: bool = False - """whether to upload the saved model to huggingface""" - hf_entity: str = "" - """the user or org name of the model repository from the Hugging Face Hub""" - - # RoboVerse specific arguments - task: str = "stand" - """the RoboVerse task name""" - robot: str = "h1" - """the robot type""" - sim: Literal["isaaclab", "isaacgym", "mujoco", "genesis", "mjx"] = "mjx" - """the simulator backend""" - headless: bool = False - """whether to run in headless mode""" - device: str = "cuda" - """device to run on""" - - # Algorithm specific arguments - total_timesteps: int = 10000000 - """total timesteps of the experiments""" - learning_rate: float = 3e-4 - """the learning rate of the optimizer""" - num_envs: int = 2048 - """the number of parallel game environments""" - num_steps: int = 64 - """the number of steps to run in each environment per policy rollout""" - anneal_lr: bool = True - """Toggle learning rate annealing for policy and value networks""" - gamma: float = 0.99 - """the discount factor gamma""" - gae_lambda: float = 0.95 - """the lambda for the general advantage estimation""" - num_minibatches: int = 128 - """the number of mini-batches""" - update_epochs: int = 5 - """the K epochs to update the policy""" - norm_adv: bool = True - """Toggles advantages normalization""" - clip_coef: float = 0.2 - """the surrogate clipping coefficient""" - clip_vloss: bool = True - """Toggles whether or not to use a clipped loss for the value function, as per the paper.""" - ent_coef: float = 0.0 - """coefficient of the entropy""" - vf_coef: float = 0.5 - """coefficient of the value function""" - max_grad_norm: float = 0.5 - """the maximum norm for the gradient clipping""" - target_kl: float = None - """the target KL divergence threshold""" - - # to be filled in runtime - batch_size: int = 0 - """the batch size (computed in runtime)""" - minibatch_size: int = 0 - """the mini-batch size (computed in runtime)""" - num_iterations: int = 0 - """the number of iterations (computed in runtime)""" +from roboverse_learn.rl.configs.clean_rl.ppo import CleanRLPPOConfig @@ -169,7 +88,7 @@ def get_action_and_value(self, x, action=None): if __name__ == "__main__": - args = tyro.cli(Args) + args = tyro.cli(CleanRLPPOConfig) args.batch_size = int(args.num_envs * args.num_steps) args.minibatch_size = int(args.batch_size // args.num_minibatches) args.num_iterations = args.total_timesteps // args.batch_size @@ -178,7 +97,7 @@ def get_action_and_value(self, x, action=None): import wandb wandb.init( - project=args.wandb_project_name, + project=args.wandb_project, entity=args.wandb_entity, sync_tensorboard=True, config=vars(args), diff --git a/roboverse_learn/rl/clean_rl/sac.py b/roboverse_learn/rl/clean_rl/sac.py index d55fa9449..300d751f5 100644 --- a/roboverse_learn/rl/clean_rl/sac.py +++ b/roboverse_learn/rl/clean_rl/sac.py @@ -3,12 +3,15 @@ # This file is based on CleanRL's SAC implementation and has been adapted for RoboVerse. # Original CleanRL code is licensed under MIT License. -import os import random import time -from dataclasses import dataclass from typing import Literal +try: + import isaacgym # noqa: F401 +except ImportError: + pass + import gymnasium as gym import numpy as np import rootutils @@ -20,10 +23,6 @@ from torch.utils.tensorboard import SummaryWriter # RoboVerse imports -try: - import isaacgym # noqa: F401 -except ImportError: - pass rootutils.setup_root(__file__, pythonpath=True) from gymnasium import make_vec @@ -31,66 +30,7 @@ from roboverse_learn.rl.clean_rl.buffer import ReplayBuffer from roboverse_learn.rl.episode_tracker import EpisodeTracker - - -@dataclass -class Args: - exp_name: str = os.path.basename(__file__)[: -len(".py")] - """the name of this experiment""" - seed: int = 1 - """seed of the experiment""" - torch_deterministic: bool = True - """if toggled, `torch.backends.cudnn.deterministic=False`""" - cuda: bool = True - """if toggled, cuda will be enabled by default""" - track: bool = False - """if toggled, this experiment will be tracked with Weights and Biases""" - wandb_project_name: str = "cleanRL" - """the wandb's project name""" - wandb_entity: str = None - """the entity (team) of wandb's project""" - capture_video: bool = False - """whether to capture videos of the agent performances (check out `videos` folder)""" - - # RoboVerse specific arguments - task: str = "reach_origin" - """the RoboVerse task name""" - robot: str = "franka" - """the robot type""" - sim: Literal["isaaclab", "isaacgym", "mujoco", "genesis", "mjx"] = "mjx" - """the simulator backend""" - headless: bool = False - """whether to run in headless mode""" - device: str = "cuda" - """device to run on""" - - """the environment id of the task (for non-RoboVerse environments)""" - total_timesteps: int = 1000000 - """total timesteps of the experiments""" - num_envs: int = 128 - """the number of parallel game environments""" - buffer_size: int = int(1e6) - """the replay memory buffer size""" - gamma: float = 0.99 - """the discount factor gamma""" - tau: float = 0.005 - """target smoothing coefficient (default: 0.005)""" - batch_size: int = 256 - """the batch size of sample from the reply memory""" - learning_starts: int = 10 - """timestep to start learning""" - policy_lr: float = 3e-4 - """the learning rate of the policy network optimizer""" - q_lr: float = 1e-3 - """the learning rate of the Q network network optimizer""" - policy_frequency: int = 2 - """the frequency of training policy (delayed)""" - target_network_frequency: int = 1 # Denis Yarats' implementation delays this by 2. - """the frequency of updates for the target nerworks""" - alpha: float = 0.2 - """Entropy regularization coefficient.""" - autotune: bool = True - """automatic tuning of the entropy coefficient""" +from roboverse_learn.rl.configs.clean_rl.sac import CleanRLSACConfig def make_roboverse_env(args): @@ -181,13 +121,13 @@ def get_action(self, x): if __name__ == "__main__": - args = tyro.cli(Args) + args = tyro.cli(CleanRLSACConfig) run_name = f"{args.exp_name}__{args.seed}__{int(time.time())}" if args.track: import wandb wandb.init( - project=args.wandb_project_name, + project=args.wandb_project, entity=args.wandb_entity, sync_tensorboard=True, config=vars(args), diff --git a/roboverse_learn/rl/clean_rl/td3.py b/roboverse_learn/rl/clean_rl/td3.py index af6387cc6..9d8f6e94a 100644 --- a/roboverse_learn/rl/clean_rl/td3.py +++ b/roboverse_learn/rl/clean_rl/td3.py @@ -3,11 +3,13 @@ # This file is based on CleanRL's TD3 implementation and has been adapted for RoboVerse. # Original CleanRL code is licensed under MIT License. -import os import random import time -from dataclasses import dataclass -from typing import Literal + +try: + import isaacgym # noqa: F401 +except ImportError: + pass import gymnasium as gym import numpy as np @@ -20,10 +22,6 @@ from torch.utils.tensorboard import SummaryWriter # RoboVerse imports -try: - import isaacgym # noqa: F401 -except ImportError: - pass rootutils.setup_root(__file__, pythonpath=True) from gymnasium import make_vec @@ -31,71 +29,7 @@ from roboverse_learn.rl.clean_rl.buffer import ReplayBuffer from roboverse_learn.rl.episode_tracker import EpisodeTracker - - -@dataclass -class Args: - exp_name: str = os.path.basename(__file__)[: -len(".py")] - """the name of this experiment""" - seed: int = 1 - """seed of the experiment""" - torch_deterministic: bool = True - """if toggled, `torch.backends.cudnn.deterministic=False`""" - cuda: bool = True - """if toggled, cuda will be enabled by default""" - track: bool = False - """if toggled, this experiment will be tracked with Weights and Biases""" - wandb_project_name: str = "cleanRL" - """the wandb's project name""" - wandb_entity: str = None - """the entity (team) of wandb's project""" - capture_video: bool = False - """whether to capture videos of the agent performances (check out `videos` folder)""" - save_model: bool = False - """whether to save model into the `runs/{run_name}` folder""" - upload_model: bool = False - """whether to upload the saved model to huggingface""" - hf_entity: str = "" - """the user or org name of the model repository from the Hugging Face Hub""" - - # RoboVerse specific arguments - task: str = "reach_origin" - """the RoboVerse task name""" - robot: str = "franka" - """the robot type""" - sim: Literal["isaaclab", "isaacgym", "mujoco", "genesis", "mjx"] = "mjx" - """the simulator backend""" - headless: bool = False - """whether to run in headless mode""" - device: str = "cuda" - """device to run on""" - - # Algorithm specific arguments - """the id of the environment""" - total_timesteps: int = 10000 - """total timesteps of the experiments""" - learning_rate: float = 3e-4 - """the learning rate of the optimizer""" - num_envs: int = 128 - """the number of parallel game environments""" - buffer_size: int = int(1e6) - """the replay memory buffer size""" - gamma: float = 0.99 - """the discount factor gamma""" - tau: float = 0.005 - """target smoothing coefficient (default: 0.005)""" - batch_size: int = 256 - """the batch size of sample from the reply memory""" - policy_noise: float = 0.2 - """the scale of policy noise""" - exploration_noise: float = 0.1 - """the scale of exploration noise""" - learning_starts: int = 10 - """timestep to start learning""" - policy_frequency: int = 2 - """the frequency of training policy (delayed)""" - noise_clip: float = 0.5 - """noise clip parameter of the Target Policy Smoothing Regularization""" +from roboverse_learn.rl.configs.clean_rl.td3 import CleanRLTD3Config @@ -164,13 +98,13 @@ def forward(self, x): if __name__ == "__main__": - args = tyro.cli(Args) + args = tyro.cli(CleanRLTD3Config) run_name = f"{args.exp_name}__{args.seed}__{int(time.time())}" if args.track: import wandb wandb.init( - project=args.wandb_project_name, + project=args.wandb_project, entity=args.wandb_entity, sync_tensorboard=True, config=vars(args), diff --git a/roboverse_learn/rl/configs/__init__.py b/roboverse_learn/rl/configs/__init__.py new file mode 100644 index 000000000..d07af8802 --- /dev/null +++ b/roboverse_learn/rl/configs/__init__.py @@ -0,0 +1,13 @@ +from roboverse_learn.rl.configs.clean_rl.ppo import CleanRLPPOConfig +from roboverse_learn.rl.configs.clean_rl.td3 import CleanRLTD3Config +from roboverse_learn.rl.configs.clean_rl.sac import CleanRLSACConfig +from roboverse_learn.rl.configs.rsl_rl.ppo import RslRlPPOConfig +from roboverse_learn.rl.configs.fast_td3 import FastTD3Config + +__all__ = [ + "CleanRLPPOConfig", + "CleanRLTD3Config", + "CleanRLSACConfig", + "RslRlPPOConfig", + "FastTD3Config", +] diff --git a/roboverse_learn/rl/configs/clean_rl/__init__.py b/roboverse_learn/rl/configs/clean_rl/__init__.py new file mode 100644 index 000000000..42ca6cdc4 --- /dev/null +++ b/roboverse_learn/rl/configs/clean_rl/__init__.py @@ -0,0 +1,12 @@ +from roboverse_learn.rl.configs.clean_rl.base import BaseRLConfig, SimBackend +from roboverse_learn.rl.configs.clean_rl.ppo import CleanRLPPOConfig +from roboverse_learn.rl.configs.clean_rl.td3 import CleanRLTD3Config +from roboverse_learn.rl.configs.clean_rl.sac import CleanRLSACConfig + +__all__ = [ + "BaseRLConfig", + "SimBackend", + "CleanRLPPOConfig", + "CleanRLTD3Config", + "CleanRLSACConfig", +] diff --git a/roboverse_learn/rl/configs/clean_rl/base.py b/roboverse_learn/rl/configs/clean_rl/base.py new file mode 100644 index 000000000..fea9dd6cc --- /dev/null +++ b/roboverse_learn/rl/configs/clean_rl/base.py @@ -0,0 +1,52 @@ +from typing import Literal, Optional + +from metasim.utils import configclass + + +SimBackend = Literal[ + "isaacgym", + "isaacsim", + "isaaclab", + "mujoco", + "genesis", + "mjx", +] + + +@configclass +class BaseRLConfig: + """Base configuration for all RL algorithms in RoboVerse.""" + + # Experiment + exp_name: str = "rl_experiment" + seed: int = 1 + torch_deterministic: bool = True + + # Device + cuda: bool = True + device: str = "cuda:0" + + # Task & Environment + task: str = "walk_g1_dof29" + robot: str = "g1_dof29" + sim: SimBackend = "isaacgym" + num_envs: int = 4096 + headless: bool = False + + # Training + max_iterations: int = 50000 + save_interval: int = 100 + + # Logging + use_wandb: bool = False + wandb_project: str = "roboverse_rl" + wandb_entity: Optional[str] = None + + # Model directory + model_dir: Optional[str] = None + + def __post_init__(self) -> None: + import os + + if self.model_dir is None: + self.model_dir = os.path.join("outputs", self.exp_name, self.task) diff --git a/roboverse_learn/rl/configs/clean_rl/ppo.py b/roboverse_learn/rl/configs/clean_rl/ppo.py new file mode 100644 index 000000000..8bc5e3308 --- /dev/null +++ b/roboverse_learn/rl/configs/clean_rl/ppo.py @@ -0,0 +1,51 @@ +from typing import Optional + +from metasim.utils import configclass + +from roboverse_learn.rl.configs.clean_rl.base import BaseRLConfig, SimBackend + + +@configclass +class CleanRLPPOConfig(BaseRLConfig): + """CleanRL PPO configuration adapted for RoboVerse.""" + + # Experiment + exp_name: str = "clean_rl_ppo" + + # Tracking / logging flags (CleanRL-style) + track: bool = False + wandb_project: str = "cleanRL" + capture_video: bool = False + save_model: bool = False + upload_model: bool = False + hf_entity: str = "" + + # Task & Environment overrides + task: str = "stand" + robot: str = "h1" + sim: SimBackend = "mjx" + headless: bool = False + device: str = "cuda" + + # Algorithm-specific arguments (CleanRL naming) + total_timesteps: int = 10000000 + learning_rate: float = 3e-4 + num_envs: int = 2048 + num_steps: int = 64 + anneal_lr: bool = True + gamma: float = 0.99 + gae_lambda: float = 0.95 + num_minibatches: int = 128 + update_epochs: int = 5 + norm_adv: bool = True + clip_coef: float = 0.2 + clip_vloss: bool = True + ent_coef: float = 0.0 + vf_coef: float = 0.5 + max_grad_norm: float = 0.5 + target_kl: Optional[float] = None + + # Runtime-computed (filled in main script) + batch_size: int = 0 + minibatch_size: int = 0 + num_iterations: int = 0 diff --git a/roboverse_learn/rl/configs/clean_rl/sac.py b/roboverse_learn/rl/configs/clean_rl/sac.py new file mode 100644 index 000000000..cafa68ae1 --- /dev/null +++ b/roboverse_learn/rl/configs/clean_rl/sac.py @@ -0,0 +1,43 @@ +from typing import Optional + +from metasim.utils import configclass + +from roboverse_learn.rl.configs.clean_rl.base import BaseRLConfig, SimBackend + + +@configclass +class CleanRLSACConfig(BaseRLConfig): + """CleanRL SAC configuration adapted for RoboVerse.""" + + # Experiment + exp_name: str = "sac" + + # Tracking / logging flags (CleanRL-style) + track: bool = False + wandb_project: str = "cleanRL" + capture_video: bool = False + + # RoboVerse specific arguments + task: str = "reach_origin" + robot: str = "franka" + sim: SimBackend = "mjx" + headless: bool = False + device: str = "cuda" + + # Algorithm specific arguments + total_timesteps: int = 1_000_000 + num_envs: int = 128 + buffer_size: int = int(1e6) + gamma: float = 0.99 + tau: float = 0.005 + batch_size: int = 256 + learning_starts: int = 10 + policy_lr: float = 3e-4 + q_lr: float = 1e-3 + policy_frequency: int = 2 + target_network_frequency: int = 1 + alpha: float = 0.2 + autotune: bool = True + + +__all__ = ["CleanRLSACConfig"] diff --git a/roboverse_learn/rl/configs/clean_rl/td3.py b/roboverse_learn/rl/configs/clean_rl/td3.py new file mode 100644 index 000000000..c4fa020f4 --- /dev/null +++ b/roboverse_learn/rl/configs/clean_rl/td3.py @@ -0,0 +1,45 @@ +from typing import Optional + +from metasim.utils import configclass + +from roboverse_learn.rl.configs.clean_rl.base import BaseRLConfig, SimBackend + + +@configclass +class CleanRLTD3Config(BaseRLConfig): + """CleanRL TD3 configuration adapted for RoboVerse.""" + + # Experiment + exp_name: str = "td3" + + # Tracking / logging flags (CleanRL-style) + track: bool = False + wandb_project: str = "cleanRL" + capture_video: bool = False + save_model: bool = False + upload_model: bool = False + hf_entity: str = "" + + # RoboVerse specific arguments + task: str = "reach_origin" + robot: str = "franka" + sim: SimBackend = "mjx" + headless: bool = False + device: str = "cuda" + + # Algorithm specific arguments + total_timesteps: int = 10000 + learning_rate: float = 3e-4 + num_envs: int = 128 + buffer_size: int = int(1e6) + gamma: float = 0.99 + tau: float = 0.005 + batch_size: int = 256 + policy_noise: float = 0.2 + exploration_noise: float = 0.1 + learning_starts: int = 10 + policy_frequency: int = 2 + noise_clip: float = 0.5 + + +__all__ = ["CleanRLTD3Config"] diff --git a/roboverse_learn/rl/configs/fast_td3.py b/roboverse_learn/rl/configs/fast_td3.py new file mode 100644 index 000000000..c49767ebd --- /dev/null +++ b/roboverse_learn/rl/configs/fast_td3.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +from typing import Literal, Optional, Sequence + +from metasim.utils import configclass + +SimBackend = Literal[ + "isaacgym", + "isaacsim", + "isaaclab", + "mujoco", + "genesis", + "mjx", +] + + +@configclass +class FastTD3Config: + """FastTD3 training configuration (Python equivalent of YAML configs).""" + + # Experiment + exp_name: str = "get_started_fttd3" + seed: int = 1 + torch_deterministic: bool = True + + # Device + cuda: bool = True + device: str = "cuda:0" + device_rank: int = 0 + + # Environment + sim: SimBackend = "isaacgym" + robots: Sequence[str] = ("h1",) + task: str = "walk" + decimation: int = 10 + train_or_eval: str = "train" + headless: bool = True + + # Rollout & Timesteps + num_envs: int = 1024 + num_eval_envs: int = 1024 + total_timesteps: int = 1500 + learning_starts: int = 10 + num_steps: int = 1 + + # Replay, Batching, Discounting + buffer_size: int = 20480 + batch_size: int = 32768 + gamma: float = 0.99 + tau: float = 0.1 + + # Update Schedule + policy_frequency: int = 2 + num_updates: int = 12 + + # Optimizer & Network + critic_learning_rate: float = 3e-4 + actor_learning_rate: float = 3e-4 + weight_decay: float = 0.1 + critic_hidden_dim: int = 1024 + actor_hidden_dim: int = 512 + init_scale: float = 0.01 + num_atoms: int = 101 + + # Value Distribution & Exploration + v_min: float = -250.0 + v_max: float = 250.0 + policy_noise: float = 0.001 + std_min: float = 0.001 + std_max: float = 0.4 + noise_clip: float = 0.5 + + # Algorithm Flags + use_cdq: bool = True + compile: bool = True + obs_normalization: bool = True + max_grad_norm: float = 0.0 + amp: bool = True + amp_dtype: str = "fp16" + disable_bootstrap: bool = False + measure_burnin: int = 3 + + # Logging & Checkpointing + wandb_project: str = "get_started_fttd3" + use_wandb: bool = False + wandb_entity: Optional[str] = None + checkpoint_path: Optional[str] = None + eval_interval: int = 700 + save_interval: int = 700 + video_width: int = 1024 + video_height: int = 1024 + max_iterations: int = 50000 + + # Model directory + model_dir: Optional[str] = None + + # Extra fields used by some task-specific configs + state_file_path: Optional[str] = None + + def __post_init__(self) -> None: + import os + + if self.model_dir is None: + self.model_dir = os.path.join("outputs", self.exp_name, self.task) + + +__all__ = ["FastTD3Config"] diff --git a/roboverse_learn/rl/unitree_rl/configs/algorithm/rsl_rl/rl_cfg.py b/roboverse_learn/rl/configs/rsl_rl/algorithm.py similarity index 80% rename from roboverse_learn/rl/unitree_rl/configs/algorithm/rsl_rl/rl_cfg.py rename to roboverse_learn/rl/configs/rsl_rl/algorithm.py index f63677345..b116606ab 100644 --- a/roboverse_learn/rl/unitree_rl/configs/algorithm/rsl_rl/rl_cfg.py +++ b/roboverse_learn/rl/configs/rsl_rl/algorithm.py @@ -6,7 +6,7 @@ from __future__ import annotations from dataclasses import MISSING -from typing import Literal +from typing import List, Literal from metasim.utils import configclass @@ -35,10 +35,10 @@ class RslRlPpoActorCriticCfg: critic_obs_normalization: bool = MISSING """Whether to normalize the observation for the critic network.""" - actor_hidden_dims: list[int] = MISSING + actor_hidden_dims: List[int] = MISSING """The hidden dimensions of the actor network.""" - critic_hidden_dims: list[int] = MISSING + critic_hidden_dims: List[int] = MISSING """The hidden dimensions of the critic network.""" activation: str = MISSING @@ -156,21 +156,6 @@ class RslRlBaseRunnerCfg: The keys of the dictionary are predefined observation sets used by the underlying algorithm and values are lists of observation groups provided by the environment. - - For instance, if the environment provides a dictionary of observations with groups "policy", "images", - and "privileged", these can be mapped to algorithmic observation sets as follows: - - .. code-block:: python - - obs_groups = { - "policy": ["policy", "images"], - "critic": ["policy", "privileged"], - } - - This way, the policy will receive the "policy" and "images" observations, and the critic will - receive the "policy" and "privileged" observations. - - For more details, please check ``vec_env.py`` in the rsl_rl library. """ clip_actions: float | None = None @@ -187,12 +172,7 @@ class RslRlBaseRunnerCfg: """The experiment name.""" run_name: str = "" - """The run name. Default is empty string. - - The name of the run directory is typically the time-stamp at execution. If the run name is not empty, - then it is appended to the run directory's name, i.e. the logging directory's name will become - ``{time-stamp}_{run_name}``. - """ + """The run name. Default is empty string.""" logger: Literal["tensorboard", "neptune", "wandb"] = "tensorboard" """The logger to use. Default is tensorboard.""" @@ -203,23 +183,14 @@ class RslRlBaseRunnerCfg: wandb_project: str = "isaaclab" """The wandb project name. Default is "isaaclab".""" - resume: bool = False - """Whether to resume a previous training. Default is False. - - This flag will be ignored for distillation. - """ + resume: str | None = None + """Resume directory name (timestamp) for training/evaluation.""" load_run: str = ".*" - """The run directory to load. Default is ".*" (all). - - If regex expression, the latest (alphabetical order) matching run will be loaded. - """ + """The run directory to load. Default is ".*" (all).""" load_checkpoint: str = "model_.*.pt" - """The checkpoint file to load. Default is ``"model_.*.pt"`` (all). - - If regex expression, the latest (alphabetical order) matching file will be loaded. - """ + """The checkpoint file to load. Default is ``"model_.*.pt"`` (all).""" @configclass @@ -234,3 +205,12 @@ class RslRlOnPolicyRunnerCfg(RslRlBaseRunnerCfg): algorithm: RslRlPpoAlgorithmCfg = MISSING """The algorithm configuration.""" + + +__all__ = [ + "RslRlPpoActorCriticCfg", + "RslRlPpoActorCriticRecurrentCfg", + "RslRlPpoAlgorithmCfg", + "RslRlBaseRunnerCfg", + "RslRlOnPolicyRunnerCfg", +] diff --git a/roboverse_learn/rl/configs/rsl_rl/ppo.py b/roboverse_learn/rl/configs/rsl_rl/ppo.py new file mode 100644 index 000000000..15d8b24b5 --- /dev/null +++ b/roboverse_learn/rl/configs/rsl_rl/ppo.py @@ -0,0 +1,129 @@ +from __future__ import annotations + +from typing import Dict, List, Literal, Optional + +from metasim.utils import configclass + +SimBackend = Literal[ + "isaacgym", + "isaacsim", + "isaaclab", + "mujoco", + "genesis", + "mjx", +] + +from roboverse_learn.rl.configs.rsl_rl.algorithm import ( + RslRlOnPolicyRunnerCfg, + RslRlPpoActorCriticCfg, + RslRlPpoAlgorithmCfg, +) + + +@configclass +class RslRlPPOConfig(RslRlOnPolicyRunnerCfg): + """RSL-RL PPO configuration mirroring Unitree on-policy train config.""" + + # Experiment / runner settings + exp_name: str = "rsl_rl_ppo" + experiment_name: str = "" # defaults to task name if left empty + run_name: str = "" + seed: int = 1 + num_steps_per_env: int = 24 + max_iterations: int = 50000 + save_interval: int = 100 + empirical_normalization: bool = False + obs_groups: Optional[Dict[str, List[str]]] = None + clip_actions: Optional[float] = None + logger: Literal["tensorboard", "neptune", "wandb"] = "tensorboard" + neptune_project: str = "isaaclab" + wandb_project: str = "rsl_rl_ppo" + resume: Optional[str] = None + load_run: str = ".*" + load_checkpoint: str = "model_.*.pt" + checkpoint: int = -1 + + # Environment / device + task: str = "walk_g1_dof29" + robot: str = "g1_dof29" + sim: SimBackend = "isaacgym" + num_envs: int = 4096 + headless: bool = False + cuda: bool = True + device: str = "cuda:0" + torch_deterministic: bool = True + + # Logging + use_wandb: bool = False + wandb_entity: Optional[str] = None + model_dir: Optional[str] = None + train_cfg: Optional[dict] = None + + # Policy configuration + policy: RslRlPpoActorCriticCfg = RslRlPpoActorCriticCfg( + init_noise_std=1.0, + actor_obs_normalization=False, + critic_obs_normalization=False, + actor_hidden_dims=[512, 256, 128], + critic_hidden_dims=[512, 256, 128], + activation="elu", + ) + + # Algorithm configuration + algorithm: RslRlPpoAlgorithmCfg = RslRlPpoAlgorithmCfg( + value_loss_coef=1.0, + use_clipped_value_loss=True, + clip_param=0.2, + entropy_coef=0.01, + num_learning_epochs=5, + num_mini_batches=4, + learning_rate=1.0e-3, + schedule="adaptive", + gamma=0.99, + lam=0.95, + desired_kl=0.01, + max_grad_norm=1.0, + ) + + def __post_init__(self) -> None: + import os + + if not self.experiment_name: + self.experiment_name = self.task + + if self.model_dir is None: + name = self.exp_name or self.experiment_name + self.model_dir = os.path.join("outputs", name, self.task) + + if self.obs_groups is None: + self.obs_groups = {"policy": ["policy"], "critic": ["policy", "critic"]} + + # Build runner training config for RSL-RL + policy_cfg = self.policy.to_dict() if hasattr(self.policy, "to_dict") else dict(self.policy.__dict__) + algo_cfg = self.algorithm.to_dict() if hasattr(self.algorithm, "to_dict") else dict(self.algorithm.__dict__) + + self.train_cfg = { + "seed": self.seed, + "device": self.device, + "num_steps_per_env": self.num_steps_per_env, + "max_iterations": self.max_iterations, + "save_interval": self.save_interval, + "experiment_name": self.experiment_name, + "empirical_normalization": self.empirical_normalization, + "run_name": self.run_name, + "logger": self.logger, + "neptune_project": self.neptune_project, + "wandb_project": self.wandb_project, + "resume": bool(self.resume), + "load_run": self.load_run, + "load_checkpoint": self.load_checkpoint, + "obs_groups": self.obs_groups, + "policy": policy_cfg, + "algorithm": algo_cfg, + } + + if self.clip_actions is not None: + self.train_cfg["clip_actions"] = self.clip_actions + + +__all__ = ["RslRlPPOConfig"] diff --git a/roboverse_learn/rl/rsl_rl/README.md b/roboverse_learn/rl/rsl_rl/README.md new file mode 100644 index 000000000..ab9b229c3 --- /dev/null +++ b/roboverse_learn/rl/rsl_rl/README.md @@ -0,0 +1,72 @@ +# RSL-RL for RoboVerse + +Proximal Policy Optimization (PPO) implementation using the [rsl_rl](https://github.com/leggedrobotics/rsl_rl) library. + +## Installation + +```bash +pip install rsl_rl +``` + +## Quick Start + +```bash +# Train G1 walking with default settings +python -m roboverse_learn.rl.rsl_rl.ppo \ + --task walk_g1_dof29 \ + --robot g1 \ + --sim isaacgym \ + --num-envs 4096 + +# Train with custom hyperparameters +python -m roboverse_learn.rl.rsl_rl.ppo \ + --task walk_h1_dof29 \ + --learning-rate 5e-4 \ + --num-learning-epochs 8 \ + --clip-param 0.3 + +# Enable WandB logging +python -m roboverse_learn.rl.rsl_rl.ppo \ + --task walk_g1_dof29 \ + --use-wandb \ + --wandb-project my-project +``` + +## Configuration + +All parameters are specified via command-line arguments. Use `--help` to see all options: + +```bash +python -m roboverse_learn.rl.rsl_rl.ppo --help +``` + +### Key Parameters + +**Environment**: +- `--task`: Task name (e.g., walk_g1_dof29, walk_h1_dof29) +- `--robot`: Robot type (g1, h1, etc.) +- `--sim`: Simulator backend (isaacgym, isaacsim, mujoco, mjx) +- `--num-envs`: Number of parallel environments (default: 4096) + +**Training**: +- `--max-iterations`: Training iterations (default: 50000) +- `--num-steps-per-env`: Steps per environment per update (default: 24) +- `--save-interval`: Checkpoint save frequency (default: 100) + +**PPO Algorithm**: +- `--learning-rate`: Learning rate (default: 1e-3) +- `--num-learning-epochs`: Epochs per update (default: 5) +- `--clip-param`: PPO clipping parameter (default: 0.2) +- `--gamma`: Discount factor (default: 0.99) +- `--lam`: GAE lambda (default: 0.95) + +## Output + +Models are saved to `models/{exp_name}/{task}/`: +- Checkpoints: `model_*.pt` (every `save_interval` iterations) +- Final policy: `policy.pt` (JIT-scripted for inference) +- Logs: TensorBoard logs + +## Credits + +Based on [rsl_rl](https://github.com/leggedrobotics/rsl_rl) by Robotic Systems Lab, ETH Zurich. diff --git a/roboverse_learn/rl/rsl_rl/env_wrapper.py b/roboverse_learn/rl/rsl_rl/env_wrapper.py new file mode 100644 index 000000000..319abc057 --- /dev/null +++ b/roboverse_learn/rl/rsl_rl/env_wrapper.py @@ -0,0 +1,88 @@ +from __future__ import annotations +from typing import Union +import torch +from tensordict import TensorDict +from roboverse_pack.tasks.unitree_rl.base import AgentTask + + +class RslRlEnvWrapper: + """Wraps RoboVerse environments for RSL-RL OnPolicyRunner compatibility. + + Works with all RoboVerse environments as they + all provide obs_buf and priv_obs_buf properties. + + Provides the interface expected by rsl_rl.runners.OnPolicyRunner: + - obs_buf as TensorDict with "policy" and "critic" keys + - step() returning (obs, rewards, dones, extras) + - Properties: num_envs, num_actions, max_episode_length, device, cfg + """ + + def __init__(self, env: AgentTask, train_cfg: dict | object = None): + self.env = env + self.train_cfg = train_cfg + + def step(self, actions: torch.Tensor) -> tuple[TensorDict, torch.Tensor, torch.Tensor, dict]: + """Execute actions and return observations, rewards, dones, extras. + + RSL-RL expects combined done flags (terminated OR truncated). + """ + # Call step and get Gymnasium format returns + # Note: We use obs_buf property instead of returned obs_tensor for consistency + _, rewards, terminated, truncated, info = self.env.step(actions) + + # RSL-RL expects combined done flags (terminated OR truncated) + dones = torch.logical_or(terminated, truncated) + + # Merge info into extras + extras = {**getattr(self.env, 'extras', {}), **info} + + # Return RSL-RL format with TensorDict observations + return self.obs_buf, rewards, dones, extras + + def get_observations(self) -> TensorDict: + """Return current observations as TensorDict.""" + return self.obs_buf + + @property + def num_envs(self) -> int: + return self.env.num_envs + + @property + def num_actions(self) -> int: + return self.env.num_actions + + @property + def max_episode_length(self) -> int: + return self.env.max_episode_steps + + @property + def episode_length_buf(self) -> torch.Tensor: + return self.env._episode_steps + + @episode_length_buf.setter + def episode_length_buf(self, value): + self.env._episode_steps = value + + @property + def device(self) -> torch.device: + return self.env.device + + @property + def cfg(self) -> dict | object: + return self.train_cfg + + @property + def obs_buf(self) -> TensorDict: + """Return observations as TensorDict with 'policy' and 'critic' keys. + + RSL-RL expects asymmetric observations: + - policy: observations for actor network + - critic: privileged observations for critic network + + All RoboVerse RL environments provide + obs_buf and priv_obs_buf properties, so we simply wrap them. + """ + return TensorDict( + policy=self.env.obs_buf, + critic=self.env.priv_obs_buf + ) diff --git a/roboverse_learn/rl/rsl_rl/eval.py b/roboverse_learn/rl/rsl_rl/eval.py new file mode 100644 index 000000000..8fe00e339 --- /dev/null +++ b/roboverse_learn/rl/rsl_rl/eval.py @@ -0,0 +1,138 @@ +from __future__ import annotations + +import os +import random + +try: + import isaacgym # noqa: F401 +except ImportError: + pass + +import numpy as np +import rootutils +import torch +import tyro + +rootutils.setup_root(__file__, pythonpath=True) + +from roboverse_learn.rl.configs.rsl_rl.ppo import RslRlPPOConfig +from roboverse_learn.rl.rsl_rl.env_wrapper import RslRlEnvWrapper +from roboverse_learn.rl.unitree_rl.helper import get_load_path, get_log_dir +from metasim.task.registry import get_task_class + + +def make_roboverse_env(args: RslRlPPOConfig): + """Create RoboVerse task environment""" + task_cls = get_task_class(args.task) + + scenario = task_cls.scenario.update( + robots=[args.robot], + simulator=args.sim, + num_envs=args.num_envs, + headless=args.headless, + cameras=[] + ) + device = torch.device(args.device if torch.cuda.is_available() and args.cuda else "cpu") + + env = task_cls(scenario=scenario, device=device) + return env, task_cls + + +def evaluate(args: RslRlPPOConfig): + """Evaluate a trained RSL-RL PPO policy""" + # Setup + random.seed(args.seed) + np.random.seed(args.seed) + torch.manual_seed(args.seed) + torch.backends.cudnn.deterministic = args.torch_deterministic + + device = torch.device(args.device if torch.cuda.is_available() and args.cuda else "cpu") + print(f"Using device: {device}") + + # Create environment + print(f"Creating environment: {args.task} with {args.num_envs} environments") + env, task_cls = make_roboverse_env(args) + + # Load checkpoint + if not args.resume: + raise ValueError("Please provide --resume (timestamp/log dir) for evaluation.") + + # Convert resume string to full log directory path (legacy unitree_rl runner convention) + log_dir = ( + args.resume + if os.path.isdir(args.resume) + else get_log_dir(task_name=args.task, now=args.resume) + ) + + # Use get_load_path helper to handle checkpoint loading logic + # If checkpoint is None, default to -1 (latest checkpoint) + checkpoint_num = args.checkpoint if args.checkpoint is not None else -1 + checkpoint_path = get_load_path(load_root=log_dir, checkpoint=checkpoint_num) + + print(f"Loading checkpoint from {checkpoint_path}") + checkpoint = torch.load(checkpoint_path, map_location=device) + + print(f"Loaded training config from task: {task_cls.__name__}") + + # Create environment wrapper + wrapped_env = RslRlEnvWrapper(env, train_cfg=args.train_cfg) + + # Get observations from environment (needed for resolve_obs_groups) + from rsl_rl.utils import resolve_obs_groups + from rsl_rl.modules import ActorCritic + + obs = wrapped_env.get_observations() + + # Resolve obs_groups (mimicking OnPolicyRunner.__init__) + default_sets = ["critic"] + args.obs_groups = resolve_obs_groups(obs, {}, default_sets) + obs_groups = args.obs_groups + + # Extract policy config + policy_cfg = args.policy + + # Create actor-critic model with obs and obs_groups + actor_critic = ActorCritic( + obs=obs, + obs_groups=obs_groups, + num_actions=env.num_actions, + actor_hidden_dims=policy_cfg.actor_hidden_dims, + critic_hidden_dims=policy_cfg.critic_hidden_dims, + activation=policy_cfg.activation, + init_noise_std=policy_cfg.init_noise_std, + ).to(device) + + # Load the model weights + actor_critic.load_state_dict(checkpoint['model_state_dict']) + actor_critic.eval() + + # Create inference policy (just the actor part) + policy = actor_critic.act_inference + + # Disable curriculum and command resampling for eval + env.cfg.curriculum.enabled = False + env.cfg.commands.resampling_time = 1e6 # effectively disable command changes + + # Reset environment + env.reset() + obs, _, _, _, _ = env.step(torch.zeros(env.num_envs, env.num_actions, device=device)) + obs = wrapped_env.get_observations() + + print(f"Starting evaluation for 1000000 steps...") + for i in range(1000000): + # set fixed command + env.commands_manager.value[:, 0] = 0.5 + env.commands_manager.value[:, 1] = 0.0 + env.commands_manager.value[:, 2] = 0.0 + actions = policy(obs) + obs, _, _, _ = wrapped_env.step(actions) + + if (i + 1) % 1000 == 0: + print(f"Step {i + 1}/1000000") + + print("Evaluation complete!") + + +if __name__ == "__main__": + args = tyro.cli(RslRlPPOConfig) + evaluate(args) diff --git a/roboverse_learn/rl/rsl_rl/ppo.py b/roboverse_learn/rl/rsl_rl/ppo.py new file mode 100644 index 000000000..16d1ce5ea --- /dev/null +++ b/roboverse_learn/rl/rsl_rl/ppo.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +import os +import random + +try: + import isaacgym # noqa: F401 +except ImportError: + pass + +import numpy as np +import rootutils +import torch +import tyro +from rsl_rl.runners import OnPolicyRunner + +rootutils.setup_root(__file__, pythonpath=True) + +from roboverse_learn.rl.configs.rsl_rl.ppo import RslRlPPOConfig +from roboverse_learn.rl.rsl_rl.env_wrapper import RslRlEnvWrapper +from metasim.task.registry import get_task_class + + +def make_roboverse_env(args: RslRlPPOConfig): + """Create RoboVerse task environment""" + task_cls = get_task_class(args.task) + + # Load environment configuration from task + + scenario = task_cls.scenario.update( + robots=[args.robot], + simulator=args.sim, + num_envs=args.num_envs, + headless=args.headless, + cameras=[] + ) + device = torch.device(args.device if torch.cuda.is_available() and args.cuda else "cpu") + + # Pass env_cfg to task constructor + env = task_cls(scenario=scenario, device=device) + return env + + +def train(args: RslRlPPOConfig): + """Train RSL-RL PPO""" + # Setup + random.seed(args.seed) + np.random.seed(args.seed) + torch.manual_seed(args.seed) + torch.backends.cudnn.deterministic = args.torch_deterministic + + device = torch.device(args.device if torch.cuda.is_available() and args.cuda else "cpu") + print(f"Using device: {device}") + os.makedirs(args.model_dir, exist_ok=True) + + # Initialize WandB + if args.use_wandb: + import wandb + wandb.init( + project=args.wandb_project, + entity=args.wandb_entity, + config=vars(args), + name=args.exp_name, + save_code=True + ) + + # Create environment and wrapper + print(f"Creating environment: {args.task} with {args.num_envs} environments") + env = make_roboverse_env(args) + + # Use training config directly from args + train_cfg = args.train_cfg + + # Create environment wrapper + env_wrapper = RslRlEnvWrapper(env, train_cfg=train_cfg) + + + runner = OnPolicyRunner( + env=env_wrapper, + train_cfg=train_cfg, + log_dir=args.model_dir, + device=device + ) + + # Train + print(f"Training RSL-RL PPO on {args.task} with {args.num_envs} environments") + print(f"Model directory: {args.model_dir}") + runner.learn( + num_learning_iterations=args.max_iterations, + init_at_random_ep_len=True + ) + + # Export policy + print("Exporting policy...") + policy = runner.get_inference_policy() + policy_path = os.path.join(args.model_dir, "policy.pt") + torch.jit.script(policy).save(policy_path) + print(f"Policy exported to {policy_path}") + + if args.use_wandb: + wandb.finish() + + print("Training complete!") + + +if __name__ == "__main__": + args = tyro.cli(RslRlPPOConfig) + train(args) diff --git a/roboverse_learn/rl/unitree_rl/configs/algorithm/__init__.py b/roboverse_learn/rl/unitree_rl/configs/algorithm/__init__.py deleted file mode 100644 index bd60c8aff..000000000 --- a/roboverse_learn/rl/unitree_rl/configs/algorithm/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .rsl_rl.rl_cfg import * diff --git a/roboverse_learn/rl/unitree_rl/configs/loco_manipulation/catch_humanoid.py b/roboverse_learn/rl/unitree_rl/configs/loco_manipulation/catch_humanoid.py index 46ea76365..f7d29c3d4 100644 --- a/roboverse_learn/rl/unitree_rl/configs/loco_manipulation/catch_humanoid.py +++ b/roboverse_learn/rl/unitree_rl/configs/loco_manipulation/catch_humanoid.py @@ -1,7 +1,11 @@ from typing import Callable from metasim.utils import configclass from roboverse_learn.rl.unitree_rl.configs.cfg_base import BaseEnvCfg -from roboverse_learn.rl.unitree_rl.configs.algorithm.rsl_rl.rl_cfg import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg +from roboverse_learn.rl.configs.rsl_rl.algorithm import ( + RslRlOnPolicyRunnerCfg, + RslRlPpoActorCriticCfg, + RslRlPpoAlgorithmCfg, +) @configclass class CatchHumanoidTaskCfg(BaseEnvCfg): diff --git a/roboverse_learn/rl/unitree_rl/configs/locomotion/walk_g1_dof12.py b/roboverse_learn/rl/unitree_rl/configs/locomotion/walk_g1_dof12.py index acddf350b..d18b83337 100644 --- a/roboverse_learn/rl/unitree_rl/configs/locomotion/walk_g1_dof12.py +++ b/roboverse_learn/rl/unitree_rl/configs/locomotion/walk_g1_dof12.py @@ -1,7 +1,7 @@ import math from metasim.utils import configclass from roboverse_learn.rl.unitree_rl.configs.cfg_base import BaseEnvCfg -from roboverse_learn.rl.unitree_rl.configs.algorithm import ( +from roboverse_learn.rl.configs.rsl_rl.algorithm import ( RslRlOnPolicyRunnerCfg, RslRlPpoAlgorithmCfg, RslRlPpoActorCriticRecurrentCfg, diff --git a/roboverse_learn/rl/unitree_rl/configs/locomotion/walk_g1_dof29.py b/roboverse_learn/rl/unitree_rl/configs/locomotion/walk_g1_dof29.py index f6bfeb9e2..0d2529feb 100644 --- a/roboverse_learn/rl/unitree_rl/configs/locomotion/walk_g1_dof29.py +++ b/roboverse_learn/rl/unitree_rl/configs/locomotion/walk_g1_dof29.py @@ -3,11 +3,7 @@ from metasim.utils import configclass from roboverse_learn.rl.unitree_rl.configs.cfg_base import BaseEnvCfg -from roboverse_learn.rl.unitree_rl.configs.algorithm import ( - RslRlOnPolicyRunnerCfg, - RslRlPpoActorCriticCfg, - RslRlPpoAlgorithmCfg, -) +from roboverse_learn.rl.configs.rsl_rl.algorithm import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg import roboverse_learn.rl.unitree_rl.helper.curriculum_utils as curr_funs from metasim.queries import ContactForces from roboverse_learn.rl.unitree_rl.configs.cfg_queries import LidarPointCloud @@ -112,7 +108,7 @@ class RewardsScales: }, ) - callbacks_query = {"contact_forces": ContactForces(history_length=3), "lidar_point_cloud": LidarPointCloud(enabled=True)} + callbacks_query = {"contact_forces": ContactForces(history_length=3), "lidar_point_cloud": LidarPointCloud(enabled=False)} callbacks_setup = { "material_randomizer": MaterialRandomizer( obj_name="g1_dof29", diff --git a/roboverse_learn/rl/unitree_rl/helper/utils.py b/roboverse_learn/rl/unitree_rl/helper/utils.py index acbb871bb..9a187e187 100644 --- a/roboverse_learn/rl/unitree_rl/helper/utils.py +++ b/roboverse_learn/rl/unitree_rl/helper/utils.py @@ -3,156 +3,11 @@ from typing import Callable import re import os -import copy -import argparse import datetime -import importlib from loguru import logger as log from functools import lru_cache -import random import torch -import numpy as np - -from metasim.utils.setup_util import get_robot -from metasim.utils.string_util import is_camel_case, is_snake_case, to_camel_case -from metasim.scenario.scenario import ScenarioCfg - - -def parse_arguments(description="humanoid rl task arguments", custom_parameters=None): - """Parse command line arguments.""" - if custom_parameters is None: - custom_parameters = [] - parser = argparse.ArgumentParser(description=description) - for argument in custom_parameters: - if ("name" in argument) and ("type" in argument or "action" in argument): - help_str = "" - if "help" in argument: - help_str = argument["help"] - - if "type" in argument: - if "default" in argument: - parser.add_argument( - argument["name"], - type=argument["type"], - default=argument["default"], - help=help_str, - ) - else: - parser.add_argument( - argument["name"], type=argument["type"], help=help_str - ) - elif "action" in argument: - parser.add_argument( - argument["name"], action=argument["action"], help=help_str - ) - - else: - log.error( - "ERROR: command line argument name, type/action must be defined, argument not added to parser" - ) - log.error("supported keys: name, type, default, action, help") - - return parser.parse_args() - - -def get_args(test=False): - """Get the command line arguments.""" - custom_parameters = [ - { - "name": "--task", - "type": str, - "default": "walk_g1_dof29", - "help": "Task name for training/testing.", - }, - {"name": "--robots", "type": str, "default": "", "help": "The used robots."}, - { - "name": "--objects", - "type": str, - "default": None, - "help": "The used objects.", - }, - { - "name": "--num_envs", - "type": int, - "default": 128, - "help": "number of parallel environments.", - }, - { - "name": "--iter", - "type": int, - "default": 15000, - "help": "Max number of training iterations.", - }, - { - "name": "--sim", - "type": str, - "default": "isaacgym", - "help": "simulator type, currently only isaacgym is supported", - }, - { - "name": "--ground", - "type": str, - "default": None, - "help": "The ground to load.", - }, - { - "name": "--headless", - "action": "store_true", - "default": True, - "help": "Force display off at all times", - }, - { - "name": "--resume", - "type": str, - "default": None, - "help": "Resume training from a checkpoint", - }, - { - "name": "--checkpoint", - "type": int, - "default": -1, - "help": "Saved model checkpoint number. If -1: will load the last checkpoint. Overrides config file if provided.", - }, - { - "name": "--seed", - "type": int, - "default": -1, - "help": "The random seed for the run. If -1, will be randomly generated.", - }, - { - "name": "--eval", - "action": "store_true", - "default": False, - "help": "Whether to run in eval mode", - }, - { - "name": "--jit_load", - "action": "store_true", - "default": False, - "help": "Whether to load the JIT model", - } - # {"name": "--run_name", "type": str, "required": True if not test else False, "help": "Name of the run. Overrides config file if provided."}, - # {"name": "--load_run", "type": str, "default": None, "help": "Path to the config file. If provided, will override command line arguments."}, - # {"name": "--use_wandb", "action": "store_true", "default": True, "help": "Use wandb for logging"}, - # {"name": "--wandb", "type": str, "default": "g1_walking", "help": "Wandb project name"}, - # {"name": "--log", "type": str, "default": None, "help": "log directory. If None, will be set automatically."}, - ] - args = parse_arguments(custom_parameters=custom_parameters) - return args - - -def set_seed(seed=-1): - if seed == -1: - seed = np.random.randint(0, 10000) - log.info(f"Setting seed: {seed}") - - random.seed(seed) - np.random.seed(seed) - torch.manual_seed(seed) - os.environ["PYTHONHASHSEED"] = str(seed) - torch.cuda.manual_seed(seed) - torch.cuda.manual_seed_all(seed) def get_log_dir(task_name: str, now=None) -> str: @@ -166,22 +21,6 @@ def get_log_dir(task_name: str, now=None) -> str: return log_dir -def get_class(name: str, suffix: str, library="roboverse_learn.rl.unitree_rl"): - """Get the class wrappers. - Example: - get_class("ReachOrigin", "Cfg") -> ReachOriginCfg - get_class("reach_origin", "Cfg") -> ReachOriginCfg - """ - if is_camel_case(name): - task_name_camel = name - elif is_snake_case(name): - task_name_camel = to_camel_case(name) - - wrapper_module = importlib.import_module(library) - wrapper_cls = getattr(wrapper_module, f"{task_name_camel}{suffix}") - return wrapper_cls - - def get_load_path(load_root: str, checkpoint: int | str = None) -> str: """Get the path to load the model from.""" if isinstance(checkpoint, int): @@ -202,47 +41,6 @@ def get_load_path(load_root: str, checkpoint: int | str = None) -> str: return load_path -def make_robots(robots_str: str) -> list[any]: - robot_names = robots_str.split() - robots = [] - for _name in robot_names: - robots.append(get_robot(_name)) - return robots - - -def make_objects(objects_str: str) -> list[any]: - object_names = objects_str.split() - objects = [] - for _name in object_names: - objects.append( - get_class( - _name, - suffix="Cfg", - library="roboverse_learn.rl.unitree_rl.configs.cfg_objects", - )() - ) - return objects - - -def find_unique_candidate(candidates: list[any], data_base: list[any]) -> int: - found_candidates = [] - found_indices = [] - - for candidate in candidates: - if candidate in data_base: - found_candidates.append(candidate) - found_indices.append(data_base.index(candidate)) - - if len(found_candidates) == 0: - raise ValueError(f"None of the candidates {candidates} found in {data_base}") - elif len(found_candidates) > 1: - raise ValueError( - f"Multiple candidates found: {found_candidates}. Only one naming convention should be used." - ) - - return found_indices[0] - - def get_indices_from_substring( candidates_list: list[str] | tuple[str] | str, data_base: list[str], @@ -289,68 +87,6 @@ def get_indices_from_substring( return torch.tensor(found_indices, dtype=torch.int32, requires_grad=False) -def reindex_func( - data: torch.Tensor, new_idx: torch.Tensor, start_idx: int | torch.Tensor -) -> torch.Tensor: - assert data.dim() == 2, "data must be a 2D tensor" - assert new_idx.dim() == 1, "new_idx must be a 1D tensor" - reindex_length = len(new_idx) - for start in start_idx: - data[:, start : start + reindex_length] = data[ - :, start : start + reindex_length - ][:, new_idx] - return data - - -class PolicyExporterLSTM(torch.nn.Module): - def __init__(self, actor_critic): - super().__init__() - self.actor = copy.deepcopy(actor_critic.actor) - self.is_recurrent = actor_critic.is_recurrent - self.memory = copy.deepcopy(actor_critic.memory_a.rnn) - self.memory.cpu() - self.register_buffer( - "hidden_state", - torch.zeros(self.memory.num_layers, 1, self.memory.hidden_size), - ) - self.register_buffer( - "cell_state", - torch.zeros(self.memory.num_layers, 1, self.memory.hidden_size), - ) - - def forward(self, x): - out, (h, c) = self.memory(x.unsqueeze(0), (self.hidden_state, self.cell_state)) - self.hidden_state[:] = h - self.cell_state[:] = c - return self.actor(out.squeeze(0)) - - @torch.jit.export - def reset_memory(self): - self.hidden_state[:] = 0.0 - self.cell_state[:] = 0.0 - - def export(self, path): - if not path.endswith(".pt"): - path = os.path.join(path, "policy.pt") - self.to("cpu") - traced_script_module = torch.jit.script(self) - traced_script_module.save(path) - - -def export_policy_as_jit(actor, path, filename=None): - """Export the policy as a JIT model.""" - model = copy.deepcopy(actor).to("cpu") - traced_script_module = torch.jit.script(model) - traced_script_module.save(path) - - -def get_export_jit_path(load_root: str, scenario: ScenarioCfg) -> str: - """Get the path to export the JIT model.""" - exported_root_dir = f"{load_root}/exported" - os.makedirs(exported_root_dir, exist_ok=True) - return f"{load_root}/exported/model_exported_jit.pt" - - def pattern_match(sub_names: dict[str, any], all_names: list[str]) -> dict[str, any]: """Pattern match the sub_names to all_names using regex.""" matched_names = {_key: 0.0 for _key in all_names} diff --git a/roboverse_learn/rl/unitree_rl/main.py b/roboverse_learn/rl/unitree_rl/main.py deleted file mode 100644 index 0af475830..000000000 --- a/roboverse_learn/rl/unitree_rl/main.py +++ /dev/null @@ -1,126 +0,0 @@ -from __future__ import annotations - -import copy - -import rootutils - -rootutils.setup_root(__file__, pythonpath=True) - -try: - import isaacgym # noqa: F401 -except ImportError: - pass - -import torch - -from metasim.scenario.scenario import ScenarioCfg -from metasim.task.registry import get_task_class - -from roboverse_pack.tasks.unitree_rl.base.types import EnvTypes -from roboverse_learn.rl.unitree_rl.helper import (get_args, make_objects, get_log_dir, - make_robots, set_seed, get_load_path, - PolicyExporterLSTM, export_policy_as_jit, - get_export_jit_path) -from roboverse_learn.rl.unitree_rl.runners import EnvWrapperTypes, MasterRunner - -def prepare(args): - task_cls = get_task_class(args.task) - scenario_template = getattr(task_cls, "scenario", ScenarioCfg()) - scenario = copy.deepcopy(scenario_template) - - overrides = { - "num_envs": args.num_envs, - "simulator": args.sim, - "headless": args.headless, - } - - if args.robots: - overrides["robots"] = make_robots(args.robots) - overrides["cameras"] = [ - camera - for robot in overrides["robots"] - if hasattr(robot, "cameras") - for camera in getattr(robot, "cameras", []) - ] - - if args.objects: - overrides["objects"] = make_objects(args.objects) - - ## Set - if args.ground: - overrides["ground"] = args.ground - - - scenario.update(**overrides) - - device = "cpu" if args.sim == "mujoco" else ("cuda" if torch.cuda.is_available() else "cpu") - - master_runner = MasterRunner( - task_cls=task_cls, - scenario=scenario, - log_path=args.resume, - lib_name="rsl_rl", - device=device, - ) - - return master_runner - -def play(args): - master_runner = prepare(args) - name_0 = list(master_runner.runners.keys())[0] - if args.resume: - if args.jit_load: - log_dir = get_log_dir(task_name=master_runner.task_name, now=args.resume) - policy_0 = torch.jit.load(get_load_path(load_root=log_dir, checkpoint=args.checkpoint)) - else: - policys = master_runner.load(resume_dir=args.resume, checkpoint=args.checkpoint) - policy_0 = policys[name_0] - else: - raise ValueError("Please provide the resume dir for eval policy.") - - runner_0 = master_runner.runners[name_0] - env_0: EnvTypes = runner_0.env - envwrapper_0: EnvWrapperTypes = runner_0.env_wrapper - cfg_0 = env_0.cfg - - cfg_0.curriculum.enabled = False - cfg_0.commands.resampling_time = 1e6 # effectively disable command changes - - # export jit policy - export_jit_path = get_export_jit_path(get_log_dir(task_name=master_runner.task_name, now=args.resume), master_runner.scenario) - actor_critic = runner_0.runner.alg.policy - if hasattr(actor_critic, "memory_a"): - exporter = PolicyExporterLSTM(actor_critic) - exporter.export(export_jit_path) - else: - export_policy_as_jit(actor_critic.actor, export_jit_path) - print("Exported policy as jit script to: ", export_jit_path) - - # unenable noise and randomization for eval - - env_0.reset() - obs, _, _, _, _ = env_0.step(torch.zeros(env_0.num_envs, env_0.num_actions, device=env_0.device)) - obs = envwrapper_0.get_observations() - - - for i in range(1000000): - # set fixed command - env_0.commands_manager.value[:, 0] = 0.5 - env_0.commands_manager.value[:, 1] = 0.0 - env_0.commands_manager.value[:, 2] = 0.0 - actions = policy_0(obs) - obs, _, _, _ = envwrapper_0.step(actions) - -def train(args): - master_runner = prepare(args) - if args.resume: - master_runner.load(resume_dir=args.resume, checkpoint=args.checkpoint) - master_runner.learn(max_iterations=args.iter) - -if __name__ == "__main__": - args = get_args() - set_seed(args.seed) - if args.eval: - play(args) - else: - train(args) diff --git a/roboverse_learn/rl/unitree_rl/runners/__init__.py b/roboverse_learn/rl/unitree_rl/runners/__init__.py deleted file mode 100644 index 3c5024adb..000000000 --- a/roboverse_learn/rl/unitree_rl/runners/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from typing import Union - -from .master import MasterRunner -from .rsl_rl import RslRlEnvWrapper, RslRlWrapper - -EnvWrapperTypes = Union[RslRlEnvWrapper] diff --git a/roboverse_learn/rl/unitree_rl/runners/master.py b/roboverse_learn/rl/unitree_rl/runners/master.py deleted file mode 100644 index 760b15089..000000000 --- a/roboverse_learn/rl/unitree_rl/runners/master.py +++ /dev/null @@ -1,109 +0,0 @@ -from __future__ import annotations -from typing import Type - -import copy -import datetime -import os -import pickle as pkl -import shutil -import sys - -import torch - -from metasim.scenario.scenario import ScenarioCfg -from roboverse_learn.rl.unitree_rl.configs.cfg_base import BaseEnvCfg -from roboverse_learn.rl.unitree_rl.helper import get_class, get_log_dir, get_load_path -from roboverse_pack.tasks.unitree_rl.base.types import EnvTypes - - -class BaseRunnerWrapper: - def __init__(self, env: EnvTypes, train_cfg: dict, log_dir: str): - self.env = env - self.device = env.device - if not isinstance(train_cfg, dict): - train_cfg = train_cfg.to_dict() - self.train_cfg = train_cfg - self.log_dir = log_dir - - def load(self, path): - raise NotImplementedError - - def learn(self, max_iterations): - raise NotImplementedError - - def get_policy(self): - raise NotImplementedError - - -class MasterRunner: - def __init__( - self, - task_cls: Type[EnvTypes], - scenario: ScenarioCfg, - log_path: str | None = None, - lib_name: str = "rsl_rl", - device: str | torch.device | None = None, - ): - self.task_cls = task_cls - self.task_name = getattr(task_cls, "task_name", task_cls.__name__) - self.runners = {} - self.envs = {} - self.scenario = scenario - - env_cfg_cls: Type[BaseEnvCfg] = getattr(task_cls, "env_cfg_cls", BaseEnvCfg) - train_cfg_cls = getattr(task_cls, "train_cfg_cls", None) - runner_cls = get_class(lib_name, suffix="Wrapper", library="roboverse_learn.rl.unitree_rl.runners") - - module = sys.modules[task_cls.__module__] - env_cls_path = getattr(module, "__file__", None) - - now = log_path if log_path else datetime.datetime.now().strftime("%Y_%m%d_%H%M%S") - - robot_cfgs = scenario.robots if isinstance(scenario.robots, list) else [scenario.robots] - for robot in robot_cfgs: - scenario_copy = copy.deepcopy(scenario) - scenario_copy.robots = [robot] - scenario_copy.__post_init__() - - resolved_device = device - if resolved_device is None: - resolved_device = ( - "cpu" if scenario_copy.simulator == "mujoco" else ("cuda" if torch.cuda.is_available() else "cpu") - ) - - env_cfg = env_cfg_cls() - env: EnvTypes = task_cls( - scenario=scenario_copy, - device=resolved_device, - env_cfg=env_cfg, - ) - - train_cfg = train_cfg_cls() if callable(train_cfg_cls) else train_cfg_cls - - log_dir = get_log_dir(task_name=self.task_name, now=now) - runner: BaseRunnerWrapper = runner_cls(env=env, train_cfg=train_cfg, log_dir=log_dir) - self.runners[env.robot.name] = runner - self.envs[env.robot.name] = env - - if not log_path: - params_path = f"{log_dir}/params" - if not os.path.exists(params_path): - os.makedirs(params_path, exist_ok=True) - if env_cls_path: - shutil.copy2(env_cls_path, params_path) - pkl.dump(env_cfg, open(f"{params_path}/env_cfg.pkl", "wb")) - pkl.dump(train_cfg, open(f"{params_path}/train_cfg.pkl", "wb")) - - def learn(self, max_iterations=10000): - if not self.runners: - raise RuntimeError("No runners instantiated for training.") - first_runner = next(iter(self.runners.values())) - first_runner.learn(max_iterations=max_iterations) - - def load(self, resume_dir: str, checkpoint: int = None): - self.policys = {} - for _robot_name, _runner in self.runners.items(): - log_dir = get_log_dir(task_name=self.task_name, now=resume_dir) - _runner.load(get_load_path(load_root=log_dir, checkpoint=checkpoint)) - self.policys[_robot_name] = _runner.get_policy() - return self.policys diff --git a/roboverse_learn/rl/unitree_rl/runners/rsl_rl.py b/roboverse_learn/rl/unitree_rl/runners/rsl_rl.py deleted file mode 100644 index 55f5912d9..000000000 --- a/roboverse_learn/rl/unitree_rl/runners/rsl_rl.py +++ /dev/null @@ -1,157 +0,0 @@ -from __future__ import annotations -from typing import Union -import torch -from tensordict import TensorDict - -from roboverse_pack.tasks.unitree_rl.base import AgentTask -from .master import BaseRunnerWrapper - - -''' -class RslRlEnvWrapper: - def __init__(self, env: AgentTask): - self.env = env - - def step(self, actions: torch.Tensor) -> tuple[torch.Tensor, Union[torch.Tensor, None], torch.Tensor, torch.Tensor, dict]: - _ = self.env.step(actions) - return self.obs_buf, self.privileged_obs_buf, self.rew_buf, self.reset_buf, self.extras - - def reset(self): - _ = self.env.reset(list(range(self.num_envs))) - _ = self.step(torch.zeros(size=(self.num_envs, self.num_actions), device=self.device, requires_grad=False)) - return self.obs_buf, self.privileged_obs_buf - - def get_observations(self) -> torch.Tensor: - return self.obs_buf - - def get_privileged_observations(self) -> Union[torch.Tensor, None]: - return self.privileged_obs_buf - - @property - def num_envs(self): - return self.env.num_envs - - @property - def num_obs(self): - return self.env.num_obs - - @property - def num_privileged_obs(self): - return self.env.num_priv_obs - - @property - def num_actions(self): - return self.env.num_actions - - @property - def max_episode_length(self): - return self.env.max_episode_steps - - @property - def privileged_obs_buf(self): - return self.env.priv_obs_buf - - @property - def obs_buf(self): - return self.env.obs_buf - - @property - def rew_buf(self): - return self.env.rew_buf - - @property - def reset_buf(self): - return self.env.reset_buf - - @property - def episode_length_buf(self): - return self.env._episode_steps - - @episode_length_buf.setter - def episode_length_buf(self, value): - self.env._episode_steps = value - - @property - def extras(self): - return self.env.extras - - @property - def device(self): - return self.env.device -''' - -class RslRlEnvWrapper: - def __init__(self, env: AgentTask, train_cfg: dict | object=None): - self.env = env - self.train_cfg = train_cfg - - def get_observations(self) -> TensorDict: - """Return the current observations. - - Returns: - observations (TensorDict): Observations from the environment. - """ - raise - - def step(self, actions: torch.Tensor) -> tuple[torch.Tensor, Union[torch.Tensor, None], torch.Tensor, torch.Tensor, dict]: - _ = self.env.step(actions) - return self.obs_buf, self.env.rew_buf, self.env.reset_buf, self.env.extras - - def get_observations(self) -> TensorDict: - return self.obs_buf - - @property - def num_envs(self): - return self.env.num_envs - - @property - def num_actions(self): - return self.env.num_actions - - @property - def max_episode_length(self): - return self.env.max_episode_steps - - @property - def episode_length_buf(self): - return self.env._episode_steps - - @episode_length_buf.setter - def episode_length_buf(self, value): - self.env._episode_steps = value - - @property - def device(self): - return self.env.device - - @property - def cfg(self): - return self.train_cfg - - @property - def obs_buf(self) -> TensorDict: - return TensorDict(policy=self.env.obs_buf, - critic=self.env.priv_obs_buf) - - -class RslRlWrapper(BaseRunnerWrapper): - def __init__(self, env: AgentTask, train_cfg: dict, log_dir:str): - super().__init__(env, train_cfg, log_dir) - from rsl_rl.runners import OnPolicyRunner, DistillationRunner - - self.env_wrapper = RslRlEnvWrapper(self.env) - self.runner = OnPolicyRunner( - env=self.env_wrapper, - train_cfg=self.train_cfg, - device=self.device, - log_dir=log_dir, - ) - - def learn(self, max_iterations=10000): - self.runner.learn(num_learning_iterations=max_iterations, init_at_random_ep_len=True) - - def load(self, path): - self.runner.load(path) - - def get_policy(self): - return self.runner.get_inference_policy() diff --git a/roboverse_learn/rl/unitree_rl/runners/sb3.py b/roboverse_learn/rl/unitree_rl/runners/sb3.py deleted file mode 100644 index fe9c88263..000000000 --- a/roboverse_learn/rl/unitree_rl/runners/sb3.py +++ /dev/null @@ -1,10 +0,0 @@ -from .base import EnvTypes - - -class SB3EnvWrapper: - def __init__(self, env: EnvTypes): - self.env = env - -class RlLibEnvWrapper: - def __init__(self, env: EnvTypes): - self.env = env diff --git a/roboverse_pack/tasks/unitree_rl/base/base_agent.py b/roboverse_pack/tasks/unitree_rl/base/base_agent.py index 14b5a1429..0b7335107 100644 --- a/roboverse_pack/tasks/unitree_rl/base/base_agent.py +++ b/roboverse_pack/tasks/unitree_rl/base/base_agent.py @@ -28,6 +28,12 @@ def __init__( _callbacks_cfg = asdict(getattr(self.cfg, "callbacks", CallbacksCfg())) self._query: dict = _callbacks_cfg.pop("query", {}) self.robot = scenario.robots[0] + + # Initialize observation/action space attributes that RLTaskEnv would set + # We call BaseTaskEnv.__init__ directly to avoid early reset() call + self._observation_space = None + self._action_space = None + BaseTaskEnv.__init__(self, scenario=scenario, device=device) self._initial_states = list_state_to_tensor(self.handler, self._get_initial_states(), self.device) # buffers will be allocated lazily once handler is available diff --git a/roboverse_pack/tasks/unitree_rl/base/base_legged_robot.py b/roboverse_pack/tasks/unitree_rl/base/base_legged_robot.py index 484b21efd..a1191b247 100644 --- a/roboverse_pack/tasks/unitree_rl/base/base_legged_robot.py +++ b/roboverse_pack/tasks/unitree_rl/base/base_legged_robot.py @@ -201,6 +201,7 @@ def _init_buffers(self): self.rew_buf = torch.zeros(size=(self.num_envs,), dtype=torch.float, device=self.device) self.reset_buf = torch.zeros(size=(self.num_envs,), dtype=torch.bool, device=self.device) self.time_out_buf = torch.zeros(size=(self.num_envs,), dtype=torch.bool, device=self.device) + self._terminated_buf = torch.zeros(size=(self.num_envs,), dtype=torch.bool, device=self.device) self.up_axis_idx = 2 self.gravity_vec = torch.tensor( @@ -291,12 +292,23 @@ def reset(self, env_ids: torch.Tensor | list[int] | None = None): torch.mean(self.episode_not_terminations[key][env_ids]) / self.max_episode_steps ) self.episode_not_terminations[key][env_ids] = 0.0 + states = self.handler.get_states() + info = {"privileged_observation": self._privileged_observation(states)} + return self.obs_buf, info def step( self, actions: torch.Tensor, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, dict]: - """Apply actions, simulate for `decimation` steps, and compute RLTask-style outputs.""" + """Apply actions, simulate for `decimation` steps, and return Gymnasium-style outputs. + + Returns: + obs: Observations for next step (includes history) + reward: Rewards from this step + terminated: Episode terminated due to failure condition + truncated: Episode truncated due to timeout + info: Additional information dictionary + """ if not isinstance(actions, torch.Tensor): actions = torch.as_tensor(actions, device=self.device, dtype=torch.float32) if actions.ndim == 1: @@ -317,27 +329,42 @@ def step( self._post_physics_step(env_states) + # Return Gymnasium-compatible format return ( - self.obs_buf, - self.rew_buf, - self.reset_buf, - self.time_out_buf, - self.extras, + self.obs_buf, # Observations with history + self.rew_buf, # Rewards + self._terminated_buf, # Pure termination flags (NOT combined) + self.time_out_buf, # Truncation/timeout flags + self.extras, # Info dict ) def _post_physics_step(self, env_states: TensorState): self._episode_steps += 1 self.common_step_counter += 1 - # gym-style return values + # Compute termination and timeout separately for Gymnasium compatibility + self._terminated_buf[:] = self._terminated(env_states) self.time_out_buf[:] = self._time_out(env_states) - self.reset_buf[:] = torch.logical_or(self.time_out_buf, self._terminated(env_states)) + + # Keep reset_buf for backward compatibility (RSL-RL wrapper may use it) + self.reset_buf[:] = torch.logical_or(self.time_out_buf, self._terminated_buf) self.rew_buf[:] = self._reward(env_states) + # Compute "true" next observations BEFORE reset (for off-policy RL) + # This is the observation that would be returned if the env didn't auto-reset + true_obs_single, _ = self._compute_task_observations(env_states) + # Temporarily append to queue to compute obs with full history + self.obs_buf_queue.append(true_obs_single) + true_next_obs_with_history = self.obs_buf.clone() + # Remove the temporary observation + self.obs_buf_queue.pop() + # reset envs reset_env_idx = self.reset_buf.nonzero(as_tuple=False).squeeze(-1) if len(reset_env_idx) > 0: self.reset(env_ids=reset_env_idx) + # Get updated states after reset + env_states = self.handler.get_states() self.commands_manager.resample(self) @@ -350,6 +377,10 @@ def _post_physics_step(self, env_states: TensorState): self.priv_obs_buf_queue.append(priv_single) ####### Compute observations after resets ######## + # Store true next obs for off-policy algorithms (TD3, SAC) + # This is needed for proper truncation handling in replay buffers + self.extras["observations"] = {"raw": {"obs": true_next_obs_with_history}} + # copy to the history buffer for key, history in self.history_buffer.items(): if hasattr(self, key): @@ -429,3 +460,34 @@ def _get_initial_states(self): def max_episode_steps(self): """Maximum episode length in steps.""" return math.ceil(self.cfg.episode_length_s / self.step_dt) + + @property + def observation_space(self): + """Observation space for the policy (includes history).""" + if self._observation_space is None: + import gymnasium as gym + import numpy as np + + obs_dim = self.obs_buf.shape[-1] + self._observation_space = gym.spaces.Box( + low=-np.inf, + high=np.inf, + shape=(obs_dim,), + dtype=np.float32, + ) + return self._observation_space + + @property + def action_space(self): + """Action space (normalized to [-1, 1]).""" + if self._action_space is None: + import gymnasium as gym + import numpy as np + + self._action_space = gym.spaces.Box( + low=-1.0, + high=1.0, + shape=(self.num_actions,), + dtype=np.float32, + ) + return self._action_space diff --git a/roboverse_pack/tasks/unitree_rl/locomotion/walk_g1_dof12.py b/roboverse_pack/tasks/unitree_rl/locomotion/walk_g1_dof12.py index 785519efd..b5cd2609b 100644 --- a/roboverse_pack/tasks/unitree_rl/locomotion/walk_g1_dof12.py +++ b/roboverse_pack/tasks/unitree_rl/locomotion/walk_g1_dof12.py @@ -81,14 +81,14 @@ def __init__( def _init_buffers(self): # commands + base_ang_vel + projected_gravity + dof pos/vel/prev actions + gait phase - self.num_obs_single = 3 + 3 + 3 + self.num_actions * 3 + 2 + self.num_obs = 3 + 3 + 3 + self.num_actions * 3 + 2 # commands + base_lin_vel + base_ang_vel + projected_gravity + dof pos/vel/prev actions + gait phase - self.num_priv_obs_single = 3 + 3 + 3 + 3 + self.num_actions * 3 + 2 + self.num_priv_obs = 3 + 3 + 3 + 3 + self.num_actions * 3 + 2 # Rewrite SOME Hyfer-Parameters self.obs_clip_limit = 100.0 - self.obs_scale = torch.ones(size=(self.num_obs_single,), dtype=torch.float, device=self.device) - self.priv_obs_scale = torch.ones(size=(self.num_priv_obs_single,), dtype=torch.float, device=self.device) - self.obs_noise = torch.zeros(size=(self.num_obs_single,), dtype=torch.float, device=self.device) + self.obs_scale = torch.ones(size=(self.num_obs,), dtype=torch.float, device=self.device) + self.priv_obs_scale = torch.ones(size=(self.num_priv_obs,), dtype=torch.float, device=self.device) + self.obs_noise = torch.zeros(size=(self.num_obs,), dtype=torch.float, device=self.device) ##################### for observation scale ##################### self.obs_scale[0:2] = 0.2 # linear vel commands diff --git a/roboverse_pack/tasks/unitree_rl/locomotion/walk_g1_dof29.py b/roboverse_pack/tasks/unitree_rl/locomotion/walk_g1_dof29.py index 61b8f0c14..e7bd95186 100644 --- a/roboverse_pack/tasks/unitree_rl/locomotion/walk_g1_dof29.py +++ b/roboverse_pack/tasks/unitree_rl/locomotion/walk_g1_dof29.py @@ -81,14 +81,14 @@ def __init__( def _init_buffers(self): # commands + base_ang_vel + projected_gravity + dof pos/vel/prev actions - self.num_obs_single = 3 + 3 + 3 + self.num_actions * 3 + self.num_obs = 3 + 3 + 3 + self.num_actions * 3 # commands + base_lin_vel + base_ang_vel + projected_gravity + dof pos/vel/prev actions - self.num_priv_obs_single = 3 + 3 + 3 + 3 + self.num_actions * 3 + self.num_priv_obs = 3 + 3 + 3 + 3 + self.num_actions * 3 # Rewrite SOME Hyfer-Parameters self.obs_clip_limit = 100.0 - self.obs_scale = torch.ones(size=(self.num_obs_single,), dtype=torch.float, device=self.device) - self.priv_obs_scale = torch.ones(size=(self.num_priv_obs_single,), dtype=torch.float, device=self.device) - self.obs_noise = torch.zeros(size=(self.num_obs_single,), dtype=torch.float, device=self.device) + self.obs_scale = torch.ones(size=(self.num_obs,), dtype=torch.float, device=self.device) + self.priv_obs_scale = torch.ones(size=(self.num_priv_obs,), dtype=torch.float, device=self.device) + self.obs_noise = torch.zeros(size=(self.num_obs,), dtype=torch.float, device=self.device) ##################### for observation scale ##################### self.obs_scale[3:6] = 0.2 # angular velocity From 18b0d1c593a05e49261369033d06f044ac8a8467 Mon Sep 17 00:00:00 2001 From: gxyes Date: Thu, 4 Dec 2025 14:00:55 +0700 Subject: [PATCH 04/50] add dr to collect demo --- .../randomization/presets/scene_presets.py | 80 +- metasim/randomization/scene_randomizer.py | 42 +- scripts/advanced/collect_demo.py | 829 ++++++++++++------ 3 files changed, 628 insertions(+), 323 deletions(-) diff --git a/metasim/randomization/presets/scene_presets.py b/metasim/randomization/presets/scene_presets.py index 63d4166dd..6d4c3131f 100644 --- a/metasim/randomization/presets/scene_presets.py +++ b/metasim/randomization/presets/scene_presets.py @@ -134,7 +134,6 @@ def slug(self) -> str: "decorations": ( FamilyInfo("embodiedgen", "dataset/desktop_supplies/decorations", "Decorations from EmbodiedGen"), ), - "fruits": (FamilyInfo("embodiedgen", "dataset/desktop_supplies/fruits", "Fruits from EmbodiedGen"),), "office_stationery": ( FamilyInfo( "embodiedgen", "dataset/desktop_supplies/office_stationery", "Office stationery from EmbodiedGen" @@ -393,7 +392,7 @@ class SceneUSDCollections: >>> tables = SceneUSDCollections.table_assets(max_assets=10) >>> >>> # Get object assets (general) - >>> objects = SceneUSDCollections.object_assets(families=("fruits", "decorations"), max_assets=20) + >>> objects = SceneUSDCollections.object_assets(families=("office_stationery", "decorations"), max_assets=20) >>> >>> # Get Table785 curated set (specific - 5 tables) >>> table785 = SceneUSDCollections.table785(indices=[0, 1, 2]) @@ -401,13 +400,13 @@ class SceneUSDCollections: >>> # Get Kujiale scenes (specific - 12 scenes) >>> scenes = SceneUSDCollections.kujiale_scenes() >>> - >>> # Get desktop supplies objects (specific - 10 fruits) + >>> # Get desktop supplies objects (specific - 10 office stationery) >>> desktop_objects = SceneUSDCollections.desktop_supplies(indices=[0, 1, 2]) """ TABLE_FAMILIES = ("table",) SCENE_FAMILIES = ("kujiale",) - OBJECT_FAMILIES = ("decorations", "fruits", "office_stationery", "office_tools", "remote_control") + OBJECT_FAMILIES = ("decorations", "office_stationery", "office_tools", "remote_control") @staticmethod def table_assets( @@ -464,7 +463,7 @@ def object_assets( ) -> list[str]: """Return desktop object USD assets from the USD family registry. - Includes decorations, fruits, office supplies, tools, and remote controls + Includes decorations, office_stationery, office supplies, tools, and remote controls from the EmbodiedGen desktop_supplies dataset. Args: @@ -577,10 +576,10 @@ def desktop_supplies( indices: list[int] | None = None, return_configs: bool = False, ) -> list[str] | tuple[list[str], dict[str, dict]]: - """Get desktop supplies curated set (10 specific fruit objects from EmbodiedGen). + """Get desktop supplies curated set (10 specific office stationery objects from EmbodiedGen). - This is a convenience method that returns a hardcoded list of 10 fruit objects - from the desktop_supplies/fruits category. This demonstrates the specific pattern + This is a convenience method that returns a hardcoded list of 10 office stationery objects + from the desktop_supplies/office_stationery category. This demonstrates the specific pattern similar to table785() and kujiale_scenes(). For general object access with flexible category selection, use object_assets() instead. @@ -600,20 +599,20 @@ def desktop_supplies( >>> paths, configs = SceneUSDCollections.desktop_supplies(return_configs=True) >>> USDAssetPoolCfg(usd_paths=paths, per_path_overrides=configs) """ - # Curated desktop supplies: 10 fruits from EmbodiedGen - # Source: https://huggingface.co/datasets/HorizonRobotics/EmbodiedGenData/tree/main/dataset/desktop_supplies/fruits + # Curated desktop supplies: 10 office stationery from EmbodiedGen + # Source: https://huggingface.co/datasets/HorizonRobotics/EmbodiedGenData/tree/main/dataset/desktop_supplies/office_stationery DESKTOP_SUPPLIES_UUIDS = { - "fruits": ( - "0308c3ddcd2a5823ba0c74d624ae6e16", - "20e6e6d0a512585b83fdea02a7a73207", - "2db0218135735e7691d6a0af3bf1f36f", - "3452299858935fcea7e3efb69ad2550c", - "39189a4317b454f6a10f153dcb7a29ec", - "43b757b2d85051bbaec62e136a506dba", - "50b88ffa707b53adaad3229d94bb24fa", - "9bb9f582fd875638bb1362edfc064aad", - "ae283eef0c1f5ca0b2af059fc073a3eb", - "c3dc4a1606405f408ed5793f75c4dd36", + "office_stationery": ( + "0634f388c3845f1e929f367581352d20", + "0773f8fc18b45b85a3a5a65c99e746e6", + "09b4f19c0be9527883c97921b7f5d736", + "10ab616ea78652a8a5611334723ad931", + "1695ee4d1917544cb55ab8477ede5060", + "1ad9e289b3f35c4e94bea6fdcc794af3", + "1c3090016ed053e2bb444e9470aaf9cb", + "1e7cfd9a38ca56a891842b62f92cebfa", + "1f9eb044e10857beb7fa41f71d738e7a", + "2faedaa2fd0d580a8c00f8d94877c446", ), } paths = _collect_desktop_supplies( @@ -730,7 +729,7 @@ def _collect_desktop_supplies( """Collect desktop supplies asset paths from EmbodiedGen repository. Returns a curated set of objects from desktop_supplies dataset. - Currently includes 10 fruits. Can be extended with more categories. + Currently includes 10 office stationery items. Can be extended with more categories. Returns USD paths if available (after conversion), otherwise URDF paths. @@ -961,7 +960,7 @@ def get_desktop_object_configs() -> dict[str, dict]: Dictionary mapping object UUIDs to their config overrides Note: - - This can include objects from all categories (fruits, decorations, etc.) + - This can include objects from all categories (office_stationery, decorations, etc.) - Configurations are automatically matched by UUID found in path - Missing configurations will use default values (no error) @@ -972,66 +971,67 @@ def get_desktop_object_configs() -> dict[str, dict]: ``` """ return { - # Fruits (curated set - 10 objects) - # Positioned directly on table surface (z=0.75) - # Pure visual decoration - no physics simulation + # Office stationery (curated set - 10 objects) + # Positioned at table surface (z=0.75) + # Static colliders: have collision but no dynamic physics (cannot fall) + # This allows randomization between demos without PhysX errors # Layout: distributed around the table edges, away from task area # Left front area - "0308c3ddcd2a5823ba0c74d624ae6e16": { - "position": (-0.5, 0.4, 0.75), # On table surface + "0634f388c3845f1e929f367581352d20": { + "position": (-0.5, 0.4, 0.75), "rotation": (1.0, 0.0, 0.0, 0.0), "scale": (0.8, 0.8, 0.8), }, # Right front area - "20e6e6d0a512585b83fdea02a7a73207": { + "0773f8fc18b45b85a3a5a65c99e746e6": { "position": (0.5, 0.4, 0.75), "rotation": (1.0, 0.0, 0.0, 0.0), "scale": (0.8, 0.8, 0.8), }, # Right side - "2db0218135735e7691d6a0af3bf1f36f": { + "09b4f19c0be9527883c97921b7f5d736": { "position": (0.6, 0.0, 0.75), "rotation": (1.0, 0.0, 0.0, 0.0), "scale": (0.8, 0.8, 0.8), }, # Left side - "3452299858935fcea7e3efb69ad2550c": { + "10ab616ea78652a8a5611334723ad931": { "position": (-0.6, 0.0, 0.75), "rotation": (1.0, 0.0, 0.0, 0.0), "scale": (0.8, 0.8, 0.8), }, # Center front - "39189a4317b454f6a10f153dcb7a29ec": { + "1695ee4d1917544cb55ab8477ede5060": { "position": (0.0, 0.5, 0.75), "rotation": (1.0, 0.0, 0.0, 0.0), "scale": (0.8, 0.8, 0.8), }, # Far left front - "43b757b2d85051bbaec62e136a506dba": { + "1ad9e289b3f35c4e94bea6fdcc794af3": { "position": (-0.7, 0.3, 0.75), "rotation": (1.0, 0.0, 0.0, 0.0), "scale": (0.8, 0.8, 0.8), }, # Far right front - "50b88ffa707b53adaad3229d94bb24fa": { + "1c3090016ed053e2bb444e9470aaf9cb": { "position": (0.7, 0.3, 0.75), "rotation": (1.0, 0.0, 0.0, 0.0), "scale": (0.8, 0.8, 0.8), }, # Right center - "9bb9f582fd875638bb1362edfc064aad": { + "1e7cfd9a38ca56a891842b62f92cebfa": { "position": (0.5, 0.1, 0.75), "rotation": (1.0, 0.0, 0.0, 0.0), "scale": (0.8, 0.8, 0.8), }, # Left front near - "ae283eef0c1f5ca0b2af059fc073a3eb": { + "1f9eb044e10857beb7fa41f71d738e7a": { "position": (-0.4, 0.5, 0.75), "rotation": (1.0, 0.0, 0.0, 0.0), "scale": (0.8, 0.8, 0.8), }, # Right front near - "c3dc4a1606405f408ed5793f75c4dd36": { + "2faedaa2fd0d580a8c00f8d94877c446": { "position": (0.4, 0.5, 0.75), "rotation": (1.0, 0.0, 0.0, 0.0), "scale": (0.8, 0.8, 0.8), @@ -1259,6 +1259,7 @@ def empty_room( size=(room_size, room_size, wall_thickness), position=(0.0, 0.0, 0.005), default_material="roboverse_data/materials/arnold/Carpet/Carpet_Beige.mdl", + add_collision=True, ), # Front wall (positive Y) ManualGeometryCfg( @@ -1267,6 +1268,7 @@ def empty_room( size=(room_size + 2 * wall_thickness, wall_thickness, wall_height), position=(0.0, half_room + half_thickness, wall_height / 2), default_material="roboverse_data/materials/arnold/Masonry/Brick_Pavers.mdl", + add_collision=True, ), # Back wall (negative Y) ManualGeometryCfg( @@ -1275,6 +1277,7 @@ def empty_room( size=(room_size + 2 * wall_thickness, wall_thickness, wall_height), position=(0.0, -half_room - half_thickness, wall_height / 2), default_material="roboverse_data/materials/arnold/Masonry/Brick_Pavers.mdl", + add_collision=True, ), # Left wall (negative X) ManualGeometryCfg( @@ -1283,6 +1286,7 @@ def empty_room( size=(wall_thickness, room_size, wall_height), position=(-half_room - half_thickness, 0.0, wall_height / 2), default_material="roboverse_data/materials/arnold/Masonry/Brick_Pavers.mdl", + add_collision=True, ), # Right wall (positive X) ManualGeometryCfg( @@ -1291,6 +1295,7 @@ def empty_room( size=(wall_thickness, room_size, wall_height), position=(half_room + half_thickness, 0.0, wall_height / 2), default_material="roboverse_data/materials/arnold/Masonry/Brick_Pavers.mdl", + add_collision=True, ), # Ceiling ManualGeometryCfg( @@ -1299,6 +1304,7 @@ def empty_room( size=(room_size, room_size, wall_thickness), position=(0.0, 0.0, wall_height + wall_thickness / 2), default_material="roboverse_data/materials/arnold/Architecture/Roof_Tiles.mdl", + add_collision=True, ), ], ), diff --git a/metasim/randomization/scene_randomizer.py b/metasim/randomization/scene_randomizer.py index 7c9948523..18f9fe52e 100644 --- a/metasim/randomization/scene_randomizer.py +++ b/metasim/randomization/scene_randomizer.py @@ -88,6 +88,7 @@ class USDAssetCfg: rotation: Orientation quaternion (w, x, y, z) scale: Scale factor (x, y, z) auto_download: Enable automatic asset download + add_collision: Whether to add static collision (prevents penetration without physics) enabled: Whether this element is active Note: Scene objects are always pure visual (physics disabled) @@ -99,6 +100,7 @@ class USDAssetCfg: rotation: tuple[float, float, float, float] = (1.0, 0.0, 0.0, 0.0) scale: tuple[float, float, float] = (1.0, 1.0, 1.0) auto_download: bool = True + add_collision: bool = False enabled: bool = True def __post_init__(self): @@ -121,6 +123,7 @@ class USDAssetPoolCfg: scale: Default scale for all USDs selection_strategy: Selection strategy (random, sequential) auto_download: Enable automatic download + add_collision: Whether to add static collision (prevents penetration without physics) enabled: Whether this pool is active """ @@ -132,6 +135,7 @@ class USDAssetPoolCfg: scale: tuple[float, float, float] = (1.0, 1.0, 1.0) selection_strategy: Literal["random", "sequential"] = "random" auto_download: bool = True + add_collision: bool = False enabled: bool = True candidates: list[USDAssetCfg] | None = None # Will be auto-generated @@ -149,6 +153,7 @@ def __post_init__(self): "rotation": self.rotation, "scale": self.scale, "auto_download": self.auto_download, + "add_collision": self.add_collision, "enabled": self.enabled, } @@ -159,7 +164,7 @@ def __post_init__(self): override = self.per_path_overrides.get(path) or self.per_path_overrides.get(Path(path).name) if override: # Only update valid USDAssetCfg fields - valid_keys = {"position", "rotation", "scale", "auto_download", "enabled"} + valid_keys = {"position", "rotation", "scale", "auto_download", "add_collision", "enabled"} for k, v in override.items(): if k in valid_keys: cfg_kwargs[k] = v @@ -692,8 +697,8 @@ def _load_usd(self, prim_path: str, element: USDAssetCfg, z_offset: float): ) xform.AddScaleOp().Set(Gf.Vec3d(*element.scale)) - # Disable physics (all scene objects are pure visual) - self._disable_physics_for_prim(prim) + # Disable physics (remove RigidBodyAPI, optionally keep CollisionAPI) + self._disable_physics_for_prim(prim, keep_collision=element.add_collision) def _delete_usd(self, prim_path: str): """Delete USD prim. @@ -709,24 +714,39 @@ def _delete_usd(self, prim_path: str): if prim_utils.is_prim_path_valid(prim_path): prim_utils.delete_prim(prim_path) - def _disable_physics_for_prim(self, prim): + def _disable_physics_for_prim(self, prim, keep_collision: bool = False, is_root: bool = True): """Recursively disable physics for a prim. Args: prim: USD prim + keep_collision: If True, apply CollisionAPI to root prim (for static collision) + is_root: Whether this is the root prim of the USD asset """ from pxr import UsdPhysics - # Recursive processing - for descendant in prim.GetAllChildren(): - self._disable_physics_for_prim(descendant) - - # Remove physics APIs + # Always remove RigidBodyAPI (we don't want dynamic physics) if prim.HasAPI(UsdPhysics.RigidBodyAPI): prim.RemoveAPI(UsdPhysics.RigidBodyAPI) - if prim.HasAPI(UsdPhysics.CollisionAPI): - prim.RemoveAPI(UsdPhysics.CollisionAPI) + # For root prim: apply or remove CollisionAPI based on keep_collision + if is_root: + if keep_collision: + # Apply CollisionAPI to the root prim + # PhysX will use the visual geometry for collision + if not prim.HasAPI(UsdPhysics.CollisionAPI): + UsdPhysics.CollisionAPI.Apply(prim) + else: + # Remove CollisionAPI if exists + if prim.HasAPI(UsdPhysics.CollisionAPI): + prim.RemoveAPI(UsdPhysics.CollisionAPI) + else: + # For non-root prims: always remove CollisionAPI to avoid conflicts + if prim.HasAPI(UsdPhysics.CollisionAPI): + prim.RemoveAPI(UsdPhysics.CollisionAPI) + + # Recursively process all children (no longer root) + for child in prim.GetAllChildren(): + self._disable_physics_for_prim(child, keep_collision=keep_collision, is_root=False) # ------------------------------------------------------------------------- # URDF Conversion diff --git a/scripts/advanced/collect_demo.py b/scripts/advanced/collect_demo.py index 51fed6066..733ab9d88 100644 --- a/scripts/advanced/collect_demo.py +++ b/scripts/advanced/collect_demo.py @@ -1,3 +1,20 @@ +"""Demo collection script with domain randomization support. + +Collects demonstration data by replaying trajectories with optional domain randomization. + +Randomization Levels: +- Level 0: No randomization +- Level 1: Scene + Material randomization +- Level 2: Level 1 + Lighting randomization +- Level 3: Level 2 + Camera randomization + +Scene Modes: +- Mode 0: Manual geometry +- Mode 1: USD Table + Manual environment +- Mode 2: USD Scene (Kujiale) + USD Table +- Mode 3: Full USD (Scene + Table + Desktop objects) +""" + from __future__ import annotations from copy import deepcopy @@ -27,8 +44,6 @@ class Args: """Simulator backend""" demo_start_idx: int | None = None """The index of the first demo to collect, None for all demos""" - # max_demo_idx: int | None = None - # """Maximum number of demos to collect, None for all demos""" num_demo_success: int | None = None """Target number of successful demos to collect""" retry_num: int = 0 @@ -55,19 +70,11 @@ class Args: """Rollout unfinished and failed trajectories""" renderer: Literal["isaaclab", "mujoco", "isaacgym", "genesis", "pybullet", "sapien2", "sapien3"] = "mujoco" - ## Domain randomization options - enable_randomization: bool = False - """Enable domain randomization during demo collection""" - randomize_materials: bool = True - """Enable material randomization (when randomization is enabled)""" - randomize_lights: bool = False - """Enable light randomization (when randomization is enabled)""" - randomize_cameras: bool = True - """Enable camera randomization (when randomization is enabled)""" - randomize_physics: bool = True - """Enable physics (mass/friction/pose) randomization using ObjectRandomizer""" - randomization_frequency: Literal["per_demo", "per_episode"] = "per_demo" - """When to apply randomization: per_demo (once at start) or per_episode (every episode)""" + # Domain randomization options + level: Literal[0, 1, 2, 3] = 0 + """Randomization level: 0=None, 1=Scene+Material, 2=+Light, 3=+Camera""" + scene_mode: Literal[0, 1, 2, 3] = 0 + """Scene mode: 0=Manual, 1=USD Table, 2=USD Scene, 3=Full USD""" randomization_seed: int | None = None """Seed for reproducible randomization. If None, uses random seed""" @@ -75,32 +82,24 @@ def __post_init__(self): assert self.run_all or self.run_unfinished or self.run_failed, ( "At least one of run_all, run_unfinished, or run_failed must be True" ) - # if self.max_demo_idx is None: - # self.max_demo_idx = math.inf if self.num_demo_success is None: self.num_demo_success = 100 if self.demo_start_idx is None: self.demo_start_idx = 0 - # Validate randomization settings - if self.enable_randomization: - if not ( - self.randomize_materials or self.randomize_lights or self.randomize_cameras or self.randomize_physics - ): - log.warning("Randomization enabled but no randomization types selected, disabling randomization") - self.enable_randomization = False - log.info(f"Args: {self}") # Log randomization settings - if self.enable_randomization: + if self.level > 0: + mode_names = {0: "Manual", 1: "USD Table", 2: "USD Scene", 3: "Full USD"} log.info("=" * 60) log.info("DOMAIN RANDOMIZATION CONFIGURATION") - log.info(f" Materials: {'✓' if self.randomize_materials else '✗'}") - log.info(f" Lights: {'✓' if self.randomize_lights else '✗'}") - log.info(f" Cameras: {'✓' if self.randomize_cameras else '✗'}") - log.info(f" Physics: {'✓' if self.randomize_physics else '✗'} (ObjectRandomizer)") - log.info(f" Frequency: {self.randomization_frequency}") + log.info(f" Level: {self.level}") + log.info(f" Scene Mode: {self.scene_mode} ({mode_names[self.scene_mode]})") + log.info(" Randomization:") + log.info(" Level 1+: Scene + Material") + log.info(" Level 2+: + Lighting") + log.info(" Level 3+: + Camera") log.info(f" Seed: {self.randomization_seed if self.randomization_seed else 'Random'}") log.info("=" * 60) @@ -120,6 +119,7 @@ def __post_init__(self): from tqdm.rich import tqdm_rich as tqdm from metasim.scenario.cameras import PinholeCameraCfg +from metasim.scenario.lights import DiskLightCfg, SphereLightCfg from metasim.scenario.robot import RobotCfg from metasim.sim import BaseSimHandler from metasim.task.registry import get_task_class @@ -130,18 +130,31 @@ def __post_init__(self): rootutils.setup_root(__file__, pythonpath=True) -# Import randomization components (after rootutils setup) +# Import randomization components try: - from roboverse_pack.randomization import ( - CameraPresets, + from metasim.randomization import ( + # Randomizers CameraRandomizer, - LightPresets, + # Scene Configuration + EnvironmentLayerCfg, + # Light configuration + LightColorRandomCfg, + LightIntensityRandomCfg, + LightOrientationRandomCfg, + LightPositionRandomCfg, + LightRandomCfg, LightRandomizer, + ManualGeometryCfg, MaterialPresets, MaterialRandomizer, - ObjectPresets, - ObjectRandomizer, + # Core (usually transparent) + ObjectsLayerCfg, + SceneRandomCfg, + SceneRandomizer, + USDAssetPoolCfg, + WorkspaceLayerCfg, ) + from metasim.randomization.presets.scene_presets import ScenePresets, SceneUSDCollections RANDOMIZATION_AVAILABLE = True except ImportError as e: @@ -149,54 +162,67 @@ def __post_init__(self): RANDOMIZATION_AVAILABLE = False -def log_randomization_result( - randomizer_type: str, obj_name: str, property_name: str, before_value, after_value, unit: str = "" -): - """Log randomization results in a consistent format.""" - if hasattr(before_value, "cpu"): - before_str = str(before_value.cpu().numpy().round(3) if hasattr(before_value, "numpy") else before_value) - else: - before_str = str(before_value) - - if hasattr(after_value, "cpu"): - after_str = str(after_value.cpu().numpy().round(3) if hasattr(after_value, "numpy") else after_value) - else: - after_str = str(after_value) - - log.info(f" [{randomizer_type}] {obj_name}.{property_name}: {before_str} -> {after_str} {unit}") - - -def log_randomization_header(randomizer_name: str, description: str = ""): - """Log a consistent header for randomization sections.""" - log.info("=" * 50) - if description: - log.info(f"{randomizer_name}: {description}") - else: - log.info(randomizer_name) - - class DomainRandomizationManager: - """Manages domain randomization for demo collection with unified interface.""" + """Manages domain randomization for demo collection. - def __init__(self, args: Args, scenario, handler): + Architecture: + - Static Objects: Handler-managed (Robot, task objects, Camera, Light) + - Dynamic Objects: SceneRandomizer-managed (Floor, Table, Distractors) + - Level system: 0=None, 1=Scene+Material, 2=+Light, 3=+Camera + - Mode system: 0=Manual, 1=USD Table, 2=USD Scene, 3=Full USD + """ + + def __init__(self, args: Args, scenario, handler, init_states: list): self.args = args self.scenario = scenario self.handler = handler - self.randomizers = [] - self._demo_count = 0 + self.init_states = init_states + self.randomizers = {} + + # Store original camera positions BEFORE any randomization + self.original_camera_positions = {} + for camera in self.handler.cameras: + self.original_camera_positions[camera.name] = { + "pos": tuple(camera.pos), + "look_at": tuple(camera.look_at), + } + + # Store original positions for ALL demos (before any modification) + # This ensures we always have the true original positions from the trajectory file + self.original_positions = {} + for demo_idx, init_state in enumerate(init_states): + demo_key = f"demo_{demo_idx}" + self.original_positions[demo_key] = {} + + if "objects" in init_state: + for obj_name, obj_state in init_state["objects"].items(): + self.original_positions[demo_key][f"obj_{obj_name}"] = { + "x": float(obj_state["pos"][0]), + "y": float(obj_state["pos"][1]), + "z": float(obj_state["pos"][2]), + } + + if "robots" in init_state: + for robot_name, robot_state in init_state["robots"].items(): + self.original_positions[demo_key][f"robot_{robot_name}"] = { + "x": float(robot_state["pos"][0]), + "y": float(robot_state["pos"][1]), + "z": float(robot_state["pos"][2]), + } # Early validation if not self._validate_setup(): return - log_randomization_header("DOMAIN RANDOMIZATION SETUP", "Initializing randomizers") + log.info("=" * 50) + log.info("DOMAIN RANDOMIZATION SETUP: Initializing randomizers") self._setup_randomizers() - log.info(f"Setup complete: {len(self.randomizers)} randomizers ready") + log.info(f"Setup complete: Randomizers ready (Level {args.level}, Mode {args.scene_mode})") def _validate_setup(self) -> bool: """Validate if randomization can be set up.""" - if not self.args.enable_randomization: - log.info("Domain randomization disabled") + if self.args.level == 0: + log.info("Domain randomization disabled (level=0)") return False if not RANDOMIZATION_AVAILABLE: @@ -206,211 +232,455 @@ def _validate_setup(self) -> bool: return True def _setup_randomizers(self): - """Initialize all randomizers based on configuration.""" + """Initialize all randomizers based on level and mode.""" seed = self.args.randomization_seed self._setup_reproducibility(seed) - # Setup each randomization type symmetrically - if self.args.randomize_materials: - self._setup_material_randomizers(seed) + self.randomizers = { + "scene": None, + "material_dynamic": [], + "light": [], + "camera": [], + } + + # Scene Randomizer + self._setup_scene_randomizer(seed) - if self.args.randomize_lights: + # Material Randomization + self._setup_material_randomizers(seed) + + # Light Randomization (Level 2+) + if self.args.level >= 2: self._setup_light_randomizers(seed) - if self.args.randomize_cameras: + # Camera Randomization (Level 3+) + if self.args.level >= 3: self._setup_camera_randomizers(seed) - if self.args.randomize_physics: - self._setup_physics_randomizers(seed) - def _setup_reproducibility(self, seed: int | None): """Setup global reproducibility if seed is provided.""" if seed is not None: log.info(f"Setting up reproducible randomization with seed: {seed}") torch.manual_seed(seed) + import random + import numpy as np np.random.seed(seed) + random.seed(seed) if torch.cuda.is_available(): torch.cuda.manual_seed(seed) + def _setup_scene_randomizer(self, seed: int | None): + """Setup SceneRandomizer based on scene_mode.""" + mode = self.args.scene_mode + level = self.args.level + + log.info(f"\nScene Creation (Mode {mode})") + log.info("-" * 50) + + # Environment Layer + if mode >= 2: + # USD Scene + scene_paths, scene_configs = SceneUSDCollections.kujiale_scenes(return_configs=True) + log.info(f"Environment: Kujiale USD ({len(scene_paths)} scenes)") + env_element = USDAssetPoolCfg( + name="kujiale_scene", + usd_paths=scene_paths, + per_path_overrides=scene_configs, + selection_strategy="random" if level >= 1 else "sequential", + ) + environment_layer = EnvironmentLayerCfg(elements=[env_element]) + else: + # Manual Scene + log.info("Environment: Manual geometry (10m x 10m x 5m)") + base_cfg = ScenePresets.empty_room(room_size=10.0, wall_height=5.0) + environment_layer = base_cfg.environment_layer + + # Workspace Layer + if mode >= 1: + # USD Table + table_paths, table_configs = SceneUSDCollections.table785(return_configs=True) + log.info(f"Workspace: Table785 USD ({len(table_paths)} tables)") + workspace_element = USDAssetPoolCfg( + name="table", + usd_paths=table_paths, + per_path_overrides=table_configs, + selection_strategy="random" if level >= 1 else "sequential", + add_collision=True, + ) + workspace_layer = WorkspaceLayerCfg(elements=[workspace_element]) + else: + # Manual Table + log.info("Workspace: Manual table (Plywood default, randomized in level 1+)") + workspace_layer = WorkspaceLayerCfg( + elements=[ + ManualGeometryCfg( + name="table", + geometry_type="cube", + size=(1.8, 1.8, 0.1), + position=(0.0, 0.0, 0.7 - 0.05), + default_material="roboverse_data/materials/arnold/Wood/Plywood.mdl", + add_collision=True, + ) + ] + ) + + # Objects Layer + if mode >= 3: + object_paths, object_configs = SceneUSDCollections.desktop_supplies(return_configs=True) + log.info(f"Objects: Desktop supplies ({len(object_paths)} items, placing 3)") + objects_layer = ObjectsLayerCfg( + elements=[ + USDAssetPoolCfg( + name=f"desktop_object_{i + 1}", + usd_paths=object_paths, + per_path_overrides=object_configs, + selection_strategy="random" if level >= 1 else "sequential", + add_collision=True, + ) + for i in range(3) + ] + ) + else: + objects_layer = None + + # Create SceneRandomizer + scene_cfg = SceneRandomCfg( + environment_layer=environment_layer, + workspace_layer=workspace_layer, + objects_layer=objects_layer, + ) + + scene_rand = SceneRandomizer(scene_cfg, seed=seed) + scene_rand.bind_handler(self.handler) + self.randomizers["scene"] = scene_rand + log.info("SceneRandomizer created") + def _setup_material_randomizers(self, seed: int | None): - """Setup material randomizers for all objects.""" - objects = getattr(self.scenario, "objects", []) - if not objects: - log.info(" No objects found for material randomization") - return + """Setup material randomizers for dynamic objects (environment).""" + mode = self.args.scene_mode + level = self.args.level - log.info(f" Setting up material randomizers for {len(objects)} objects") - for obj in objects: - obj_name = obj.name - config = self._get_material_config(obj_name) + if level == 0: + return - randomizer = MaterialRandomizer(config, seed=seed) - randomizer.bind_handler(self.handler) - self.randomizers.append(randomizer) - log.info(f" Added MaterialRandomizer for {obj_name}") + log.info("\nMaterial Randomization") + log.info("-" * 50) + + # Dynamic Objects (Manual geometry only) + if mode == 0: + # Manual table + table_mat = MaterialRandomizer( + MaterialPresets.mdl_family_object("table", family=("wood", "metal")), + seed=seed + 2 if seed is not None else None, + ) + table_mat.bind_handler(self.handler) + self.randomizers["material_dynamic"].append(table_mat) + log.info(" Dynamic Object: table (Manual)") + + # Manual environment (mode < 2 and level >= 1) + if mode < 2 and level >= 1: + # Floor + floor_mat = MaterialRandomizer( + MaterialPresets.mdl_family_object("floor", family=("carpet", "wood", "stone")), + seed=seed + 101 if seed is not None else None, + ) + floor_mat.bind_handler(self.handler) + self.randomizers["material_dynamic"].append(floor_mat) + + # Walls + wall_seed = seed + 102 if seed is not None else None + for wall_name in ["wall_front", "wall_back", "wall_left", "wall_right"]: + wall_mat = MaterialRandomizer( + MaterialPresets.mdl_family_object(wall_name, family=("masonry", "architecture")), + seed=wall_seed, + ) + wall_mat.bind_handler(self.handler) + self.randomizers["material_dynamic"].append(wall_mat) + + # Ceiling + ceiling_mat = MaterialRandomizer( + MaterialPresets.mdl_family_object("ceiling", family=("architecture", "wall_board")), + seed=seed + 103 if seed is not None else None, + ) + ceiling_mat.bind_handler(self.handler) + self.randomizers["material_dynamic"].append(ceiling_mat) + + log.info(" Dynamic Objects: floor + 4 walls + ceiling") def _setup_light_randomizers(self, seed: int | None): - """Setup light randomizers for all lights.""" + """Setup light randomizers (Level 2+).""" from metasim.scenario.lights import DiskLightCfg, DomeLightCfg, SphereLightCfg + log.info("\nLight Randomization") + log.info("-" * 50) + lights = getattr(self.scenario, "lights", []) if not lights: - log.info(" No lights found for light randomization") + log.info(" No lights found") return - log.info(f" Setting up light randomizers for {len(lights)} lights") - for light in lights: - light_name = getattr(light, "name", f"light_{len(self.randomizers)}") + # Determine intensity ranges based on render mode + if hasattr(self.args.render, "mode") and self.args.render.mode == "pathtracing": + main_range = (22000.0, 40000.0) + corner_range = (10000.0, 18000.0) + else: + main_range = (16000.0, 30000.0) + corner_range = (6000.0, 12000.0) + + for i, light in enumerate(lights): + light_name = getattr(light, "name", f"light_{i}") + + if isinstance(light, DiskLightCfg): + # Main ceiling light with orientation + light_rand = LightRandomizer( + LightRandomCfg( + light_name=light_name, + intensity=LightIntensityRandomCfg(intensity_range=main_range, enabled=True), + color=LightColorRandomCfg( + temperature_range=(3000.0, 6000.0), use_temperature=True, enabled=True + ), + orientation=LightOrientationRandomCfg( + angle_range=((-15.0, 15.0), (-15.0, 15.0), (-15.0, 15.0)), + relative_to_origin=True, + distribution="uniform", + enabled=True, + ), + ), + seed=seed + 4 + i if seed is not None else None, + ) + elif isinstance(light, SphereLightCfg): + # Corner lights with position and color + light_rand = LightRandomizer( + LightRandomCfg( + light_name=light_name, + intensity=LightIntensityRandomCfg(intensity_range=corner_range, enabled=True), + color=LightColorRandomCfg( + temperature_range=(2700.0, 5500.0), use_temperature=True, enabled=True + ), + position=LightPositionRandomCfg( + position_range=((-0.5, 0.5), (-0.5, 0.5), (-0.3, 0.3)), + relative_to_origin=True, + distribution="uniform", + enabled=True, + ), + ), + seed=seed + 5 + i if seed is not None else None, + ) + elif isinstance(light, DomeLightCfg): + # Dome light (ambient) + from metasim.randomization.presets.light_presets import LightPresets - if isinstance(light, DomeLightCfg): config = LightPresets.dome_ambient(light_name) - elif isinstance(light, (SphereLightCfg, DiskLightCfg)): - config = LightPresets.sphere_ceiling_light(light_name) + light_rand = LightRandomizer(config, seed=seed + 4 + i if seed else None) else: - log.warning(f"Unknown light type for {light_name}, using sphere_ceiling_light preset") - config = LightPresets.sphere_ceiling_light(light_name) + log.warning(f" Unknown light type: {light_name}") + continue - randomizer = LightRandomizer(config, seed=seed) - randomizer.bind_handler(self.handler) - self.randomizers.append(randomizer) - log.info(f" Added LightRandomizer for {light_name}") + light_rand.bind_handler(self.handler) + self.randomizers["light"].append(light_rand) + log.info(f" Light: {light_name}") def _setup_camera_randomizers(self, seed: int | None): - """Setup camera randomizers for all cameras.""" + """Setup camera randomizers (Level 3+).""" + from metasim.randomization import ( + CameraIntrinsicsRandomCfg, + CameraPositionRandomCfg, + CameraRandomCfg, + ) + from metasim.randomization.presets.camera_presets import CameraProperties + + log.info("\nCamera Randomization") + log.info("-" * 50) + cameras = getattr(self.scenario, "cameras", []) if not cameras: - log.info(" No cameras found for camera randomization") + log.info(" No cameras found") return - log.info(f" Setting up camera randomizers for {len(cameras)} cameras") for camera in cameras: - camera_name = getattr(camera, "name", f"camera_{len(self.randomizers)}") - config = CameraPresets.surveillance_camera(camera_name) - - randomizer = CameraRandomizer(config, seed=seed) - randomizer.bind_handler(self.handler) - self.randomizers.append(randomizer) - log.info(f" Added CameraRandomizer for {camera_name}") - - def _get_material_config(self, obj_name: str): - """Get appropriate material configuration based on object type.""" - obj_lower = obj_name.lower() - if "cube" in obj_lower: - return MaterialPresets.mdl_family_object(obj_name, family="metal") - elif "sphere" in obj_lower: - return MaterialPresets.rubber_object(obj_name) - else: - return MaterialPresets.mdl_family_object(obj_name, family="wood") - - def _setup_physics_randomizers(self, seed: int | None): - """Setup unified ObjectRandomizers for robots and objects.""" - robots = getattr(self.scenario, "robots", []) - objects = getattr(self.scenario, "objects", []) - - self._setup_object_randomizers(robots, objects, seed) - - def _setup_object_randomizers(self, robots: list, objects: list, seed: int | None): - """Setup unified ObjectRandomizers for all physical entities.""" - log.info(" Setting up ObjectRandomizers for physics randomization") - - # Robot randomization - if robots: - robot_name = robots[0] if isinstance(robots[0], str) else robots[0].name - robot_randomizer = ObjectRandomizer(ObjectPresets.robot_base(robot_name), seed=seed) - robot_randomizer.bind_handler(self.handler) - self.randomizers.append(robot_randomizer) - log.info(f" Added ObjectRandomizer for robot {robot_name}") - - # Object randomization - if objects: - for obj in objects: - obj_name = obj.name - config = self._get_object_physics_config(obj_name) - - obj_randomizer = ObjectRandomizer(config, seed=seed) - obj_randomizer.bind_handler(self.handler) - self.randomizers.append(obj_randomizer) - log.info(f" Added ObjectRandomizer for {obj_name}") - - if not robots and not objects: - log.info(" No robots or objects found for physics randomization") - - def _get_object_physics_config(self, obj_name: str): - """Get appropriate physics configuration based on object type.""" - obj_lower = obj_name.lower() - if "cube" in obj_lower: - return ObjectPresets.grasping_target(obj_name) - elif "sphere" in obj_lower: - return ObjectPresets.bouncy_object(obj_name) - else: - return ObjectPresets.physics_only(obj_name) + camera_name = getattr(camera, "name", "camera") + + # Orbit camera configuration (no roll to keep camera horizontal) + # Z range kept positive to ensure camera stays above table (no upward view) + cam_config = CameraRandomCfg( + camera_name=camera_name, + position=CameraPositionRandomCfg( + delta_range=((-0.1, 0.1), (-0.1, 0.1), (0.0, 0.2)), # Z: only upward movement + use_delta=True, + distribution="uniform", + enabled=True, + ), + intrinsics=CameraIntrinsicsRandomCfg( + fov_range=CameraProperties.FOV_NORMAL, + use_fov=True, + distribution="uniform", + enabled=True, + ), + ) + + cam_rand = CameraRandomizer(cam_config, seed=seed + 10 if seed is not None else None) + cam_rand.bind_handler(self.handler) + self.randomizers["camera"].append(cam_rand) + log.info(f" Camera: {camera_name}") + + def apply_randomization(self, demo_idx: int, is_initial: bool = False): + """Apply randomization with global deferred visual flush. + + Args: + demo_idx: Demo index for logging + is_initial: Whether this is the initial call (always creates scene) + """ + if self.args.level == 0 or not self.randomizers: + return - def randomize_for_demo(self, demo_idx: int): - """Apply randomization for a new demo.""" - if not self._should_randomize(demo_idx): + log.info("=" * 50) + log.info(f"DOMAIN RANDOMIZATION: Demo {demo_idx}") + + # Enable global defer flag + if self.handler: + self.handler._defer_all_visual_flushes = True + + try: + # Scene creation/switching + if self.randomizers["scene"]: + if is_initial or self.args.level >= 1: + scene_rand = self.randomizers["scene"] + original_auto_flush = scene_rand.cfg.auto_flush_visuals + scene_rand.cfg.auto_flush_visuals = False + scene_rand() + scene_rand.cfg.auto_flush_visuals = original_auto_flush + log.info(" Applied SceneRandomizer") + + # Level 1+: Material randomization (environment only) + if self.args.level >= 1: + for mat_rand in self.randomizers["material_dynamic"]: + mat_rand() + if self.randomizers["material_dynamic"]: + log.info(f" Applied MaterialRandomizers ({len(self.randomizers['material_dynamic'])})") + + # Level 2+: Lighting + if self.args.level >= 2: + for light_rand in self.randomizers["light"]: + light_rand() + if self.randomizers["light"]: + log.info(f" Applied LightRandomizers ({len(self.randomizers['light'])})") + + # Level 3+: Camera + if self.args.level >= 3: + for cam_rand in self.randomizers["camera"]: + cam_rand() + if self.randomizers["camera"]: + log.info(f" Applied CameraRandomizers ({len(self.randomizers['camera'])})") + + finally: + # Disable global defer and flush once + if self.handler: + self.handler._defer_all_visual_flushes = False + if hasattr(self.handler, "flush_visual_updates"): + try: + self.handler.flush_visual_updates(wait_for_materials=True, settle_passes=2) + except Exception as e: + log.debug(f"Failed to flush visual updates: {e}") + + def update_camera_look_at(self, env_id: int = 0): + """Update camera position and look_at to focus on table after scene switch. + + Adjusts both camera position and look-at point to maintain the same relative + viewing angle but account for the table's height. + """ + if not self.randomizers.get("scene"): return - log_randomization_header("DOMAIN RANDOMIZATION", f"Demo {demo_idx}") + table_bounds = self.randomizers["scene"].get_table_bounds(env_id=env_id) + if not table_bounds or abs(table_bounds.get("height", 0)) > 100: + return - # Apply all randomizers and collect statistics - stats = self._apply_all_randomizers() + table_height = table_bounds["height"] - # Log summary - self._log_randomization_summary(stats) - self._demo_count += 1 + # Use original camera positions stored in __init__ (before any randomization) + clearance = 0.05 + target_look_at_z = table_height + clearance - def _should_randomize(self, demo_idx: int) -> bool: - """Check if randomization should be applied for this demo.""" - if not self.args.enable_randomization or not self.randomizers: - return False + for camera in self.handler.cameras: + orig = self.original_camera_positions[camera.name] + orig_look_at_z = orig["look_at"][2] + orig_pos_z = orig["pos"][2] - return self.args.randomization_frequency == "per_demo" or ( - self.args.randomization_frequency == "per_episode" and demo_idx == 0 - ) + # Compute Z offset needed + z_offset = target_look_at_z - orig_look_at_z - def _apply_all_randomizers(self) -> dict[str, int]: - """Apply all randomizers and return statistics.""" - stats = {"ObjectRandomizer": 0, "MaterialRandomizer": 0, "LightRandomizer": 0, "CameraRandomizer": 0} - - for randomizer in self.randomizers: - try: - obj_name = self._get_randomizer_target_name(randomizer) - randomizer_type = type(randomizer).__name__ - - # Apply randomization - randomizer() - stats[randomizer_type] = stats.get(randomizer_type, 0) + 1 - log.info(f" Applied {randomizer_type} for {obj_name}") - - except Exception as e: - log.warning(f" {type(randomizer).__name__} failed for {obj_name}: {e}") - - return stats - - def _get_randomizer_target_name(self, randomizer) -> str: - """Extract target object name from randomizer configuration.""" - if not hasattr(randomizer, "cfg"): - return "unknown" - - cfg = randomizer.cfg - if hasattr(cfg, "obj_name"): - return cfg.obj_name - elif hasattr(cfg, "light_name"): - return cfg.light_name - elif hasattr(cfg, "camera_name"): - return cfg.camera_name - else: - return "unknown" + # Apply same offset to both position and look_at + camera.pos = (orig["pos"][0], orig["pos"][1], orig_pos_z + z_offset) + camera.look_at = (orig["look_at"][0], orig["look_at"][1], target_look_at_z) - def _log_randomization_summary(self, stats: dict[str, int]): - """Log a summary of applied randomizers.""" - applied_types = [f"{name}: {count}" for name, count in stats.items() if count > 0] - if applied_types: - log.info(f"Applied randomizers: {', '.join(applied_types)}") - else: - log.info("No randomizers were applied") + if hasattr(self.handler, "_update_camera_pose"): + self.handler._update_camera_pose() + + def update_positions_to_table(self, demo_idx: int, env_id: int = 0): + """Update object positions to align with current table after scene switch. + + Maintains relative positions of all objects and robots (rigid body translation). + The entire system is translated such that the original ground level aligns with the table surface. + """ + if not self.randomizers.get("scene"): + return + + # Get current state + if demo_idx >= len(self.init_states): + return + + init_state = self.init_states[demo_idx] + + # Get this demo's original positions (stored in __init__) + demo_key = f"demo_{demo_idx}" + if demo_key not in self.original_positions: + log.warning(f"No original positions found for demo {demo_idx}") + return + + demo_original_positions = self.original_positions[demo_key] + + # Get table bounds + table_bounds = self.randomizers["scene"].get_table_bounds(env_id=env_id) + if not table_bounds or abs(table_bounds.get("height", 0)) > 100: + return + + table_height = table_bounds["height"] + table_center_x = (table_bounds["x_min"] + table_bounds["x_max"]) / 2 + table_center_y = (table_bounds["y_min"] + table_bounds["y_max"]) / 2 + + # Compute system center (XY) + all_x = [demo_original_positions[k]["x"] for k in demo_original_positions] + all_y = [demo_original_positions[k]["y"] for k in demo_original_positions] + system_center_x = sum(all_x) / len(all_x) + system_center_y = sum(all_y) / len(all_y) + + # Compute XY offset (to center system on table) + offset_x = table_center_x - system_center_x + offset_y = table_center_y - system_center_y + + # Compute Z offset: move the reference plane from ground to table + # Find the ground level (minimum Z in original trajectory) + all_z = [demo_original_positions[k]["z"] for k in demo_original_positions] + ground_level = min(all_z) + + # The reference plane (ground) moves to table surface + # All objects and robots maintain their relative height from this plane + z_offset = table_height - ground_level + + # Apply same offset to everything (rigid body translation) + for obj_name, obj_state in init_state["objects"].items(): + orig = demo_original_positions[f"obj_{obj_name}"] + obj_state["pos"][0] = orig["x"] + offset_x + obj_state["pos"][1] = orig["y"] + offset_y + obj_state["pos"][2] = orig["z"] + z_offset + + for robot_name, robot_state in init_state["robots"].items(): + orig = demo_original_positions[f"robot_{robot_name}"] + robot_state["pos"][0] = orig["x"] + offset_x + robot_state["pos"][1] = orig["y"] + offset_y + robot_state["pos"][2] = orig["z"] + z_offset def get_actions(all_actions, env, demo_idxs: list[int], robot: RobotCfg): @@ -455,15 +725,12 @@ def ensure_clean_state(handler, expected_state=None): handler.simulate() current_state = handler.get_states() - # Only start checking after minimum steps if step >= min_steps: if prev_state is not None: - # Check if key states are stable (focus on articulated objects) is_stable = True if hasattr(current_state, "objects") and hasattr(prev_state, "objects"): for obj_name, obj_state in current_state.objects.items(): if obj_name in prev_state.objects: - # Check DOF positions for articulated objects curr_dof = getattr(obj_state, "dof_pos", None) prev_dof = getattr(prev_state.objects[obj_name], "dof_pos", None) if curr_dof is not None and prev_dof is not None: @@ -471,51 +738,45 @@ def ensure_clean_state(handler, expected_state=None): is_stable = False break - # Additional validation: check if we're stable at the RIGHT state if is_stable and expected_state is not None: is_correct_state = _validate_state_correctness(current_state, expected_state) if not is_correct_state: - # We're stable but at wrong state - force more simulation log.debug(f"State stable but incorrect at step {step}, continuing simulation...") stable_count = 0 is_stable = False - # Continue simulating to let physics settle properly if is_stable: stable_count += 1 - if stable_count >= 2: # Stable for 2 consecutive steps at correct state + if stable_count >= 2: break else: stable_count = 0 prev_state = current_state - # Final validation if we ran out of steps if expected_state is not None: final_state = handler.get_states() is_final_correct = _validate_state_correctness(final_state, expected_state) if not is_final_correct: log.warning(f"State validation failed after {max_steps} steps - reset may not have taken full effect") - # Final state refresh handler.get_states() def _validate_state_correctness(current_state, expected_state): """Validate that current state matches expected initial state for critical objects.""" if not hasattr(current_state, "objects") or not hasattr(expected_state, "objects"): - return True # Can't validate, assume correct + return True - # Focus on articulated objects which are most prone to reset issues critical_objects = [] for obj_name, expected_obj in expected_state.objects.items(): if hasattr(expected_obj, "dof_pos") and getattr(expected_obj, "dof_pos", None) is not None: critical_objects.append(obj_name) if not critical_objects: - return True # No critical objects to validate + return True - tolerance = 5e-3 # Reasonable tolerance for DOF positions + tolerance = 5e-3 for obj_name in critical_objects: if obj_name not in current_state.objects: @@ -524,13 +785,11 @@ def _validate_state_correctness(current_state, expected_state): expected_obj = expected_state.objects[obj_name] current_obj = current_state.objects[obj_name] - # Check DOF positions for articulated objects (most critical for demo consistency) expected_dof = getattr(expected_obj, "dof_pos", None) current_dof = getattr(current_obj, "dof_pos", None) if expected_dof is not None and current_dof is not None: if not torch.allclose(current_dof, expected_dof, atol=tolerance): - # Log the specific difference for debugging diff = torch.abs(current_dof - expected_dof).max().item() log.debug(f"DOF mismatch for {obj_name}: max diff = {diff:.6f} (tolerance = {tolerance})") return False @@ -541,9 +800,7 @@ def _validate_state_correctness(current_state, expected_state): def force_reset_to_state(env, state, env_id): """Force reset environment to specific state with validation.""" env.reset(states=[state], env_ids=[env_id]) - # Pass expected state for validation ensure_clean_state(env.handler, expected_state=state) - # Reset episode counter AFTER stabilization to ensure demo starts from action 0 if hasattr(env, "_episode_steps"): env._episode_steps[env_id] = 0 @@ -603,7 +860,6 @@ def save(self, demo_idx: int, status: str): assert demo_idx in self.cache assert status in ["success", "failed"], f"Invalid status: {status}" - # Use demo_idx directly as continuous_idx to maintain consistency continuous_idx = demo_idx save_dir = os.path.join(self.base_save_dir, status, f"demo_{continuous_idx:04d}") @@ -611,9 +867,7 @@ def save(self, demo_idx: int, status: str): os.remove(os.path.join(save_dir, "status.txt")) os.makedirs(save_dir, exist_ok=True) - log.info(f"Saving demo {demo_idx} (original) as {continuous_idx:04d} (continuous) to {save_dir}") - - ## Option 1: Save immediately, blocking and slower + log.info(f"Saving demo {demo_idx} as {continuous_idx:04d} to {save_dir}") from metasim.utils.save_util import save_demo @@ -623,9 +877,6 @@ def save(self, demo_idx: int, status: str): with open(os.path.join(save_dir, "status.txt"), "w") as f: f.write(status) - ## Option 2: Save in a separate process, non-blocking, not friendly to KeyboardInterrupt - # self.save_request_queue.put({"demo": self.cache[demo_idx], "save_dir": save_dir}) - def delete(self, demo_idx: int): assert demo_idx in self.cache del self.cache[demo_idx] @@ -709,21 +960,48 @@ def main(): if is_libero_dataset: dp_pos = (2.0, 0.0, 2) elif dp_camera: - # import warnings - # warnings.warn("Using dp camera position!") dp_pos = (1.0, 0.0, 0.75) else: dp_pos = (1.5, 0.0, 1.5) - # libero specific camera position - # dp_pos = (0.8, -0, 1.6) - # look_at = (-2.5, 0.0, 0.0) - camera = PinholeCameraCfg(data_types=["rgb", "depth"], pos=dp_pos, look_at=(0.0, 0.0, 0.0)) + + # Lighting setup + if args.render.mode == "pathtracing": + ceiling_main = 18000.0 + ceiling_corners = 8000.0 + else: + ceiling_main = 12000.0 + ceiling_corners = 5000.0 + + lights = [ + DiskLightCfg( + name="ceiling_main", + intensity=ceiling_main, + color=(1.0, 1.0, 1.0), + radius=1.2, + pos=(0.0, 0.0, 2.8), + rot=(0.7071, 0.0, 0.0, 0.7071), + ), + SphereLightCfg( + name="ceiling_ne", intensity=ceiling_corners, color=(1.0, 1.0, 1.0), radius=0.6, pos=(1.0, 1.0, 2.5) + ), + SphereLightCfg( + name="ceiling_nw", intensity=ceiling_corners, color=(1.0, 1.0, 1.0), radius=0.6, pos=(-1.0, 1.0, 2.5) + ), + SphereLightCfg( + name="ceiling_sw", intensity=ceiling_corners, color=(1.0, 1.0, 1.0), radius=0.6, pos=(-1.0, -1.0, 2.5) + ), + SphereLightCfg( + name="ceiling_se", intensity=ceiling_corners, color=(1.0, 1.0, 1.0), radius=0.6, pos=(1.0, -1.0, 2.5) + ), + ] + scenario = task_cls.scenario.update( robots=[args.robot], scene=args.scene, cameras=[camera], + lights=lights, render=args.render, simulator=args.sim, renderer=args.renderer, @@ -734,12 +1012,13 @@ def main(): device = torch.device("cuda" if torch.cuda.is_available() else "cpu") env = task_cls(scenario, device=device) - # Initialize domain randomization manager - randomization_manager = DomainRandomizationManager(args, scenario, env.handler) ## Data assert os.path.exists(env.traj_filepath), f"Trajectory file does not exist: {env.traj_filepath}" init_states, all_actions, all_states = get_traj(env.traj_filepath, robot, env.handler) + # Initialize domain randomization manager + randomization_manager = DomainRandomizationManager(args, scenario, env.handler, init_states) + tot_demo = len(all_actions) if args.split == "train": init_states = init_states[: int(tot_demo * 0.9)] @@ -756,11 +1035,6 @@ def main(): ######################################################## ## Main ######################################################## - # if args.max_demo_idx > n_demo: - # log.warning( - # f"Max demo {args.max_demo_idx} is greater than the number of demos in the dataset {n_demo}, using {n_demo}" - # ) - # max_demo = min(args.max_demo_idx, n_demo) max_demo = n_demo try_num = args.retry_num + 1 @@ -769,10 +1043,8 @@ def main(): ## CollectingDemo -> Timeout -> Retry/GiveUp -> NextDemo ## Setup - # Get task description from environment task_desc = getattr(env, "task_desc", "") collector = DemoCollector(env.handler, robot, task_desc) - # pbar = tqdm(total=max_demo - args.demo_start_idx, desc="Collecting demos") pbar = tqdm(total=args.num_demo_success, desc="Collecting successful demos") ## State variables @@ -803,25 +1075,27 @@ def main(): demo_indexer.move_on() log.info(f"Initialize with demo idxs: {demo_idxs}") - ## Apply initial randomization + ## Apply initial randomization (create scene and update positions) for env_id, demo_idx in enumerate(demo_idxs): - randomization_manager.randomize_for_demo(demo_idx) + randomization_manager.apply_randomization(demo_idx, is_initial=True) + randomization_manager.update_positions_to_table(demo_idx, env_id) + randomization_manager.update_camera_look_at(env_id) - ## Reset to initial states + ## Reset to initial states (after position adjustment) obs, extras = env.reset(states=[init_states[demo_idx] for demo_idx in demo_idxs]) - ## Wait for environment to stabilize after reset (before counting demo steps) - # For initial setup, we can't validate individual states easily, so just ensure stability + ## Wait for environment to stabilize after reset ensure_clean_state(env.handler) - ## Reset episode step counters AFTER stabilization + ## Reset episode step counters after stabilization if hasattr(env, "_episode_steps"): for env_id in range(env.handler.num_envs): env._episode_steps[env_id] = 0 - ## Now record the clean, stabilized initial state + ## Record the clean, stabilized initial state obs = env.handler.get_states() obs = state_tensor_to_nested(env.handler, obs) + for env_id, demo_idx in enumerate(demo_idxs): log.info(f"Starting Demo {demo_idx} in Env {env_id}") collector.create(demo_idx, obs[env_id]) @@ -874,7 +1148,8 @@ def main(): demo_idxs[env_id] = new_demo_idx log.info(f"Transitioning Env {env_id}: Demo {demo_idx} to Demo {new_demo_idx}") - randomization_manager.randomize_for_demo(new_demo_idx) + randomization_manager.apply_randomization(new_demo_idx, is_initial=False) + randomization_manager.update_positions_to_table(new_demo_idx, env_id) force_reset_to_state(env, init_states[new_demo_idx], env_id) obs = env.handler.get_states() @@ -897,7 +1172,9 @@ def main(): if failure_count[env_id] < try_num: log.info(f"Demo {demo_idx} failed {failure_count[env_id]} times, retrying...") - randomization_manager.randomize_for_demo(demo_idx) + randomization_manager.apply_randomization(demo_idx, is_initial=False) + randomization_manager.update_positions_to_table(demo_idx, env_id) + randomization_manager.update_camera_look_at(env_id) force_reset_to_state(env, init_states[demo_idx], env_id) obs = env.handler.get_states() @@ -913,7 +1190,9 @@ def main(): if demo_indexer.next_idx < max_demo: new_demo_idx = demo_indexer.next_idx demo_idxs[env_id] = new_demo_idx - randomization_manager.randomize_for_demo(new_demo_idx) + randomization_manager.apply_randomization(new_demo_idx, is_initial=False) + randomization_manager.update_positions_to_table(new_demo_idx, env_id) + randomization_manager.update_camera_look_at(env_id) force_reset_to_state(env, init_states[new_demo_idx], env_id) obs = env.handler.get_states() From 9b5107483a7df8fc8ae0f08e8add0a426e9c6fa8 Mon Sep 17 00:00:00 2001 From: gxyes Date: Thu, 4 Dec 2025 14:44:20 +0700 Subject: [PATCH 05/50] clean up code & create a manager --- metasim/randomization/__init__.py | 5 + metasim/randomization/dr_manager.py | 603 ++++++++++++++++++++++++++++ scripts/advanced/collect_demo.py | 557 +------------------------ 3 files changed, 620 insertions(+), 545 deletions(-) create mode 100644 metasim/randomization/dr_manager.py diff --git a/metasim/randomization/__init__.py b/metasim/randomization/__init__.py index 0c2abaae7..8d94ce14f 100644 --- a/metasim/randomization/__init__.py +++ b/metasim/randomization/__init__.py @@ -27,6 +27,9 @@ # Core infrastructure from .core import IsaacSimAdapter, ObjectMetadata, ObjectRegistry + +# Unified DR Manager +from .dr_manager import DomainRandomizationManager, DRConfig from .light_randomizer import ( LightColorRandomCfg, LightIntensityRandomCfg, @@ -76,6 +79,8 @@ "CameraPresets", "CameraRandomCfg", "CameraRandomizer", + "DRConfig", + "DomainRandomizationManager", "EnvironmentLayerCfg", "IsaacSimAdapter", "LightColorRandomCfg", diff --git a/metasim/randomization/dr_manager.py b/metasim/randomization/dr_manager.py new file mode 100644 index 000000000..a36b6c694 --- /dev/null +++ b/metasim/randomization/dr_manager.py @@ -0,0 +1,603 @@ +""" +Unified Domain Randomization Manager for Demo Collection and Policy Evaluation. + +This module provides a centralized DR system that can be used by: +- collect_demo.py (demo data collection) +- IL evaluation runners (ACT, Diffusion Policy) +- 12_domain_randomization.py (continues to use individual randomizers directly) + +Architecture: +- Static Objects: Handler-managed (Robot, task objects, Camera, Light) +- Dynamic Objects: SceneRandomizer-managed (Floor, Table, Distractors) +- Level system: 0=None, 1=Scene+Material, 2=+Light, 3=+Camera +- Mode system: 0=Manual, 1=USD Table, 2=USD Scene, 3=Full USD +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, Literal + +import torch +from loguru import logger as log + +if TYPE_CHECKING: + from metasim.scenario.scenario import ScenarioCfg + +# Try to import randomization components +try: + from .camera_randomizer import ( + CameraIntrinsicsRandomCfg, + CameraPositionRandomCfg, + CameraRandomCfg, + CameraRandomizer, + ) + from .light_randomizer import ( + LightColorRandomCfg, + LightIntensityRandomCfg, + LightOrientationRandomCfg, + LightPositionRandomCfg, + LightRandomCfg, + LightRandomizer, + ) + from .material_randomizer import MaterialRandomizer + from .scene_randomizer import ( + EnvironmentLayerCfg, + ManualGeometryCfg, + ObjectsLayerCfg, + SceneRandomCfg, + SceneRandomizer, + USDAssetPoolCfg, + WorkspaceLayerCfg, + ) + from .presets.camera_presets import CameraProperties + from .presets.material_presets import MaterialPresets + from .presets.scene_presets import ScenePresets, SceneUSDCollections + + RANDOMIZATION_AVAILABLE = True +except ImportError as e: + log.warning(f"Randomization components not available: {e}") + RANDOMIZATION_AVAILABLE = False + + +@dataclass +class DRConfig: + """Domain Randomization configuration. + + Attributes: + level: Randomization level (0=None, 1=Scene+Material, 2=+Light, 3=+Camera) + scene_mode: Scene mode (0=Manual, 1=USD Table, 2=USD Scene, 3=Full USD) + randomization_seed: Seed for reproducibility. If None, uses random seed + """ + + level: Literal[0, 1, 2, 3] = 0 + scene_mode: Literal[0, 1, 2, 3] = 0 + randomization_seed: int | None = None + + +class DomainRandomizationManager: + """Unified Domain Randomization Manager. + + Can be used for both demo collection and policy evaluation. + """ + + def __init__( + self, + config: DRConfig, + scenario: ScenarioCfg, + handler, + init_states: list | None = None, + render_cfg=None, + ): + """Initialize DR Manager. + + Args: + config: DR configuration + scenario: Scenario configuration + handler: Simulation handler + init_states: Initial states for position adjustment (optional) + render_cfg: Render configuration for light intensity adjustment (optional) + """ + self.config = config + self.scenario = scenario + self.handler = handler + self.init_states = init_states or [] + self.render_cfg = render_cfg + self.randomizers = {} + + # Store original camera positions BEFORE any randomization + self.original_camera_positions = {} + for camera in self.handler.cameras: + self.original_camera_positions[camera.name] = { + "pos": tuple(camera.pos), + "look_at": tuple(camera.look_at), + } + + # Store original positions for ALL demos (for position adjustment) + self.original_positions = {} + if init_states: + for demo_idx, init_state in enumerate(init_states): + demo_key = f"demo_{demo_idx}" + self.original_positions[demo_key] = {} + + if "objects" in init_state: + for obj_name, obj_state in init_state["objects"].items(): + self.original_positions[demo_key][f"obj_{obj_name}"] = { + "x": float(obj_state["pos"][0]), + "y": float(obj_state["pos"][1]), + "z": float(obj_state["pos"][2]), + } + + if "robots" in init_state: + for robot_name, robot_state in init_state["robots"].items(): + self.original_positions[demo_key][f"robot_{robot_name}"] = { + "x": float(robot_state["pos"][0]), + "y": float(robot_state["pos"][1]), + "z": float(robot_state["pos"][2]), + } + + # Early validation + if not self._validate_setup(): + return + + log.info("=" * 50) + log.info("DOMAIN RANDOMIZATION SETUP: Initializing randomizers") + self._setup_randomizers() + log.info(f"Setup complete: Randomizers ready (Level {config.level}, Mode {config.scene_mode})") + + def _validate_setup(self) -> bool: + """Validate if randomization can be set up.""" + if self.config.level == 0: + log.info("Domain randomization disabled (level=0)") + return False + + if not RANDOMIZATION_AVAILABLE: + log.warning("Domain randomization requested but components not available") + return False + + return True + + def _setup_randomizers(self): + """Initialize all randomizers based on level and mode.""" + seed = self.config.randomization_seed + self._setup_reproducibility(seed) + + self.randomizers = { + "scene": None, + "material_dynamic": [], + "light": [], + "camera": [], + } + + # Scene Randomizer + self._setup_scene_randomizer(seed) + + # Material Randomization + self._setup_material_randomizers(seed) + + # Light Randomization (Level 2+) + if self.config.level >= 2: + self._setup_light_randomizers(seed) + + # Camera Randomization (Level 3+) + if self.config.level >= 3: + self._setup_camera_randomizers(seed) + + def _setup_reproducibility(self, seed: int | None): + """Setup global reproducibility if seed is provided.""" + if seed is not None: + log.info(f"Setting up reproducible randomization with seed: {seed}") + torch.manual_seed(seed) + import random + + import numpy as np + + np.random.seed(seed) + random.seed(seed) + if torch.cuda.is_available(): + torch.cuda.manual_seed(seed) + + def _setup_scene_randomizer(self, seed: int | None): + """Setup SceneRandomizer based on scene_mode.""" + mode = self.config.scene_mode + level = self.config.level + + log.info(f"\nScene Creation (Mode {mode})") + log.info("-" * 50) + + # Environment Layer + if mode >= 2: + # USD Scene + scene_paths, scene_configs = SceneUSDCollections.kujiale_scenes(return_configs=True) + log.info(f"Environment: Kujiale USD ({len(scene_paths)} scenes)") + env_element = USDAssetPoolCfg( + name="kujiale_scene", + usd_paths=scene_paths, + per_path_overrides=scene_configs, + selection_strategy="random" if level >= 1 else "sequential", + ) + environment_layer = EnvironmentLayerCfg(elements=[env_element]) + else: + # Manual Scene + log.info("Environment: Manual geometry (10m x 10m x 5m)") + base_cfg = ScenePresets.empty_room(room_size=10.0, wall_height=5.0) + environment_layer = base_cfg.environment_layer + + # Workspace Layer + if mode >= 1: + # USD Table + table_paths, table_configs = SceneUSDCollections.table785(return_configs=True) + log.info(f"Workspace: Table785 USD ({len(table_paths)} tables)") + workspace_element = USDAssetPoolCfg( + name="table", + usd_paths=table_paths, + per_path_overrides=table_configs, + selection_strategy="random" if level >= 1 else "sequential", + add_collision=True, + ) + workspace_layer = WorkspaceLayerCfg(elements=[workspace_element]) + else: + # Manual Table + log.info("Workspace: Manual table (Plywood default, randomized in level 1+)") + workspace_layer = WorkspaceLayerCfg( + elements=[ + ManualGeometryCfg( + name="table", + geometry_type="cube", + size=(1.8, 1.8, 0.1), + position=(0.0, 0.0, 0.7 - 0.05), + default_material="roboverse_data/materials/arnold/Wood/Plywood.mdl", + add_collision=True, + ) + ] + ) + + # Objects Layer + if mode >= 3: + object_paths, object_configs = SceneUSDCollections.desktop_supplies(return_configs=True) + log.info(f"Objects: Desktop supplies ({len(object_paths)} items, placing 3)") + objects_layer = ObjectsLayerCfg( + elements=[ + USDAssetPoolCfg( + name=f"desktop_object_{i + 1}", + usd_paths=object_paths, + per_path_overrides=object_configs, + selection_strategy="random" if level >= 1 else "sequential", + add_collision=True, + ) + for i in range(3) + ] + ) + else: + objects_layer = None + + # Create SceneRandomizer + scene_cfg = SceneRandomCfg( + environment_layer=environment_layer, + workspace_layer=workspace_layer, + objects_layer=objects_layer, + ) + + scene_rand = SceneRandomizer(scene_cfg, seed=seed) + scene_rand.bind_handler(self.handler) + self.randomizers["scene"] = scene_rand + log.info("SceneRandomizer created") + + def _setup_material_randomizers(self, seed: int | None): + """Setup material randomizers for dynamic objects (environment).""" + mode = self.config.scene_mode + level = self.config.level + + if level == 0: + return + + log.info("\nMaterial Randomization") + log.info("-" * 50) + + # Dynamic Objects (Manual geometry only) + if mode == 0: + # Manual table + table_mat = MaterialRandomizer( + MaterialPresets.mdl_family_object("table", family=("wood", "metal")), + seed=seed + 2 if seed is not None else None, + ) + table_mat.bind_handler(self.handler) + self.randomizers["material_dynamic"].append(table_mat) + log.info(" Dynamic Object: table (Manual)") + + # Manual environment (mode < 2 and level >= 1) + if mode < 2 and level >= 1: + # Floor + floor_mat = MaterialRandomizer( + MaterialPresets.mdl_family_object("floor", family=("carpet", "wood", "stone")), + seed=seed + 101 if seed is not None else None, + ) + floor_mat.bind_handler(self.handler) + self.randomizers["material_dynamic"].append(floor_mat) + + # Walls + wall_seed = seed + 102 if seed is not None else None + for wall_name in ["wall_front", "wall_back", "wall_left", "wall_right"]: + wall_mat = MaterialRandomizer( + MaterialPresets.mdl_family_object(wall_name, family=("masonry", "architecture")), + seed=wall_seed, + ) + wall_mat.bind_handler(self.handler) + self.randomizers["material_dynamic"].append(wall_mat) + + # Ceiling + ceiling_mat = MaterialRandomizer( + MaterialPresets.mdl_family_object("ceiling", family=("architecture", "wall_board")), + seed=seed + 103 if seed is not None else None, + ) + ceiling_mat.bind_handler(self.handler) + self.randomizers["material_dynamic"].append(ceiling_mat) + + log.info(" Dynamic Objects: floor + 4 walls + ceiling") + + def _setup_light_randomizers(self, seed: int | None): + """Setup light randomizers (Level 2+).""" + from metasim.scenario.lights import DiskLightCfg, DomeLightCfg, SphereLightCfg + + log.info("\nLight Randomization") + log.info("-" * 50) + + lights = getattr(self.scenario, "lights", []) + if not lights: + log.info(" No lights found") + return + + # Determine intensity ranges based on render mode + if self.render_cfg and hasattr(self.render_cfg, "mode") and self.render_cfg.mode == "pathtracing": + main_range = (22000.0, 40000.0) + corner_range = (10000.0, 18000.0) + else: + main_range = (16000.0, 30000.0) + corner_range = (6000.0, 12000.0) + + for i, light in enumerate(lights): + light_name = getattr(light, "name", f"light_{i}") + + if isinstance(light, DiskLightCfg): + # Main ceiling light with orientation + light_rand = LightRandomizer( + LightRandomCfg( + light_name=light_name, + intensity=LightIntensityRandomCfg(intensity_range=main_range, enabled=True), + color=LightColorRandomCfg( + temperature_range=(3000.0, 6000.0), use_temperature=True, enabled=True + ), + orientation=LightOrientationRandomCfg( + angle_range=((-15.0, 15.0), (-15.0, 15.0), (-15.0, 15.0)), + relative_to_origin=True, + distribution="uniform", + enabled=True, + ), + ), + seed=seed + 4 + i if seed is not None else None, + ) + elif isinstance(light, SphereLightCfg): + # Corner lights with position and color + light_rand = LightRandomizer( + LightRandomCfg( + light_name=light_name, + intensity=LightIntensityRandomCfg(intensity_range=corner_range, enabled=True), + color=LightColorRandomCfg( + temperature_range=(2700.0, 5500.0), use_temperature=True, enabled=True + ), + position=LightPositionRandomCfg( + position_range=((-0.5, 0.5), (-0.5, 0.5), (-0.3, 0.3)), + relative_to_origin=True, + distribution="uniform", + enabled=True, + ), + ), + seed=seed + 5 + i if seed is not None else None, + ) + elif isinstance(light, DomeLightCfg): + # Dome light (ambient) + from metasim.randomization.presets.light_presets import LightPresets + + config = LightPresets.dome_ambient(light_name) + light_rand = LightRandomizer(config, seed=seed + 4 + i if seed else None) + else: + log.warning(f" Unknown light type: {light_name}") + continue + + light_rand.bind_handler(self.handler) + self.randomizers["light"].append(light_rand) + log.info(f" Light: {light_name}") + + def _setup_camera_randomizers(self, seed: int | None): + """Setup camera randomizers (Level 3+).""" + log.info("\nCamera Randomization") + log.info("-" * 50) + + cameras = getattr(self.scenario, "cameras", []) + if not cameras: + log.info(" No cameras found") + return + + for camera in cameras: + camera_name = getattr(camera, "name", "camera") + + # Orbit camera configuration (no roll to keep camera horizontal) + # Z range kept positive to ensure camera stays above table (no upward view) + cam_config = CameraRandomCfg( + camera_name=camera_name, + position=CameraPositionRandomCfg( + delta_range=((-0.1, 0.1), (-0.1, 0.1), (0.0, 0.2)), # Z: only upward movement + use_delta=True, + distribution="uniform", + enabled=True, + ), + intrinsics=CameraIntrinsicsRandomCfg( + fov_range=CameraProperties.FOV_NORMAL, + use_fov=True, + distribution="uniform", + enabled=True, + ), + ) + + cam_rand = CameraRandomizer(cam_config, seed=seed + 10 if seed is not None else None) + cam_rand.bind_handler(self.handler) + self.randomizers["camera"].append(cam_rand) + log.info(f" Camera: {camera_name}") + + def apply_randomization(self, demo_idx: int = 0, is_initial: bool = False): + """Apply randomization with global deferred visual flush. + + Args: + demo_idx: Demo index for logging + is_initial: Whether this is the initial call (always creates scene) + """ + if self.config.level == 0 or not self.randomizers: + return + + log.info("=" * 50) + log.info(f"DOMAIN RANDOMIZATION: Demo {demo_idx}") + + # Enable global defer flag + if self.handler: + self.handler._defer_all_visual_flushes = True + + try: + # Scene creation/switching + if self.randomizers["scene"]: + if is_initial or self.config.level >= 1: + scene_rand = self.randomizers["scene"] + original_auto_flush = scene_rand.cfg.auto_flush_visuals + scene_rand.cfg.auto_flush_visuals = False + scene_rand() + scene_rand.cfg.auto_flush_visuals = original_auto_flush + log.info(" Applied SceneRandomizer") + + # Level 1+: Material randomization (environment only) + if self.config.level >= 1: + for mat_rand in self.randomizers["material_dynamic"]: + mat_rand() + if self.randomizers["material_dynamic"]: + log.info(f" Applied MaterialRandomizers ({len(self.randomizers['material_dynamic'])})") + + # Level 2+: Lighting + if self.config.level >= 2: + for light_rand in self.randomizers["light"]: + light_rand() + if self.randomizers["light"]: + log.info(f" Applied LightRandomizers ({len(self.randomizers['light'])})") + + # Level 3+: Camera + if self.config.level >= 3: + for cam_rand in self.randomizers["camera"]: + cam_rand() + if self.randomizers["camera"]: + log.info(f" Applied CameraRandomizers ({len(self.randomizers['camera'])})") + + finally: + # Disable global defer and flush once + if self.handler: + self.handler._defer_all_visual_flushes = False + if hasattr(self.handler, "flush_visual_updates"): + try: + self.handler.flush_visual_updates(wait_for_materials=True, settle_passes=2) + except Exception as e: + log.debug(f"Failed to flush visual updates: {e}") + + def update_camera_look_at(self, env_id: int = 0): + """Update camera position and look_at to focus on table after scene switch. + + Adjusts both camera position and look-at point to maintain the same relative + viewing angle but account for the table's height. + """ + if not self.randomizers.get("scene"): + return + + table_bounds = self.randomizers["scene"].get_table_bounds(env_id=env_id) + if not table_bounds or abs(table_bounds.get("height", 0)) > 100: + return + + table_height = table_bounds["height"] + + # Use original camera positions stored in __init__ (before any randomization) + clearance = 0.05 + target_look_at_z = table_height + clearance + + for camera in self.handler.cameras: + orig = self.original_camera_positions[camera.name] + orig_look_at_z = orig["look_at"][2] + orig_pos_z = orig["pos"][2] + + # Compute Z offset needed + z_offset = target_look_at_z - orig_look_at_z + + # Apply same offset to both position and look_at + camera.pos = (orig["pos"][0], orig["pos"][1], orig_pos_z + z_offset) + camera.look_at = (orig["look_at"][0], orig["look_at"][1], target_look_at_z) + + if hasattr(self.handler, "_update_camera_pose"): + self.handler._update_camera_pose() + + def update_positions_to_table(self, demo_idx: int, env_id: int = 0): + """Update object positions to align with current table after scene switch. + + Maintains relative positions of all objects and robots (rigid body translation). + The entire system is translated such that the original ground level aligns with the table surface. + """ + if not self.randomizers.get("scene"): + return + + # Get current state + if demo_idx >= len(self.init_states): + return + + init_state = self.init_states[demo_idx] + + # Get this demo's original positions (stored in __init__) + demo_key = f"demo_{demo_idx}" + if demo_key not in self.original_positions: + log.warning(f"No original positions found for demo {demo_idx}") + return + + demo_original_positions = self.original_positions[demo_key] + + # Get table bounds + table_bounds = self.randomizers["scene"].get_table_bounds(env_id=env_id) + if not table_bounds or abs(table_bounds.get("height", 0)) > 100: + return + + table_height = table_bounds["height"] + table_center_x = (table_bounds["x_min"] + table_bounds["x_max"]) / 2 + table_center_y = (table_bounds["y_min"] + table_bounds["y_max"]) / 2 + + # Compute system center (XY) + all_x = [demo_original_positions[k]["x"] for k in demo_original_positions] + all_y = [demo_original_positions[k]["y"] for k in demo_original_positions] + system_center_x = sum(all_x) / len(all_x) + system_center_y = sum(all_y) / len(all_y) + + # Compute XY offset (to center system on table) + offset_x = table_center_x - system_center_x + offset_y = table_center_y - system_center_y + + # Compute Z offset: move the reference plane from ground to table + # Find the ground level (minimum Z in original trajectory) + all_z = [demo_original_positions[k]["z"] for k in demo_original_positions] + ground_level = min(all_z) + + # The reference plane (ground) moves to table surface + # All objects and robots maintain their relative height from this plane + z_offset = table_height - ground_level + + # Apply same offset to everything (rigid body translation) + for obj_name, obj_state in init_state["objects"].items(): + orig = demo_original_positions[f"obj_{obj_name}"] + obj_state["pos"][0] = orig["x"] + offset_x + obj_state["pos"][1] = orig["y"] + offset_y + obj_state["pos"][2] = orig["z"] + z_offset + + for robot_name, robot_state in init_state["robots"].items(): + orig = demo_original_positions[f"robot_{robot_name}"] + robot_state["pos"][0] = orig["x"] + offset_x + robot_state["pos"][1] = orig["y"] + offset_y + robot_state["pos"][2] = orig["z"] + z_offset + diff --git a/scripts/advanced/collect_demo.py b/scripts/advanced/collect_demo.py index 733ab9d88..e69a3419a 100644 --- a/scripts/advanced/collect_demo.py +++ b/scripts/advanced/collect_demo.py @@ -132,29 +132,7 @@ def __post_init__(self): # Import randomization components try: - from metasim.randomization import ( - # Randomizers - CameraRandomizer, - # Scene Configuration - EnvironmentLayerCfg, - # Light configuration - LightColorRandomCfg, - LightIntensityRandomCfg, - LightOrientationRandomCfg, - LightPositionRandomCfg, - LightRandomCfg, - LightRandomizer, - ManualGeometryCfg, - MaterialPresets, - MaterialRandomizer, - # Core (usually transparent) - ObjectsLayerCfg, - SceneRandomCfg, - SceneRandomizer, - USDAssetPoolCfg, - WorkspaceLayerCfg, - ) - from metasim.randomization.presets.scene_presets import ScenePresets, SceneUSDCollections + from metasim.randomization import DomainRandomizationManager, DRConfig RANDOMIZATION_AVAILABLE = True except ImportError as e: @@ -162,527 +140,6 @@ def __post_init__(self): RANDOMIZATION_AVAILABLE = False -class DomainRandomizationManager: - """Manages domain randomization for demo collection. - - Architecture: - - Static Objects: Handler-managed (Robot, task objects, Camera, Light) - - Dynamic Objects: SceneRandomizer-managed (Floor, Table, Distractors) - - Level system: 0=None, 1=Scene+Material, 2=+Light, 3=+Camera - - Mode system: 0=Manual, 1=USD Table, 2=USD Scene, 3=Full USD - """ - - def __init__(self, args: Args, scenario, handler, init_states: list): - self.args = args - self.scenario = scenario - self.handler = handler - self.init_states = init_states - self.randomizers = {} - - # Store original camera positions BEFORE any randomization - self.original_camera_positions = {} - for camera in self.handler.cameras: - self.original_camera_positions[camera.name] = { - "pos": tuple(camera.pos), - "look_at": tuple(camera.look_at), - } - - # Store original positions for ALL demos (before any modification) - # This ensures we always have the true original positions from the trajectory file - self.original_positions = {} - for demo_idx, init_state in enumerate(init_states): - demo_key = f"demo_{demo_idx}" - self.original_positions[demo_key] = {} - - if "objects" in init_state: - for obj_name, obj_state in init_state["objects"].items(): - self.original_positions[demo_key][f"obj_{obj_name}"] = { - "x": float(obj_state["pos"][0]), - "y": float(obj_state["pos"][1]), - "z": float(obj_state["pos"][2]), - } - - if "robots" in init_state: - for robot_name, robot_state in init_state["robots"].items(): - self.original_positions[demo_key][f"robot_{robot_name}"] = { - "x": float(robot_state["pos"][0]), - "y": float(robot_state["pos"][1]), - "z": float(robot_state["pos"][2]), - } - - # Early validation - if not self._validate_setup(): - return - - log.info("=" * 50) - log.info("DOMAIN RANDOMIZATION SETUP: Initializing randomizers") - self._setup_randomizers() - log.info(f"Setup complete: Randomizers ready (Level {args.level}, Mode {args.scene_mode})") - - def _validate_setup(self) -> bool: - """Validate if randomization can be set up.""" - if self.args.level == 0: - log.info("Domain randomization disabled (level=0)") - return False - - if not RANDOMIZATION_AVAILABLE: - log.warning("Domain randomization requested but components not available") - return False - - return True - - def _setup_randomizers(self): - """Initialize all randomizers based on level and mode.""" - seed = self.args.randomization_seed - self._setup_reproducibility(seed) - - self.randomizers = { - "scene": None, - "material_dynamic": [], - "light": [], - "camera": [], - } - - # Scene Randomizer - self._setup_scene_randomizer(seed) - - # Material Randomization - self._setup_material_randomizers(seed) - - # Light Randomization (Level 2+) - if self.args.level >= 2: - self._setup_light_randomizers(seed) - - # Camera Randomization (Level 3+) - if self.args.level >= 3: - self._setup_camera_randomizers(seed) - - def _setup_reproducibility(self, seed: int | None): - """Setup global reproducibility if seed is provided.""" - if seed is not None: - log.info(f"Setting up reproducible randomization with seed: {seed}") - torch.manual_seed(seed) - import random - - import numpy as np - - np.random.seed(seed) - random.seed(seed) - if torch.cuda.is_available(): - torch.cuda.manual_seed(seed) - - def _setup_scene_randomizer(self, seed: int | None): - """Setup SceneRandomizer based on scene_mode.""" - mode = self.args.scene_mode - level = self.args.level - - log.info(f"\nScene Creation (Mode {mode})") - log.info("-" * 50) - - # Environment Layer - if mode >= 2: - # USD Scene - scene_paths, scene_configs = SceneUSDCollections.kujiale_scenes(return_configs=True) - log.info(f"Environment: Kujiale USD ({len(scene_paths)} scenes)") - env_element = USDAssetPoolCfg( - name="kujiale_scene", - usd_paths=scene_paths, - per_path_overrides=scene_configs, - selection_strategy="random" if level >= 1 else "sequential", - ) - environment_layer = EnvironmentLayerCfg(elements=[env_element]) - else: - # Manual Scene - log.info("Environment: Manual geometry (10m x 10m x 5m)") - base_cfg = ScenePresets.empty_room(room_size=10.0, wall_height=5.0) - environment_layer = base_cfg.environment_layer - - # Workspace Layer - if mode >= 1: - # USD Table - table_paths, table_configs = SceneUSDCollections.table785(return_configs=True) - log.info(f"Workspace: Table785 USD ({len(table_paths)} tables)") - workspace_element = USDAssetPoolCfg( - name="table", - usd_paths=table_paths, - per_path_overrides=table_configs, - selection_strategy="random" if level >= 1 else "sequential", - add_collision=True, - ) - workspace_layer = WorkspaceLayerCfg(elements=[workspace_element]) - else: - # Manual Table - log.info("Workspace: Manual table (Plywood default, randomized in level 1+)") - workspace_layer = WorkspaceLayerCfg( - elements=[ - ManualGeometryCfg( - name="table", - geometry_type="cube", - size=(1.8, 1.8, 0.1), - position=(0.0, 0.0, 0.7 - 0.05), - default_material="roboverse_data/materials/arnold/Wood/Plywood.mdl", - add_collision=True, - ) - ] - ) - - # Objects Layer - if mode >= 3: - object_paths, object_configs = SceneUSDCollections.desktop_supplies(return_configs=True) - log.info(f"Objects: Desktop supplies ({len(object_paths)} items, placing 3)") - objects_layer = ObjectsLayerCfg( - elements=[ - USDAssetPoolCfg( - name=f"desktop_object_{i + 1}", - usd_paths=object_paths, - per_path_overrides=object_configs, - selection_strategy="random" if level >= 1 else "sequential", - add_collision=True, - ) - for i in range(3) - ] - ) - else: - objects_layer = None - - # Create SceneRandomizer - scene_cfg = SceneRandomCfg( - environment_layer=environment_layer, - workspace_layer=workspace_layer, - objects_layer=objects_layer, - ) - - scene_rand = SceneRandomizer(scene_cfg, seed=seed) - scene_rand.bind_handler(self.handler) - self.randomizers["scene"] = scene_rand - log.info("SceneRandomizer created") - - def _setup_material_randomizers(self, seed: int | None): - """Setup material randomizers for dynamic objects (environment).""" - mode = self.args.scene_mode - level = self.args.level - - if level == 0: - return - - log.info("\nMaterial Randomization") - log.info("-" * 50) - - # Dynamic Objects (Manual geometry only) - if mode == 0: - # Manual table - table_mat = MaterialRandomizer( - MaterialPresets.mdl_family_object("table", family=("wood", "metal")), - seed=seed + 2 if seed is not None else None, - ) - table_mat.bind_handler(self.handler) - self.randomizers["material_dynamic"].append(table_mat) - log.info(" Dynamic Object: table (Manual)") - - # Manual environment (mode < 2 and level >= 1) - if mode < 2 and level >= 1: - # Floor - floor_mat = MaterialRandomizer( - MaterialPresets.mdl_family_object("floor", family=("carpet", "wood", "stone")), - seed=seed + 101 if seed is not None else None, - ) - floor_mat.bind_handler(self.handler) - self.randomizers["material_dynamic"].append(floor_mat) - - # Walls - wall_seed = seed + 102 if seed is not None else None - for wall_name in ["wall_front", "wall_back", "wall_left", "wall_right"]: - wall_mat = MaterialRandomizer( - MaterialPresets.mdl_family_object(wall_name, family=("masonry", "architecture")), - seed=wall_seed, - ) - wall_mat.bind_handler(self.handler) - self.randomizers["material_dynamic"].append(wall_mat) - - # Ceiling - ceiling_mat = MaterialRandomizer( - MaterialPresets.mdl_family_object("ceiling", family=("architecture", "wall_board")), - seed=seed + 103 if seed is not None else None, - ) - ceiling_mat.bind_handler(self.handler) - self.randomizers["material_dynamic"].append(ceiling_mat) - - log.info(" Dynamic Objects: floor + 4 walls + ceiling") - - def _setup_light_randomizers(self, seed: int | None): - """Setup light randomizers (Level 2+).""" - from metasim.scenario.lights import DiskLightCfg, DomeLightCfg, SphereLightCfg - - log.info("\nLight Randomization") - log.info("-" * 50) - - lights = getattr(self.scenario, "lights", []) - if not lights: - log.info(" No lights found") - return - - # Determine intensity ranges based on render mode - if hasattr(self.args.render, "mode") and self.args.render.mode == "pathtracing": - main_range = (22000.0, 40000.0) - corner_range = (10000.0, 18000.0) - else: - main_range = (16000.0, 30000.0) - corner_range = (6000.0, 12000.0) - - for i, light in enumerate(lights): - light_name = getattr(light, "name", f"light_{i}") - - if isinstance(light, DiskLightCfg): - # Main ceiling light with orientation - light_rand = LightRandomizer( - LightRandomCfg( - light_name=light_name, - intensity=LightIntensityRandomCfg(intensity_range=main_range, enabled=True), - color=LightColorRandomCfg( - temperature_range=(3000.0, 6000.0), use_temperature=True, enabled=True - ), - orientation=LightOrientationRandomCfg( - angle_range=((-15.0, 15.0), (-15.0, 15.0), (-15.0, 15.0)), - relative_to_origin=True, - distribution="uniform", - enabled=True, - ), - ), - seed=seed + 4 + i if seed is not None else None, - ) - elif isinstance(light, SphereLightCfg): - # Corner lights with position and color - light_rand = LightRandomizer( - LightRandomCfg( - light_name=light_name, - intensity=LightIntensityRandomCfg(intensity_range=corner_range, enabled=True), - color=LightColorRandomCfg( - temperature_range=(2700.0, 5500.0), use_temperature=True, enabled=True - ), - position=LightPositionRandomCfg( - position_range=((-0.5, 0.5), (-0.5, 0.5), (-0.3, 0.3)), - relative_to_origin=True, - distribution="uniform", - enabled=True, - ), - ), - seed=seed + 5 + i if seed is not None else None, - ) - elif isinstance(light, DomeLightCfg): - # Dome light (ambient) - from metasim.randomization.presets.light_presets import LightPresets - - config = LightPresets.dome_ambient(light_name) - light_rand = LightRandomizer(config, seed=seed + 4 + i if seed else None) - else: - log.warning(f" Unknown light type: {light_name}") - continue - - light_rand.bind_handler(self.handler) - self.randomizers["light"].append(light_rand) - log.info(f" Light: {light_name}") - - def _setup_camera_randomizers(self, seed: int | None): - """Setup camera randomizers (Level 3+).""" - from metasim.randomization import ( - CameraIntrinsicsRandomCfg, - CameraPositionRandomCfg, - CameraRandomCfg, - ) - from metasim.randomization.presets.camera_presets import CameraProperties - - log.info("\nCamera Randomization") - log.info("-" * 50) - - cameras = getattr(self.scenario, "cameras", []) - if not cameras: - log.info(" No cameras found") - return - - for camera in cameras: - camera_name = getattr(camera, "name", "camera") - - # Orbit camera configuration (no roll to keep camera horizontal) - # Z range kept positive to ensure camera stays above table (no upward view) - cam_config = CameraRandomCfg( - camera_name=camera_name, - position=CameraPositionRandomCfg( - delta_range=((-0.1, 0.1), (-0.1, 0.1), (0.0, 0.2)), # Z: only upward movement - use_delta=True, - distribution="uniform", - enabled=True, - ), - intrinsics=CameraIntrinsicsRandomCfg( - fov_range=CameraProperties.FOV_NORMAL, - use_fov=True, - distribution="uniform", - enabled=True, - ), - ) - - cam_rand = CameraRandomizer(cam_config, seed=seed + 10 if seed is not None else None) - cam_rand.bind_handler(self.handler) - self.randomizers["camera"].append(cam_rand) - log.info(f" Camera: {camera_name}") - - def apply_randomization(self, demo_idx: int, is_initial: bool = False): - """Apply randomization with global deferred visual flush. - - Args: - demo_idx: Demo index for logging - is_initial: Whether this is the initial call (always creates scene) - """ - if self.args.level == 0 or not self.randomizers: - return - - log.info("=" * 50) - log.info(f"DOMAIN RANDOMIZATION: Demo {demo_idx}") - - # Enable global defer flag - if self.handler: - self.handler._defer_all_visual_flushes = True - - try: - # Scene creation/switching - if self.randomizers["scene"]: - if is_initial or self.args.level >= 1: - scene_rand = self.randomizers["scene"] - original_auto_flush = scene_rand.cfg.auto_flush_visuals - scene_rand.cfg.auto_flush_visuals = False - scene_rand() - scene_rand.cfg.auto_flush_visuals = original_auto_flush - log.info(" Applied SceneRandomizer") - - # Level 1+: Material randomization (environment only) - if self.args.level >= 1: - for mat_rand in self.randomizers["material_dynamic"]: - mat_rand() - if self.randomizers["material_dynamic"]: - log.info(f" Applied MaterialRandomizers ({len(self.randomizers['material_dynamic'])})") - - # Level 2+: Lighting - if self.args.level >= 2: - for light_rand in self.randomizers["light"]: - light_rand() - if self.randomizers["light"]: - log.info(f" Applied LightRandomizers ({len(self.randomizers['light'])})") - - # Level 3+: Camera - if self.args.level >= 3: - for cam_rand in self.randomizers["camera"]: - cam_rand() - if self.randomizers["camera"]: - log.info(f" Applied CameraRandomizers ({len(self.randomizers['camera'])})") - - finally: - # Disable global defer and flush once - if self.handler: - self.handler._defer_all_visual_flushes = False - if hasattr(self.handler, "flush_visual_updates"): - try: - self.handler.flush_visual_updates(wait_for_materials=True, settle_passes=2) - except Exception as e: - log.debug(f"Failed to flush visual updates: {e}") - - def update_camera_look_at(self, env_id: int = 0): - """Update camera position and look_at to focus on table after scene switch. - - Adjusts both camera position and look-at point to maintain the same relative - viewing angle but account for the table's height. - """ - if not self.randomizers.get("scene"): - return - - table_bounds = self.randomizers["scene"].get_table_bounds(env_id=env_id) - if not table_bounds or abs(table_bounds.get("height", 0)) > 100: - return - - table_height = table_bounds["height"] - - # Use original camera positions stored in __init__ (before any randomization) - clearance = 0.05 - target_look_at_z = table_height + clearance - - for camera in self.handler.cameras: - orig = self.original_camera_positions[camera.name] - orig_look_at_z = orig["look_at"][2] - orig_pos_z = orig["pos"][2] - - # Compute Z offset needed - z_offset = target_look_at_z - orig_look_at_z - - # Apply same offset to both position and look_at - camera.pos = (orig["pos"][0], orig["pos"][1], orig_pos_z + z_offset) - camera.look_at = (orig["look_at"][0], orig["look_at"][1], target_look_at_z) - - if hasattr(self.handler, "_update_camera_pose"): - self.handler._update_camera_pose() - - def update_positions_to_table(self, demo_idx: int, env_id: int = 0): - """Update object positions to align with current table after scene switch. - - Maintains relative positions of all objects and robots (rigid body translation). - The entire system is translated such that the original ground level aligns with the table surface. - """ - if not self.randomizers.get("scene"): - return - - # Get current state - if demo_idx >= len(self.init_states): - return - - init_state = self.init_states[demo_idx] - - # Get this demo's original positions (stored in __init__) - demo_key = f"demo_{demo_idx}" - if demo_key not in self.original_positions: - log.warning(f"No original positions found for demo {demo_idx}") - return - - demo_original_positions = self.original_positions[demo_key] - - # Get table bounds - table_bounds = self.randomizers["scene"].get_table_bounds(env_id=env_id) - if not table_bounds or abs(table_bounds.get("height", 0)) > 100: - return - - table_height = table_bounds["height"] - table_center_x = (table_bounds["x_min"] + table_bounds["x_max"]) / 2 - table_center_y = (table_bounds["y_min"] + table_bounds["y_max"]) / 2 - - # Compute system center (XY) - all_x = [demo_original_positions[k]["x"] for k in demo_original_positions] - all_y = [demo_original_positions[k]["y"] for k in demo_original_positions] - system_center_x = sum(all_x) / len(all_x) - system_center_y = sum(all_y) / len(all_y) - - # Compute XY offset (to center system on table) - offset_x = table_center_x - system_center_x - offset_y = table_center_y - system_center_y - - # Compute Z offset: move the reference plane from ground to table - # Find the ground level (minimum Z in original trajectory) - all_z = [demo_original_positions[k]["z"] for k in demo_original_positions] - ground_level = min(all_z) - - # The reference plane (ground) moves to table surface - # All objects and robots maintain their relative height from this plane - z_offset = table_height - ground_level - - # Apply same offset to everything (rigid body translation) - for obj_name, obj_state in init_state["objects"].items(): - orig = demo_original_positions[f"obj_{obj_name}"] - obj_state["pos"][0] = orig["x"] + offset_x - obj_state["pos"][1] = orig["y"] + offset_y - obj_state["pos"][2] = orig["z"] + z_offset - - for robot_name, robot_state in init_state["robots"].items(): - orig = demo_original_positions[f"robot_{robot_name}"] - robot_state["pos"][0] = orig["x"] + offset_x - robot_state["pos"][1] = orig["y"] + offset_y - robot_state["pos"][2] = orig["z"] + z_offset - - def get_actions(all_actions, env, demo_idxs: list[int], robot: RobotCfg): action_idxs = env._episode_steps @@ -1017,7 +474,17 @@ def main(): init_states, all_actions, all_states = get_traj(env.traj_filepath, robot, env.handler) # Initialize domain randomization manager - randomization_manager = DomainRandomizationManager(args, scenario, env.handler, init_states) + randomization_manager = DomainRandomizationManager( + config=DRConfig( + level=args.level, + scene_mode=args.scene_mode, + randomization_seed=args.randomization_seed, + ), + scenario=scenario, + handler=env.handler, + init_states=init_states, + render_cfg=args.render, + ) tot_demo = len(all_actions) if args.split == "train": From f0c065e11e910d031de5889a031e2996f28a4f1a Mon Sep 17 00:00:00 2001 From: gxyes Date: Thu, 4 Dec 2025 16:46:07 +0700 Subject: [PATCH 06/50] add DR to act eval --- metasim/randomization/dr_manager.py | 136 ++++++++---- roboverse_learn/il/act/act_eval_runner.py | 257 ++++++++++++++++++++-- roboverse_learn/il/act/act_run.sh | 13 +- scripts/advanced/collect_demo.py | 5 + 4 files changed, 346 insertions(+), 65 deletions(-) diff --git a/metasim/randomization/dr_manager.py b/metasim/randomization/dr_manager.py index a36b6c694..1b9d7fab3 100644 --- a/metasim/randomization/dr_manager.py +++ b/metasim/randomization/dr_manager.py @@ -1,5 +1,4 @@ -""" -Unified Domain Randomization Manager for Demo Collection and Policy Evaluation. +"""Unified Domain Randomization Manager for Demo Collection and Policy Evaluation. This module provides a centralized DR system that can be used by: - collect_demo.py (demo data collection) @@ -27,7 +26,6 @@ # Try to import randomization components try: from .camera_randomizer import ( - CameraIntrinsicsRandomCfg, CameraPositionRandomCfg, CameraRandomCfg, CameraRandomizer, @@ -41,6 +39,8 @@ LightRandomizer, ) from .material_randomizer import MaterialRandomizer + from .presets.material_presets import MaterialPresets + from .presets.scene_presets import ScenePresets, SceneUSDCollections from .scene_randomizer import ( EnvironmentLayerCfg, ManualGeometryCfg, @@ -50,9 +50,6 @@ USDAssetPoolCfg, WorkspaceLayerCfg, ) - from .presets.camera_presets import CameraProperties - from .presets.material_presets import MaterialPresets - from .presets.scene_presets import ScenePresets, SceneUSDCollections RANDOMIZATION_AVAILABLE = True except ImportError as e: @@ -63,7 +60,7 @@ @dataclass class DRConfig: """Domain Randomization configuration. - + Attributes: level: Randomization level (0=None, 1=Scene+Material, 2=+Light, 3=+Camera) scene_mode: Scene mode (0=Manual, 1=USD Table, 2=USD Scene, 3=Full USD) @@ -77,7 +74,7 @@ class DRConfig: class DomainRandomizationManager: """Unified Domain Randomization Manager. - + Can be used for both demo collection and policy evaluation. """ @@ -90,7 +87,7 @@ def __init__( render_cfg=None, ): """Initialize DR Manager. - + Args: config: DR configuration scenario: Scenario configuration @@ -349,11 +346,11 @@ def _setup_light_randomizers(self, seed: int | None): # Determine intensity ranges based on render mode if self.render_cfg and hasattr(self.render_cfg, "mode") and self.render_cfg.mode == "pathtracing": - main_range = (22000.0, 40000.0) - corner_range = (10000.0, 18000.0) + main_range = (18000.0, 45000.0) + corner_range = (8000.0, 20000.0) else: - main_range = (16000.0, 30000.0) - corner_range = (6000.0, 12000.0) + main_range = (12000.0, 35000.0) + corner_range = (5000.0, 15000.0) for i, light in enumerate(lights): light_name = getattr(light, "name", f"light_{i}") @@ -365,7 +362,7 @@ def _setup_light_randomizers(self, seed: int | None): light_name=light_name, intensity=LightIntensityRandomCfg(intensity_range=main_range, enabled=True), color=LightColorRandomCfg( - temperature_range=(3000.0, 6000.0), use_temperature=True, enabled=True + temperature_range=(2800.0, 6500.0), use_temperature=True, enabled=True ), orientation=LightOrientationRandomCfg( angle_range=((-15.0, 15.0), (-15.0, 15.0), (-15.0, 15.0)), @@ -383,7 +380,7 @@ def _setup_light_randomizers(self, seed: int | None): light_name=light_name, intensity=LightIntensityRandomCfg(intensity_range=corner_range, enabled=True), color=LightColorRandomCfg( - temperature_range=(2700.0, 5500.0), use_temperature=True, enabled=True + temperature_range=(2500.0, 6000.0), use_temperature=True, enabled=True ), position=LightPositionRandomCfg( position_range=((-0.5, 0.5), (-0.5, 0.5), (-0.3, 0.3)), @@ -421,22 +418,18 @@ def _setup_camera_randomizers(self, seed: int | None): for camera in cameras: camera_name = getattr(camera, "name", "camera") - # Orbit camera configuration (no roll to keep camera horizontal) - # Z range kept positive to ensure camera stays above table (no upward view) + # Conservative camera randomization to avoid clipping + # - Small position deltas (±5cm XY, +10cm Z) + # - look_at stays fixed on workspace center + # - No FOV randomization to maintain consistent perspective cam_config = CameraRandomCfg( camera_name=camera_name, position=CameraPositionRandomCfg( - delta_range=((-0.1, 0.1), (-0.1, 0.1), (0.0, 0.2)), # Z: only upward movement + delta_range=((-0.05, 0.05), (-0.05, 0.05), (0.0, 0.1)), use_delta=True, distribution="uniform", enabled=True, ), - intrinsics=CameraIntrinsicsRandomCfg( - fov_range=CameraProperties.FOV_NORMAL, - use_fov=True, - distribution="uniform", - enabled=True, - ), ) cam_rand = CameraRandomizer(cam_config, seed=seed + 10 if seed is not None else None) @@ -484,14 +477,10 @@ def apply_randomization(self, demo_idx: int = 0, is_initial: bool = False): for light_rand in self.randomizers["light"]: light_rand() if self.randomizers["light"]: - log.info(f" Applied LightRandomizers ({len(self.randomizers['light'])})") + log.info(f" Applied LightRandomizers ({len(self.randomizers['light'])} lights)") - # Level 3+: Camera - if self.config.level >= 3: - for cam_rand in self.randomizers["camera"]: - cam_rand() - if self.randomizers["camera"]: - log.info(f" Applied CameraRandomizers ({len(self.randomizers['camera'])})") + # Level 3+: Camera randomization is handled separately in apply_camera_randomization() + # after update_camera_look_at() adjusts the baseline camera position finally: # Disable global defer and flush once @@ -508,6 +497,9 @@ def update_camera_look_at(self, env_id: int = 0): Adjusts both camera position and look-at point to maintain the same relative viewing angle but account for the table's height. + + IMPORTANT: After calling this, you should call apply_camera_randomization() + to apply camera randomization based on the new baseline position. """ if not self.randomizers.get("scene"): return @@ -531,12 +523,37 @@ def update_camera_look_at(self, env_id: int = 0): z_offset = target_look_at_z - orig_look_at_z # Apply same offset to both position and look_at - camera.pos = (orig["pos"][0], orig["pos"][1], orig_pos_z + z_offset) - camera.look_at = (orig["look_at"][0], orig["look_at"][1], target_look_at_z) + new_pos = (orig["pos"][0], orig["pos"][1], orig_pos_z + z_offset) + new_look_at = (orig["look_at"][0], orig["look_at"][1], target_look_at_z) + + camera.pos = new_pos + camera.look_at = new_look_at + + # Update the stored original positions for camera randomizer + # This is the NEW baseline for camera randomization + if self.config.level >= 3 and camera.name in self.randomizers.get("camera_originals", {}): + # Update camera randomizer's original position reference + for cam_rand in self.randomizers.get("camera", []): + if cam_rand.cfg.camera_name == camera.name: + cam_rand._original_positions[camera.name] = new_pos if hasattr(self.handler, "_update_camera_pose"): self.handler._update_camera_pose() + def apply_camera_randomization(self): + """Apply camera randomization after camera baseline has been adjusted. + + This should be called AFTER update_camera_look_at() to ensure camera + randomization is based on the adjusted baseline position (accounting for + table height changes). + """ + if self.config.level < 3 or not self.randomizers.get("camera"): + return + + for cam_rand in self.randomizers["camera"]: + cam_rand() + log.info(f" Applied CameraRandomizers ({len(self.randomizers['camera'])} cameras)") + def update_positions_to_table(self, demo_idx: int, env_id: int = 0): """Update object positions to align with current table after scene switch. @@ -544,10 +561,14 @@ def update_positions_to_table(self, demo_idx: int, env_id: int = 0): The entire system is translated such that the original ground level aligns with the table surface. """ if not self.randomizers.get("scene"): + log.warning("[update_positions_to_table] Skipped: No scene randomizer") return # Get current state if demo_idx >= len(self.init_states): + log.warning( + f"[update_positions_to_table] Skipped: demo_idx {demo_idx} >= len(init_states) {len(self.init_states)}" + ) return init_state = self.init_states[demo_idx] @@ -555,7 +576,7 @@ def update_positions_to_table(self, demo_idx: int, env_id: int = 0): # Get this demo's original positions (stored in __init__) demo_key = f"demo_{demo_idx}" if demo_key not in self.original_positions: - log.warning(f"No original positions found for demo {demo_idx}") + log.warning(f"[update_positions_to_table] No original positions found for demo {demo_idx}") return demo_original_positions = self.original_positions[demo_key] @@ -563,9 +584,11 @@ def update_positions_to_table(self, demo_idx: int, env_id: int = 0): # Get table bounds table_bounds = self.randomizers["scene"].get_table_bounds(env_id=env_id) if not table_bounds or abs(table_bounds.get("height", 0)) > 100: + log.warning(f"[update_positions_to_table] Skipped: Invalid table_bounds {table_bounds}") return table_height = table_bounds["height"] + log.info(f"[update_positions_to_table] Demo {demo_idx}: Table height = {table_height:.3f}m") table_center_x = (table_bounds["x_min"] + table_bounds["x_max"]) / 2 table_center_y = (table_bounds["y_min"] + table_bounds["y_max"]) / 2 @@ -588,16 +611,49 @@ def update_positions_to_table(self, demo_idx: int, env_id: int = 0): # All objects and robots maintain their relative height from this plane z_offset = table_height - ground_level + log.info( + f"[update_positions_to_table] Offsets: X={offset_x:.3f}, Y={offset_y:.3f}, Z={z_offset:.3f} (ground_level={ground_level:.3f})" + ) + # Apply same offset to everything (rigid body translation) for obj_name, obj_state in init_state["objects"].items(): orig = demo_original_positions[f"obj_{obj_name}"] - obj_state["pos"][0] = orig["x"] + offset_x - obj_state["pos"][1] = orig["y"] + offset_y - obj_state["pos"][2] = orig["z"] + z_offset + old_pos = ( + obj_state["pos"].clone() + if hasattr(obj_state["pos"], "clone") + else obj_state["pos"].copy() + if hasattr(obj_state["pos"], "copy") + else list(obj_state["pos"]) + ) + + # Create new position tensor with offsets + new_pos = torch.tensor( + [orig["x"] + offset_x, orig["y"] + offset_y, orig["z"] + z_offset], + dtype=obj_state["pos"].dtype, + device=obj_state["pos"].device, + ) + + # Replace the entire tensor (in-place modification doesn't work for all tensor types) + obj_state["pos"] = new_pos + log.info(f"[update_positions_to_table] Object '{obj_name}': {old_pos} -> {obj_state['pos']}") for robot_name, robot_state in init_state["robots"].items(): orig = demo_original_positions[f"robot_{robot_name}"] - robot_state["pos"][0] = orig["x"] + offset_x - robot_state["pos"][1] = orig["y"] + offset_y - robot_state["pos"][2] = orig["z"] + z_offset + old_pos = ( + robot_state["pos"].clone() + if hasattr(robot_state["pos"], "clone") + else robot_state["pos"].copy() + if hasattr(robot_state["pos"], "copy") + else list(robot_state["pos"]) + ) + + # Create new position tensor with offsets + new_pos = torch.tensor( + [orig["x"] + offset_x, orig["y"] + offset_y, orig["z"] + z_offset], + dtype=robot_state["pos"].dtype, + device=robot_state["pos"].device, + ) + # Replace the entire tensor + robot_state["pos"] = new_pos + log.info(f"[update_positions_to_table] Robot '{robot_name}': {old_pos} -> {robot_state['pos']}") diff --git a/roboverse_learn/il/act/act_eval_runner.py b/roboverse_learn/il/act/act_eval_runner.py index 07ea48c7f..5bc721307 100755 --- a/roboverse_learn/il/act/act_eval_runner.py +++ b/roboverse_learn/il/act/act_eval_runner.py @@ -14,11 +14,18 @@ rootutils.setup_root(__file__, pythonpath=True) log.configure(handlers=[{"sink": RichHandler(), "format": "{message}"}]) -# from metasim.scenario.scenario import ScenarioCfg -from metasim.utils.kinematics import get_curobo_models +from metasim.utils.kinematics import get_curobo_models from metasim.task.registry import get_task_class +# Try to import randomization components +try: + from metasim.randomization import DomainRandomizationManager, DRConfig + RANDOMIZATION_AVAILABLE = True +except ImportError as e: + log.warning(f"Domain randomization not available: {e}") + RANDOMIZATION_AVAILABLE = False + def images_to_video(images, video_path, frame_size=(1920, 1080), fps=30): @@ -40,6 +47,89 @@ def images_to_video(images, video_path, frame_size=(1920, 1080), fps=30): print("Video created successfully!") +def ensure_clean_state(handler, expected_state=None): + """Ensure environment is in clean initial state with intelligent validation.""" + prev_state = None + stable_count = 0 + max_steps = 10 + min_steps = 2 + + for step in range(max_steps): + handler.simulate() + current_state = handler.get_states() + + if step >= min_steps: + if prev_state is not None: + is_stable = True + if hasattr(current_state, "objects") and hasattr(prev_state, "objects"): + for obj_name, obj_state in current_state.objects.items(): + if obj_name in prev_state.objects: + curr_dof = getattr(obj_state, "dof_pos", None) + prev_dof = getattr(prev_state.objects[obj_name], "dof_pos", None) + if curr_dof is not None and prev_dof is not None: + if not torch.allclose(curr_dof, prev_dof, atol=1e-5): + is_stable = False + break + + if is_stable and expected_state is not None: + is_correct_state = _validate_state_correctness(current_state, expected_state) + if not is_correct_state: + log.debug(f"State stable but incorrect at step {step}, continuing simulation...") + stable_count = 0 + is_stable = False + + if is_stable: + stable_count += 1 + if stable_count >= 2: + break + else: + stable_count = 0 + + prev_state = current_state + + if expected_state is not None: + final_state = handler.get_states() + is_final_correct = _validate_state_correctness(final_state, expected_state) + if not is_final_correct: + log.warning(f"State validation failed after {max_steps} steps - reset may not have taken full effect") + + handler.get_states() + + +def _validate_state_correctness(current_state, expected_state): + """Validate that current state matches expected initial state for critical objects.""" + if not hasattr(current_state, "objects") or not hasattr(expected_state, "objects"): + return True + + critical_objects = [] + for obj_name, expected_obj in expected_state.objects.items(): + if hasattr(expected_obj, "dof_pos") and getattr(expected_obj, "dof_pos", None) is not None: + critical_objects.append(obj_name) + + if not critical_objects: + return True + + tolerance = 5e-3 + + for obj_name in critical_objects: + if obj_name not in current_state.objects: + continue + + expected_obj = expected_state.objects[obj_name] + current_obj = current_state.objects[obj_name] + + expected_dof = getattr(expected_obj, "dof_pos", None) + current_dof = getattr(current_obj, "dof_pos", None) + + if expected_dof is not None and current_dof is not None: + if not torch.allclose(current_dof, expected_dof, atol=tolerance): + diff = torch.abs(current_dof - expected_dof).max().item() + log.debug(f"DOF mismatch for {obj_name}: max diff = {diff:.6f} (tolerance = {tolerance})") + return False + + return True + + def parse_args(): parser = argparse.ArgumentParser() parser.add_argument("--task", type=str, required=True) @@ -84,6 +174,28 @@ def parse_args(): default=400, ) + # Domain Randomization options + parser.add_argument( + "--level", + type=int, + default=0, + choices=[0, 1, 2, 3], + help="Randomization level: 0=None, 1=Scene+Material, 2=+Light, 3=+Camera" + ) + parser.add_argument( + "--scene_mode", + type=int, + default=0, + choices=[0, 1, 2, 3], + help="Scene mode: 0=Manual, 1=USD Table, 2=USD Scene, 3=Full USD" + ) + parser.add_argument( + "--randomization_seed", + type=int, + default=None, + help="Seed for reproducible randomization. If None, uses random seed" + ) + args = parser.parse_args() return args @@ -108,20 +220,72 @@ def main(): # from metasim.scenario.scenario import RandomizationCfg from metasim.scenario.cameras import PinholeCameraCfg + from metasim.scenario.lights import DiskLightCfg, SphereLightCfg from metasim.constants import SimType from metasim.utils.demo_util import get_traj from metasim.utils.setup_util import get_robot # from metasim.utils.setup_util import get_sim_env_class + # Camera configuration (same logic as collect_demo.py) + task_cls = get_task_class(args.task) + + if args.task in {"stack_cube", "pick_cube", "pick_butter"}: + dp_camera = True + else: + dp_camera = args.task != "close_box" + + is_libero_dataset = "libero_90" in args.task + + if is_libero_dataset: + dp_pos = (2.0, 0.0, 2) + elif dp_camera: + dp_pos = (1.0, 0.0, 0.75) + else: + dp_pos = (1.5, 0.0, 1.5) + camera = PinholeCameraCfg( name="camera", data_types=["rgb", "depth"], width=256, height=256, - pos=(1.5, 0.0, 1.5), + pos=dp_pos, look_at=(0.0, 0.0, 0.0), ) + + # Lighting setup (same logic as collect_demo.py) + # Determine intensity based on render mode (if available) + render_mode = getattr(args, 'render_mode', 'raytracing') + if render_mode == "pathtracing": + ceiling_main = 18000.0 + ceiling_corners = 8000.0 + else: + ceiling_main = 12000.0 + ceiling_corners = 5000.0 + + lights = [ + DiskLightCfg( + name="ceiling_main", + intensity=ceiling_main, + color=(1.0, 1.0, 1.0), + radius=1.2, + pos=(0.0, 0.0, 2.8), + rot=(0.7071, 0.0, 0.0, 0.7071), + ), + SphereLightCfg( + name="ceiling_ne", intensity=ceiling_corners, color=(1.0, 1.0, 1.0), radius=0.6, pos=(1.0, 1.0, 2.5) + ), + SphereLightCfg( + name="ceiling_nw", intensity=ceiling_corners, color=(1.0, 1.0, 1.0), radius=0.6, pos=(-1.0, 1.0, 2.5) + ), + SphereLightCfg( + name="ceiling_sw", intensity=ceiling_corners, color=(1.0, 1.0, 1.0), radius=0.6, pos=(-1.0, -1.0, 2.5) + ), + SphereLightCfg( + name="ceiling_se", intensity=ceiling_corners, color=(1.0, 1.0, 1.0), radius=0.6, pos=(1.0, -1.0, 2.5) + ), + ] + # randomization = RandomizationCfg(camera=False, light=False, ground=False, reflection=False) # scenario = ScenarioCfg( # task=args.task, @@ -134,12 +298,12 @@ def main(): # headless=args.headless, # ) - task_cls = get_task_class(args.task) scenario = task_cls.scenario.update( robots=[args.robot], simulator=args.sim, num_envs=args.num_envs, headless=args.headless, + lights=lights, cameras=[camera] ) @@ -152,6 +316,43 @@ def main(): toc = time.time() log.trace(f"Time to launch: {toc - tic:.2f}s") + ## Data + tic = time.time() + assert os.path.exists(env.traj_filepath), ( + f"Trajectory file: {env.traj_filepath} does not exist." + ) + init_states, all_actions, all_states = get_traj(env.traj_filepath, robot, env.handler) + toc = time.time() + log.trace(f"Time to load data: {toc - tic:.2f}s") + + # Initialize Domain Randomization Manager (same logic as collect_demo.py) + # Note: DR Manager is always created, even for level=0 + # The apply_randomization() method will skip operations when level=0 + if not RANDOMIZATION_AVAILABLE: + log.warning("Randomization components not available!") + raise ImportError("Domain Randomization not available. Please check installation.") + + # Determine render mode from args (if available) + render_mode = getattr(args, 'render_mode', 'raytracing') + + # Create a simple render config for DR (needed for light intensity adjustment) + from dataclasses import dataclass + @dataclass + class SimpleRenderCfg: + mode: str = render_mode + + randomization_manager = DomainRandomizationManager( + config=DRConfig( + level=args.level, + scene_mode=args.scene_mode, + randomization_seed=args.randomization_seed, + ), + scenario=scenario, + handler=env.handler, + init_states=init_states, + render_cfg=SimpleRenderCfg(mode=render_mode) + ) + if args.algo == "act": state_dim = 9 franka_state_dim = 9 @@ -215,29 +416,39 @@ def post_process(a): ckpt_name = args.ckpt_path.split("/")[-1] os.makedirs(f"tmp/{args.algo}/{args.task}/{ckpt_name}", exist_ok=True) - ## Data - tic = time.time() - assert os.path.exists(env.traj_filepath), ( - f"Trajectory file: {env.traj_filepath} does not exist." - ) - init_states, all_actions, all_states = get_traj(env.traj_filepath, robot, env.handler) - toc = time.time() - log.trace(f"Time to load data: {toc - tic:.2f}s") - - ## cuRobo controller - *_, robot_ik = get_curobo_models(scenario.robots[0]) - curobo_n_dof = len(robot_ik.robot_config.cspace.joint_names) - ee_n_dof = len(scenario.robots[0].gripper_open_q) + ## cuRobo controller (commented out - not needed for ACT joint control) + # *_, robot_ik = get_curobo_models(scenario.robots[0]) + # curobo_n_dof = len(robot_ik.robot_config.cspace.joint_names) + # ee_n_dof = len(scenario.robots[0].gripper_open_q) ## Reset before first step TotalSuccess = 0 num_eval: int = args.num_eval for i in range(num_eval): + # Use positive indexing (same as collect_demo.py) + demo_idx = i + + # Apply domain randomization BEFORE reset (same logic as collect_demo.py) + # Note: If level=0, apply_randomization() will skip but update_positions_to_table() still runs + log.info(f"[ACT Eval] Episode {i}: Applying DR for demo_idx={demo_idx}") + randomization_manager.apply_randomization(demo_idx=demo_idx, is_initial=(i == 0)) + randomization_manager.update_positions_to_table(demo_idx=demo_idx, env_id=0) + randomization_manager.update_camera_look_at(env_id=0) + randomization_manager.apply_camera_randomization() # Apply camera randomization after baseline adjustment + tic = time.time() - obs, extras = env.reset(states=[init_states[-(i + 1)]]) + obs, extras = env.reset(states=[init_states[demo_idx]]) toc = time.time() log.trace(f"Time to reset: {toc - tic:.2f}s") + + # Ensure environment stabilizes after reset (same as collect_demo.py) + ensure_clean_state(env.handler, expected_state=init_states[demo_idx]) + + # Reset episode step counter after stabilization + if hasattr(env, "_episode_steps"): + env._episode_steps[0] = 0 + # save_obs(obs, 0) log.debug(f"Env: {i}") @@ -259,15 +470,15 @@ def post_process(a): log.debug(f"Step {step}") robot_joint_limits = scenario.robots[0].joint_limits - image_list.append(np.array(obs.cameras['camera'].rgb)[0]) + image_list.append(np.array(obs.cameras['camera'].rgb.cpu())[0]) - qpos_numpy = np.array(obs.robots['franka'].joint_pos) + qpos_numpy = np.array(obs.robots['franka'].joint_pos.cpu()) # qpos_numpy = np.array(obs["joint_qpos"]) qpos = pre_process(qpos_numpy) # qpos = np.concatenate([qpos, np.zeros((qpos.shape[0], 14 - qpos.shape[1]))], axis=1) qpos = torch.from_numpy(qpos).float().cuda() qpos_history[:, step] = qpos - curr_image = np.array(obs.cameras['camera'].rgb).transpose(0, 3, 1, 2) + curr_image = np.array(obs.cameras['camera'].rgb.cpu()).transpose(0, 3, 1, 2) # cur_image = np.stack([curr_image, curr_image], axis=0) curr_image = torch.from_numpy(curr_image / 255.0).float().cuda().unsqueeze(0) # breakpoint() @@ -300,8 +511,8 @@ def post_process(a): inverse_reorder_idx = [reorder_idx.index(i) for i in range(len(reorder_idx))] actions = action[inverse_reorder_idx] inner_actions = {"dof_pos_target": dict(zip(scenario.robots[0].joint_limits.keys(), actions))} - actions = {"franka": inner_actions} - #actions = [{"dof_pos_target": dict(zip(scenario.robots[0].joint_limits.keys(), action))}] + # Format: actions[env_id][robot_name][action_type] + actions = [{"franka": inner_actions}] #log.debug(f"Actions: {actions}") # log.debug(f"Action: {actions}") obs, reward, success, time_out, extras = env.step(actions) diff --git a/roboverse_learn/il/act/act_run.sh b/roboverse_learn/il/act/act_run.sh index abdf190d8..5af5c14af 100755 --- a/roboverse_learn/il/act/act_run.sh +++ b/roboverse_learn/il/act/act_run.sh @@ -50,6 +50,12 @@ if [ "${eval_enable}" = "true" ]; then echo "=== Evaluation ===" # # export TORCH_CUDA_ARCH_LIST="8.9" ckpt_path=$(cat ./roboverse_learn/il/act/ckpt_dir_path.txt) + + # Domain Randomization parameters for evaluation + eval_level=3 # 0=None, 1=Scene+Material, 2=+Light, 3=+Camera + eval_scene_mode=2 # 0=Manual, 1=USD Table, 2=USD Scene, 3=Full USD + eval_seed=42 # Randomization seed (optional) + python -m roboverse_learn.il.act.act_eval_runner \ --task ${task_name_set} \ --robot franka \ @@ -58,7 +64,10 @@ if [ "${eval_enable}" = "true" ]; then --algo act \ --ckpt_path ./${ckpt_path} \ --headless True \ - --num_eval 100 \ + --num_eval 5 \ --temporal_agg True \ - --chunk_size ${chunk_size} + --chunk_size ${chunk_size} \ + --level ${eval_level} \ + --scene_mode ${eval_scene_mode} \ + --randomization_seed ${eval_seed} fi diff --git a/scripts/advanced/collect_demo.py b/scripts/advanced/collect_demo.py index e69a3419a..617d1a02a 100644 --- a/scripts/advanced/collect_demo.py +++ b/scripts/advanced/collect_demo.py @@ -547,6 +547,7 @@ def main(): randomization_manager.apply_randomization(demo_idx, is_initial=True) randomization_manager.update_positions_to_table(demo_idx, env_id) randomization_manager.update_camera_look_at(env_id) + randomization_manager.apply_camera_randomization() # Apply camera randomization after baseline adjustment ## Reset to initial states (after position adjustment) obs, extras = env.reset(states=[init_states[demo_idx] for demo_idx in demo_idxs]) @@ -617,6 +618,8 @@ def main(): randomization_manager.apply_randomization(new_demo_idx, is_initial=False) randomization_manager.update_positions_to_table(new_demo_idx, env_id) + randomization_manager.update_camera_look_at(env_id) + randomization_manager.apply_camera_randomization() # Apply camera randomization force_reset_to_state(env, init_states[new_demo_idx], env_id) obs = env.handler.get_states() @@ -642,6 +645,7 @@ def main(): randomization_manager.apply_randomization(demo_idx, is_initial=False) randomization_manager.update_positions_to_table(demo_idx, env_id) randomization_manager.update_camera_look_at(env_id) + randomization_manager.apply_camera_randomization() # Apply camera randomization force_reset_to_state(env, init_states[demo_idx], env_id) obs = env.handler.get_states() @@ -660,6 +664,7 @@ def main(): randomization_manager.apply_randomization(new_demo_idx, is_initial=False) randomization_manager.update_positions_to_table(new_demo_idx, env_id) randomization_manager.update_camera_look_at(env_id) + randomization_manager.apply_camera_randomization() # Apply camera randomization force_reset_to_state(env, init_states[new_demo_idx], env_id) obs = env.handler.get_states() From 533ff48c70ec35515107fe5762e0b6206a1d69c7 Mon Sep 17 00:00:00 2001 From: gxyes Date: Thu, 4 Dec 2025 17:15:39 +0700 Subject: [PATCH 07/50] add DR to dp eval --- metasim/randomization/dr_manager.py | 85 +--- roboverse_learn/il/act/act_eval_runner.py | 42 +- roboverse_learn/il/dp/dp_run.sh | 26 +- .../il/dp/models/ddpm_image_policy.py | 3 + roboverse_learn/il/dp/runner/dp_runner.py | 420 +++++++++--------- roboverse_learn/il/utils/common/eval_args.py | 15 +- 6 files changed, 250 insertions(+), 341 deletions(-) diff --git a/metasim/randomization/dr_manager.py b/metasim/randomization/dr_manager.py index 1b9d7fab3..a27134749 100644 --- a/metasim/randomization/dr_manager.py +++ b/metasim/randomization/dr_manager.py @@ -137,10 +137,8 @@ def __init__( if not self._validate_setup(): return - log.info("=" * 50) - log.info("DOMAIN RANDOMIZATION SETUP: Initializing randomizers") self._setup_randomizers() - log.info(f"Setup complete: Randomizers ready (Level {config.level}, Mode {config.scene_mode})") + log.info(f"Domain Randomization initialized (Level {config.level}, Mode {config.scene_mode})") def _validate_setup(self) -> bool: """Validate if randomization can be set up.""" @@ -183,7 +181,6 @@ def _setup_randomizers(self): def _setup_reproducibility(self, seed: int | None): """Setup global reproducibility if seed is provided.""" if seed is not None: - log.info(f"Setting up reproducible randomization with seed: {seed}") torch.manual_seed(seed) import random @@ -288,23 +285,17 @@ def _setup_material_randomizers(self, seed: int | None): if level == 0: return - log.info("\nMaterial Randomization") - log.info("-" * 50) - # Dynamic Objects (Manual geometry only) if mode == 0: - # Manual table table_mat = MaterialRandomizer( MaterialPresets.mdl_family_object("table", family=("wood", "metal")), seed=seed + 2 if seed is not None else None, ) table_mat.bind_handler(self.handler) self.randomizers["material_dynamic"].append(table_mat) - log.info(" Dynamic Object: table (Manual)") # Manual environment (mode < 2 and level >= 1) if mode < 2 and level >= 1: - # Floor floor_mat = MaterialRandomizer( MaterialPresets.mdl_family_object("floor", family=("carpet", "wood", "stone")), seed=seed + 101 if seed is not None else None, @@ -312,7 +303,6 @@ def _setup_material_randomizers(self, seed: int | None): floor_mat.bind_handler(self.handler) self.randomizers["material_dynamic"].append(floor_mat) - # Walls wall_seed = seed + 102 if seed is not None else None for wall_name in ["wall_front", "wall_back", "wall_left", "wall_right"]: wall_mat = MaterialRandomizer( @@ -322,7 +312,6 @@ def _setup_material_randomizers(self, seed: int | None): wall_mat.bind_handler(self.handler) self.randomizers["material_dynamic"].append(wall_mat) - # Ceiling ceiling_mat = MaterialRandomizer( MaterialPresets.mdl_family_object("ceiling", family=("architecture", "wall_board")), seed=seed + 103 if seed is not None else None, @@ -330,18 +319,12 @@ def _setup_material_randomizers(self, seed: int | None): ceiling_mat.bind_handler(self.handler) self.randomizers["material_dynamic"].append(ceiling_mat) - log.info(" Dynamic Objects: floor + 4 walls + ceiling") - def _setup_light_randomizers(self, seed: int | None): """Setup light randomizers (Level 2+).""" from metasim.scenario.lights import DiskLightCfg, DomeLightCfg, SphereLightCfg - log.info("\nLight Randomization") - log.info("-" * 50) - lights = getattr(self.scenario, "lights", []) if not lights: - log.info(" No lights found") return # Determine intensity ranges based on render mode @@ -398,30 +381,20 @@ def _setup_light_randomizers(self, seed: int | None): config = LightPresets.dome_ambient(light_name) light_rand = LightRandomizer(config, seed=seed + 4 + i if seed else None) else: - log.warning(f" Unknown light type: {light_name}") continue light_rand.bind_handler(self.handler) self.randomizers["light"].append(light_rand) - log.info(f" Light: {light_name}") def _setup_camera_randomizers(self, seed: int | None): """Setup camera randomizers (Level 3+).""" - log.info("\nCamera Randomization") - log.info("-" * 50) - cameras = getattr(self.scenario, "cameras", []) if not cameras: - log.info(" No cameras found") return for camera in cameras: camera_name = getattr(camera, "name", "camera") - # Conservative camera randomization to avoid clipping - # - Small position deltas (±5cm XY, +10cm Z) - # - look_at stays fixed on workspace center - # - No FOV randomization to maintain consistent perspective cam_config = CameraRandomCfg( camera_name=camera_name, position=CameraPositionRandomCfg( @@ -435,7 +408,7 @@ def _setup_camera_randomizers(self, seed: int | None): cam_rand = CameraRandomizer(cam_config, seed=seed + 10 if seed is not None else None) cam_rand.bind_handler(self.handler) self.randomizers["camera"].append(cam_rand) - log.info(f" Camera: {camera_name}") + self.randomizers.setdefault("camera_originals", {})[camera_name] = camera.pos def apply_randomization(self, demo_idx: int = 0, is_initial: bool = False): """Apply randomization with global deferred visual flush. @@ -447,9 +420,6 @@ def apply_randomization(self, demo_idx: int = 0, is_initial: bool = False): if self.config.level == 0 or not self.randomizers: return - log.info("=" * 50) - log.info(f"DOMAIN RANDOMIZATION: Demo {demo_idx}") - # Enable global defer flag if self.handler: self.handler._defer_all_visual_flushes = True @@ -463,24 +433,16 @@ def apply_randomization(self, demo_idx: int = 0, is_initial: bool = False): scene_rand.cfg.auto_flush_visuals = False scene_rand() scene_rand.cfg.auto_flush_visuals = original_auto_flush - log.info(" Applied SceneRandomizer") # Level 1+: Material randomization (environment only) if self.config.level >= 1: for mat_rand in self.randomizers["material_dynamic"]: mat_rand() - if self.randomizers["material_dynamic"]: - log.info(f" Applied MaterialRandomizers ({len(self.randomizers['material_dynamic'])})") # Level 2+: Lighting if self.config.level >= 2: for light_rand in self.randomizers["light"]: light_rand() - if self.randomizers["light"]: - log.info(f" Applied LightRandomizers ({len(self.randomizers['light'])} lights)") - - # Level 3+: Camera randomization is handled separately in apply_camera_randomization() - # after update_camera_look_at() adjusts the baseline camera position finally: # Disable global defer and flush once @@ -493,14 +455,7 @@ def apply_randomization(self, demo_idx: int = 0, is_initial: bool = False): log.debug(f"Failed to flush visual updates: {e}") def update_camera_look_at(self, env_id: int = 0): - """Update camera position and look_at to focus on table after scene switch. - - Adjusts both camera position and look-at point to maintain the same relative - viewing angle but account for the table's height. - - IMPORTANT: After calling this, you should call apply_camera_randomization() - to apply camera randomization based on the new baseline position. - """ + """Update camera position and look_at to focus on table after scene switch.""" if not self.randomizers.get("scene"): return @@ -509,8 +464,6 @@ def update_camera_look_at(self, env_id: int = 0): return table_height = table_bounds["height"] - - # Use original camera positions stored in __init__ (before any randomization) clearance = 0.05 target_look_at_z = table_height + clearance @@ -519,20 +472,15 @@ def update_camera_look_at(self, env_id: int = 0): orig_look_at_z = orig["look_at"][2] orig_pos_z = orig["pos"][2] - # Compute Z offset needed z_offset = target_look_at_z - orig_look_at_z - - # Apply same offset to both position and look_at new_pos = (orig["pos"][0], orig["pos"][1], orig_pos_z + z_offset) new_look_at = (orig["look_at"][0], orig["look_at"][1], target_look_at_z) camera.pos = new_pos camera.look_at = new_look_at - # Update the stored original positions for camera randomizer - # This is the NEW baseline for camera randomization + # Update camera randomizer's baseline position if self.config.level >= 3 and camera.name in self.randomizers.get("camera_originals", {}): - # Update camera randomizer's original position reference for cam_rand in self.randomizers.get("camera", []): if cam_rand.cfg.camera_name == camera.name: cam_rand._original_positions[camera.name] = new_pos @@ -541,54 +489,33 @@ def update_camera_look_at(self, env_id: int = 0): self.handler._update_camera_pose() def apply_camera_randomization(self): - """Apply camera randomization after camera baseline has been adjusted. - - This should be called AFTER update_camera_look_at() to ensure camera - randomization is based on the adjusted baseline position (accounting for - table height changes). - """ + """Apply camera randomization after camera baseline has been adjusted.""" if self.config.level < 3 or not self.randomizers.get("camera"): return for cam_rand in self.randomizers["camera"]: cam_rand() - log.info(f" Applied CameraRandomizers ({len(self.randomizers['camera'])} cameras)") def update_positions_to_table(self, demo_idx: int, env_id: int = 0): - """Update object positions to align with current table after scene switch. - - Maintains relative positions of all objects and robots (rigid body translation). - The entire system is translated such that the original ground level aligns with the table surface. - """ + """Update object positions to align with current table after scene switch.""" if not self.randomizers.get("scene"): - log.warning("[update_positions_to_table] Skipped: No scene randomizer") return - # Get current state if demo_idx >= len(self.init_states): - log.warning( - f"[update_positions_to_table] Skipped: demo_idx {demo_idx} >= len(init_states) {len(self.init_states)}" - ) return init_state = self.init_states[demo_idx] - - # Get this demo's original positions (stored in __init__) demo_key = f"demo_{demo_idx}" if demo_key not in self.original_positions: - log.warning(f"[update_positions_to_table] No original positions found for demo {demo_idx}") return demo_original_positions = self.original_positions[demo_key] - # Get table bounds table_bounds = self.randomizers["scene"].get_table_bounds(env_id=env_id) if not table_bounds or abs(table_bounds.get("height", 0)) > 100: - log.warning(f"[update_positions_to_table] Skipped: Invalid table_bounds {table_bounds}") return table_height = table_bounds["height"] - log.info(f"[update_positions_to_table] Demo {demo_idx}: Table height = {table_height:.3f}m") table_center_x = (table_bounds["x_min"] + table_bounds["x_max"]) / 2 table_center_y = (table_bounds["y_min"] + table_bounds["y_max"]) / 2 diff --git a/roboverse_learn/il/act/act_eval_runner.py b/roboverse_learn/il/act/act_eval_runner.py index 5bc721307..022cbbf00 100755 --- a/roboverse_learn/il/act/act_eval_runner.py +++ b/roboverse_learn/il/act/act_eval_runner.py @@ -209,25 +209,14 @@ def main(): args = parse_args() num_envs: int = args.num_envs - # specificly for isaacgym - if args.sim == "isaacgym": - pass - - ## Import put here to support isaacgym - import numpy as np import torch - # from metasim.scenario.scenario import RandomizationCfg from metasim.scenario.cameras import PinholeCameraCfg from metasim.scenario.lights import DiskLightCfg, SphereLightCfg - from metasim.constants import SimType from metasim.utils.demo_util import get_traj from metasim.utils.setup_util import get_robot -# from metasim.utils.setup_util import get_sim_env_class - - # Camera configuration (same logic as collect_demo.py) task_cls = get_task_class(args.task) if args.task in {"stack_cube", "pick_cube", "pick_butter"}: @@ -286,18 +275,6 @@ def main(): ), ] - # randomization = RandomizationCfg(camera=False, light=False, ground=False, reflection=False) - # scenario = ScenarioCfg( - # task=args.task, - # robots=[args.robot], - # cameras=[camera], - # # random=randomization, - # sim=args.sim, - # num_envs=args.num_envs, - # try_add_table=True, - # headless=args.headless, - # ) - scenario = task_cls.scenario.update( robots=[args.robot], simulator=args.sim, @@ -308,8 +285,6 @@ def main(): ) tic = time.time() - # env_class = get_sim_env_class(SimType(args.sim)) - # env = env_class(scenario) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") env = task_cls(scenario, device=device) robot = get_robot(args.robot) @@ -325,17 +300,15 @@ def main(): toc = time.time() log.trace(f"Time to load data: {toc - tic:.2f}s") - # Initialize Domain Randomization Manager (same logic as collect_demo.py) - # Note: DR Manager is always created, even for level=0 - # The apply_randomization() method will skip operations when level=0 + # Initialize Domain Randomization Manager if not RANDOMIZATION_AVAILABLE: log.warning("Randomization components not available!") raise ImportError("Domain Randomization not available. Please check installation.") - # Determine render mode from args (if available) + # Determine render mode from args render_mode = getattr(args, 'render_mode', 'raytracing') - # Create a simple render config for DR (needed for light intensity adjustment) + # Create render config for DR from dataclasses import dataclass @dataclass class SimpleRenderCfg: @@ -426,30 +399,27 @@ def post_process(a): num_eval: int = args.num_eval for i in range(num_eval): - # Use positive indexing (same as collect_demo.py) demo_idx = i - # Apply domain randomization BEFORE reset (same logic as collect_demo.py) - # Note: If level=0, apply_randomization() will skip but update_positions_to_table() still runs + # Apply domain randomization before reset log.info(f"[ACT Eval] Episode {i}: Applying DR for demo_idx={demo_idx}") randomization_manager.apply_randomization(demo_idx=demo_idx, is_initial=(i == 0)) randomization_manager.update_positions_to_table(demo_idx=demo_idx, env_id=0) randomization_manager.update_camera_look_at(env_id=0) - randomization_manager.apply_camera_randomization() # Apply camera randomization after baseline adjustment + randomization_manager.apply_camera_randomization() tic = time.time() obs, extras = env.reset(states=[init_states[demo_idx]]) toc = time.time() log.trace(f"Time to reset: {toc - tic:.2f}s") - # Ensure environment stabilizes after reset (same as collect_demo.py) + # Ensure environment stabilizes after reset ensure_clean_state(env.handler, expected_state=init_states[demo_idx]) # Reset episode step counter after stabilization if hasattr(env, "_episode_steps"): env._episode_steps[0] = 0 - # save_obs(obs, 0) log.debug(f"Env: {i}") step = 0 diff --git a/roboverse_learn/il/dp/dp_run.sh b/roboverse_learn/il/dp/dp_run.sh index ae52b8fb0..415d6cf7b 100644 --- a/roboverse_learn/il/dp/dp_run.sh +++ b/roboverse_learn/il/dp/dp_run.sh @@ -3,10 +3,9 @@ train_enable=True # True for training, False for evaluation eval_enable=True -task_name_set=stack_cube -level=0 +task_name_set=close_box config_name=dp_runner -num_epochs=100 # Number of training epochs +num_epochs=100 port=50010 seed=42 gpu=0 @@ -17,7 +16,12 @@ eval_num_envs=1 eval_max_step=300 expert_data_num=100 sim_set=isaacsim -eval_ckpt_name=100 +eval_ckpt_name=100 # Evaluate the last checkpoint (epoch 3) + +## Domain Randomization Configuration +level=3 # 0=None, 1=Scene+Material, 2=+Light, 3=+Camera +scene_mode=3 # 0=Manual, 1=USD Table, 2=USD Scene, 3=Full USD +dr_seed=42 # Random seed for reproducible DR (null for random) ## Choose training or inference algorithm @@ -34,9 +38,14 @@ if [ "${delta_ee}" = 1 ]; then extra="${extra}_delta" fi +# Note: level variable is now used for DR, not in zarr filename +# The zarr filename should use the data collection level (e.g., L0) +data_level=0 # Level used when collecting data +zarr_path="./data_policy/${task_name_set}FrankaL${data_level}_${extra}_${expert_data_num}.zarr" + python ./roboverse_learn/il/dp/main.py --config-name=${config_name}.yaml \ task_name=${task_name_set} \ -dataset_config.zarr_path="./data_policy/${task_name_set}FrankaL${level}_${extra}_${expert_data_num}.zarr" \ +dataset_config.zarr_path="${zarr_path}" \ train_config.training_params.seed=${seed} \ train_config.training_params.num_epochs=${num_epochs} \ train_config.training_params.device=${gpu} \ @@ -48,8 +57,9 @@ eval_config.eval_args.max_step=${eval_max_step} \ eval_config.eval_args.num_envs=${eval_num_envs} \ eval_config.eval_args.sim=${sim_set} \ +eval_config.eval_args.max_demo=${expert_data_num} \ ++eval_config.eval_args.level=${level} \ ++eval_config.eval_args.scene_mode=${scene_mode} \ ++eval_config.eval_args.randomization_seed=${dr_seed} \ train_enable=${train_enable} \ eval_enable=${eval_enable} \ -eval_path=${eval_path} \ - -# eval_config.eval_args.random.level=${level} \ +eval_path=${eval_path} diff --git a/roboverse_learn/il/dp/models/ddpm_image_policy.py b/roboverse_learn/il/dp/models/ddpm_image_policy.py index d1ec3a38d..159139f81 100644 --- a/roboverse_learn/il/dp/models/ddpm_image_policy.py +++ b/roboverse_learn/il/dp/models/ddpm_image_policy.py @@ -104,6 +104,9 @@ def conditional_sample( step_kwargs = dict(self.scheduler_step_kwargs) step_kwargs.update(kwargs) + # Ensure timesteps are on the same device as trajectory + scheduler.timesteps = scheduler.timesteps.to(trajectory.device) + for t in scheduler.timesteps: # 1. Apply conditioning. trajectory[condition_mask] = condition_data[condition_mask] diff --git a/roboverse_learn/il/dp/runner/dp_runner.py b/roboverse_learn/il/dp/runner/dp_runner.py index 9bfeac8d1..68a4f0b84 100644 --- a/roboverse_learn/il/dp/runner/dp_runner.py +++ b/roboverse_learn/il/dp/runner/dp_runner.py @@ -30,175 +30,94 @@ from roboverse_learn.il.utils.common.pytorch_util import dict_apply, optimizer_to from torch.utils.data import DataLoader -from roboverse_pack.randomization import ( - CameraPresets, - CameraRandomizer, - LightPresets, - LightRandomizer, - MaterialPresets, - MaterialRandomizer, - ObjectPresets, - ObjectRandomizer, -) -# from roboverse_pack.randomization.presets.light_presets import LightScenarios - from metasim.task.registry import get_task_class -@dataclass -class DomainRandomizationCfg: - enable: bool = True - seed: int | None = 42 - - use_unified_object_randomizer: bool = True - cube_mass_range: tuple[float, float] = (0.3, 0.7) - robot_friction_range: tuple[float, float] = (0.5, 1.5) - robot_mass_range: tuple[float, float] = (0.2, 0.4) - - enable_material_random: bool = True - cube_material_type: str = "wood" - sphere_material_type: str = "rubber" - box_material_type: str = "metal" - - lighting_scenario: Literal["default", "indoor_room", "outdoor_scene", "studio", "demo"] = "default" - - camera_scenario: Literal["combined", "position_only", "orientation_only", "look_at_only", "intrinsics_only", "image_only"] = "combined" - camera_name: str = "camera0" - - -class DomainRandomizationManager: - def __init__(self, cfg: DomainRandomizationCfg, scenario, sim_handler): - - self.cfg = cfg - self.scenario = scenario - self.sim_handler = sim_handler - self.randomizers = self._init_all_randomizers() - - if self.cfg.seed is not None: - torch.manual_seed(self.cfg.seed) - np.random.seed(self.cfg.seed) - if torch.cuda.is_available(): - torch.cuda.manual_seed(self.cfg.seed) - - def _init_all_randomizers(self): - randomizers = { - "object": [], - "material": [], - "light": [], - "camera": [] - } - - if self.cfg.enable and self.cfg.use_unified_object_randomizer: - # Unified Object Randomization - robot_rand = ObjectRandomizer(ObjectPresets.robot_base(self.scenario.robots[0].name), seed=self.cfg.seed) - - # for rand in [cube_rand, sphere_rand, robot_rand]: - for rand in [robot_rand]: - rand.bind_handler(self.sim_handler) - randomizers["object"].append(rand) - - # 2. Initialize material randomizers - if self.cfg.enable and self.cfg.enable_material_random: - ## cube - # cube_mat_rand = MaterialRandomizer( - # MaterialPresets.wood_object("cube", use_mdl=True, randomization_mode="combined"), - # seed=self.cfg.seed - # ) - ## sphere - # sphere_mat_rand = MaterialRandomizer( - # MaterialPresets.rubber_object("sphere", randomization_mode="combined"), - # seed=self.cfg.seed - # ) - ## Box - box_mat_rand = MaterialRandomizer( - MaterialPresets.mdl_family_object("box_base", family="metal"), - seed=self.cfg.seed, - ) +from metasim.randomization import DomainRandomizationManager, DRConfig + +RANDOMIZATION_AVAILABLE = True + + +def ensure_clean_state(handler, expected_state=None): + """Ensure environment is in clean initial state with intelligent validation.""" + prev_state = None + stable_count = 0 + max_steps = 10 + min_steps = 2 + + for step in range(max_steps): + handler.simulate() + current_state = handler.get_states() + + if step >= min_steps: + if prev_state is not None: + is_stable = True + if hasattr(current_state, "objects") and hasattr(prev_state, "objects"): + for obj_name, obj_state in current_state.objects.items(): + if obj_name in prev_state.objects: + curr_dof = getattr(obj_state, "dof_pos", None) + prev_dof = getattr(prev_state.objects[obj_name], "dof_pos", None) + if curr_dof is not None and prev_dof is not None: + if not torch.allclose(curr_dof, prev_dof, atol=1e-5): + is_stable = False + break - # for rand in [cube_mat_rand, sphere_mat_rand, box_mat_rand]: - for rand in [box_mat_rand]: - rand.bind_handler(self.sim_handler) - randomizers["material"].append(rand) - - # 3. Initialize light randomizers - # if self.cfg.enable: - # light_configs = [] - # if self.cfg.lighting_scenario == "indoor_room": - # light_configs = LightScenarios.indoor_room() - # elif self.cfg.lighting_scenario == "outdoor_scene": - # light_configs = LightScenarios.outdoor_scene() - # elif self.cfg.lighting_scenario == "studio": - # light_configs = LightScenarios.three_point_studio() - # elif self.cfg.lighting_scenario == "demo": - # light_configs = [ - # LightPresets.demo_colors("rainbow_light"), - # LightPresets.demo_positions("disco_light"), - # LightPresets.demo_positions("shadow_light") - # ] - # else: # default - # light_configs = [ - # LightPresets.outdoor_daylight("light"), - # #LightPresets.indoor_ambient("ambient_light") - # ] - - # for cfg in light_configs: - # light_rand = LightRandomizer(cfg, seed=self.cfg.seed) - # light_rand.bind_handler(self.sim_handler) - # randomizers["light"].append(light_rand) - - # 4. Initialize camera randomizer - if self.cfg.enable: - camera_rand = CameraRandomizer( - CameraPresets.surveillance_camera(self.cfg.camera_name), - seed=self.cfg.seed - ) - camera_rand.bind_handler(self.sim_handler) - randomizers["camera"].append(camera_rand) - - return randomizers - - def randomize_for_demo(self, demo_idx: int = 0): - """ - Args: - demo_idx: current demonstration index - """ - if not self.cfg.enable: - return - - log.info(f"=== Executing Domain Randomization for Demo {demo_idx} ===") - - # 1. Object Randomization - for i, rand in enumerate(self.randomizers["object"]): - try: - rand() - log.debug(f"Object Randomizer {i+1} applied successfully") - except Exception as e: - log.warning(f"Object Randomizer {i+1} failed: {str(e)}") - - # 2. Material Randomization - for i, rand in enumerate(self.randomizers["material"]): - try: - rand() - log.debug(f"Material Randomizer {i+1} applied successfully") - except Exception as e: - log.warning(f"Material Randomizer {i+1} failed: {str(e)}") - - # 3. Light Randomization - for i, rand in enumerate(self.randomizers["light"]): - try: - rand() - log.debug(f"Light Randomizer {i+1} applied successfully") - except Exception as e: - log.warning(f"Light Randomizer {i+1} failed: {str(e)}") - - # 4. Camera Randomization - for i, rand in enumerate(self.randomizers["camera"]): - try: - rand() - log.debug(f"Camera Randomizer {i+1} applied successfully") - except Exception as e: - log.warning(f"Camera Randomizer {i+1} failed: {str(e)}") - - log.info(f"=== Domain Randomization for Demo {demo_idx} Completed ===") + if is_stable and expected_state is not None: + is_correct_state = _validate_state_correctness(current_state, expected_state) + if not is_correct_state: + log.debug(f"State stable but incorrect at step {step}, continuing simulation...") + stable_count = 0 + is_stable = False + + if is_stable: + stable_count += 1 + if stable_count >= 2: + break + else: + stable_count = 0 + + prev_state = current_state + + if expected_state is not None: + final_state = handler.get_states() + is_final_correct = _validate_state_correctness(final_state, expected_state) + if not is_final_correct: + log.warning(f"State validation failed after {max_steps} steps - reset may not have taken full effect") + + handler.get_states() + + +def _validate_state_correctness(current_state, expected_state): + """Validate that current state matches expected initial state for critical objects.""" + if not hasattr(current_state, "objects") or not hasattr(expected_state, "objects"): + return True + + critical_objects = [] + for obj_name, expected_obj in expected_state.objects.items(): + if hasattr(expected_obj, "dof_pos") and getattr(expected_obj, "dof_pos", None) is not None: + critical_objects.append(obj_name) + + if not critical_objects: + return True + + tolerance = 5e-3 + + for obj_name in critical_objects: + if obj_name not in current_state.objects: + continue + + expected_obj = expected_state.objects[obj_name] + current_obj = current_state.objects[obj_name] + + expected_dof = getattr(expected_obj, "dof_pos", None) + current_dof = getattr(current_obj, "dof_pos", None) + + if expected_dof is not None and current_dof is not None: + if not torch.allclose(current_dof, expected_dof, atol=tolerance): + diff = torch.abs(current_dof - expected_dof).max().item() + log.debug(f"DOF mismatch for {obj_name}: max diff = {diff:.6f} (tolerance = {tolerance})") + return False + + return True class DPRunner(BaseRunner): include_keys = ["global_step", "epoch"] @@ -484,45 +403,73 @@ def train(self): def evaluate(self, ckpt_path=None): args = self.eval_args - # Setup Domain Randomization Config - self.dr_cfg = DomainRandomizationCfg( - enable=False, - seed=args.dr_seed if hasattr(args, "dr_seed") else 42, - use_unified_object_randomizer=True, - lighting_scenario=args.lighting_scenario if hasattr(args, "lighting_scenario") else "default", - camera_scenario=args.camera_scenario if hasattr(args, "camera_scenario") else "combined", - camera_name="camera0" - ) - num_envs: int = args.num_envs log.info(f"Using GPU device: {args.gpu_id}") task_cls = get_task_class(args.task) - if args.task == 'stack_cube': + # Camera configuration + if args.task in {"stack_cube", "pick_cube", "pick_butter"}: dp_camera = True - elif args.task == 'close_box': - dp_camera = False else: - dp_camera = True + dp_camera = args.task != "close_box" - if dp_camera: - # import warnings - # warnings.warn("Using dp camera position!") + is_libero_dataset = "libero_90" in args.task + + if is_libero_dataset: + dp_pos = (2.0, 0.0, 2) + elif dp_camera: dp_pos = (1.0, 0.0, 0.75) else: dp_pos = (1.5, 0.0, 1.5) camera = PinholeCameraCfg( name="camera0", + data_types=["rgb", "depth"], + width=256, + height=256, pos=dp_pos, - look_at=(0.0, 0.0, 0.0) + look_at=(0.0, 0.0, 0.0), ) + # Lighting setup + render_mode = getattr(args, 'render_mode', 'raytracing') + if render_mode == "pathtracing": + ceiling_main = 18000.0 + ceiling_corners = 8000.0 + else: + ceiling_main = 12000.0 + ceiling_corners = 5000.0 + + from metasim.scenario.lights import DiskLightCfg, SphereLightCfg + lights = [ + DiskLightCfg( + name="ceiling_main", + intensity=ceiling_main, + color=(1.0, 1.0, 1.0), + radius=1.2, + pos=(0.0, 0.0, 2.8), + rot=(0.7071, 0.0, 0.0, 0.7071), + ), + SphereLightCfg( + name="ceiling_ne", intensity=ceiling_corners, color=(1.0, 1.0, 1.0), radius=0.6, pos=(1.0, 1.0, 2.5) + ), + SphereLightCfg( + name="ceiling_nw", intensity=ceiling_corners, color=(1.0, 1.0, 1.0), radius=0.6, pos=(-1.0, 1.0, 2.5) + ), + SphereLightCfg( + name="ceiling_sw", intensity=ceiling_corners, color=(1.0, 1.0, 1.0), radius=0.6, pos=(-1.0, -1.0, 2.5) + ), + SphereLightCfg( + name="ceiling_se", intensity=ceiling_corners, color=(1.0, 1.0, 1.0), radius=0.6, pos=(1.0, -1.0, 2.5) + ), + ] + scenario = task_cls.scenario.update( robots=[args.robot], simulator=args.sim, num_envs=args.num_envs, headless=args.headless, + lights=lights, cameras=[camera] ) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") @@ -530,25 +477,43 @@ def evaluate(self, ckpt_path=None): env = task_cls(scenario, device=device) robot = get_robot(args.robot) - # Initialize Domain Randomization Manager - if self.dr_cfg.enable: - self.randomization_manager = DomainRandomizationManager( - cfg=self.dr_cfg, + # Domain Randomization configuration + dr_level = getattr(args, 'level', 0) + dr_scene_mode = getattr(args, 'scene_mode', 0) + dr_seed = getattr(args, 'randomization_seed', None) + + if not RANDOMIZATION_AVAILABLE: + if dr_level > 0: + log.warning("Domain randomization requested but not available!") + randomization_manager = None + else: + from dataclasses import dataclass as dc + @dc + class SimpleRenderCfg: + mode: str = render_mode + + randomization_manager = DomainRandomizationManager( + config=DRConfig( + level=dr_level, + scene_mode=dr_scene_mode, + randomization_seed=dr_seed, + ), scenario=scenario, - sim_handler=env.handler + handler=env.handler, + init_states=None, + render_cfg=SimpleRenderCfg(mode=render_mode) ) - log.info("Domain Randomization Manager initialized successfully") - else: - self.randomization_manager = None - log.info("Domain Randomization is disabled") + if dr_level > 0: + log.info(f"Domain Randomization enabled: level={dr_level}, scene_mode={dr_scene_mode}, seed={dr_seed}") + else: + log.info("Domain Randomization disabled (level=0)") toc = time.time() log.trace(f"Time to launch: {toc - tic:.2f}s") time_str = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") checkpoint = self.get_checkpoint_path() - # checkpoint = ckpt_path if checkpoint is None else checkpoint - checkpoint = ckpt_path if ckpt_path is None else checkpoint + checkpoint = ckpt_path if ckpt_path is not None else checkpoint if checkpoint is None: raise ValueError( "No checkpoint found, please provide a valid checkpoint path." @@ -580,6 +545,30 @@ def evaluate(self, ckpt_path=None): toc = time.time() log.trace(f"Time to load data: {toc - tic:.2f}s") + # Update DR manager with init_states + if randomization_manager is not None: + randomization_manager.init_states = init_states + randomization_manager.original_positions = {} + for demo_idx, init_state in enumerate(init_states): + demo_key = f"demo_{demo_idx}" + randomization_manager.original_positions[demo_key] = {} + + if "objects" in init_state: + for obj_name, obj_state in init_state["objects"].items(): + randomization_manager.original_positions[demo_key][f"obj_{obj_name}"] = { + "x": float(obj_state["pos"][0]), + "y": float(obj_state["pos"][1]), + "z": float(obj_state["pos"][2]), + } + + if "robots" in init_state: + for robot_name, robot_state in init_state["robots"].items(): + randomization_manager.original_positions[demo_key][f"robot_{robot_name}"] = { + "x": float(robot_state["pos"][0]), + "y": float(robot_state["pos"][1]), + "z": float(robot_state["pos"][2]), + } + total_success = 0 total_completed = 0 if args.max_demo is None: @@ -595,35 +584,40 @@ def evaluate(self, ckpt_path=None): demo_end_idx = min(demo_start_idx + num_envs, num_demos) current_demo_idxs = list(range(demo_start_idx, demo_end_idx)) - ## Randomize environment for current batch of demos - if self.randomization_manager is not None: - for demo_idx in current_demo_idxs: - self.randomization_manager.randomize_for_demo(demo_idx) + # Apply domain randomization before reset + if randomization_manager is not None and dr_level > 0: + for env_id, demo_idx in enumerate(current_demo_idxs): + log.info(f"[DP Eval] Episode {demo_idx}: Applying DR") + randomization_manager.apply_randomization(demo_idx=demo_idx, is_initial=(demo_start_idx == args.task_id_range_low)) + randomization_manager.update_positions_to_table(demo_idx=demo_idx, env_id=env_id) + randomization_manager.update_camera_look_at(env_id=env_id) + randomization_manager.apply_camera_randomization() - ## Reset before first step tic = time.time() obs, extras = env.reset(states=init_states[demo_start_idx:demo_end_idx]) - policyRunner.reset() toc = time.time() log.trace(f"Time to reset: {toc - tic:.2f}s") + # Ensure environment stabilizes after reset + if randomization_manager is not None and dr_level > 0: + ensure_clean_state(env.handler) + + if hasattr(env, "_episode_steps"): + for env_id in range(num_envs): + env._episode_steps[env_id] = 0 + + policyRunner.reset() + step = 0 MaxStep = args.max_step SuccessOnce = [False] * num_envs TimeOut = [False] * num_envs images_list = [] print(policyRunner.policy_cfg) - # env.handler.refresh_render() - dynamic_dr_interval = 20 while step < MaxStep: log.debug(f"Step {step}") - ## DR after dynamic_dr_interval steps - # if self.randomization_manager is not None and step % dynamic_dr_interval == 0 and step > 0: - # log.info(f"Step {step}: Executing dynamic domain randomization") - # self.randomization_manager.randomize_for_demo(demo_idx=demo_start_idx + step//dynamic_dr_interval) - new_obs = { "rgb": obs.cameras["camera0"].rgb, "joint_qpos": obs.robots[args.robot].joint_pos, @@ -659,7 +653,9 @@ def evaluate(self, ckpt_path=None): f.write(f"SuccessOnce: {SuccessOnce[i]}\n") f.write(f"SuccessEnd: {SuccessEnd[i]}\n") f.write(f"TimeOut: {TimeOut[i]}\n") - f.write(f"Domain Randomization Enabled: {self.dr_cfg.enable}\n") # Record DR status + f.write(f"Domain Randomization Level: {dr_level}\n") + f.write(f"Domain Randomization Scene Mode: {dr_scene_mode}\n") + f.write(f"Domain Randomization Seed: {dr_seed}\n") f.write( f"Cumulative Average Success Rate: {total_success / total_completed:.4f}\n" ) @@ -672,8 +668,10 @@ def evaluate(self, ckpt_path=None): with open(f"tmp/{ckpt_name}/final_stats.txt", "w") as f: f.write(f"Total Success: {total_success}\n") f.write(f"Total Completed: {total_completed}\n") - f.write(f"Average Average Success Rate: {total_success / total_completed:.4f}\n") - f.write(f"Domain Randomization Config: {self.dr_cfg}\n") # save DR config + f.write(f"Average Success Rate: {total_success / total_completed:.4f}\n") + f.write(f"Domain Randomization Level: {dr_level}\n") + f.write(f"Domain Randomization Scene Mode: {dr_scene_mode}\n") + f.write(f"Domain Randomization Seed: {dr_seed}\n") env.close() def run( diff --git a/roboverse_learn/il/utils/common/eval_args.py b/roboverse_learn/il/utils/common/eval_args.py index 955921c7b..946d471bc 100644 --- a/roboverse_learn/il/utils/common/eval_args.py +++ b/roboverse_learn/il/utils/common/eval_args.py @@ -5,15 +5,11 @@ from loguru import logger as log -# from metasim.cfg.randomization import RandomizationCfg - @dataclass class Args: task: str """Task name""" - # random: RandomizationCfg = field(default_factory=RandomizationCfg) - """Domain randomization options""" robot: str = "franka" """Robot name""" num_envs: int = 1 @@ -43,8 +39,13 @@ class Args: gpu_id: int = 0 """GPU ID to use""" + # Domain Randomization options + level: Literal[0, 1, 2, 3] = 0 + """Randomization level: 0=None, 1=Scene+Material, 2=+Light, 3=+Camera""" + scene_mode: Literal[0, 1, 2, 3] = 0 + """Scene mode: 0=Manual, 1=USD Table, 2=USD Scene, 3=Full USD""" + randomization_seed: int | None = None + """Seed for reproducible randomization. If None, uses random seed""" + def __post_init__(self): - # if self.random.table and not self.table: - # log.warning("Cannot enable table randomization without a table, disabling table randomization") - # self.random.table = False log.info(f"Args: {self}") From b401adc94644f8224592ae996f3065c3bae8798b Mon Sep 17 00:00:00 2001 From: gxyes Date: Thu, 4 Dec 2025 20:32:35 +0700 Subject: [PATCH 08/50] add DR to smolVLA eval --- roboverse_learn/vla/SmolVLA/smolvla_eval.py | 202 ++++++++++++++++++-- 1 file changed, 182 insertions(+), 20 deletions(-) diff --git a/roboverse_learn/vla/SmolVLA/smolvla_eval.py b/roboverse_learn/vla/SmolVLA/smolvla_eval.py index cd44c30d2..a2f4dd4df 100755 --- a/roboverse_learn/vla/SmolVLA/smolvla_eval.py +++ b/roboverse_learn/vla/SmolVLA/smolvla_eval.py @@ -13,7 +13,7 @@ import multiprocessing from collections import deque from pathlib import Path -from typing import Dict, Any, Optional +from typing import Dict, Any import numpy as np import torch @@ -26,7 +26,11 @@ import gymnasium as gym from metasim.utils import configclass from metasim.scenario.cameras import PinholeCameraCfg +from metasim.scenario.lights import DiskLightCfg, SphereLightCfg from metasim.utils.obs_utils import ObsSaver +from metasim.utils.demo_util import get_traj +from metasim.utils.setup_util import get_robot +from metasim.randomization import DomainRandomizationManager, DRConfig from roboverse_learn.il.dp.runner.base_policy import BasePolicyCfg, ActionCfg, ObsCfg, EndEffectorCfg @@ -88,7 +92,7 @@ def _init_policy(self, checkpoint_path: str): print(f"Loading SmolVLA model from: {checkpoint_path}") - # Check for LeRobot checkpoint structure (checkpoints contain pretrained_model subdirectory) + # Handle LeRobot checkpoint structure pretrained_model_path = os.path.join(checkpoint_path, "pretrained_model") if os.path.exists(pretrained_model_path): checkpoint_path = pretrained_model_path @@ -176,13 +180,21 @@ def predict_action(self, observation=None): state_tensor = state.float().unsqueeze(0).to(self.device) + # Tokenize instruction + tokenized = self.model.model.vlm_with_expert.processor.tokenizer( + instruction, + return_tensors="pt", + padding=True, + truncation=True + ) + batch = { "observation.image": image_tensor, "observation.state": state_tensor, - "task": [instruction], # Task instruction as list of strings + "observation.language.tokens": tokenized["input_ids"].to(self.device), + "observation.language.attention_mask": tokenized["attention_mask"].bool().to(self.device), } - # Use select_action method (returns tensor) action = self.model.select_action(batch).squeeze(0).cpu().numpy() else: prompt = f"What action should the robot take to {instruction}?" @@ -280,9 +292,47 @@ def evaluate_episode( max_steps: int, episode_num: int, output_dir: str, + randomization_manager=None, + demo_idx: int = 0, + init_states=None, ) -> Dict[str, Any]: """Evaluate a single episode and save results.""" - obs, info = env.reset() + + # Apply domain randomization before reset + if randomization_manager is not None: + randomization_manager.apply_randomization(demo_idx=demo_idx, is_initial=(episode_num == 1)) + randomization_manager.update_positions_to_table(demo_idx=demo_idx, env_id=0) + randomization_manager.update_camera_look_at(env_id=0) + randomization_manager.apply_camera_randomization() + + # Reset environment - must use task_env directly to pass states + # (GymEnvWrapper.reset() doesn't support states parameter) + if randomization_manager is not None and init_states is not None: + from roboverse_learn.il.act.act_eval_runner import ensure_clean_state + + # Access task_env through wrapper + env_unwrapped = env + while hasattr(env_unwrapped, 'unwrapped') and env_unwrapped != env_unwrapped.unwrapped: + env_unwrapped = env_unwrapped.unwrapped + task_env = env_unwrapped.task_env if hasattr(env_unwrapped, 'task_env') else env_unwrapped + + # Reset to specific state + obs, info = task_env.reset(states=[init_states[demo_idx]], env_ids=[0]) + + # Ensure environment stabilizes after reset + ensure_clean_state(task_env.handler, expected_state=init_states[demo_idx]) + + # Update gym wrapper state to avoid "ResetNeeded" error + wrapper = env + while wrapper is not env_unwrapped: + if hasattr(wrapper, '_has_reset'): + wrapper._has_reset = True + if hasattr(wrapper, '_episode_steps'): + wrapper._episode_steps[0] = 0 + wrapper = wrapper.unwrapped if hasattr(wrapper, 'unwrapped') else wrapper.env + else: + obs, info = env.reset() + stats = { "steps": 0, "success": False, @@ -344,7 +394,6 @@ def main(): help="Simulator backend", ) parser.add_argument("--solver", type=str, default="pyroki", choices=["curobo", "pyroki"], help="IK solver") - parser.add_argument("--num_envs", type=int, default=1, help="Number of parallel environments") parser.add_argument("--num_episodes", type=int, default=10, help="Number of evaluation episodes") parser.add_argument("--max_steps", type=int, default=250, help="Maximum steps per episode") parser.add_argument( @@ -354,6 +403,28 @@ def main(): parser.add_argument( "--output_dir", type=str, default="./smolvla_eval_output", help="Output directory for videos and results" ) + + # Domain Randomization options + parser.add_argument( + "--level", + type=int, + default=0, + choices=[0, 1, 2, 3], + help="Randomization level: 0=None, 1=Scene+Material, 2=+Light, 3=+Camera" + ) + parser.add_argument( + "--scene_mode", + type=int, + default=0, + choices=[0, 1, 2, 3], + help="Scene mode: 0=Manual, 1=USD Table, 2=USD Scene, 3=Full USD" + ) + parser.add_argument( + "--randomization_seed", + type=int, + default=None, + help="Seed for reproducible randomization. If None, uses random seed" + ) args = parser.parse_args() if args.device.startswith("cuda") and not torch.cuda.is_available(): @@ -366,6 +437,7 @@ def main(): print(f" Simulator: {args.sim}") print(f" IK Solver: {args.solver}") print(f" Device: {args.device}") + print(f" DR Level: {args.level}, Scene Mode: {args.scene_mode}, Seed: {args.randomization_seed}") # Set seeds torch.manual_seed(args.seed) @@ -374,25 +446,101 @@ def main(): torch.cuda.manual_seed(args.seed) torch.cuda.manual_seed_all(args.seed) - # Create single environment (vectorized environments not supported) + # Camera configuration + if args.task in {"stack_cube", "pick_cube", "pick_butter"}: + dp_camera = True + else: + dp_camera = args.task != "close_box" + + is_libero_dataset = "libero_90" in args.task + + if is_libero_dataset: + dp_pos = (2.0, 0.0, 2) + elif dp_camera: + dp_pos = (1.0, 0.0, 0.75) + else: + dp_pos = (1.5, 0.0, 1.5) + + camera = PinholeCameraCfg( + name="camera", + data_types=["rgb"], + width=256, + height=256, + pos=dp_pos, + look_at=(0.0, 0.0, 0.0), + ) + + # Lighting setup + render_mode = "raytracing" + ceiling_main = 12000.0 + ceiling_corners = 5000.0 + + lights = [ + DiskLightCfg( + name="ceiling_main", + intensity=ceiling_main, + color=(1.0, 1.0, 1.0), + radius=1.2, + pos=(0.0, 0.0, 2.8), + rot=(0.7071, 0.0, 0.0, 0.7071), + ), + SphereLightCfg( + name="ceiling_ne", intensity=ceiling_corners, color=(1.0, 1.0, 1.0), radius=0.6, pos=(1.0, 1.0, 2.5) + ), + SphereLightCfg( + name="ceiling_nw", intensity=ceiling_corners, color=(1.0, 1.0, 1.0), radius=0.6, pos=(-1.0, 1.0, 2.5) + ), + SphereLightCfg( + name="ceiling_sw", intensity=ceiling_corners, color=(1.0, 1.0, 1.0), radius=0.6, pos=(-1.0, -1.0, 2.5) + ), + SphereLightCfg( + name="ceiling_se", intensity=ceiling_corners, color=(1.0, 1.0, 1.0), radius=0.6, pos=(1.0, -1.0, 2.5) + ), + ] + + # Create environment env = gym.make( f"RoboVerse/{args.task}", robots=[args.robot], simulator=args.sim, headless=True, - cameras=[ - PinholeCameraCfg( - name="camera", - data_types=["rgb"], - width=256, - height=256, - pos=(1.5, 0.0, 1.5), - look_at=(0.0, 0.0, 0.0), - ) - ], + cameras=[camera], + lights=lights, device=args.device, ) + # Access the actual task environment through wrapper + env_unwrapped = env + while hasattr(env_unwrapped, 'unwrapped') and env_unwrapped != env_unwrapped.unwrapped: + env_unwrapped = env_unwrapped.unwrapped + task_env = env_unwrapped.task_env if hasattr(env_unwrapped, 'task_env') else env_unwrapped + + # Load trajectories for domain randomization + traj_filepath = task_env.traj_filepath + robot_obj = get_robot(args.robot) + init_states, _, _ = get_traj(traj_filepath, robot_obj, task_env.handler) + + # Initialize Domain Randomization + randomization_manager = None + if args.level > 0: + from dataclasses import dataclass as dc + @dc + class SimpleRenderCfg: + mode: str = render_mode + + randomization_manager = DomainRandomizationManager( + config=DRConfig( + level=args.level, + scene_mode=args.scene_mode, + randomization_seed=args.randomization_seed, + ), + scenario=env.unwrapped.scenario, + handler=task_env.handler, + init_states=init_states, + render_cfg=SimpleRenderCfg(mode=render_mode) + ) + print(f"Domain Randomization enabled: level={args.level}, scene_mode={args.scene_mode}, seed={args.randomization_seed}") + # Create runner runner = SmolVLARunner( env=env, @@ -419,7 +567,15 @@ def main(): print(f"Episode {ep + 1}/{args.num_episodes}") print(f"{'=' * 50}") - ep_res = evaluate_episode(env, runner, args.max_steps, ep + 1, args.output_dir) + demo_idx = ep % len(init_states) if randomization_manager is not None else 0 + + ep_res = evaluate_episode( + env, runner, args.max_steps, ep + 1, args.output_dir, + randomization_manager=randomization_manager, + demo_idx=demo_idx, + init_states=init_states if randomization_manager is not None else None + ) + eval_stats["total_episodes"] += 1 if ep_res["success"]: eval_stats["total_successes"] += 1 @@ -430,7 +586,7 @@ def main(): print(f" Steps: {ep_res['steps']}") print(f" Success: {ep_res['success']}") print(f" Reward: {ep_res['total_reward']:.2f}") - print(f" Current success rate: {sr:.1%}") + print(f" Success rate: {sr:.1%}") total_time = time.time() - start_time final_sr = eval_stats["total_successes"] / eval_stats["total_episodes"] @@ -442,6 +598,7 @@ def main(): print(f"Success Rate: {final_sr:.1%} ({eval_stats['total_successes']}/{eval_stats['total_episodes']})") print(f"Average Reward: {final_avg_reward:.2f}") print(f"Total Time: {total_time:.1f}s") + print(f"DR Level: {args.level}, Scene Mode: {args.scene_mode}, Seed: {args.randomization_seed}") print(f"{'=' * 50}") # Save results to JSON @@ -449,7 +606,12 @@ def main(): ts = time.strftime("%Y%m%d_%H%M%S") result_path = os.path.join(args.output_dir, f"smolvla_eval_{args.task}_{ts}.json") with open(result_path, "w") as f: - json.dump({"config": vars(args), "eval_stats": eval_stats, "timestamp": ts}, f, indent=2) + json.dump({ + "config": vars(args), + "eval_stats": eval_stats, + "timestamp": ts, + "dr_config": {"level": args.level, "scene_mode": args.scene_mode, "seed": args.randomization_seed} + }, f, indent=2) print(f"\nResults saved to: {result_path}") # Cleanup From af4e2616f3498cce08999714097ffb0e1806749c Mon Sep 17 00:00:00 2001 From: gxyes Date: Thu, 4 Dec 2025 23:20:33 +0700 Subject: [PATCH 09/50] add DR to openVLA eval --- roboverse_learn/vla/OpenVLA/vla_eval.py | 199 +++++++++++++++++++++--- 1 file changed, 175 insertions(+), 24 deletions(-) diff --git a/roboverse_learn/vla/OpenVLA/vla_eval.py b/roboverse_learn/vla/OpenVLA/vla_eval.py index 101ba57a5..acae067ee 100644 --- a/roboverse_learn/vla/OpenVLA/vla_eval.py +++ b/roboverse_learn/vla/OpenVLA/vla_eval.py @@ -15,12 +15,15 @@ from scipy.spatial.transform import Rotation sys.path.append(str(Path(__file__).parent.parent.parent)) -# from metasim.task.gym_registration import make_vec import metasim from gymnasium import make_vec from metasim.utils import configclass from metasim.scenario.cameras import PinholeCameraCfg +from metasim.scenario.lights import DiskLightCfg, SphereLightCfg from metasim.utils.obs_utils import ObsSaver +from metasim.utils.demo_util import get_traj +from metasim.utils.setup_util import get_robot +from metasim.randomization import DomainRandomizationManager, DRConfig from roboverse_learn.il.dp.runner.base_policy import BasePolicyCfg, ActionCfg, ObsCfg, EndEffectorCfg @@ -68,7 +71,6 @@ def _init_policy(self, **kwargs): self.task = kwargs.get("task_name") self.subset = kwargs.get("subset") - self.policy_cfg = VLAPolicyCfg() self.policy_cfg.obs_config.obs_type = "no_proprio" @@ -107,7 +109,6 @@ def predict_action(self, observation=None): if len(self.obs) == 0: raise ValueError("No observations available") - latest_obs = self.obs[-1] # Take first camera first_cam = next(iter(latest_obs.cameras.values())) @@ -116,15 +117,12 @@ def predict_action(self, observation=None): image = x.numpy() image = Image.fromarray(image) - # instruction = self.env.task_env.task_desc if hasattr(self.env.task_env, "task_desc"): instruction = self.env.task_env.task_desc else: - # generate by task name + # Generate instruction from task name task_desc = self.task_name.replace('_', ' ') instruction = task_desc[0].upper() + task_desc[1:] - # instruction = self.env.task_env.task_desc - # 'Pick up the butter and place it in the basket' for pick butter tasks # Process inputs manually for OpenVLAForActionPrediction prompt = f"In: What action should the robot take to {instruction}?\nOut:" @@ -185,11 +183,9 @@ def ee_control_actions(self, obs) -> list[dict]: self.ee_body_idx = rs.body_names.index(self.ee_body_name) ee_p_world = robot_ee_state[:, self.ee_body_idx, 0:3] ee_q_world = robot_ee_state[:, self.ee_body_idx, 3:7] - # print(f"EE position in world: {ee_p_world}") # Base pose robot_pos, robot_quat = robot_root_state[:, 0:3], robot_root_state[:, 3:7] - # print(f"Robot position in world: {robot_pos}") # Local frame transform using scipy # Convert to scipy format and use Rotation for quaternion operations @@ -226,7 +222,6 @@ def ee_control_actions(self, obs) -> list[dict]: ee_quat_target_rot = Rotation.from_quat(curr_ee_quat_local_scipy) * Rotation.from_quat(ee_quat_delta_scipy) ee_quat_target = self.quat_from_scipy(ee_quat_target_rot.as_quat(), self.device) - # 4) IK (seed = current q) q_solution, ik_succ = self.ik_solver.solve_ik_batch(ee_pos_target, ee_quat_target, curr_robot_q) if not ik_succ.all(): @@ -244,8 +239,36 @@ def reset(self): self.obs.clear() -def evaluate_episode(env, runner: OpenVLARunner, max_steps: int, episode_num: int, output_dir: str) -> Dict[str, Any]: - obs, info = env.reset() +def evaluate_episode( + env, + runner: OpenVLARunner, + max_steps: int, + episode_num: int, + output_dir: str, + randomization_manager=None, + demo_idx: int = 0, + init_states=None, +) -> Dict[str, Any]: + """Evaluate a single episode.""" + + # Apply domain randomization before reset + if randomization_manager is not None: + randomization_manager.apply_randomization(demo_idx=demo_idx, is_initial=(episode_num == 1)) + randomization_manager.update_positions_to_table(demo_idx=demo_idx, env_id=0) + randomization_manager.update_camera_look_at(env_id=0) + randomization_manager.apply_camera_randomization() + + # Reset environment + if randomization_manager is not None and init_states is not None: + from roboverse_learn.il.act.act_eval_runner import ensure_clean_state + # Use task_env.reset() directly to pass states parameter + obs, info = env.task_env.reset(states=[init_states[demo_idx]]) + ensure_clean_state(env.task_env.handler, expected_state=None) + if hasattr(env, "_episode_steps"): + env._episode_steps[0] = 0 + else: + obs, info = env.reset() + stats = {"steps": 0, "success": False, "total_reward": 0.0, "start_time": time.time()} runner.reset() @@ -298,6 +321,28 @@ def main(): parser.add_argument("--device", type=str, default="cuda" if torch.cuda.is_available() else "cpu") parser.add_argument("--seed", type=int, default=42) parser.add_argument("--output_dir", type=str, default="./eval_output") + + # Domain Randomization options + parser.add_argument( + "--level", + type=int, + default=0, + choices=[0, 1, 2, 3], + help="Randomization level: 0=None, 1=Scene+Material, 2=+Light, 3=+Camera" + ) + parser.add_argument( + "--scene_mode", + type=int, + default=0, + choices=[0, 1, 2, 3], + help="Scene mode: 0=Manual, 1=USD Table, 2=USD Scene, 3=Full USD" + ) + parser.add_argument( + "--randomization_seed", + type=int, + default=None, + help="Seed for reproducible randomization. If None, uses random seed" + ) args = parser.parse_args() @@ -305,7 +350,13 @@ def main(): print("CUDA not available, switching to CPU") args.device = "cpu" - print(f"OpenVLA Eval: task={args.task} robot={args.robot} sim={args.sim} solver={args.solver} device={args.device}") + print(f"OpenVLA Evaluation") + print(f" Task: {args.task}") + print(f" Robot: {args.robot}") + print(f" Simulator: {args.sim}") + print(f" IK Solver: {args.solver}") + print(f" Device: {args.device}") + print(f" DR Level: {args.level}, Scene Mode: {args.scene_mode}, Seed: {args.randomization_seed}") torch.manual_seed(args.seed) np.random.seed(args.seed) @@ -313,20 +364,66 @@ def main(): torch.cuda.manual_seed(args.seed) torch.cuda.manual_seed_all(args.seed) + # Camera configuration + if args.task in {"stack_cube", "pick_cube", "pick_butter"}: + dp_camera = True + else: + dp_camera = args.task != "close_box" + + is_libero_dataset = "libero_90" in args.task + + if is_libero_dataset: + dp_pos = (2.0, 0.0, 2) + elif dp_camera: + dp_pos = (1.0, 0.0, 0.75) + else: + dp_pos = (1.5, 0.0, 1.5) + + camera = PinholeCameraCfg( + name="camera", + data_types=["rgb"], + width=256, + height=256, + pos=dp_pos, + look_at=(0.0, 0.0, 0.0), + ) + + # Lighting setup + render_mode = "raytracing" + ceiling_main = 12000.0 + ceiling_corners = 5000.0 + + lights = [ + DiskLightCfg( + name="ceiling_main", + intensity=ceiling_main, + color=(1.0, 1.0, 1.0), + radius=1.2, + pos=(0.0, 0.0, 2.8), + rot=(0.7071, 0.0, 0.0, 0.7071), + ), + SphereLightCfg( + name="ceiling_ne", intensity=ceiling_corners, color=(1.0, 1.0, 1.0), radius=0.6, pos=(1.0, 1.0, 2.5) + ), + SphereLightCfg( + name="ceiling_nw", intensity=ceiling_corners, color=(1.0, 1.0, 1.0), radius=0.6, pos=(-1.0, 1.0, 2.5) + ), + SphereLightCfg( + name="ceiling_sw", intensity=ceiling_corners, color=(1.0, 1.0, 1.0), radius=0.6, pos=(-1.0, -1.0, 2.5) + ), + SphereLightCfg( + name="ceiling_se", intensity=ceiling_corners, color=(1.0, 1.0, 1.0), radius=0.6, pos=(1.0, -1.0, 2.5) + ), + ] + env = make_vec( f"RoboVerse/{args.task}", num_envs=args.num_envs, robots=[args.robot], simulator=args.sim, headless=True, - cameras=[PinholeCameraCfg( - name="camera", - data_types=["rgb"], - width=256, - height=256, - pos=(1.5, 0.0, 1.5), - look_at=(0.0, 0.0, 0.0), - )], + cameras=[camera], + lights=lights, device=args.device, ) @@ -342,31 +439,85 @@ def main(): solver=args.solver, ) + # Load trajectories for DR + traj_filepath = env.task_env.traj_filepath + robot_obj = get_robot(args.robot) + init_states, _, _ = get_traj(traj_filepath, robot_obj, env.task_env.handler) + + # Initialize Domain Randomization Manager + randomization_manager = None + if args.level > 0: + from dataclasses import dataclass as dc + @dc + class SimpleRenderCfg: + mode: str = render_mode + + randomization_manager = DomainRandomizationManager( + config=DRConfig( + level=args.level, + scene_mode=args.scene_mode, + randomization_seed=args.randomization_seed, + ), + scenario=env.scenario, + handler=env.task_env.handler, + init_states=init_states, + render_cfg=SimpleRenderCfg(mode=render_mode) + ) + print(f"Domain Randomization enabled: level={args.level}, scene_mode={args.scene_mode}, seed={args.randomization_seed}") + start_time = time.time() eval_stats = {"total_episodes": 0, "total_successes": 0, "total_rewards": [], "episode_results": []} for ep in range(args.num_episodes): + print(f"\n{'=' * 50}") print(f"Episode {ep + 1}/{args.num_episodes}") - ep_res = evaluate_episode(env, runner, args.max_steps, ep + 1, args.output_dir) + print(f"{'=' * 50}") + + demo_idx = ep % len(init_states) if randomization_manager is not None else 0 + + ep_res = evaluate_episode( + env, runner, args.max_steps, ep + 1, args.output_dir, + randomization_manager=randomization_manager, + demo_idx=demo_idx, + init_states=init_states if randomization_manager is not None else None + ) + eval_stats["total_episodes"] += 1 if ep_res["success"]: eval_stats["total_successes"] += 1 eval_stats["total_rewards"].append(ep_res["total_reward"]) eval_stats["episode_results"].append(ep_res) + sr = eval_stats["total_successes"] / eval_stats["total_episodes"] + print(f" Steps: {ep_res['steps']}") + print(f" Success: {ep_res['success']}") + print(f" Reward: {ep_res['total_reward']:.2f}") print(f" Success rate: {sr:.1%}") total_time = time.time() - start_time final_sr = eval_stats["total_successes"] / eval_stats["total_episodes"] final_avg_reward = float(np.mean(eval_stats["total_rewards"])) if len(eval_stats["total_rewards"]) else 0.0 - print(f"\nEvaluation completed: {final_sr:.1%} | {final_avg_reward:.2f} | {total_time:.1f}s") + + print(f"\n{'=' * 50}") + print("Evaluation Summary") + print(f"{'=' * 50}") + print(f"Success Rate: {final_sr:.1%} ({eval_stats['total_successes']}/{eval_stats['total_episodes']})") + print(f"Average Reward: {final_avg_reward:.2f}") + print(f"Total Time: {total_time:.1f}s") + print(f"DR Level: {args.level}, Scene Mode: {args.scene_mode}, Seed: {args.randomization_seed}") + print(f"{'=' * 50}") if args.output_dir: os.makedirs(args.output_dir, exist_ok=True) ts = time.strftime("%Y%m%d_%H%M%S") out_path = os.path.join(args.output_dir, f"openvla_eval_{args.task}_{ts}.json") with open(out_path, "w", encoding="utf-8") as f: - json.dump({"config": vars(args), "eval_stats": eval_stats, "timestamp": ts}, f, indent=2, ensure_ascii=False) + json.dump({ + "config": vars(args), + "eval_stats": eval_stats, + "timestamp": ts, + "dr_config": {"level": args.level, "scene_mode": args.scene_mode, "seed": args.randomization_seed} + }, f, indent=2, ensure_ascii=False) try: env.close() From 58efad4b0e9d11ecf74cccdfa87d0f1fa158c9a8 Mon Sep 17 00:00:00 2001 From: gxyes Date: Thu, 4 Dec 2025 23:28:50 +0700 Subject: [PATCH 10/50] add DR to pi0 eval --- roboverse_learn/vla/pi0/pi_eval.py | 196 ++++++++++++++++++++++++++--- 1 file changed, 177 insertions(+), 19 deletions(-) diff --git a/roboverse_learn/vla/pi0/pi_eval.py b/roboverse_learn/vla/pi0/pi_eval.py index bf1c935b2..4437c211f 100644 --- a/roboverse_learn/vla/pi0/pi_eval.py +++ b/roboverse_learn/vla/pi0/pi_eval.py @@ -6,7 +6,7 @@ import sys import time from pathlib import Path -from typing import Any, Dict, Optional +from typing import Any, Dict import numpy as np import torch @@ -16,9 +16,13 @@ from gymnasium import make_vec from metasim.scenario.cameras import PinholeCameraCfg +from metasim.scenario.lights import DiskLightCfg, SphereLightCfg from metasim.utils import configclass from metasim.utils.obs_utils import ObsSaver from metasim.utils.ik_solver import process_gripper_command, setup_ik_solver +from metasim.utils.demo_util import get_traj +from metasim.utils.setup_util import get_robot +from metasim.randomization import DomainRandomizationManager, DRConfig from openpi_client import image_tools, websocket_client_policy @@ -52,7 +56,7 @@ def __init__( image_size: int = 224, gripper_threshold: float = 0.02, device: str = "cuda", - actions_per_call: Optional[int] = None, + actions_per_call: int | None = None, ): if num_envs != 1: raise ValueError("pi_eval currently supports num_envs == 1") @@ -73,7 +77,7 @@ def __init__( self.reorder_idx = None self.inverse_reorder_idx = None - self.cached_actions: Optional[np.ndarray] = None + self.cached_actions: np.ndarray | None = None self.cache_index: int = 0 self.cache_remaining: int = 0 @@ -90,7 +94,6 @@ def _extract_robot_state(self, obs) -> torch.Tensor: rs = obs.robots[self.robot_name] joint_pos = rs.joint_pos if isinstance(rs.joint_pos, torch.Tensor) else torch.tensor(rs.joint_pos) joint_pos = joint_pos.to(torch.float32) - # curr_robot_q = joint_pos[:, self.inverse_reorder_idx] return joint_pos def _build_state_vector(self, joint_pos_alpha: torch.Tensor) -> np.ndarray: @@ -153,12 +156,12 @@ def _decode_single_action(self, action: np.ndarray) -> list[dict]: return actions - def _request_action_chunk(self, policy_obs: Dict[str, Any]) -> None: response = self.client.infer(policy_obs) chunk = np.asarray(response["actions"], dtype=np.float32) if chunk.ndim != 2: raise ValueError(f"Expected action chunk with ndim=2, got {chunk.shape}") + self.cached_actions = chunk self.cache_index = 0 total = len(chunk) @@ -192,8 +195,36 @@ def close(self) -> None: pass -def evaluate_episode(env, runner: PiPolicyRunner, max_steps: int, episode: int, output_dir: str) -> Dict[str, Any]: - obs, info = env.reset() +def evaluate_episode( + env, + runner: PiPolicyRunner, + max_steps: int, + episode: int, + output_dir: str, + randomization_manager=None, + demo_idx: int = 0, + init_states=None, +) -> Dict[str, Any]: + """Evaluate a single episode.""" + + # Apply domain randomization before reset + if randomization_manager is not None: + randomization_manager.apply_randomization(demo_idx=demo_idx, is_initial=(episode == 1)) + randomization_manager.update_positions_to_table(demo_idx=demo_idx, env_id=0) + randomization_manager.update_camera_look_at(env_id=0) + randomization_manager.apply_camera_randomization() + + # Reset environment + if randomization_manager is not None and init_states is not None: + from roboverse_learn.il.act.act_eval_runner import ensure_clean_state + # Use task_env.reset() directly to pass states parameter + obs, info = env.task_env.reset(states=[init_states[demo_idx]]) + ensure_clean_state(env.task_env.handler, expected_state=init_states[demo_idx]) + if hasattr(env, "_episode_steps"): + env._episode_steps[0] = 0 + else: + obs, info = env.reset() + runner.reset() stats: Dict[str, Any] = { @@ -249,6 +280,27 @@ def parse_args() -> argparse.Namespace: help="Threshold on finger joint values to treat the gripper as open") parser.add_argument("--actions-per-call", type=int, default=0, help="Number of cached actions to use before requesting a new chunk (0 = consume entire chunk)") + # Domain Randomization options + parser.add_argument( + "--level", + type=int, + default=0, + choices=[0, 1, 2, 3], + help="Randomization level: 0=None, 1=Scene+Material, 2=+Light, 3=+Camera" + ) + parser.add_argument( + "--scene_mode", + type=int, + default=0, + choices=[0, 1, 2, 3], + help="Scene mode: 0=Manual, 1=USD Table, 2=USD Scene, 3=Full USD" + ) + parser.add_argument( + "--randomization_seed", + type=int, + default=None, + help="Seed for reproducible randomization. If None, uses random seed" + ) return parser.parse_args() @@ -259,25 +311,77 @@ def main() -> bool: print("CUDA not available, falling back to CPU") args.device = "cpu" + print(f"Pi Evaluation") + print(f" Task: {args.task}") + print(f" Robot: {args.robot}") + print(f" Simulator: {args.sim}") + print(f" DR Level: {args.level}, Scene Mode: {args.scene_mode}, Seed: {args.randomization_seed}") + torch.manual_seed(args.seed) np.random.seed(args.seed) if torch.cuda.is_available(): torch.cuda.manual_seed_all(args.seed) + # Camera configuration + if args.task in {"stack_cube", "pick_cube", "pick_butter"}: + dp_camera = True + else: + dp_camera = args.task != "close_box" + + is_libero_dataset = "libero_90" in args.task + + if is_libero_dataset: + dp_pos = (2.0, 0.0, 2) + elif dp_camera: + dp_pos = (1.0, 0.0, 0.75) + else: + dp_pos = (1.5, 0.0, 1.5) + + camera = PinholeCameraCfg( + name="camera", + data_types=["rgb"], + width=256, + height=256, + pos=dp_pos, + look_at=(0.0, 0.0, 0.0), + ) + + # Lighting setup + render_mode = "raytracing" + ceiling_main = 12000.0 + ceiling_corners = 5000.0 + + lights = [ + DiskLightCfg( + name="ceiling_main", + intensity=ceiling_main, + color=(1.0, 1.0, 1.0), + radius=1.2, + pos=(0.0, 0.0, 2.8), + rot=(0.7071, 0.0, 0.0, 0.7071), + ), + SphereLightCfg( + name="ceiling_ne", intensity=ceiling_corners, color=(1.0, 1.0, 1.0), radius=0.6, pos=(1.0, 1.0, 2.5) + ), + SphereLightCfg( + name="ceiling_nw", intensity=ceiling_corners, color=(1.0, 1.0, 1.0), radius=0.6, pos=(-1.0, 1.0, 2.5) + ), + SphereLightCfg( + name="ceiling_sw", intensity=ceiling_corners, color=(1.0, 1.0, 1.0), radius=0.6, pos=(-1.0, -1.0, 2.5) + ), + SphereLightCfg( + name="ceiling_se", intensity=ceiling_corners, color=(1.0, 1.0, 1.0), radius=0.6, pos=(1.0, -1.0, 2.5) + ), + ] + env = make_vec( f"RoboVerse/{args.task}", num_envs=args.num_envs, robots=[args.robot], simulator=args.sim, headless=True, - cameras=[PinholeCameraCfg( - name="camera", - data_types=["rgb"], - width=256, - height=256, - pos=(1.5, 0.0, 1.5), - look_at=(0.0, 0.0, 0.0), - )], + cameras=[camera], + lights=lights, device=args.device, ) @@ -295,6 +399,32 @@ def main() -> bool: device=args.device, ) + # Load trajectories for DR + traj_filepath = env.task_env.traj_filepath + robot_obj = get_robot(args.robot) + init_states, _, _ = get_traj(traj_filepath, robot_obj, env.task_env.handler) + + # Initialize Domain Randomization + randomization_manager = None + if args.level > 0: + from dataclasses import dataclass as dc + @dc + class SimpleRenderCfg: + mode: str = render_mode + + randomization_manager = DomainRandomizationManager( + config=DRConfig( + level=args.level, + scene_mode=args.scene_mode, + randomization_seed=args.randomization_seed, + ), + scenario=env.scenario, + handler=env.task_env.handler, + init_states=init_states, + render_cfg=SimpleRenderCfg(mode=render_mode) + ) + print(f"Domain Randomization enabled: level={args.level}, scene_mode={args.scene_mode}, seed={args.randomization_seed}") + start_time = time.time() aggregate = { "total_episodes": 0, @@ -304,26 +434,54 @@ def main() -> bool: } for ep in range(args.num_episodes): + print(f"\n{'=' * 50}") print(f"Episode {ep + 1}/{args.num_episodes}") - result = evaluate_episode(env, runner, args.max_steps, ep + 1, args.output_dir) + print(f"{'=' * 50}") + + demo_idx = ep % len(init_states) if randomization_manager is not None else 0 + + result = evaluate_episode( + env, runner, args.max_steps, ep + 1, args.output_dir, + randomization_manager=randomization_manager, + demo_idx=demo_idx, + init_states=init_states if randomization_manager is not None else None + ) + aggregate["total_episodes"] += 1 aggregate["episode_results"].append(result) aggregate["total_rewards"].append(result["total_reward"]) if result["success"]: aggregate["total_successes"] += 1 + sr = aggregate["total_successes"] / aggregate["total_episodes"] - print(f" Success rate so far: {sr:.1%}") + print(f" Steps: {result['steps']}") + print(f" Success: {result['success']}") + print(f" Reward: {result['total_reward']:.2f}") + print(f" Success rate: {sr:.1%}") total_time = time.time() - start_time final_sr = aggregate["total_successes"] / max(1, aggregate["total_episodes"]) final_reward = float(np.mean(aggregate["total_rewards"])) if aggregate["total_rewards"] else 0.0 - print(f"\nEvaluation finished: success rate {final_sr:.1%}, avg reward {final_reward:.2f}, elapsed {total_time:.1f}s") + + print(f"\n{'=' * 50}") + print("Evaluation Summary") + print(f"{'=' * 50}") + print(f"Success Rate: {final_sr:.1%} ({aggregate['total_successes']}/{aggregate['total_episodes']})") + print(f"Average Reward: {final_reward:.2f}") + print(f"Total Time: {total_time:.1f}s") + print(f"DR Level: {args.level}, Scene Mode: {args.scene_mode}, Seed: {args.randomization_seed}") + print(f"{'=' * 50}") os.makedirs(args.output_dir, exist_ok=True) ts = time.strftime("%Y%m%d_%H%M%S") report_path = os.path.join(args.output_dir, f"pi_eval_{args.task}_{ts}.json") with open(report_path, "w", encoding="utf-8") as f: - json.dump({"config": vars(args), "stats": aggregate, "timestamp": ts}, f, indent=2) + json.dump({ + "config": vars(args), + "stats": aggregate, + "timestamp": ts, + "dr_config": {"level": args.level, "scene_mode": args.scene_mode, "seed": args.randomization_seed} + }, f, indent=2) print(f"Saved results to {report_path}") try: From 37c1a32e0aa0cb2df56e34335140cdd7b460ccb9 Mon Sep 17 00:00:00 2001 From: Fisher Wang Date: Thu, 4 Dec 2025 21:21:06 -0800 Subject: [PATCH 11/50] Add pull_request trigger for auto_merge_enabled --- .github/workflows/premerge-ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/premerge-ci.yml b/.github/workflows/premerge-ci.yml index 757bfb684..b2951e4cf 100644 --- a/.github/workflows/premerge-ci.yml +++ b/.github/workflows/premerge-ci.yml @@ -4,6 +4,12 @@ on: workflow_dispatch: merge_group: types: [checks_requested] + pull_request: + types: + - auto_merge_enabled + branches: + - main + - develop env: REGION: us-west-2 From caf5be9f1557b65df417983ea802e9854e853576 Mon Sep 17 00:00:00 2001 From: Mingyuan Sun <64095400+myuansun@users.noreply.github.com> Date: Fri, 5 Dec 2025 14:00:14 +0800 Subject: [PATCH 12/50] Migrate unitree_rl to roboverse_pack (#707) * Move unitree_rl from roboverse_learn to roboverse_pack BREAKING CHANGE: Module path changed from roboverse_learn.rl.unitree_rl to roboverse_pack.tasks.unitree_rl Files moved: - configs/ directory (all configuration files) - deploy/ directory (deployment scripts and configs) - helper/ directory (utility functions) Files updated with new import paths: - roboverse_pack/tasks/unitree_rl/base/*.py - roboverse_pack/tasks/unitree_rl/locomotion/*.py - roboverse_pack/tasks/unitree_rl/loco_manipulation/*.py - roboverse_pack/tasks/unitree_rl/configs/**/*.py - roboverse_pack/tasks/unitree_rl/deploy/**/*.py - roboverse_learn/rl/rsl_rl/eval.py - docs/source/roboverse_learn/reinforcement_learning/unitree_rl.md - metasim/sim/isaacgym/isaacgym.py Old directory removed: - roboverse_learn/rl/unitree_rl/ Migration consolidates all unitree_rl functionality under roboverse_pack.tasks for better organization. [refractor] unitree rl to humanoid [doc] update [update] doc name * add dr to collect demo * clean up code & create a manager * add DR to act eval * add DR to dp eval * [fix] move deploy to scripts to avoid import error --------- Co-authored-by: gxyes --- docs/source/roboverse_learn/index.md | 2 +- .../reinforcement_learning/humanoid.md | 369 ++++++++++++++++ .../reinforcement_learning/unitree_rl.md | 275 ------------ metasim/sim/isaacgym/isaacgym.py | 1 - roboverse_learn/rl/rsl_rl/env_wrapper.py | 2 +- roboverse_learn/rl/rsl_rl/eval.py | 32 +- roboverse_learn/rl/unitree_rl/__init__.py | 0 .../rl/unitree_rl/configs/__init__.py | 3 - .../rl/unitree_rl/configs/cfg_objects.py | 22 - .../loco_manipulation/catch_humanoid.py | 81 ---- .../configs/locomotion/walk_g1_dof12.py | 191 -------- .../configs/locomotion/walk_g1_dof29.py | 189 -------- .../rl/unitree_rl/helper/__init__.py | 1 - .../rl/unitree_rl/helper/terrain_utils.py | 408 ------------------ roboverse_pack/callback_funcs/__init__.py | 1 + .../callback_funcs/humanoid}/reset_funcs.py | 50 ++- .../callback_funcs/humanoid}/reward_funcs.py | 88 +--- .../callback_funcs/humanoid}/step_funcs.py | 51 +-- .../humanoid}/termination_funcs.py | 24 +- .../queries/lidar.py | 73 ++-- .../randomization/humanoid.py | 197 +++------ roboverse_pack/tasks/humanoid/__init__.py | 36 ++ .../{unitree_rl => humanoid}/base/__init__.py | 0 .../base/base_agent.py | 2 +- .../base/base_legged_robot.py | 6 +- .../{unitree_rl => humanoid}/base/types.py | 0 .../tasks/humanoid}/cfg_base.py | 46 +- .../tasks/humanoid/locomotion/__init__.py | 1 + .../locomotion/walk_g1_dof12.py | 159 ++++++- .../locomotion/walk_g1_dof29.py | 160 ++++++- roboverse_pack/tasks/unitree_rl/__init__.py | 29 -- .../loco_manipulation/catch_humanoid.py | 74 ---- .../utils}/curriculum_utils.py | 22 +- .../utils/humanoid_utils.py | 55 +-- scripts/unitree_deploy/__init__.py | 1 + scripts/unitree_deploy/common/__init__.py | 1 + .../unitree_deploy}/common/command_helper.py | 6 + .../common/remote_controller.py | 5 + .../unitree_deploy}/common/rotation_helper.py | 2 + .../unitree_deploy}/config.py | 3 + .../unitree_deploy}/configs/g1_dof12.yaml | 2 +- .../unitree_deploy}/configs/g1_dof29.yaml | 2 +- .../configs/g1_dof29_dex3.yaml | 2 +- .../unitree_deploy}/deploy_real.py | 59 ++- .../unitree_deploy}/utils.py | 2 + 45 files changed, 1056 insertions(+), 1679 deletions(-) create mode 100644 docs/source/roboverse_learn/reinforcement_learning/humanoid.md delete mode 100644 docs/source/roboverse_learn/reinforcement_learning/unitree_rl.md delete mode 100644 roboverse_learn/rl/unitree_rl/__init__.py delete mode 100644 roboverse_learn/rl/unitree_rl/configs/__init__.py delete mode 100644 roboverse_learn/rl/unitree_rl/configs/cfg_objects.py delete mode 100644 roboverse_learn/rl/unitree_rl/configs/loco_manipulation/catch_humanoid.py delete mode 100644 roboverse_learn/rl/unitree_rl/configs/locomotion/walk_g1_dof12.py delete mode 100644 roboverse_learn/rl/unitree_rl/configs/locomotion/walk_g1_dof29.py delete mode 100644 roboverse_learn/rl/unitree_rl/helper/__init__.py delete mode 100644 roboverse_learn/rl/unitree_rl/helper/terrain_utils.py create mode 100644 roboverse_pack/callback_funcs/__init__.py rename {roboverse_learn/rl/unitree_rl/configs/callback_funcs => roboverse_pack/callback_funcs/humanoid}/reset_funcs.py (83%) rename {roboverse_learn/rl/unitree_rl/configs/callback_funcs => roboverse_pack/callback_funcs/humanoid}/reward_funcs.py (82%) rename {roboverse_learn/rl/unitree_rl/configs/callback_funcs => roboverse_pack/callback_funcs/humanoid}/step_funcs.py (82%) rename {roboverse_learn/rl/unitree_rl/configs/callback_funcs => roboverse_pack/callback_funcs/humanoid}/termination_funcs.py (71%) rename roboverse_learn/rl/unitree_rl/configs/cfg_queries.py => roboverse_pack/queries/lidar.py (93%) rename roboverse_learn/rl/unitree_rl/configs/cfg_randomizers.py => roboverse_pack/randomization/humanoid.py (81%) create mode 100644 roboverse_pack/tasks/humanoid/__init__.py rename roboverse_pack/tasks/{unitree_rl => humanoid}/base/__init__.py (100%) rename roboverse_pack/tasks/{unitree_rl => humanoid}/base/base_agent.py (98%) rename roboverse_pack/tasks/{unitree_rl => humanoid}/base/base_legged_robot.py (99%) rename roboverse_pack/tasks/{unitree_rl => humanoid}/base/types.py (100%) rename {roboverse_learn/rl/unitree_rl/configs => roboverse_pack/tasks/humanoid}/cfg_base.py (77%) create mode 100644 roboverse_pack/tasks/humanoid/locomotion/__init__.py rename roboverse_pack/tasks/{unitree_rl => humanoid}/locomotion/walk_g1_dof12.py (55%) rename roboverse_pack/tasks/{unitree_rl => humanoid}/locomotion/walk_g1_dof29.py (51%) delete mode 100644 roboverse_pack/tasks/unitree_rl/__init__.py delete mode 100644 roboverse_pack/tasks/unitree_rl/loco_manipulation/catch_humanoid.py rename {roboverse_learn/rl/unitree_rl/helper => roboverse_pack/utils}/curriculum_utils.py (84%) rename roboverse_learn/rl/unitree_rl/helper/utils.py => roboverse_pack/utils/humanoid_utils.py (65%) create mode 100644 scripts/unitree_deploy/__init__.py create mode 100644 scripts/unitree_deploy/common/__init__.py rename {roboverse_learn/rl/unitree_rl/deploy => scripts/unitree_deploy}/common/command_helper.py (85%) rename {roboverse_learn/rl/unitree_rl/deploy => scripts/unitree_deploy}/common/remote_controller.py (81%) rename {roboverse_learn/rl/unitree_rl/deploy => scripts/unitree_deploy}/common/rotation_helper.py (84%) rename {roboverse_learn/rl/unitree_rl/deploy => scripts/unitree_deploy}/config.py (98%) rename {roboverse_learn/rl/unitree_rl/deploy => scripts/unitree_deploy}/configs/g1_dof12.yaml (89%) rename {roboverse_learn/rl/unitree_rl/deploy => scripts/unitree_deploy}/configs/g1_dof29.yaml (93%) rename {roboverse_learn/rl/unitree_rl/deploy => scripts/unitree_deploy}/configs/g1_dof29_dex3.yaml (95%) rename {roboverse_learn/rl/unitree_rl/deploy => scripts/unitree_deploy}/deploy_real.py (87%) rename {roboverse_learn/rl/unitree_rl/deploy => scripts/unitree_deploy}/utils.py (99%) diff --git a/docs/source/roboverse_learn/index.md b/docs/source/roboverse_learn/index.md index 8ecc0c6d4..f481fa0f1 100644 --- a/docs/source/roboverse_learn/index.md +++ b/docs/source/roboverse_learn/index.md @@ -22,5 +22,5 @@ reinforcement_learning/fast_td3.md reinforcement_learning/sac.md reinforcement_learning/td3.md reinforcement_learning/skillblender_rl.md -reinforcement_learning/unitree_rl.md +reinforcement_learning/humanoid.md ``` diff --git a/docs/source/roboverse_learn/reinforcement_learning/humanoid.md b/docs/source/roboverse_learn/reinforcement_learning/humanoid.md new file mode 100644 index 000000000..171638cc3 --- /dev/null +++ b/docs/source/roboverse_learn/reinforcement_learning/humanoid.md @@ -0,0 +1,369 @@ +# Humanoid Locomotion + +Train and deploy locomotion policies for humanoid robots across three stages: +- Training in IsaacGym, IsaacSim, MuJoCo +- Sim2Sim evaluation across multiple simulators +- Real-world deployment (networked controller) + +Supported robots: `g1_dof29` (full-body without hands) and `g1_dof12` (lower-body). + + +## Environment Setup + +### Core Dependencies + +#### For Python > 3.8 (IsaacSim/Mujoco) + +You can install rsl-rl-lib directly via pip: +```bash +pip install rsl-rl-lib +``` + +#### For Python 3.8 (IsaacGym) +Due to compatibility requirements with IsaacGym's Python 3.8 environment, you'll need to install from source with modified dependencies: +```bash +# Clone the repository and checkout v3.1.0 +git clone https://github.com/leggedrobotics/rsl_rl && \ +cd rsl_rl && \ +git checkout v3.1.0 + +# Apply compatibility patches +sed -i 's/"torch>=2\.6\.0"/"torch>=2.4.1"/' pyproject.toml && \ +sed -i 's/"torchvision>=0\.5\.0"/"torchvision>=0.19.1"/' pyproject.toml && \ +sed -i 's/"tensordict>=0\.7\.0"/"tensordict>=0.5.0"/' pyproject.toml && \ +sed -i '/^# SPDX-License-Identifier: BSD-3-Clause$/a from __future__ import annotations' rsl_rl/algorithms/distillation.py + +# Install in editable mode +pip install -e . +``` + +### Optional Dependencies + +#### LiDAR Sensor Support (OmniPerception) + +The LiDAR implementation uses the [OmniPerception](https://github.com/aCodeDog/OmniPerception) package for GPU-accelerated ray tracing. + +**For IsaacGym and MuJoCo:** + +Install the LidarSensor package: +```bash +cd /path/to/OmniPerception/LidarSensor +pip install -e . +``` + +For complete IsaacGym/MuJoCo integration details, see the [OmniPerception IsaacGym example](https://github.com/aCodeDog/OmniPerception/tree/main/LidarSensor/LidarSensor/example/isaacgym). + +**For IsaacSim (IsaacLab):** + +First you need to install IsaacLab from source. Follow the official [IsaacLab installation guide](https://isaac-sim.github.io/IsaacLab/main/source/setup/installation/index.html). + +Then install the LiDAR sensor extension: + +```bash +cd /path/to/OmniPerception/LidarSensor/LidarSensor/example/isaaclab +./install_lidar_sensor.sh /path/to/your/IsaacLab +``` + +For complete IsaacSim integration details, see the [OmniPerception IsaacLab example](https://github.com/aCodeDog/OmniPerception/tree/main/LidarSensor/LidarSensor/example/isaaclab/isaaclab). + +#### Real-World Deployment (unitree_sdk2_python) + +For real-world deployment, install the `unitree_sdk2_python` package: +```bash +cd third_party +git clone https://github.com/unitreerobotics/unitree_sdk2_python.git +cd unitree_sdk2_python +pip install -e . +``` + + +## Training + +### RSL-RL PPO Training + +General form: +```bash +python roboverse_learn/rl/rsl_rl/ppo.py \ + --task \ + --sim \ + --num-envs \ + --robot +``` + +Examples: +- G1 humanoid walking (IsaacGym): +```bash +python roboverse_learn/rl/rsl_rl/ppo.py \ + --task walk_g1_dof29 \ + --sim isaacgym \ + --num-envs 8192 \ + --robot g1_dof29 +``` + +- G1 DOF12 walking (IsaacGym): +```bash +python roboverse_learn/rl/rsl_rl/ppo.py \ + --task walk_g1_dof12 \ + --sim isaacgym \ + --num-envs 8192 \ + --robot g1_dof12 +``` + +- G1 humanoid walking (IsaacSim): +```bash +python roboverse_learn/rl/rsl_rl/ppo.py \ + --task walk_g1_dof29 \ + --sim isaacsim \ + --num-envs 4096 \ + --robot g1_dof29 +``` + +### Resuming Training + +To resume training from a checkpoint: +```bash +python roboverse_learn/rl/rsl_rl/ppo.py \ + --task walk_g1_dof29 \ + --sim isaacgym \ + --num-envs 8192 \ + --robot g1_dof29 \ + --resume \ + --checkpoint -1 +``` +The `--checkpoint -1` flag loads the latest checkpoint. You can specify a specific iteration number instead. + +### Other RL Algorithms + +The framework supports other RL algorithms in `roboverse_learn/rl/`: +- **CleanRL implementations**: PPO, SAC, TD3 + - Training: `python roboverse_learn/rl/clean_rl/{ppo,sac,td3}.py` +- **Stable-Baselines3**: Available in `roboverse_learn/rl/sb3/` + +Refer to the respective training scripts for algorithm-specific parameters. + +### Output Directory Structure + +Outputs and checkpoints are saved to: +``` +outputs//// +``` +For example: +``` +outputs/g1_dof29/walk_g1_dof29/2025_1203_150000/ +``` + +Each training run contains: +- `model_.pt` - Checkpoint files saved at specified intervals +- `policy.pt` - Final exported JIT policy (for deployment) +- Training logs (TensorBoard/WandB depending on configuration) + + +## Evaluation + +Evaluate trained policies across different simulators using the evaluation script. + +General form: +```bash +python roboverse_learn/rl/rsl_rl/eval.py \ + --task \ + --sim \ + --num-envs \ + --robot \ + --resume \ + --checkpoint +``` + +Examples: + +IsaacGym evaluation: +```bash +python roboverse_learn/rl/rsl_rl/eval.py \ + --task walk_g1_dof29 \ + --sim isaacgym \ + --num-envs 1 \ + --robot g1_dof29 \ + --resume 2025_1203_150000 \ + --checkpoint -1 +``` + +MuJoCo evaluation: +```bash +python roboverse_learn/rl/rsl_rl/eval.py \ + --task walk_g1_dof12 \ + --sim mujoco \ + --num-envs 1 \ + --robot g1_dof12 \ + --resume 2025_1203_150000 \ + --checkpoint -1 +``` + +IsaacSim evaluation: +```bash +python roboverse_learn/rl/rsl_rl/eval.py \ + --task walk_g1_dof29 \ + --sim isaacsim \ + --num-envs 1 \ + --robot g1_dof29 \ + --resume 2025_1203_150000 \ + --checkpoint -1 +``` + +The evaluation script runs for 1,000,000 steps with fixed velocity commands for thorough policy assessment. + + +## Real-World Deployment + +Real-world deployment entry point: +```bash +python roboverse_pack/tasks/humanoid/unitree_deploy/deploy_real.py +``` + +Example: +```bash +python roboverse_pack/tasks/humanoid/unitree_deploy/deploy_real.py eno1 g1_dof29_dex3.yaml +``` + +Configuration files are located in `roboverse_pack/tasks/humanoid/unitree_deploy/configs/`: +- `g1_dof12.yaml` - 12 DOF lower-body control +- `g1_dof29.yaml` - 29 DOF full-body control +- `g1_dof29_dex3.yaml` - 43 DOF with DeX3 hands + +In the YAML file, set the `policy_path` to your exported JIT policy (the `policy.pt` file from training output). + +This will initialize the real controller and stream commands to the robot. Ensure your networking and safety interlocks are correctly configured. + + +## Advanced Features + +### Terrain Configuration + +The framework supports customizable terrain generation for training locomotion policies on varied ground conditions. Predefined terrain configurations are available in `roboverse_pack/grounds/`. + +#### Supported Terrain Types + +- **Slope**: Planar inclined surfaces (`slope_cfg.py`) +- **Stair**: Staircase features (`stair_cfg.py`) +- **Obstacle**: Random rectangular obstacle fields (`obstacle_cfg.py`) +- **Stone**: Stone-like protrusions (`stone_cfg.py`) +- **Gap**: Gaps that robots must traverse (`gap_cfg.py`) +- **Pit**: Rectangular pits (`pit_cfg.py`) + +#### Example Usage + +Terrain configurations are imported and used in task definitions. See task files in `roboverse_pack/tasks/humanoid/locomotion/` for examples. + +```python +from metasim.scenario.grounds import GroundCfg, SlopeCfg, StairCfg + +ground_cfg = GroundCfg( + width=20.0, + length=20.0, + horizontal_scale=0.1, + vertical_scale=0.005, + static_friction=1.0, + dynamic_friction=1.0, + elements={ + "slope": [SlopeCfg(origin=[0, 0], size=[2.0, 2.0], slope=0.3)], + "stair": [StairCfg(origin=[5, 0], size=[2.0, 2.0], step_height=0.1)] + } +) +``` + +Terrain configuration is supported across all simulators (IsaacGym, IsaacSim, MuJoCo). + +### LiDAR Sensor Support + +The framework includes LiDAR point cloud sensing capabilities for enhanced perception-based locomotion policies. + +#### Overview + +The `LidarPointCloud` query provides 3D point cloud data from a simulated LiDAR sensor: +- Supports IsaacGym, IsaacSim, and MuJoCo simulators +- Returns point clouds in both local (sensor frame) and world frames +- Configurable sensor mounting location and type +- Raycasts against terrain, ground, and scenario objects + +#### Configuration + +LiDAR configuration is defined in task files. See `roboverse_pack/tasks/humanoid/locomotion/walk_g1_dof29.py` for examples. + +#### Sensor Parameters + +- `link_name` (str): Name of the robot link where LiDAR is mounted (default: "mid360_link") +- `sensor_type` (str): Type of LiDAR sensor pattern (default: "mid360") +- `apply_optical_center_offset` (bool): Apply optical center offset correction +- `optical_center_offset_z` (float): Z-axis offset for optical center +- `enabled` (bool): Enable/disable LiDAR query + +#### Output Format + +The query returns a dictionary with: +- `points_local`: Point cloud in sensor frame (E, N, 3) +- `points_world`: Point cloud in world frame (E, N, 3) +- `dist`: Distance measurements (when available) +- `link`: Name of the link the sensor is attached to + +where E is the number of environments and N is the number of points per scan. + + +## Command-line Arguments + +### Common Training/Evaluation Arguments + +- `--task` (str): Task name. Examples: `walk_g1_dof29`, `walk_g1_dof12` +- `--robot` (str): Robot identifier. Examples: `g1_dof29`, `g1_dof12` +- `--num-envs` (int): Number of parallel environments +- `--sim` (str): Simulator. Supported: `isaacgym`, `isaacsim`, `mujoco` +- `--headless` (bool): Headless rendering (default: False) +- `--device` (str): Device for training (default: "cuda:0") +- `--seed` (int): Random seed (default: 1) + +### Training-Specific Arguments (RSL-RL PPO) + +- `--max-iterations` (int): Number of training iterations (default: 50000) +- `--save-interval` (int): Checkpoint save interval (default: 100) +- `--logger` (str): Logger type. Options: `tensorboard`, `wandb`, `neptune` (default: "tensorboard") +- `--wandb-project` (str): WandB project name (default: "rsl_rl_ppo") +- `--use-wandb` (bool): Enable WandB logging (default: False) + +### Evaluation/Checkpoint Arguments + +- `--resume` (str): Timestamp directory containing checkpoints +- `--checkpoint` (int): Checkpoint iteration to load. `-1` loads the latest (default: -1) + +### Examples + +Training with custom parameters: +```bash +python roboverse_learn/rl/rsl_rl/ppo.py \ + --task walk_g1_dof29 \ + --robot g1_dof29 \ + --sim isaacgym \ + --num-envs 8192 \ + --max-iterations 30000 \ + --save-interval 200 \ + --logger wandb \ + --use-wandb +``` + +Resume from checkpoint: +```bash +python roboverse_learn/rl/rsl_rl/ppo.py \ + --task walk_g1_dof29 \ + --robot g1_dof29 \ + --sim isaacgym \ + --num-envs 8192 \ + --resume 2025_1203_150000 \ + --checkpoint 5000 +``` + +Evaluation with specific checkpoint: +```bash +python roboverse_learn/rl/rsl_rl/eval.py \ + --task walk_g1_dof29 \ + --robot g1_dof29 \ + --sim mujoco \ + --num-envs 1 \ + --resume 2025_1203_150000 \ + --checkpoint 10000 +``` \ No newline at end of file diff --git a/docs/source/roboverse_learn/reinforcement_learning/unitree_rl.md b/docs/source/roboverse_learn/reinforcement_learning/unitree_rl.md deleted file mode 100644 index 264ef068d..000000000 --- a/docs/source/roboverse_learn/reinforcement_learning/unitree_rl.md +++ /dev/null @@ -1,275 +0,0 @@ -# Unitree RL - -Train and deploy locomotion policies for Unitree robots across three stages: -- Training in IsaacGym, IsaacSim -- Sim2Sim evaluation in IsaacGym, IsaacSim, MuJoCo -- Real-world deployment (networked controller) - -Well Supported robots: `g1_dof29` (full-body with without hands) and `g1_dof12` (lower-body). - - -## Environment Setup - -### Core Dependencies - -#### For Python > 3.8 (IsaacSim/Mujoco) - -You can install rsl-rl-lib directly via pip: -```bash -pip install rsl-rl-lib -``` - -#### For Python 3.8 (IsaacGym) -Due to compatibility requirements with IsaacGym's Python 3.8 environment, you'll need to install from source with modified dependencies: -```bash -# Clone the repository and checkout v3.1.0 -git clone https://github.com/leggedrobotics/rsl_rl && \ -cd rsl_rl && \ -git checkout v3.1.0 - -# Apply compatibility patches -sed -i 's/"torch>=2\.6\.0"/"torch>=2.4.1"/' pyproject.toml && \ -sed -i 's/"torchvision>=0\.5\.0"/"torchvision>=0.19.1"/' pyproject.toml && \ -sed -i 's/"tensordict>=0\.7\.0"/"tensordict>=0.5.0"/' pyproject.toml && \ -sed -i '/^# SPDX-License-Identifier: BSD-3-Clause$/a from __future__ import annotations' rsl_rl/algorithms/distillation.py - -# Install in editable mode -pip install -e . -``` - -### Optional Dependencies - -#### LiDAR Sensor Support (OmniPerception) - -The LiDAR implementation uses the [OmniPerception](https://github.com/aCodeDog/OmniPerception) package for GPU-accelerated ray tracing. - -**For IsaacGym and MuJoCo:** - -Install the LidarSensor package: -```bash -cd /path/to/OmniPerception/LidarSensor -pip install -e . -``` - -For complete IsaacGym/MuJoCo integration details, see the [OmniPerception IsaacGym example](https://github.com/aCodeDog/OmniPerception/tree/main/LidarSensor/LidarSensor/example/isaacgym). - -**For IsaacSim (IsaacLab):** - -First you need to install IsaacLab from source. Follow the official [IsaacLab installation guide](https://isaac-sim.github.io/IsaacLab/main/source/setup/installation/index.html). - -Then install the LiDAR sensor extension: - -```bash -cd /path/to/OmniPerception/LidarSensor/LidarSensor/example/isaaclab -./install_lidar_sensor.sh /path/to/your/IsaacLab -``` - -For complete IsaacSim integration details, see the [OmniPerception IsaacLab example](https://github.com/aCodeDog/OmniPerception/tree/main/LidarSensor/LidarSensor/example/isaaclab/isaaclab). - -#### Real-World Deployment (unitree_sdk2_python) - -For real-world deployment, install the `unitree_sdk2_python` package: -```bash -cd third_party -git clone https://github.com/unitreerobotics/unitree_sdk2_python.git -cd unitree_sdk2_python -pip install -e . -``` - - -## Training - -General form: -``` -python roboverse_learn/rl/unitree_rl/main.py \ - --task \ - --sim isaacgym \ - --num_envs 8192 \ - --robot -``` - -Examples: -- G1 humanoid walking (IsaacSim): -``` -python roboverse_learn/rl/unitree_rl/main.py --task walk_g1_dof29 --sim isaacsim --num_envs 8192 --robot g1_dof29 -``` -- G1Dof12 walking (IsaacGym): -``` -python roboverse_learn/rl/unitree_rl/main.py --task walk_g1_dof12 --sim isaacgym --num_envs 8192 --robot g1_dof12 -``` -- G1 humanoid walking with terrain (slope): -``` -python roboverse_learn/rl/unitree_rl/main.py --task walk_g1_dof29 --sim isaacgym --num_envs 8192 --robot g1_dof29 --ground slope_cfg -``` - -Outputs and checkpoints are saved to: -``` -outputs/unitree_rl/_// -``` -Each checkpoint is named `model_.pt`. - -## Evaluation / Play - -You can evaluate trained policies in both MuJoCo, Isaacsim and IsaacGym. In evaluation, `main.py` also exports the jit version policy to the directory `outputs/unitree_rl/_//exported/model_exported_jit.pt`, which can be further used for real-world deployment. - -IsaacGym evaluation: -``` -python roboverse_learn/rl/unitree_rl/main.py \ - --task walk_g1_dof29 \ - --sim isaacgym \ - --num_envs 1 \ - --robot g1_dof29 \ - --resume \ - --checkpoint \ - --eval -``` - -MuJoCo evaluation (e.g., DOF12 with public policy): -``` -python roboverse_learn/rl/unitree_rl/main.py \ - --checkpoint \ - --task walk_g1_dof12 \ - --sim mujoco \ - --robot g1_dof12 \ - --resume \ - --eval -``` -the `--resume` and `--checkpoint` option can also be used during training for checkpoint resume. - -## Real-World Deployment - -Real-world deployment entry point: -``` -python roboverse_learn/rl/unitree_rl/deploy/deploy_real.py -``` -Example: -``` -python roboverse_learn/rl/unitree_rl/deploy/deploy_real.py eno1 g1_dof29_dex3.yaml -``` -where you should modify the corresponding `yaml` file in `roboverse_learn/rl/unitree_rl/deploy/configs`, setting the `policy_path` to the exported jit policy. -This will initialize the real controller and stream commands to the robot. Ensure your networking and safety interlocks are correctly configured. - -## Advanced Features - -### Terrain Configuration - -The framework now supports customizable terrain generation for training locomotion policies on varied ground conditions. Terrain can be configured via the `ground` parameter in your scenario configuration. - -#### Supported Terrain Types - -The `GroundCfg` class supports multiple terrain primitives: -- **Slope**: Planar inclined surfaces -- **Stair**: Staircase features -- **Obstacle**: Random rectangular obstacle fields -- **Stone**: Stone-like protrusions -- **Gap**: Gaps that robots must traverse -- **Pit**: Rectangular pits - -#### Terrain Parameters - -Key configuration parameters: -- `width`, `length`: Terrain dimensions in meters -- `horizontal_scale`, `vertical_scale`: Resolution and height scaling -- `margin`: Border margin around terrain -- `static_friction`, `dynamic_friction`, `restitution`: Physics material properties -- `elements`: Dictionary of terrain primitives to include -- `difficulty`: Difficulty progression settings - -#### Example Usage - -```python -from metasim.scenario.grounds import GroundCfg, SlopeCfg, StairCfg - -ground_cfg = GroundCfg( - width=20.0, - length=20.0, - horizontal_scale=0.1, - vertical_scale=0.005, - static_friction=1.0, - dynamic_friction=1.0, - elements={ - "slope": [SlopeCfg(origin=[0, 0], size=[2.0, 2.0], slope=0.3)], - "stair": [StairCfg(origin=[5, 0], size=[2.0, 2.0], step_height=0.1)] - } -) -``` - -Terrain configuration is supported across all simulators (IsaacGym, IsaacSim, MuJoCo). - -### LiDAR Sensor Support - -The framework now includes LiDAR point cloud sensing capabilities for enhanced perception-based locomotion policies. - -#### Overview - -The `LidarPointCloud` query provides 3D point cloud data from a simulated LiDAR sensor: -- Supports IsaacGym, IsaacSim, and MuJoCo simulators -- Returns point clouds in both local (sensor frame) and world frames -- Configurable sensor mounting location and type -- Raycasts against terrain, ground, and scenario objects - -#### Configuration - -Add LiDAR to your environment via the `callbacks_query` parameter: - -```python -from roboverse_learn.rl.unitree_rl.configs.cfg_queries import LidarPointCloud - -callbacks_query = { - "lidar_point_cloud": LidarPointCloud( - link_name="mid360_link", # Robot link to attach sensor - sensor_type="mid360", # Sensor type (e.g., mid360, mid70) - apply_optical_center_offset=True, - optical_center_offset_z=0.03503, - enabled=True - ) -} -``` - -#### Sensor Parameters - -- `link_name` (str): Name of the robot link where LiDAR is mounted (default: "mid360_link") -- `sensor_type` (str): Type of LiDAR sensor pattern (default: "mid360") -- `apply_optical_center_offset` (bool): Apply optical center offset correction -- `optical_center_offset_z` (float): Z-axis offset for optical center -- `enabled` (bool): Enable/disable LiDAR query - -#### Output Format - -The query returns a dictionary with: -- `points_local`: Point cloud in sensor frame (E, N, 3) -- `points_world`: Point cloud in world frame (E, N, 3) -- `dist`: Distance measurements (when available) -- `link`: Name of the link the sensor is attached to - -where E is the number of environments and N is the number of points per scan. - -#### Example Task Configuration - -See [walk_g1_dof29.py](roboverse_learn/rl/unitree_rl/configs/locomotion/walk_g1_dof29.py) for a complete example: - -```python -callbacks_query = { - "contact_forces": ContactForces(history_length=3), - "lidar_point_cloud": LidarPointCloud(enabled=True) -} -``` - -## Command-line Arguments - -The most relevant flags (see `helper/utils.py`): -- `--task` (str): Task name. CamelCase or snake_case accepted. Examples: `walk_g1_dof29`, `walk_g1_dof12`. -- `--robot` (str): Robot identifier. Common: `g1_dof29`, `g1_dof12`. -- `--num_envs` (int): Number of parallel environments. -- `--sim` (str): Simulator. Supported: `isaacgym` (training), `mujoco` (evaluation). -- `--ground` (str): Ground/terrain configuration to load. References predefined configurations in `roboverse_pack/grounds/`. Examples: `slope_cfg`, `stair_cfg`, `obstacle_cfg`, `stone_cfg`, `gap_cfg`, `pit_cfg`. If not specified, uses default flat ground. -- `--run_name` (str): Required run tag for training logs/checkpoints. -- `--learning_iterations` (int): Number of learning iterations (default 15000). -- `--resume` (flag): Resume training from a checkpoint dir (datetime) in the specified run. -- `--checkpoint` (int): Which checkpoint to load. `-1` loads the latest. -- `--headless` (flag): Headless rendering (IsaacGym). -- `--jit_load` (flag): Load the jit policy. - -Notes: -- Checkpoints: `outputs/unitree_rl///model_.pt` -- Exported JIT model (when used): `outputs/unitree_rl///exported/model_exported_jit.pt` \ No newline at end of file diff --git a/metasim/sim/isaacgym/isaacgym.py b/metasim/sim/isaacgym/isaacgym.py index b3ea14bc8..d102858ea 100644 --- a/metasim/sim/isaacgym/isaacgym.py +++ b/metasim/sim/isaacgym/isaacgym.py @@ -31,7 +31,6 @@ # FIXME: fix this # from metasim.scenario.randomization import FrictionRandomCfg, MassRandomCfg # NOTE domain randomization for robots -# please refer to roboverse_learn.rl.unitree_rl.config.cfg_randomizer for material and mass randomization for isaacgym and isaacsim from metasim.scenario.scenario import ScenarioCfg from metasim.sim import BaseSimHandler from metasim.types import Action, DictEnvState diff --git a/roboverse_learn/rl/rsl_rl/env_wrapper.py b/roboverse_learn/rl/rsl_rl/env_wrapper.py index 319abc057..548d9bb72 100644 --- a/roboverse_learn/rl/rsl_rl/env_wrapper.py +++ b/roboverse_learn/rl/rsl_rl/env_wrapper.py @@ -2,7 +2,7 @@ from typing import Union import torch from tensordict import TensorDict -from roboverse_pack.tasks.unitree_rl.base import AgentTask +from roboverse_pack.tasks.humanoid.base import AgentTask class RslRlEnvWrapper: diff --git a/roboverse_learn/rl/rsl_rl/eval.py b/roboverse_learn/rl/rsl_rl/eval.py index 8fe00e339..3d9a34402 100644 --- a/roboverse_learn/rl/rsl_rl/eval.py +++ b/roboverse_learn/rl/rsl_rl/eval.py @@ -12,14 +12,40 @@ import rootutils import torch import tyro +import datetime +from loguru import logger as log rootutils.setup_root(__file__, pythonpath=True) from roboverse_learn.rl.configs.rsl_rl.ppo import RslRlPPOConfig from roboverse_learn.rl.rsl_rl.env_wrapper import RslRlEnvWrapper -from roboverse_learn.rl.unitree_rl.helper import get_load_path, get_log_dir from metasim.task.registry import get_task_class +def get_log_dir(robot_name: str, task_name: str, now=None) -> str: + """Get the log directory.""" + if now is None: + now = datetime.datetime.now().strftime("%Y_%m%d_%H%M%S") + log_dir = f"./outputs/{robot_name}/{task_name}/{now}" + if not os.path.exists(log_dir): + os.makedirs(log_dir, exist_ok=True) + log.info("Log directory: {}", log_dir) + return log_dir + + +def get_load_path(load_root: str, checkpoint: int | str = None) -> str: + """Get the path to load the model from.""" + if isinstance(checkpoint, int): + if checkpoint == -1: + models = [file for file in os.listdir(load_root) if "model" in file and file.endswith(".pt")] + models.sort(key=lambda m: f"{m!s:0>15}") + model = models[-1] + load_path = f"{load_root}/{model}" + else: + load_path = f"{load_root}/model_{checkpoint}.pt" + else: + load_path = f"{load_root}/{checkpoint}.pt" + log.info(f"Loading checkpoint {checkpoint} from {load_root}") + return load_path def make_roboverse_env(args: RslRlPPOConfig): """Create RoboVerse task environment""" @@ -57,11 +83,11 @@ def evaluate(args: RslRlPPOConfig): if not args.resume: raise ValueError("Please provide --resume (timestamp/log dir) for evaluation.") - # Convert resume string to full log directory path (legacy unitree_rl runner convention) + # Convert resume string to full log directory path log_dir = ( args.resume if os.path.isdir(args.resume) - else get_log_dir(task_name=args.task, now=args.resume) + else get_log_dir(robot_name=args.robot, task_name=args.task, now=args.resume) ) # Use get_load_path helper to handle checkpoint loading logic diff --git a/roboverse_learn/rl/unitree_rl/__init__.py b/roboverse_learn/rl/unitree_rl/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/roboverse_learn/rl/unitree_rl/configs/__init__.py b/roboverse_learn/rl/unitree_rl/configs/__init__.py deleted file mode 100644 index e556d2004..000000000 --- a/roboverse_learn/rl/unitree_rl/configs/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Task configs -from .locomotion.walk_g1_dof12 import WalkG1Dof12EnvCfg, WalkG1Dof12RslRlTrainCfg -from .locomotion.walk_g1_dof29 import WalkG1Dof29EnvCfg, WalkG1Dof29EnvRslRlTrainCfg diff --git a/roboverse_learn/rl/unitree_rl/configs/cfg_objects.py b/roboverse_learn/rl/unitree_rl/configs/cfg_objects.py deleted file mode 100644 index 994d928ad..000000000 --- a/roboverse_learn/rl/unitree_rl/configs/cfg_objects.py +++ /dev/null @@ -1,22 +0,0 @@ -from metasim.constants import PhysicStateType -from metasim.scenario.objects import PrimitiveSphereCfg - - -class BallCfg(PrimitiveSphereCfg): - def __init__(self): - super().__init__( - name="ball", - radius=0.05, - color=[1.0, 0.0, 1.0], - physics=PhysicStateType.RIGIDBODY, - ) - self.mass = 1.0 - self.enabled_gravity = True - self.collision_enabled = True - - self.init_position = [2.0, 0.0, 1.0] - self.init_rotation = [1.0, 0.0, 0.0, 0.0] # w, x, y, z - self.init_velocity = [-10.0, 0.0, 0.0] # vx, vy, vz - self.angular_velocity = [0.0, 0.0, 0.0] # wx, wy, wz - - self.root_state = [*self.init_position, *self.init_rotation, *self.init_velocity, *self.angular_velocity] # pos(3), rot(4), vel(3), ang_vel(3) diff --git a/roboverse_learn/rl/unitree_rl/configs/loco_manipulation/catch_humanoid.py b/roboverse_learn/rl/unitree_rl/configs/loco_manipulation/catch_humanoid.py deleted file mode 100644 index f7d29c3d4..000000000 --- a/roboverse_learn/rl/unitree_rl/configs/loco_manipulation/catch_humanoid.py +++ /dev/null @@ -1,81 +0,0 @@ -from typing import Callable -from metasim.utils import configclass -from roboverse_learn.rl.unitree_rl.configs.cfg_base import BaseEnvCfg -from roboverse_learn.rl.configs.rsl_rl.algorithm import ( - RslRlOnPolicyRunnerCfg, - RslRlPpoActorCriticCfg, - RslRlPpoAlgorithmCfg, -) - -@configclass -class CatchHumanoidTaskCfg(BaseEnvCfg): - """ - Environment configuration for humanoid Catching task. - """ - obs_len_history = 5 - priv_obs_len_history = 5 - control = BaseEnvCfg.Control(action_scale = 0.25) - noise = BaseEnvCfg.Noise(add_noise=True) # disable noise by default - normalization = BaseEnvCfg.Normalization( - obs_scales=BaseEnvCfg.Normalization.ObsScales( - lin_vel = 1.0, - ang_vel = 0.20, - dof_pos = 1.0, - dof_vel = 0.05, - # height_measurements = 5.0 - ) - ) - class rewards: - send_timeouts = True - only_positive_rewards = True # if true negative total rewards are clipped at zero (avoids early termination problems) - functions = "roboverse_learn.rl.unitree_rl.configs.cfg_reward_funcs" - class scales: - termination = -0.0 - tracking_lin_vel = 1.0 - tracking_ang_vel = 0.5 - lin_vel_z = -2.0 - ang_vel_xy = -0.05 - orientation = -0. - torques = -0.00001 - dof_vel = -0. - dof_acc = -2.5e-7 - base_height = -0. - feet_air_time = 1.0 - collision = -1. - feet_stumble = -0.0 - action_rate = -0.01 - stand_still = -0. - class InitialStates: - objects = {"ball": {"pos": [0.0, 0.0, 0.8]}} - robots = { - "g1_dof29_dex3": {"pos": [0.0, 0.0, 0.8]}, - } - initial_states = InitialStates() - -@configclass -class CatchHumanoidRslRlTrainCfg(RslRlOnPolicyRunnerCfg): - num_steps_per_env = 24 - max_iterations = 50000 - save_interval = 100 - experiment_name = "" # same as task name - empirical_normalization = False - policy = RslRlPpoActorCriticCfg( - init_noise_std=1.0, - actor_hidden_dims=[512, 256, 128], - critic_hidden_dims=[512, 256, 128], - activation="elu", - ) - algorithm = RslRlPpoAlgorithmCfg( - value_loss_coef=1.0, - use_clipped_value_loss=True, - clip_param=0.2, - entropy_coef=0.01, - num_learning_epochs=5, - num_mini_batches=4, - learning_rate=1.0e-3, - schedule="adaptive", - gamma=0.99, - lam=0.95, - desired_kl=0.01, - max_grad_norm=1.0, - ) diff --git a/roboverse_learn/rl/unitree_rl/configs/locomotion/walk_g1_dof12.py b/roboverse_learn/rl/unitree_rl/configs/locomotion/walk_g1_dof12.py deleted file mode 100644 index d18b83337..000000000 --- a/roboverse_learn/rl/unitree_rl/configs/locomotion/walk_g1_dof12.py +++ /dev/null @@ -1,191 +0,0 @@ -import math -from metasim.utils import configclass -from roboverse_learn.rl.unitree_rl.configs.cfg_base import BaseEnvCfg -from roboverse_learn.rl.configs.rsl_rl.algorithm import ( - RslRlOnPolicyRunnerCfg, - RslRlPpoAlgorithmCfg, - RslRlPpoActorCriticRecurrentCfg, -) -from roboverse_learn.rl.unitree_rl.helper.curriculum_utils import lin_vel_cmd_levels -from metasim.queries import ContactForces -from roboverse_learn.rl.unitree_rl.configs.cfg_randomizers import ( - MaterialRandomizer, - MassRandomizer, -) -from roboverse_learn.rl.unitree_rl.configs.callback_funcs import ( - termination_funcs, - reset_funcs, - step_funcs, - reward_funcs, -) - - -@configclass -class WalkG1Dof12EnvCfg(BaseEnvCfg): - episode_length_s = 20.0 - obs_len_history = 1 - priv_obs_len_history = 1 - - control = BaseEnvCfg.Control( - action_scale=0.25, action_clip=100, soft_joint_pos_limit_factor=0.9 - ) - - @configclass - class RewardsScales: - track_lin_vel_xy = (1.0, {"std": math.sqrt(0.25)}) - track_ang_vel_z = (0.5, {"std": math.sqrt(0.25)}) - lin_vel_z = -2.0 - ang_vel_xy = -0.05 - flat_orientation = -1.0 - base_height = (-10.0, {"target_height": 0.78}) - joint_acc = -2.5e-7 - joint_vel = -0.001 - action_rate = -0.01 - joint_pos_limits = -5.0 - is_alive = 0.15 - joint_deviation_legs = ( - -1.0, - {"joint_names": (".*_hip_roll_joint", ".*_hip_yaw_joint")}, - reward_funcs.joint_deviation_l1, - ) - feet_slide = (-0.2, {"body_names": (".*ankle_roll.*")}) - # feet_swing_height = -20.0 - feet_clearance = ( - 1.0, - { - "std": math.sqrt(0.05), - "tanh_mult": 2.0, - "target_height": 0.1, - "body_names": (".*ankle_roll.*"), - }, - ) - # contact = 0.18 - feet_gait = ( - 0.18, - { - "period": 0.8, - "offset": [0.0, 0.5], - "threshold": 0.55, - "body_names": (".*ankle_roll.*"), - }, - ) - energy = -1e-5 - ######################## - - rewards = BaseEnvCfg.Rewards(scales=RewardsScales(), only_positive_rewards=True) - - commands = BaseEnvCfg.Commands( - value=None, - resample=step_funcs.resample_commands, - heading_command=True, - resampling_time=10.0, - rel_standing_envs=0.02, - ranges=BaseEnvCfg.Commands.Ranges( - lin_vel_x=(-1.0, 1.0), - lin_vel_y=(-1.0, 1.0), - ang_vel_yaw=(-1.0, 1.0), - heading=(-3.14, 3.14), - ), - limit_ranges=BaseEnvCfg.Commands.Ranges( - lin_vel_x=(-1.0, 1.0), - lin_vel_y=(-1.0, 1.0), - ang_vel_yaw=(-1.0, 1.0), - heading=(-3.14, 3.14), - ), - ) - - curriculum = BaseEnvCfg.Curriculum( - enabled=False, funcs={"lin_vel_cmd_levels": lin_vel_cmd_levels} - ) - - callbacks_query = {"contact_forces": ContactForces(history_length=3)} - callbacks_setup = { - "material_randomizer": MaterialRandomizer( - obj_name="g1_dof12", - static_friction_range=(0.1, 1.25), - dynamic_friction_range=(0.1, 1.25), - restitution_range=(0.0, 0.0), - num_buckets=64, - ), - "mass_randomizer": MassRandomizer( - obj_name="g1_dof12", - body_names="pelvis", - mass_distribution_params=(-1.0, 3.0), - operation="add", - ), - } - callbacks_reset = { - "random_root_state": ( - reset_funcs.random_root_state_terrain_aware, - { - "pose_range": [ - [0., 0., 0, 0, 0, 0], # x, y, z_offset, roll, pitch, yaw - [0., 0., 0, 0, 0, 0], - ], - "velocity_range": [[-0.5] * 6, [0.5] * 6], - # base_height_offset is None by default, uses robot's default z position (0.8m from cfg_base.py) - }, - ), - "reset_joints_by_scale": ( - reset_funcs.reset_joints_by_scale, - {"position_range": (0.5, 1.5), "velocity_range": (1.0, 1.0)}, - ), - } - callbacks_post_step = { - "push_robot": ( - step_funcs.push_by_setting_velocity, - { - "interval_range_s": (5.0, 5.0), - "velocity_range": [[-1.5, -1.5, 0.0], [1.5, 1.5, 0.0]], - }, - ) - } - callbacks_terminate = { - "time_out": termination_funcs.time_out, - "undesired_contact": ( - termination_funcs.undesired_contact, - { - "contact_names": [ - ".*_elbow_.*", - ".*_shoulder_.*", - ".*_wrist_.*", - "pelvis", - "torso_link", - ], - "limit_range": 1.0, - }, - ), - "bad_orientation": (termination_funcs.bad_orientation, {"limit_angle": 0.8}), - } - - -@configclass -class WalkG1Dof12RslRlTrainCfg(RslRlOnPolicyRunnerCfg): - num_steps_per_env = 24 - max_iterations = 50000 - save_interval = 100 - experiment_name = "" # same as task name - empirical_normalization = False - policy = RslRlPpoActorCriticRecurrentCfg( - init_noise_std=0.8, - actor_hidden_dims=[32], - critic_hidden_dims=[32], - activation="elu", - rnn_type="lstm", - rnn_hidden_dim=64, - rnn_num_layers=1, - ) - algorithm = RslRlPpoAlgorithmCfg( - value_loss_coef=1.0, - use_clipped_value_loss=True, - clip_param=0.2, - entropy_coef=0.01, - num_learning_epochs=5, - num_mini_batches=4, # mini batch size = num_envs*nsteps / nminibatches - learning_rate=1.0e-3, # 5.e-4 - schedule="adaptive", # could be adaptive, fixed - gamma=0.99, - lam=0.95, - desired_kl=0.01, - max_grad_norm=1.0, - ) diff --git a/roboverse_learn/rl/unitree_rl/configs/locomotion/walk_g1_dof29.py b/roboverse_learn/rl/unitree_rl/configs/locomotion/walk_g1_dof29.py deleted file mode 100644 index 0d2529feb..000000000 --- a/roboverse_learn/rl/unitree_rl/configs/locomotion/walk_g1_dof29.py +++ /dev/null @@ -1,189 +0,0 @@ -import math - -from metasim.utils import configclass - -from roboverse_learn.rl.unitree_rl.configs.cfg_base import BaseEnvCfg -from roboverse_learn.rl.configs.rsl_rl.algorithm import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg -import roboverse_learn.rl.unitree_rl.helper.curriculum_utils as curr_funs -from metasim.queries import ContactForces -from roboverse_learn.rl.unitree_rl.configs.cfg_queries import LidarPointCloud -from roboverse_learn.rl.unitree_rl.configs.cfg_randomizers import ( - MaterialRandomizer, - MassRandomizer, -) - -from roboverse_learn.rl.unitree_rl.configs.callback_funcs import ( - termination_funcs, - reset_funcs, - step_funcs, - reward_funcs, -) - - -@configclass -class WalkG1Dof29EnvCfg(BaseEnvCfg): - """ - Environment configuration for humanoid walking task. - """ - - obs_len_history = 5 - priv_obs_len_history = 5 - episode_length_s = 20.0 - - control = BaseEnvCfg.Control(action_scale=0.25, soft_joint_pos_limit_factor=0.9) - - @configclass - class RewardsScales: - track_lin_vel_xy = (1.0, {"std": math.sqrt(0.25)}) - track_ang_vel_z = (0.5, {"std": math.sqrt(0.25)}) - is_alive = 0.15 - lin_vel_z = -2.0 - ang_vel_xy = -0.05 - joint_vel = -0.001 - joint_acc = -2.5e-7 - action_rate = -0.05 - joint_pos_limits = -5.0 - energy = -2e-5 - joint_deviation_arms = ( - -0.1, - {"joint_names": (".*_shoulder_.*_joint", ".*_elbow_joint", ".*_wrist_.*")}, - reward_funcs.joint_deviation_l1, - ) - joint_deviation_waists = ( - -1.0, - {"joint_names": "waist.*"}, - reward_funcs.joint_deviation_l1, - ) - joint_deviation_legs = ( - -1.0, - {"joint_names": (".*_hip_roll_joint", ".*_hip_yaw_joint")}, - reward_funcs.joint_deviation_l1, - ) - flat_orientation = -5.0 - base_height = (-10.0, {"target_height": 0.78}) - feet_gait = ( - 0.5, - { - "period": 0.8, - "offset": [0.0, 0.5], - "threshold": 0.55, - "body_names": (".*ankle_roll.*"), - }, - ) - feet_slide = (-0.2, {"body_names": (".*ankle_roll.*")}) - feet_clearance = ( - 1.0, - { - "std": math.sqrt(0.05), - "tanh_mult": 2.0, - "target_height": 0.1, - "body_names": (".*ankle_roll.*"), - }, - ) - undesired_contacts = (-1.0, {"threshold": 1, "body_names": ("(?!.*ankle.*).*")}) - - rewards = BaseEnvCfg.Rewards( - only_positive_rewards=False, - scales=RewardsScales(), - ) - - commands = BaseEnvCfg.Commands( - value=None, - resample=step_funcs.resample_commands, - heading_command=False, - rel_standing_envs=0.02, - ranges=BaseEnvCfg.Commands.Ranges( - lin_vel_x=(-0.1, 0.1), lin_vel_y=(-0.1, 0.1), ang_vel_yaw=(-0.1, 0.1) - ), - limit_ranges=BaseEnvCfg.Commands.Ranges( - lin_vel_x=(-0.5, 1.0), lin_vel_y=(-0.3, 0.3), ang_vel_yaw=(-0.2, 0.2) - ), - ) - - curriculum = BaseEnvCfg.Curriculum( - enabled=True, - funcs={ - "lin_vel_cmd_levels": curr_funs.lin_vel_cmd_levels, - # "terrain_levels": curr_funs.terrain_levels_vel - }, - ) - - callbacks_query = {"contact_forces": ContactForces(history_length=3), "lidar_point_cloud": LidarPointCloud(enabled=False)} - callbacks_setup = { - "material_randomizer": MaterialRandomizer( - obj_name="g1_dof29", - static_friction_range=(0.3, 1.0), - dynamic_friction_range=(0.3, 1.0), - restitution_range=(0.0, 0.0), - num_buckets=64, - ), - "mass_randomizer": MassRandomizer( - obj_name="g1_dof29", - body_names="torso_link", - mass_distribution_params=(-1.0, 3.0), - operation="add", - ), - } - callbacks_reset = { - "random_root_state": ( - reset_funcs.random_root_state_terrain_aware, - { - "pose_range": [ - [-0.5, -0.5, 0.0, 0, 0, -3.14], # x, y, z_offset, roll, pitch, yaw - [0.5, 0.5, 0.05, 0, 0, 3.14], # z_offset can vary slightly - ], - "velocity_range": [[0] * 6, [0] * 6], - # base_height_offset is None by default, uses robot's default z position (0.8m from cfg_base.py) - }, - ), - "reset_joints_by_scale": ( - reset_funcs.reset_joints_by_scale, - {"position_range": (1.0, 1.0), "velocity_range": (-1.0, 1.0)}, - ), - } - callbacks_post_step = { - "push_robot": ( - step_funcs.push_by_setting_velocity, - { - "interval_range_s": (5.0, 5.0), - "velocity_range": [[-0.5, -0.5, 0.0], [0.5, 0.5, 0.0]], - }, - ) - } - callbacks_terminate = { - "time_out": termination_funcs.time_out, - "base_height": ( - termination_funcs.root_height_below_minimum, - {"minimum_height": 0.2}, - ), - "bad_orientation": (termination_funcs.bad_orientation, {"limit_angle": 0.8}), - } - - -@configclass -class WalkG1Dof29EnvRslRlTrainCfg(RslRlOnPolicyRunnerCfg): - num_steps_per_env = 24 - max_iterations = 50000 - save_interval = 100 - experiment_name = "" # same as task name - empirical_normalization = False - policy = RslRlPpoActorCriticCfg( - init_noise_std=1.0, - actor_hidden_dims=[512, 256, 128], - critic_hidden_dims=[512, 256, 128], - activation="elu", - ) - algorithm = RslRlPpoAlgorithmCfg( - value_loss_coef=1.0, - use_clipped_value_loss=True, - clip_param=0.2, - entropy_coef=0.01, - num_learning_epochs=5, - num_mini_batches=4, - learning_rate=1.0e-3, - schedule="adaptive", - gamma=0.99, - lam=0.95, - desired_kl=0.01, - max_grad_norm=1.0, - ) diff --git a/roboverse_learn/rl/unitree_rl/helper/__init__.py b/roboverse_learn/rl/unitree_rl/helper/__init__.py deleted file mode 100644 index 16281fe0b..000000000 --- a/roboverse_learn/rl/unitree_rl/helper/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .utils import * diff --git a/roboverse_learn/rl/unitree_rl/helper/terrain_utils.py b/roboverse_learn/rl/unitree_rl/helper/terrain_utils.py deleted file mode 100644 index f8f2cd26e..000000000 --- a/roboverse_learn/rl/unitree_rl/helper/terrain_utils.py +++ /dev/null @@ -1,408 +0,0 @@ -# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. -# NVIDIA CORPORATION and its licensors retain all intellectual property -# and proprietary rights in and to this software, related documentation -# and any modifications thereto. Any use, reproduction, disclosure or -# distribution of this software and related documentation without an express -# license agreement from NVIDIA CORPORATION is strictly prohibited. - - -import numpy as np -from scipy.interpolate import RegularGridInterpolator - -# from scipy import interpolate - - -def random_uniform_terrain( - terrain, - min_height, - max_height, - step=1, - downsampled_scale=None, -): - """ - Generate a uniform noise terrain - - Parameters - terrain (SubTerrain): the terrain - min_height (float): the minimum height of the terrain [meters] - max_height (float): the maximum height of the terrain [meters] - step (float): minimum height change between two points [meters] - downsampled_scale (float): distance between two randomly sampled points ( musty be larger or equal to terrain.horizontal_scale) - - """ - if downsampled_scale is None: - downsampled_scale = terrain.horizontal_scale - - # switch parameters to discrete units - min_height = int(min_height / terrain.vertical_scale) - max_height = int(max_height / terrain.vertical_scale) - step = int(step / terrain.vertical_scale) - - heights_range = np.arange(min_height, max_height + step, step) - height_field_downsampled = np.random.choice( - heights_range, - ( - int(terrain.width * terrain.horizontal_scale / downsampled_scale), - int(terrain.length * terrain.horizontal_scale / downsampled_scale), - ), - ) - - x = np.linspace(0, terrain.width * terrain.horizontal_scale, height_field_downsampled.shape[0]) - y = np.linspace(0, terrain.length * terrain.horizontal_scale, height_field_downsampled.shape[1]) - - # from scipy import interpolate - # f = interpolate.interp2d(y, x, height_field_downsampled, kind="linear") - # x_upsampled = np.linspace(0, terrain.width * terrain.horizontal_scale, terrain.width) - # y_upsampled = np.linspace(0, terrain.length * terrain.horizontal_scale, terrain.length) - # z_upsampled = np.rint(f(y_upsampled, x_upsampled)) - - f = RegularGridInterpolator((x, y), height_field_downsampled, method="linear") - x_upsampled = np.linspace(0, terrain.width * terrain.horizontal_scale, terrain.width) - y_upsampled = np.linspace(0, terrain.length * terrain.horizontal_scale, terrain.length) - xx, yy = np.meshgrid(x_upsampled, y_upsampled, indexing="ij") - points = np.stack([xx.ravel(), yy.ravel()], axis=-1) - z_upsampled = np.rint(f(points)).reshape(terrain.width, terrain.length) - - terrain.height_field_raw += z_upsampled.astype(np.int16) - return terrain - - -def sloped_terrain(terrain, slope=1): - """ - Generate a sloped terrain - - Parameters: - terrain (SubTerrain): the terrain - slope (int): positive or negative slope - Returns: - terrain (SubTerrain): update terrain - """ - - x = np.arange(0, terrain.width) - y = np.arange(0, terrain.length) - xx, yy = np.meshgrid(x, y, sparse=True) - xx = xx.reshape(terrain.width, 1) - max_height = int(slope * (terrain.horizontal_scale / terrain.vertical_scale) * terrain.width) - terrain.height_field_raw[:, np.arange(terrain.length)] += (max_height * xx / terrain.width).astype( - terrain.height_field_raw.dtype - ) - return terrain - - -def pyramid_sloped_terrain(terrain, slope=1, platform_size=1.0): - """ - Generate a sloped terrain - - Parameters: - terrain (terrain): the terrain - slope (int): positive or negative slope - platform_size (float): size of the flat platform at the center of the terrain [meters] - Returns: - terrain (SubTerrain): update terrain - """ - x = np.arange(0, terrain.width) - y = np.arange(0, terrain.length) - center_x = int(terrain.width / 2) - center_y = int(terrain.length / 2) - xx, yy = np.meshgrid(x, y, sparse=True) - xx = (center_x - np.abs(center_x - xx)) / center_x - yy = (center_y - np.abs(center_y - yy)) / center_y - xx = xx.reshape(terrain.width, 1) - yy = yy.reshape(1, terrain.length) - max_height = int(slope * (terrain.horizontal_scale / terrain.vertical_scale) * (terrain.width / 2)) - terrain.height_field_raw += (max_height * xx * yy).astype(terrain.height_field_raw.dtype) - - platform_size = int(platform_size / terrain.horizontal_scale / 2) - x1 = terrain.width // 2 - platform_size - x2 = terrain.width // 2 + platform_size - y1 = terrain.length // 2 - platform_size - y2 = terrain.length // 2 + platform_size - - min_h = min(terrain.height_field_raw[x1, y1], 0) - max_h = max(terrain.height_field_raw[x1, y1], 0) - terrain.height_field_raw = np.clip(terrain.height_field_raw, min_h, max_h) - return terrain - - -def discrete_obstacles_terrain(terrain, max_height, min_size, max_size, num_rects, platform_size=1.0): - """ - Generate a terrain with gaps - - Parameters: - terrain (terrain): the terrain - max_height (float): maximum height of the obstacles (range=[-max, -max/2, max/2, max]) [meters] - min_size (float): minimum size of a rectangle obstacle [meters] - max_size (float): maximum size of a rectangle obstacle [meters] - num_rects (int): number of randomly generated obstacles - platform_size (float): size of the flat platform at the center of the terrain [meters] - Returns: - terrain (SubTerrain): update terrain - """ - # switch parameters to discrete units - max_height = int(max_height / terrain.vertical_scale) - min_size = int(min_size / terrain.horizontal_scale) - max_size = int(max_size / terrain.horizontal_scale) - platform_size = int(platform_size / terrain.horizontal_scale) - - (i, j) = terrain.height_field_raw.shape - height_range = [-max_height, -max_height // 2, max_height // 2, max_height] - width_range = range(min_size, max_size, 4) - length_range = range(min_size, max_size, 4) - - for _ in range(num_rects): - width = np.random.choice(width_range) - length = np.random.choice(length_range) - start_i = np.random.choice(range(0, i - width, 4)) - start_j = np.random.choice(range(0, j - length, 4)) - terrain.height_field_raw[start_i : start_i + width, start_j : start_j + length] = np.random.choice(height_range) - - x1 = (terrain.width - platform_size) // 2 - x2 = (terrain.width + platform_size) // 2 - y1 = (terrain.length - platform_size) // 2 - y2 = (terrain.length + platform_size) // 2 - terrain.height_field_raw[x1:x2, y1:y2] = 0 - return terrain - - -def wave_terrain(terrain, num_waves=1, amplitude=1.0): - """ - Generate a wavy terrain - - Parameters: - terrain (terrain): the terrain - num_waves (int): number of sine waves across the terrain length - Returns: - terrain (SubTerrain): update terrain - """ - amplitude = int(0.5 * amplitude / terrain.vertical_scale) - if num_waves > 0: - div = terrain.length / (num_waves * np.pi * 2) - x = np.arange(0, terrain.width) - y = np.arange(0, terrain.length) - xx, yy = np.meshgrid(x, y, sparse=True) - xx = xx.reshape(terrain.width, 1) - yy = yy.reshape(1, terrain.length) - terrain.height_field_raw += (amplitude * np.cos(yy / div) + amplitude * np.sin(xx / div)).astype( - terrain.height_field_raw.dtype - ) - return terrain - - -def stairs_terrain(terrain, step_width, step_height): - """ - Generate a stairs - - Parameters: - terrain (terrain): the terrain - step_width (float): the width of the step [meters] - step_height (float): the height of the step [meters] - Returns: - terrain (SubTerrain): update terrain - """ - # switch parameters to discrete units - step_width = int(step_width / terrain.horizontal_scale) - step_height = int(step_height / terrain.vertical_scale) - - num_steps = terrain.width // step_width - height = step_height - for i in range(num_steps): - terrain.height_field_raw[i * step_width : (i + 1) * step_width, :] += height - height += step_height - return terrain - - -def pyramid_stairs_terrain(terrain, step_width, step_height, platform_size=1.0): - """ - Generate stairs - - Parameters: - terrain (terrain): the terrain - step_width (float): the width of the step [meters] - step_height (float): the step_height [meters] - platform_size (float): size of the flat platform at the center of the terrain [meters] - Returns: - terrain (SubTerrain): update terrain - """ - # switch parameters to discrete units - step_width = int(step_width / terrain.horizontal_scale) - step_height = int(step_height / terrain.vertical_scale) - platform_size = int(platform_size / terrain.horizontal_scale) - - height = 0 - start_x = 0 - stop_x = terrain.width - start_y = 0 - stop_y = terrain.length - while (stop_x - start_x) > platform_size and (stop_y - start_y) > platform_size: - start_x += step_width - stop_x -= step_width - start_y += step_width - stop_y -= step_width - height += step_height - terrain.height_field_raw[start_x:stop_x, start_y:stop_y] = height - return terrain - - -def stepping_stones_terrain(terrain, stone_size, stone_distance, max_height, platform_size=1.0, depth=-10): - """ - Generate a stepping stones terrain - - Parameters: - terrain (terrain): the terrain - stone_size (float): horizontal size of the stepping stones [meters] - stone_distance (float): distance between stones (i.e size of the holes) [meters] - max_height (float): maximum height of the stones (positive and negative) [meters] - platform_size (float): size of the flat platform at the center of the terrain [meters] - depth (float): depth of the holes (default=-10.) [meters] - Returns: - terrain (SubTerrain): update terrain - """ - # switch parameters to discrete units - stone_size = int(stone_size / terrain.horizontal_scale) - stone_distance = int(stone_distance / terrain.horizontal_scale) - max_height = int(max_height / terrain.vertical_scale) - platform_size = int(platform_size / terrain.horizontal_scale) - height_range = np.arange(-max_height - 1, max_height, step=1) - - start_x = 0 - start_y = 0 - terrain.height_field_raw[:, :] = int(depth / terrain.vertical_scale) - if terrain.length >= terrain.width: - while start_y < terrain.length: - stop_y = min(terrain.length, start_y + stone_size) - start_x = np.random.randint(0, stone_size) - # fill first hole - stop_x = max(0, start_x - stone_distance) - terrain.height_field_raw[0:stop_x, start_y:stop_y] = np.random.choice(height_range) - # fill row - while start_x < terrain.width: - stop_x = min(terrain.width, start_x + stone_size) - terrain.height_field_raw[start_x:stop_x, start_y:stop_y] = np.random.choice(height_range) - start_x += stone_size + stone_distance - start_y += stone_size + stone_distance - elif terrain.width > terrain.length: - while start_x < terrain.width: - stop_x = min(terrain.width, start_x + stone_size) - start_y = np.random.randint(0, stone_size) - # fill first hole - stop_y = max(0, start_y - stone_distance) - terrain.height_field_raw[start_x:stop_x, 0:stop_y] = np.random.choice(height_range) - # fill column - while start_y < terrain.length: - stop_y = min(terrain.length, start_y + stone_size) - terrain.height_field_raw[start_x:stop_x, start_y:stop_y] = np.random.choice(height_range) - start_y += stone_size + stone_distance - start_x += stone_size + stone_distance - - x1 = (terrain.width - platform_size) // 2 - x2 = (terrain.width + platform_size) // 2 - y1 = (terrain.length - platform_size) // 2 - y2 = (terrain.length + platform_size) // 2 - terrain.height_field_raw[x1:x2, y1:y2] = 0 - return terrain - - -def convert_heightfield_to_trimesh(height_field_raw, horizontal_scale, vertical_scale, slope_threshold=None): - """ - Convert a heightfield array to a triangle mesh represented by vertices and triangles. - Optionally, corrects vertical surfaces above the provide slope threshold: - - If (y2-y1)/(x2-x1) > slope_threshold -> Move A to A' (set x1 = x2). Do this for all directions. - B(x2,y2) - /| - / | - / | - (x1,y1)A---A'(x2',y1) - - Parameters: - height_field_raw (np.array): input heightfield - horizontal_scale (float): horizontal scale of the heightfield [meters] - vertical_scale (float): vertical scale of the heightfield [meters] - slope_threshold (float): the slope threshold above which surfaces are made vertical. If None no correction is applied (default: None) - Returns: - vertices (np.array(float)): array of shape (num_vertices, 3). Each row represents the location of each vertex [meters] - triangles (np.array(int)): array of shape (num_triangles, 3). Each row represents the indices of the 3 vertices connected by this triangle. - """ - hf = height_field_raw - num_rows = hf.shape[0] - num_cols = hf.shape[1] - - y = np.linspace(0, (num_cols - 1) * horizontal_scale, num_cols) - x = np.linspace(0, (num_rows - 1) * horizontal_scale, num_rows) - yy, xx = np.meshgrid(y, x) - - if slope_threshold is not None: - slope_threshold *= horizontal_scale / vertical_scale - move_x = np.zeros((num_rows, num_cols)) - move_y = np.zeros((num_rows, num_cols)) - move_corners = np.zeros((num_rows, num_cols)) - move_x[: num_rows - 1, :] += hf[1:num_rows, :] - hf[: num_rows - 1, :] > slope_threshold - move_x[1:num_rows, :] -= hf[: num_rows - 1, :] - hf[1:num_rows, :] > slope_threshold - move_y[:, : num_cols - 1] += hf[:, 1:num_cols] - hf[:, : num_cols - 1] > slope_threshold - move_y[:, 1:num_cols] -= hf[:, : num_cols - 1] - hf[:, 1:num_cols] > slope_threshold - move_corners[: num_rows - 1, : num_cols - 1] += ( - hf[1:num_rows, 1:num_cols] - hf[: num_rows - 1, : num_cols - 1] > slope_threshold - ) - move_corners[1:num_rows, 1:num_cols] -= ( - hf[: num_rows - 1, : num_cols - 1] - hf[1:num_rows, 1:num_cols] > slope_threshold - ) - xx += (move_x + move_corners * (move_x == 0)) * horizontal_scale - yy += (move_y + move_corners * (move_y == 0)) * horizontal_scale - - # create triangle mesh vertices and triangles from the heightfield grid - vertices = np.zeros((num_rows * num_cols, 3), dtype=np.float32) - vertices[:, 0] = xx.flatten() - vertices[:, 1] = yy.flatten() - vertices[:, 2] = hf.flatten() * vertical_scale - triangles = -np.ones((2 * (num_rows - 1) * (num_cols - 1), 3), dtype=np.uint32) - for i in range(num_rows - 1): - ind0 = np.arange(0, num_cols - 1) + i * num_cols - ind1 = ind0 + 1 - ind2 = ind0 + num_cols - ind3 = ind2 + 1 - start = 2 * i * (num_cols - 1) - stop = start + 2 * (num_cols - 1) - triangles[start:stop:2, 0] = ind0 - triangles[start:stop:2, 1] = ind3 - triangles[start:stop:2, 2] = ind1 - triangles[start + 1 : stop : 2, 0] = ind0 - triangles[start + 1 : stop : 2, 1] = ind2 - triangles[start + 1 : stop : 2, 2] = ind3 - - return vertices, triangles - - -class SubTerrain: - def __init__(self, terrain_name="terrain", width=256, length=256, vertical_scale=1.0, horizontal_scale=1.0): - self.terrain_name = terrain_name - self.vertical_scale = vertical_scale - self.horizontal_scale = horizontal_scale - self.width = width - self.length = length - self.height_field_raw = np.zeros((self.width, self.length), dtype=np.int16) - - -################################ Custom Development ################################ -def gap_terrain(terrain, gap_size, platform_size=1.0): - gap_size = int(gap_size / terrain.horizontal_scale) - platform_size = int(platform_size / terrain.horizontal_scale) - - center_x = terrain.length // 2 - center_y = terrain.width // 2 - x1 = (terrain.length - platform_size) // 2 - x2 = x1 + gap_size - y1 = (terrain.width - platform_size) // 2 - y2 = y1 + gap_size - - terrain.height_field_raw[center_x - x2 : center_x + x2, center_y - y2 : center_y + y2] = -1000 - terrain.height_field_raw[center_x - x1 : center_x + x1, center_y - y1 : center_y + y1] = 0 - - -def pit_terrain(terrain, depth, platform_size=1.0): - depth = int(depth / terrain.vertical_scale) - platform_size = int(platform_size / terrain.horizontal_scale / 2) - x1 = terrain.length // 2 - platform_size - x2 = terrain.length // 2 + platform_size - y1 = terrain.width // 2 - platform_size - y2 = terrain.width // 2 + platform_size - terrain.height_field_raw[x1:x2, y1:y2] = -depth diff --git a/roboverse_pack/callback_funcs/__init__.py b/roboverse_pack/callback_funcs/__init__.py new file mode 100644 index 000000000..51f31611f --- /dev/null +++ b/roboverse_pack/callback_funcs/__init__.py @@ -0,0 +1 @@ +"""Callback function registry for RoboVerse tasks.""" diff --git a/roboverse_learn/rl/unitree_rl/configs/callback_funcs/reset_funcs.py b/roboverse_pack/callback_funcs/humanoid/reset_funcs.py similarity index 83% rename from roboverse_learn/rl/unitree_rl/configs/callback_funcs/reset_funcs.py rename to roboverse_pack/callback_funcs/humanoid/reset_funcs.py index 697ecb544..083603b9d 100644 --- a/roboverse_learn/rl/unitree_rl/configs/callback_funcs/reset_funcs.py +++ b/roboverse_pack/callback_funcs/humanoid/reset_funcs.py @@ -3,17 +3,23 @@ import numpy as np import torch -from metasim.types import TensorState -from metasim.utils.math import quat_from_euler_xyz, sample_uniform, quat_mul +from metasim.utils.math import quat_from_euler_xyz, quat_mul, sample_uniform +from roboverse_pack.tasks.humanoid.base.types import EnvTypes -from roboverse_pack.tasks.unitree_rl.base.types import EnvTypes - -def random_root_state(env: EnvTypes, env_ids: torch.Tensor | list, pose_range: list[list]=[[0]*6, [0]*6], velocity_range: list[list]=[[0]*6, [0]*6]) -> torch.Tensor: +def random_root_state( + env: EnvTypes, + env_ids: torch.Tensor | list, + pose_range: list[list] | None = None, + velocity_range: list[list] | None = None, +) -> torch.Tensor: + """Randomize root pose and velocity for selected environments.""" if len(env_ids) == 0: return root_states = env.default_env_states.robots[env.name].root_state[env_ids].clone() + pose_range = pose_range or [[0] * 6, [0] * 6] + velocity_range = velocity_range or [[0] * 6, [0] * 6] # poses pose_range = torch.tensor(pose_range, device=env.device) @@ -35,7 +41,13 @@ def random_root_state(env: EnvTypes, env_ids: torch.Tensor | list, pose_range: l # env.write_robot_root_state(torch.cat([positions, orientations, velocities], dim=-1), env_ids=env_ids) -def reset_joints_by_scale(env: EnvTypes, env_ids: torch.Tensor | list, position_range: list|tuple=(1.0, 1.0), velocity_range: list|tuple=(1.0, 1.0)) -> torch.Tensor: +def reset_joints_by_scale( + env: EnvTypes, + env_ids: torch.Tensor | list, + position_range: list | tuple = (1.0, 1.0), + velocity_range: list | tuple = (1.0, 1.0), +) -> torch.Tensor: + """Scale default joint states by random factors for a batch of environments.""" if len(env_ids) == 0: return @@ -61,8 +73,7 @@ def reset_joints_by_scale(env: EnvTypes, env_ids: torch.Tensor | list, position_ def get_terrain_height_at_position(env: EnvTypes, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor: - """ - Query the terrain height at given (x, y) positions. + """Query the terrain height at given (x, y) positions. All simulators now center the terrain at world origin (0,0). The terrain center is at heightfield ((rows-1)/2, (cols-1)/2) which maps to world (0,0). @@ -77,7 +88,7 @@ def get_terrain_height_at_position(env: EnvTypes, x: torch.Tensor, y: torch.Tens """ # Access the terrain data from the simulator handler = env.handler - if not hasattr(handler, '_height_mat') or handler._height_mat is None: + if not hasattr(handler, "_height_mat") or handler._height_mat is None: # No terrain, return zero height return torch.zeros_like(x) @@ -106,10 +117,12 @@ def get_terrain_height_at_position(env: EnvTypes, x: torch.Tensor, y: torch.Tens # Use bilinear interpolation for smooth height queries from scipy.interpolate import RegularGridInterpolator + x_coords = np.arange(rows) y_coords = np.arange(cols) - interpolator = RegularGridInterpolator((x_coords, y_coords), height_mat, - method='linear', bounds_error=False, fill_value=0.0) + interpolator = RegularGridInterpolator( + (x_coords, y_coords), height_mat, method="linear", bounds_error=False, fill_value=0.0 + ) points = np.column_stack([x_idx, y_idx]) heights = interpolator(points) @@ -120,12 +133,11 @@ def get_terrain_height_at_position(env: EnvTypes, x: torch.Tensor, y: torch.Tens def random_root_state_terrain_aware( env: EnvTypes, env_ids: torch.Tensor | list, - pose_range: list[list]=[[0]*6, [0]*6], - velocity_range: list[list]=[[0]*6, [0]*6], - base_height_offset: float | None = None + pose_range: list[list] | None = None, + velocity_range: list[list] | None = None, + base_height_offset: float | None = None, ) -> torch.Tensor: - """ - Reset robot root state with terrain-aware height adjustment. + """Reset robot root state with terrain-aware height adjustment. The robot will be spawned at base_height_offset above the terrain surface at the randomly sampled (x, y) position. @@ -143,13 +155,13 @@ def random_root_state_terrain_aware( return root_states = env.default_env_states.robots[env.name].root_state[env_ids].clone() + pose_range = pose_range or [[0] * 6, [0] * 6] + velocity_range = velocity_range or [[0] * 6, [0] * 6] # Get per-env world-frame origins (if available) so we can convert # local robot positions to world coordinates for terrain queries. env_ids_idx = ( - env_ids - if isinstance(env_ids, torch.Tensor) - else torch.as_tensor(env_ids, device=env.device, dtype=torch.long) + env_ids if isinstance(env_ids, torch.Tensor) else torch.as_tensor(env_ids, device=env.device, dtype=torch.long) ) env_origins_xy = torch.zeros((len(env_ids_idx), 2), device=env.device, dtype=root_states.dtype) if hasattr(env.handler, "scene") and hasattr(env.handler.scene, "env_origins"): diff --git a/roboverse_learn/rl/unitree_rl/configs/callback_funcs/reward_funcs.py b/roboverse_pack/callback_funcs/humanoid/reward_funcs.py similarity index 82% rename from roboverse_learn/rl/unitree_rl/configs/callback_funcs/reward_funcs.py rename to roboverse_pack/callback_funcs/humanoid/reward_funcs.py index 969d1f53e..4311fae98 100644 --- a/roboverse_learn/rl/unitree_rl/configs/callback_funcs/reward_funcs.py +++ b/roboverse_pack/callback_funcs/humanoid/reward_funcs.py @@ -2,17 +2,14 @@ import torch +from metasim.queries import ContactForces from metasim.types import TensorState from metasim.utils.math import quat_rotate_inverse - -from metasim.queries import ContactForces -from roboverse_pack.tasks.unitree_rl.base.types import EnvTypes -from roboverse_learn.rl.unitree_rl.helper import get_indices_from_substring, hash_names +from roboverse_pack.tasks.humanoid.base.types import EnvTypes +from roboverse_pack.utils.humanoid_utils import get_indices_from_substring, hash_names -def track_lin_vel_xy( - env: EnvTypes, env_states: TensorState, std: float -) -> torch.Tensor: +def track_lin_vel_xy(env: EnvTypes, env_states: TensorState, std: float) -> torch.Tensor: """Reward tracking of linear velocity commands (xy axes) in the gravity aligned robot frame using exponential kernel.""" # extract the used quantities (to enable type-hinting) robot_state = env_states.robots[env.name] @@ -24,9 +21,7 @@ def track_lin_vel_xy( def track_ang_vel_z(env: EnvTypes, env_states: TensorState, std: float) -> torch.Tensor: - """ - Track angular velocity commands (yaw). - """ + """Track angular velocity commands (yaw).""" robot_state = env_states.robots[env.name] base_quat = robot_state.root_state[:, 3:7] base_ang_vel = quat_rotate_inverse(base_quat, robot_state.root_state[:, 10:13]) @@ -77,18 +72,14 @@ def joint_acc(env: EnvTypes, env_states: TensorState) -> torch.Tensor: # extract the used quantities (to enable type-hinting) robot_state = env_states.robots[env.name] return torch.sum( - torch.square( - (env.history_buffer["joint_vel"][-1] - robot_state.joint_vel) / env.step_dt - ), + torch.square((env.history_buffer["joint_vel"][-1] - robot_state.joint_vel) / env.step_dt), dim=1, ) def action_rate(env: EnvTypes, env_states: TensorState) -> torch.Tensor: """Penalize the rate of change of the actions using L2 squared kernel.""" - return torch.sum( - torch.square(env.history_buffer["actions"][-1] - env.actions), dim=1 - ) + return torch.sum(torch.square(env.history_buffer["actions"][-1] - env.actions), dim=1) def joint_pos_limits(env: EnvTypes, env_states: TensorState) -> torch.Tensor: @@ -97,17 +88,13 @@ def joint_pos_limits(env: EnvTypes, env_states: TensorState) -> torch.Tensor: This is computed as a sum of the absolute value of the difference between the joint position and the soft limits. """ robot_state = env_states.robots[env.name] - out_of_limits = -(robot_state.joint_pos - env.soft_dof_pos_limits[:, 0]).clip( - max=0.0 - ) - out_of_limits += (robot_state.joint_pos - env.soft_dof_pos_limits[:, 1]).clip( - min=0.0 - ) + out_of_limits = -(robot_state.joint_pos - env.soft_dof_pos_limits[:, 0]).clip(max=0.0) + out_of_limits += (robot_state.joint_pos - env.soft_dof_pos_limits[:, 1]).clip(min=0.0) return torch.sum(out_of_limits, dim=1) def energy(env: EnvTypes, env_states: TensorState) -> torch.Tensor: - """Sum |qdot|*|tau| across joints (\"energy\" usage).""" + r"""Sum |qdot|*|tau| across joints ("energy" usage).""" base = env_states.robots[env.name] qvel = base.joint_vel qfrc = base.joint_effort_target @@ -115,20 +102,14 @@ def energy(env: EnvTypes, env_states: TensorState) -> torch.Tensor: return torch.sum(torch.abs(qvel) * torch.abs(qfrc), dim=-1) -def _get_indices( - env: EnvTypes, sub_names: tuple[str] | str, all_names: list[str] | tuple[str] -): +def _get_indices(env: EnvTypes, sub_names: tuple[str] | str, all_names: list[str] | tuple[str]): hash_key = hash_names(sub_names) if hash_key not in env.extras_buffer: - env.extras_buffer[hash_key] = get_indices_from_substring( - sub_names, all_names, fullmatch=True - ).to(env.device) + env.extras_buffer[hash_key] = get_indices_from_substring(sub_names, all_names, fullmatch=True).to(env.device) return env.extras_buffer[hash_key] -def joint_deviation_l1( - env: EnvTypes, env_states: TensorState, joint_names: str | tuple[str] -) -> torch.Tensor: +def joint_deviation_l1(env: EnvTypes, env_states: TensorState, joint_names: str | tuple[str]) -> torch.Tensor: """Penalize joint positions that deviate from the default one.""" indices = _get_indices(env, joint_names, env.sorted_joint_names) # extract the used quantities (to enable type-hinting) @@ -150,9 +131,7 @@ def flat_orientation(env: EnvTypes, env_states: TensorState) -> torch.Tensor: return torch.sum(torch.square(projected_gravity[:, :2]), dim=1) -def base_height( - env: EnvTypes, env_states: TensorState, target_height: float -) -> torch.Tensor: +def base_height(env: EnvTypes, env_states: TensorState, target_height: float) -> torch.Tensor: """Penalize asset height from its target using L2 squared kernel. Note: @@ -162,15 +141,8 @@ def base_height( # extract the used quantities (to enable type-hinting) robot_state = env_states.robots[env.name] base_height = robot_state.root_state[:, 2] - if False: # height scanner - sensor: RayCaster = env.scene[sensor_cfg.name] - # Adjust the target height using the sensor data - adjusted_target_height = target_height + torch.mean( - sensor.data.ray_hits_w[..., 2], dim=1 - ) - else: - # Use the provided target height directly for flat terrain - adjusted_target_height = target_height + # Use the provided target height directly for flat terrain + adjusted_target_height = target_height # Compute the L2 squared penalty return torch.square(base_height - adjusted_target_height) @@ -183,16 +155,12 @@ def feet_gait( threshold: float = 0.55, body_names: str | tuple[str] = ".*ankle_roll.*", ) -> torch.Tensor: + """Reward alternating stance phases across feet following a target gait pattern.""" indices = _get_indices(env, body_names, env_states.robots[env.name].body_names) command_name = "base_velocity" contact_forces: ContactForces = env_states.extras["contact_forces"][env.name] - is_contact = ( - contact_forces.contact_forces_history[:, :, indices, :] - .norm(dim=-1) - .max(dim=1)[0] - > 1.0 - ) + is_contact = contact_forces.contact_forces_history[:, :, indices, :].norm(dim=-1).max(dim=1)[0] > 1.0 # contact_sensor = env.handler.contact_sensor # is_contact = contact_sensor.data.current_contact_time[:, env.body_ids_reindex][:, env.extras_buffer[bodies_key]] > 0 @@ -246,12 +214,7 @@ def feet_slide( indices = _get_indices(env, body_names, env_states.robots[env.name].body_names) contact_forces: ContactForces = env_states.extras["contact_forces"][env.name] - contacts = ( - contact_forces.contact_forces_history[:, :, indices, :] - .norm(dim=-1) - .max(dim=1)[0] - > 1.0 - ) + contacts = contact_forces.contact_forces_history[:, :, indices, :].norm(dim=-1).max(dim=1)[0] > 1.0 body_vel = env_states.robots[env.name].body_state[:, indices, 7:9] reward = torch.sum(body_vel.norm(dim=-1) * contacts, dim=1) @@ -266,13 +229,11 @@ def feet_clearance( tanh_mult: float, body_names: str | tuple[str] = ".*ankle_roll.*", ) -> torch.Tensor: - """Reward the swinging feet for clearing a specified height off the ground""" + """Reward the swinging feet for clearing a specified height off the ground.""" indices = _get_indices(env, body_names, env_states.robots[env.name].body_names) base = env_states.robots[env.name] foot_z_target_error = torch.square(base.body_state[:, indices, 2] - target_height) - foot_velocity_tanh = torch.tanh( - tanh_mult * torch.norm(base.body_state[:, indices, 7:9], dim=2) - ) + foot_velocity_tanh = torch.tanh(tanh_mult * torch.norm(base.body_state[:, indices, 7:9], dim=2)) reward = foot_z_target_error * foot_velocity_tanh return torch.exp(-torch.sum(reward, dim=1) / std**2) @@ -286,12 +247,7 @@ def undesired_contacts( """Penalize undesired contacts as the number of violations that are above a threshold.""" indices = _get_indices(env, body_names, env_states.robots[env.name].body_names) contact_forces: ContactForces = env_states.extras["contact_forces"][env.name] - is_contact = ( - contact_forces.contact_forces_history[:, :, indices, :] - .norm(dim=-1) - .max(dim=1)[0] - > threshold - ) + is_contact = contact_forces.contact_forces_history[:, :, indices, :].norm(dim=-1).max(dim=1)[0] > threshold # sum over contacts for each environment return torch.sum(is_contact, dim=1) diff --git a/roboverse_learn/rl/unitree_rl/configs/callback_funcs/step_funcs.py b/roboverse_pack/callback_funcs/humanoid/step_funcs.py similarity index 82% rename from roboverse_learn/rl/unitree_rl/configs/callback_funcs/step_funcs.py rename to roboverse_pack/callback_funcs/humanoid/step_funcs.py index 43fcd607f..09678bf80 100644 --- a/roboverse_learn/rl/unitree_rl/configs/callback_funcs/step_funcs.py +++ b/roboverse_pack/callback_funcs/humanoid/step_funcs.py @@ -1,30 +1,29 @@ from __future__ import annotations from collections import deque + import torch from metasim.types import TensorState from metasim.utils.math import ( quat_apply, - wrap_to_pi, sample_uniform, + wrap_to_pi, ) - -from roboverse_pack.tasks.unitree_rl.base.types import EnvTypes -from roboverse_learn.rl.unitree_rl.configs.cfg_base import BaseEnvCfg +from roboverse_pack.tasks.humanoid.base.types import EnvTypes +from roboverse_pack.tasks.humanoid.cfg_base import BaseEnvCfg def resample_commands(env: EnvTypes, env_states: TensorState = None): - """Randomly select commands for some environments. + """Randomly resample commanded velocities for environments. Args: - env_ids (List[int]): Environments ids for which new commands are needed. + env: Task environment providing command manager and device. + env_states: Optional cached simulator state to reuse when heading commands are enabled. """ cfg: BaseEnvCfg.Commands = env.commands_manager if cfg.value is None: - cfg.value = torch.zeros( - size=(env.num_envs, cfg.num_commands), dtype=torch.float, device=env.device - ) + cfg.value = torch.zeros(size=(env.num_envs, cfg.num_commands), dtype=torch.float, device=env.device) ranges_tensor = torch.tensor( [ cfg.ranges.lin_vel_x, @@ -34,11 +33,7 @@ def resample_commands(env: EnvTypes, env_states: TensorState = None): device=env.device, ) - env_ids = ( - (env._episode_steps % int(cfg.resampling_time / env.step_dt) == 0) - .nonzero(as_tuple=False) - .flatten() - ) + env_ids = (env._episode_steps % int(cfg.resampling_time / env.step_dt) == 0).nonzero(as_tuple=False).flatten() if len(env_ids) == 0: return @@ -50,10 +45,7 @@ def resample_commands(env: EnvTypes, env_states: TensorState = None): ) # low_cmd_mask = torch.norm(cfg.value[env_ids, :2], dim=1) < 0.1 - random_mask = ( - sample_uniform(0, 1, (len(env_ids),), device=env.device) - <= cfg.rel_standing_envs - ) + random_mask = sample_uniform(0, 1, (len(env_ids),), device=env.device) <= cfg.rel_standing_envs final_env_ids = random_mask.nonzero(as_tuple=False).flatten() cfg.value[env_ids][final_env_ids, :] = 0.0 @@ -61,22 +53,19 @@ def resample_commands(env: EnvTypes, env_states: TensorState = None): env_states = env.get_states() if env_states is None else env_states robot_state = env_states.robots[env.name] base_quat = robot_state.root_state[:, 3:7] - forward = quat_apply( - base_quat, env.forward_vec - ) # quat:[w, x, y, z], forward:[x, y, z] + forward = quat_apply(base_quat, env.forward_vec) # quat:[w, x, y, z], forward:[x, y, z] heading = torch.atan2(forward[:, 1], forward[:, 0]) - cfg.value[:, 2] = torch.clip( - 0.5 * wrap_to_pi(cfg.value[:, 2] - heading), -1.0, 1.0 - ) + cfg.value[:, 2] = torch.clip(0.5 * wrap_to_pi(cfg.value[:, 2] - heading), -1.0, 1.0) def push_by_setting_velocity( env: EnvTypes, env_states: TensorState, interval_range_s: tuple | int = 5.0, - velocity_range: list[list] = [[0] * 3, [0] * 3], + velocity_range: list[list] | None = None, ): """Randomly set robot's root velocity to simulate a push.""" + velocity_range = velocity_range or [[0] * 3, [0] * 3] if not hasattr(env, "push_interval"): env.push_interval = ( sample_uniform( @@ -88,9 +77,7 @@ def push_by_setting_velocity( / env.step_dt ).to(torch.int) push_env_ids = ( - torch.logical_and( - env._episode_steps % env.push_interval == 0, env._episode_steps > 0 - ) + torch.logical_and(env._episode_steps % env.push_interval == 0, env._episode_steps > 0) .nonzero(as_tuple=False) .flatten() ) @@ -104,6 +91,7 @@ def push_by_setting_velocity( env.handler.set_states(env_states, push_env_ids.tolist()) + class HistoryBuffer(deque): """A simple LIFO buffer that stores multiple tensors per entry under specified keys. @@ -161,8 +149,10 @@ def peek(self, index: int = 0) -> dict: # Convenience methods ------------------------------------------------------ def push_from_env(self, env: EnvTypes, env_states: TensorState, env_name: str | None = None): - """Collect configured keys from env_states.robots[env_name] (or from - the buffer instance) and push them as one entry. + """Collect configured keys and push them as one entry. + + The values are taken from env_states.robots[env_name] or from attributes + on the HistoryBuffer instance when present. """ robot_name = env_name or self.name or getattr(env, "name", None) if robot_name is None: @@ -192,4 +182,5 @@ def __call__(self, env: EnvTypes, env_states: TensorState, *args, **kwds): # keep clear and len behavior from deque; add explicit alias def clear_all(self): + """Remove all entries from the buffer.""" super().clear() diff --git a/roboverse_learn/rl/unitree_rl/configs/callback_funcs/termination_funcs.py b/roboverse_pack/callback_funcs/humanoid/termination_funcs.py similarity index 71% rename from roboverse_learn/rl/unitree_rl/configs/callback_funcs/termination_funcs.py rename to roboverse_pack/callback_funcs/humanoid/termination_funcs.py index 9f935e3d3..fdf2b80fb 100644 --- a/roboverse_learn/rl/unitree_rl/configs/callback_funcs/termination_funcs.py +++ b/roboverse_pack/callback_funcs/humanoid/termination_funcs.py @@ -4,15 +4,11 @@ from metasim.types import TensorState from metasim.utils.math import quat_rotate_inverse +from roboverse_pack.tasks.humanoid.base.types import EnvTypes +from roboverse_pack.utils.humanoid_utils import get_indices_from_substring -from roboverse_learn.rl.unitree_rl.helper import get_indices_from_substring -from roboverse_pack.tasks.unitree_rl.base.types import EnvTypes - - -def root_height_below_minimum( - env: EnvTypes, env_states: TensorState, minimum_height: float -) -> torch.Tensor: +def root_height_below_minimum(env: EnvTypes, env_states: TensorState, minimum_height: float) -> torch.Tensor: """Terminate when the asset's root height is below the minimum height. Note: @@ -22,9 +18,7 @@ def root_height_below_minimum( return robot_state.root_state[:, 2] < minimum_height -def bad_orientation( - env: EnvTypes, env_states: TensorState, limit_angle: float -) -> torch.Tensor: +def bad_orientation(env: EnvTypes, env_states: TensorState, limit_angle: float) -> torch.Tensor: """Terminate when the asset's orientation is too far from the desired orientation limits. This is computed by checking the angle between the projected gravity vector and the z-axis. @@ -49,7 +43,13 @@ def undesired_contact( ) -> torch.Tensor: """Terminate when undesired contacts are detected.""" if not hasattr(env, "termination_contact_indices"): - env.termination_contact_indices = get_indices_from_substring(contact_names, env.sorted_body_names).to(env.device) + env.termination_contact_indices = get_indices_from_substring(contact_names, env.sorted_body_names).to( + env.device + ) contact_forces = env_states.extras["contact_forces"][env.name] - return torch.any(contact_forces.contact_forces_history[:, :, env.termination_contact_indices, :].norm(dim=-1).max(dim=1)[0] > limit_range, dim=1) + return torch.any( + contact_forces.contact_forces_history[:, :, env.termination_contact_indices, :].norm(dim=-1).max(dim=1)[0] + > limit_range, + dim=1, + ) diff --git a/roboverse_learn/rl/unitree_rl/configs/cfg_queries.py b/roboverse_pack/queries/lidar.py similarity index 93% rename from roboverse_learn/rl/unitree_rl/configs/cfg_queries.py rename to roboverse_pack/queries/lidar.py index d9e0a6655..f4ee93484 100644 --- a/roboverse_learn/rl/unitree_rl/configs/cfg_queries.py +++ b/roboverse_pack/queries/lidar.py @@ -1,21 +1,21 @@ from __future__ import annotations import os -import torch -import numpy as np import warnings -from collections import deque -from metasim.sim.base import BaseSimHandler, BaseQueryType -from metasim.utils.math import quat_apply, convert_quat +import numpy as np +import torch + from metasim.scenario.objects import ( + ArticulationObjCfg, BaseObjCfg, PrimitiveCubeCfg, - PrimitiveSphereCfg, PrimitiveCylinderCfg, + PrimitiveSphereCfg, RigidObjCfg, - ArticulationObjCfg, ) +from metasim.sim.base import BaseQueryType, BaseSimHandler +from metasim.utils.math import convert_quat, quat_apply try: import isaacgym # noqa: F401 @@ -31,7 +31,7 @@ class LidarPointCloud(BaseQueryType): """Optional query that produces a LiDAR point cloud using LidarSensor + Warp. - Notes + Notes: - Supports IsaacGym and MuJoCo via common state interface; raycasting is done against a generated mesh that includes the terrain and static scenario objects (primitives) replicated across environments. @@ -59,6 +59,7 @@ def __init__( self.enabled = enabled def bind_handler(self, handler: BaseSimHandler, *args, **kwargs): + """Attach to a simulator handler and initialize backends.""" super().bind_handler(handler, *args, **kwargs) self.simulator = handler.scenario.simulator self.handler = handler @@ -85,8 +86,8 @@ def _init_backend_mujoco_isaacgym(self): Objects are replicated across envs for IsaacGym, or instantiated once for MuJoCo. """ - import warp as wp import trimesh + import warp as wp from LidarSensor.lidar_sensor import LidarSensor from LidarSensor.sensor_config.lidar_sensor_config import LidarConfig @@ -103,8 +104,8 @@ def _init_backend_mujoco_isaacgym(self): scene_triangles = getattr(self.handler, "_ground_mesh_triangles", None) if scene_vertices is None or scene_triangles is None: warnings.warn( - "LidarPointCloud: ground mesh not available on handler; " - "LiDAR will not include terrain/objects." + "LidarPointCloud: ground mesh not available on handler; LiDAR will not include terrain/objects.", + stacklevel=2, ) self._backend_ready = False return @@ -166,12 +167,15 @@ def _object_to_trimesh(obj: BaseObjCfg): return loaded except Exception as e: warnings.warn( - f"LidarPointCloud: failed to load mesh for object '{obj.name}' " - f"from '{mesh_path}': {e}" + f"LidarPointCloud: failed to load mesh for object '{obj.name}' from '{mesh_path}': {e}", + stacklevel=2, ) return None except Exception as e: # pragma: no cover - defensive - warnings.warn(f"LidarPointCloud: failed to build mesh for object '{obj.name}': {e}") + warnings.warn( + f"LidarPointCloud: failed to build mesh for object '{obj.name}': {e}", + stacklevel=2, + ) return None return None @@ -192,7 +196,10 @@ def _object_to_trimesh(obj: BaseObjCfg): default_quat = getattr(obj, "default_orientation", (1.0, 0.0, 0.0, 0.0)) # Validate quaternion format if not (isinstance(default_quat, (tuple, list, np.ndarray)) and len(default_quat) == 4): - warnings.warn(f"Invalid orientation for object '{obj.name}', using identity quaternion") + warnings.warn( + f"Invalid orientation for object '{obj.name}', using identity quaternion", + stacklevel=2, + ) default_quat = (1.0, 0.0, 0.0, 0.0) rot = _quat_wxyz_to_matrix(default_quat) @@ -373,8 +380,8 @@ def _init_backend_isaacsim(self): dynamic_env_mesh_prim_paths=dynamic_env_mesh_paths, max_distance=20.0, min_range=0.2, - return_pointcloud=True, # request point cloud output - pointcloud_in_world_frame=False, # get local-frame points and transform ourselves + return_pointcloud=True, # request point cloud output + pointcloud_in_world_frame=False, # get local-frame points and transform ourselves enable_sensor_noise=False, update_frequency=25.0, debug_vis=False, @@ -418,7 +425,10 @@ def mujoco_call(self, robot_name: str): bid = i break if bid is None: - warnings.warn(f"LidarPointCloud: link '{self.link_name}' not found in MuJoCo body names.") + warnings.warn( + f"LidarPointCloud: link '{self.link_name}' not found in MuJoCo body names.", + stacklevel=2, + ) return {robot_name: None} self._mj_link_bid = int(bid) @@ -435,13 +445,15 @@ def isaacsim_call(self, robot_name: str): Returns a dict containing local and world point clouds for the target robot. """ - # Ensure the sensor produces up-to-date data; if scene already updates it, this is a no-op self._isaacsim_lidar.update(dt=0.0) data = getattr(self._isaacsim_lidar, "data", None) if data is None: - warnings.warn("LidarPointCloud(isaacsim): LidarSensor has no data. Returning None.") + warnings.warn( + "LidarPointCloud(isaacsim): LidarSensor has no data. Returning None.", + stacklevel=2, + ) return {robot_name: None} # Helper to pull a candidate attribute from sensor data @@ -520,7 +532,8 @@ def _to_tensor(x): else: # Unknown shape; return None rather than crashing warnings.warn( - f"LidarPointCloud(isaacsim): Unsupported local point shape {tuple(pts_local.shape)}. Returning None." + f"LidarPointCloud(isaacsim): Unsupported local point shape {tuple(pts_local.shape)}. Returning None.", + stacklevel=2, ) return {robot_name: None} @@ -558,7 +571,8 @@ def _to_tensor(x): pass else: warnings.warn( - f"LidarPointCloud(isaacsim): Unsupported world point shape {tuple(pts_world.shape)}. Returning None." + f"LidarPointCloud(isaacsim): Unsupported world point shape {tuple(pts_world.shape)}. Returning None.", + stacklevel=2, ) return {robot_name: None} @@ -574,7 +588,10 @@ def _to_tensor(x): # If neither points tensor is available, return None if pts_local is None and pts_world is None: - warnings.warn("LidarPointCloud(isaacsim): No point cloud fields found in sensor data.") + warnings.warn( + "LidarPointCloud(isaacsim): No point cloud fields found in sensor data.", + stacklevel=2, + ) return {robot_name: None} return { @@ -596,9 +613,9 @@ def _compute_lidar_points(self, robot_name: str, pos_w: torch.Tensor, quat_wxyz: pos_w = pos_w + quat_apply(quat_wxyz, offset_local) # Update sensor pose buffers (LidarSensor expects XYZW) - self.sensor_pos_tensor[:, :pos_w.shape[1]] = pos_w + self.sensor_pos_tensor[:, : pos_w.shape[1]] = pos_w quat_xyzw = convert_quat(quat_wxyz, to="xyzw") - self.sensor_quat_tensor_xyzw[:, :quat_xyzw.shape[1]] = quat_xyzw + self.sensor_quat_tensor_xyzw[:, : quat_xyzw.shape[1]] = quat_xyzw # Run LiDAR update lidar_tensor_local, dist_tensor = self.sensor.update() @@ -625,6 +642,7 @@ def _compute_lidar_points(self, robot_name: str, pos_w: torch.Tensor, quat_wxyz: } def __call__(self): + """Dispatch LiDAR queries for the configured simulator backend.""" if not self.enabled or not getattr(self, "_backend_ready", False): return {self.robots[0].name: None} @@ -638,5 +656,8 @@ def __call__(self): elif sim_type == "isaacsim": return self.isaacsim_call(robot_name) else: - warnings.warn(f"LidarPointCloud: simulator '{sim_type}' not supported for LiDAR pose fetch.") + warnings.warn( + f"LidarPointCloud: simulator '{sim_type}' not supported for LiDAR pose fetch.", + stacklevel=2, + ) return {robot_name: None} diff --git a/roboverse_learn/rl/unitree_rl/configs/cfg_randomizers.py b/roboverse_pack/randomization/humanoid.py similarity index 81% rename from roboverse_learn/rl/unitree_rl/configs/cfg_randomizers.py rename to roboverse_pack/randomization/humanoid.py index ead1b6442..873e7463b 100644 --- a/roboverse_learn/rl/unitree_rl/configs/cfg_randomizers.py +++ b/roboverse_pack/randomization/humanoid.py @@ -1,11 +1,13 @@ from __future__ import annotations -from typing import Literal + from copy import deepcopy -from loguru import logger as log +from typing import Literal + import torch +from loguru import logger as log -from metasim.sim.base import BaseSimHandler, BaseQueryType -from metasim.utils.math import sample_uniform, sample_log_uniform, sample_gaussian +from metasim.sim.base import BaseQueryType, BaseSimHandler +from metasim.utils.math import sample_gaussian, sample_log_uniform, sample_uniform try: import isaacgym # noqa: F401 @@ -19,6 +21,8 @@ class MaterialRandomizer(BaseQueryType): + """Randomize material properties (friction, restitution) for selected bodies.""" + handler: BaseSimHandler def __init__( @@ -33,9 +37,7 @@ def __init__( ): super().__init__() self.obj_name = obj_name - self.set_body_names = ( - [body_names] if isinstance(body_names, str) else body_names - ) + self.set_body_names = [body_names] if isinstance(body_names, str) else body_names self.static_friction_range = static_friction_range self.dynamic_friction_range = dynamic_friction_range self.restitution_range = restitution_range @@ -43,11 +45,13 @@ def __init__( self.make_consistent = make_consistent def bind_handler(self, handler: BaseSimHandler, *args, **kwargs): + """Attach to the simulator handler and initialize randomization buffers.""" super().bind_handler(handler, *args, **kwargs) self.simulator_name = handler.scenario.simulator self.initialize() def __call__(self, env_ids=None): + """Apply material randomization for provided environment ids.""" # resolve environment ids if env_ids is None: env_ids = torch.arange(self.handler.num_envs, device="cpu") @@ -56,6 +60,7 @@ def __call__(self, env_ids=None): self.randomize(env_ids) def initialize(self): + """Sample material buckets and cache body indices for randomization.""" # sample material properties from the given ranges # note: we only sample the materials once during initialization # afterwards these are randomly assigned to the geometries of the asset @@ -65,15 +70,11 @@ def initialize(self): self.restitution_range, ] ranges = torch.tensor(range_list, device="cpu") - self.material_buckets = sample_uniform( - ranges[:, 0], ranges[:, 1], (self.num_buckets, 3), device="cpu" - ) + self.material_buckets = sample_uniform(ranges[:, 0], ranges[:, 1], (self.num_buckets, 3), device="cpu") # ensure dynamic friction is always less than static friction if self.make_consistent: - self.material_buckets[:, 1] = torch.min( - self.material_buckets[:, 0], self.material_buckets[:, 1] - ) + self.material_buckets[:, 1] = torch.min(self.material_buckets[:, 0], self.material_buckets[:, 1]) self.body_names = self.handler.get_body_names(self.obj_name, sort=False) self.set_body_ids = ( @@ -109,20 +110,12 @@ def _get_set_shape_indices(self): if self.obj_name in self.all_robot_names: _tmp_handle = self.handler._robot_handles[0] elif self.obj_name in self.all_object_names: - _tmp_handle = self.handler._obj_handles[0][ - self.handler.objects.index(self.obj_name) - ] - body_shape_indices = self.handler.gym.get_actor_rigid_body_shape_indices( - self.handler._envs[0], _tmp_handle - ) + _tmp_handle = self.handler._obj_handles[0][self.handler.objects.index(self.obj_name)] + body_shape_indices = self.handler.gym.get_actor_rigid_body_shape_indices(self.handler._envs[0], _tmp_handle) num_shapes_per_body = [] for body_shape in body_shape_indices: num_shapes_per_body.append(body_shape.count) - expected_shapes = len( - self.handler.gym.get_actor_rigid_shape_properties( - self.handler._envs[0], _tmp_handle - ) - ) + expected_shapes = len(self.handler.gym.get_actor_rigid_shape_properties(self.handler._envs[0], _tmp_handle)) elif self.simulator_name == "mujoco": model = self.handler.physics.model num_shapes_per_body = [0] * model.nbody @@ -130,10 +123,7 @@ def _get_set_shape_indices(self): for geom_bodyid in model.geom_bodyid: num_shapes_per_body[geom_bodyid] += 1 expected_shapes = model.ngeom - if ( - num_shapes_per_body is not None - and sum(num_shapes_per_body) != expected_shapes - ): + if num_shapes_per_body is not None and sum(num_shapes_per_body) != expected_shapes: raise ValueError( "Randomization term 'randomize_rigid_body_material' failed to parse the number of shapes per body." f" Expected total shapes: {expected_shapes}, but got: {sum(num_shapes_per_body)}." @@ -155,6 +145,7 @@ def _get_set_shape_indices(self): return set_shape_indices def randomize(self, env_ids: torch.Tensor): + """Randomize material properties across supported simulators.""" if self.simulator_name == "isaacsim": self._randomize_isaacsim(env_ids) elif self.simulator_name == "isaacgym": @@ -180,9 +171,7 @@ def _randomize_isaacsim(self, env_ids: torch.Tensor): materials = obj_inst.root_physx_view.get_material_properties() # randomly assign material IDs to the geometries total_num_shapes = obj_inst.root_physx_view.max_shapes - bucket_ids = torch.randint( - 0, self.num_buckets, (len(env_ids), total_num_shapes), device="cpu" - ) + bucket_ids = torch.randint(0, self.num_buckets, (len(env_ids), total_num_shapes), device="cpu") material_samples = self.material_buckets[bucket_ids] # update material buffer with new samples @@ -207,17 +196,9 @@ def _randomize_isaacgym(self, env_ids: torch.Tensor): raise ValueError( f"Randomization term 'randomize_rigid_body_material' not supported for asset: {self.obj_name}." ) - max_body_shape = len( - self.handler.gym.get_actor_rigid_shape_properties( - self.handler._envs[0], _all_handles[0] - ) - ) - bucket_ids = torch.randint( - 0, self.num_buckets, (len(env_ids), max_body_shape), device="cpu" - ) - material_samples = self.material_buckets[ - bucket_ids - ] # static friction, dynamic friction and restitution + max_body_shape = len(self.handler.gym.get_actor_rigid_shape_properties(self.handler._envs[0], _all_handles[0])) + bucket_ids = torch.randint(0, self.num_buckets, (len(env_ids), max_body_shape), device="cpu") + material_samples = self.material_buckets[bucket_ids] # static friction, dynamic friction and restitution roll_friction_factor = 0.05 spin_friction_factor = 0.02 @@ -226,39 +207,25 @@ def _randomize_isaacgym(self, env_ids: torch.Tensor): # For objects, find the corresponding object handle _tmp_handle = _all_handles[env_id] # Get current rigid shape properties - shape_props = self.handler.gym.get_actor_rigid_shape_properties( - env, _tmp_handle - ) + shape_props = self.handler.gym.get_actor_rigid_shape_properties(env, _tmp_handle) for _id in self.set_shape_indices: shape_prop = shape_props[_id] shape_prop.friction = material_samples[i, _id, 0] - shape_prop.rolling_friction = ( - roll_friction_factor * material_samples[i, _id, 1] - ) - shape_prop.torsion_friction = ( - spin_friction_factor * material_samples[i, _id, 1] - ) + shape_prop.rolling_friction = roll_friction_factor * material_samples[i, _id, 1] + shape_prop.torsion_friction = spin_friction_factor * material_samples[i, _id, 1] shape_prop.restitution = material_samples[i, _id, 2] # Apply the modified properties - self.handler.gym.set_actor_rigid_shape_properties( - env, _tmp_handle, shape_props - ) + self.handler.gym.set_actor_rigid_shape_properties(env, _tmp_handle, shape_props) def _randomize_mujoco(self, env_ids: torch.Tensor): """Randomize friction and restitution for MuJoCo simulator.""" - assert ( - self.handler.num_envs == 1 - ), "MuJoCo handler only supports single environment." + assert self.handler.num_envs == 1, "MuJoCo handler only supports single environment." model = self.handler.physics.model - bucket_ids = torch.randint( - 0, self.num_buckets, (len(env_ids), model.ngeom), device="cpu" - ) - material_samples = self.material_buckets[ - bucket_ids - ] # static friction, dynamic friction and restitution + bucket_ids = torch.randint(0, self.num_buckets, (len(env_ids), model.ngeom), device="cpu") + material_samples = self.material_buckets[bucket_ids] # static friction, dynamic friction and restitution static_friction = material_samples[env_ids, self.set_shape_indices, 0] solimp_value = 0.1 * static_friction @@ -266,32 +233,24 @@ def _randomize_mujoco(self, env_ids: torch.Tensor): # model.geom_friction --> friction for (slide, spin, roll) dynamic_friction = material_samples[env_ids, self.set_shape_indices, 1] - model.geom_friction[self.set_shape_indices, 0] = ( - dynamic_friction # slide friction - ) - model.geom_friction[self.set_shape_indices, 1] = ( - 0.01 * dynamic_friction - ) # spin friction - model.geom_friction[self.set_shape_indices, 2] = ( - 0.01 * dynamic_friction - ) # roll friction + model.geom_friction[self.set_shape_indices, 0] = dynamic_friction # slide friction + model.geom_friction[self.set_shape_indices, 1] = 0.01 * dynamic_friction # spin friction + model.geom_friction[self.set_shape_indices, 2] = 0.01 * dynamic_friction # roll friction # restitution and damping calculation restitution_scale = 1.0 # from 0.5 - 2.0 - restitution = ( - material_samples[env_ids, self.set_shape_indices, 2] * restitution_scale - + 1e-6 + restitution = material_samples[env_ids, self.set_shape_indices, 2] * restitution_scale + 1e-6 + damping = (-torch.log(restitution) / torch.sqrt(torch.pi**2 + torch.log(restitution) ** 2)).clamp( + min=0.0, max=1.0 ) - damping = ( - -torch.log(restitution) - / torch.sqrt(torch.pi**2 + torch.log(restitution) ** 2) - ).clamp(min=0.0, max=1.0) # solref:timeconst & damping ratio model.geom_solref[self.set_shape_indices, 1] = damping class MassRandomizer(BaseQueryType): + """Randomize body masses for a given asset.""" + handler: BaseSimHandler def __init__( @@ -305,20 +264,20 @@ def __init__( ): super().__init__() self.obj_name = obj_name - self.set_body_names = ( - [body_names] if isinstance(body_names, str) else body_names - ) + self.set_body_names = [body_names] if isinstance(body_names, str) else body_names self.mass_distribution_params = mass_distribution_params self.operation = operation self.distribution = distribution self.recompute_inertia = recompute_inertia def bind_handler(self, handler: BaseSimHandler, *args, **kwargs): + """Attach to the simulator handler and prepare mass buffers.""" super().bind_handler(handler, *args, **kwargs) self.simulator_name = handler.scenario.simulator self.initialize() def initialize(self): + """Validate configuration and cache default masses.""" # check for valid operation if self.operation == "scale": _validate_scale_range( @@ -328,8 +287,7 @@ def initialize(self): ) elif self.operation not in ("abs", "add"): raise ValueError( - "Randomization term 'randomize_rigid_body_mass' does not support operation:" - f" '{self.operation}'." + f"Randomization term 'randomize_rigid_body_mass' does not support operation: '{self.operation}'." ) self.all_robot_names = [robot.name for robot in self.handler.robots] @@ -347,6 +305,7 @@ def initialize(self): self.default_masses = deepcopy(self._get_masses()) def __call__(self, env_ids: torch.Tensor | None = None): + """Apply mass randomization for selected environments.""" # resolve environment ids if env_ids is None: env_ids = torch.arange(self.handler.num_envs, device="cpu") @@ -373,8 +332,8 @@ def _get_masses_isaacsim(self): return masses def _get_masses_isaacgym(self): - """ - Get masses for IsaacGym simulator. + """Get masses for IsaacGym simulator. + Note that the isaacgym handler only support 1 robot and multiple objects, currently. """ # Initialize masses tensor @@ -393,15 +352,11 @@ def _get_masses_isaacgym(self): _tmp_handle = self.handler._robot_handles[0] elif self.obj_name in self.all_object_names: # Find the object handle - _tmp_handle = self.handler._obj_handles[env_id][ - self.handler.objects.index(self.obj_name) - ] + _tmp_handle = self.handler._obj_handles[env_id][self.handler.objects.index(self.obj_name)] else: raise ValueError(f"Not found: {self.obj_name}.") - body_props = self.handler.gym.get_actor_rigid_body_properties( - env, _tmp_handle - ) + body_props = self.handler.gym.get_actor_rigid_body_properties(env, _tmp_handle) for i, prop in enumerate(body_props): masses[env_id, i] = prop.mass @@ -409,18 +364,14 @@ def _get_masses_isaacgym(self): def _get_masses_mujoco(self): """Get masses for MuJoCo simulator.""" - assert ( - self.handler.num_envs == 1 - ), "MuJoCo handler only supports single environment." + assert self.handler.num_envs == 1, "MuJoCo handler only supports single environment." model = self.handler.physics.model body_masses = model.body_mass return torch.tensor( body_masses, dtype=torch.float32, device=self.handler.device, - ).unsqueeze( - 0 - ) # shape: (1, num_bodies) + ).unsqueeze(0) # shape: (1, num_bodies) def _set_masses(self, masses: torch.Tensor, env_ids: torch.Tensor): if self.simulator_name == "isaacsim": @@ -445,16 +396,12 @@ def _set_masses_isaacgym(self, masses: torch.Tensor, env_ids: torch.Tensor): _tmp_handle = self.handler._robot_handles[env_id] elif self.obj_name in self.all_object_names: # Find the object handle - _tmp_handle = self.handler._obj_handles[env_id][ - self.handler.objects.index(self.obj_name) - ] + _tmp_handle = self.handler._obj_handles[env_id][self.handler.objects.index(self.obj_name)] else: raise ValueError(f"Not found: {self.obj_name}.") # Get current body properties - body_props = self.handler.gym.get_actor_rigid_body_properties( - env, _tmp_handle - ) + body_props = self.handler.gym.get_actor_rigid_body_properties(env, _tmp_handle) # Update masses for specified bodies for body_idx in self.set_body_ids: @@ -462,9 +409,7 @@ def _set_masses_isaacgym(self, masses: torch.Tensor, env_ids: torch.Tensor): body_props[body_idx].mass = float(masses[env_id, body_idx]) # Apply the modified properties - self.handler.gym.set_actor_rigid_body_properties( - env, _tmp_handle, body_props - ) + self.handler.gym.set_actor_rigid_body_properties(env, _tmp_handle, body_props) def _set_masses_mujoco(self, masses: torch.Tensor, env_ids: torch.Tensor): model = self.handler.physics.model @@ -479,8 +424,7 @@ def _recompute_inertias(self, ratios: torch.Tensor, env_ids: torch.Tensor): inertias = obj_inst.root_physx_view.get_inertias() # inertia has shape: (num_envs, num_bodies, 9) for articulation inertias[env_ids[:, None], self.set_body_ids] = ( - obj_inst.data.default_inertia[env_ids[:, None], self.set_body_ids] - * ratios[..., None] + obj_inst.data.default_inertia[env_ids[:, None], self.set_body_ids] * ratios[..., None] ) elif self.obj_name in self.handler.scene.rigid_objects: obj_inst = self.handler.scene.rigid_objects[self.obj_name] @@ -502,6 +446,7 @@ def _recompute_inertias(self, ratios: torch.Tensor, env_ids: torch.Tensor): ) # only single env def randomize(self, env_ids: torch.Tensor): + """Randomize masses and optionally recompute inertias.""" if self.simulator_name not in ("isaacsim", "isaacgym", "mujoco"): log.warning( f"Mass randomization not implemented for simulator: {self.simulator_name}. This randomization step will be skipped." @@ -512,9 +457,7 @@ def randomize(self, env_ids: torch.Tensor): # apply randomization on default values # this is to make sure when calling the function multiple times, the randomization is applied on the # default values and not the previously randomized values - masses[env_ids[:, None], self.set_body_ids] = self.default_masses[ - env_ids[:, None], self.set_body_ids - ].clone() + masses[env_ids[:, None], self.set_body_ids] = self.default_masses[env_ids[:, None], self.set_body_ids].clone() # sample from the given range # note: we modify the masses in-place for all environments # however, the setter takes care that only the masses of the specified environments are modified @@ -531,8 +474,7 @@ def randomize(self, env_ids: torch.Tensor): if self.recompute_inertia: # compute the ratios of the new masses to the initial masses ratios = ( - masses[env_ids[:, None], self.set_body_ids] - / self.default_masses[env_ids[:, None], self.set_body_ids] + masses[env_ids[:, None], self.set_body_ids] / self.default_masses[env_ids[:, None], self.set_body_ids] ) self._recompute_inertias(ratios, env_ids) @@ -596,17 +538,11 @@ def _randomize_prop_by_op( ) # perform the operation if operation == "add": - data[dim_0_ids, dim_1_ids] += dist_fn( - *distribution_parameters, (n_dim_0, n_dim_1), device=data.device - ) + data[dim_0_ids, dim_1_ids] += dist_fn(*distribution_parameters, (n_dim_0, n_dim_1), device=data.device) elif operation == "scale": - data[dim_0_ids, dim_1_ids] *= dist_fn( - *distribution_parameters, (n_dim_0, n_dim_1), device=data.device - ) + data[dim_0_ids, dim_1_ids] *= dist_fn(*distribution_parameters, (n_dim_0, n_dim_1), device=data.device) elif operation == "abs": - data[dim_0_ids, dim_1_ids] = dist_fn( - *distribution_parameters, (n_dim_0, n_dim_1), device=data.device - ) + data[dim_0_ids, dim_1_ids] = dist_fn(*distribution_parameters, (n_dim_0, n_dim_1), device=data.device) else: raise NotImplementedError( f"Unknown operation: '{operation}' for property randomization. Please use 'add', 'scale', or 'abs'." @@ -621,8 +557,7 @@ def _validate_scale_range( allow_negative: bool = False, allow_zero: bool = True, ) -> None: - """ - Validates a (low, high) tuple used in scale-based randomization. + """Validates a (low, high) tuple used in scale-based randomization. This function ensures the tuple follows expected rules when applying a 'scale' operation. It performs type and value checks, optionally allowing negative or @@ -649,17 +584,11 @@ def _validate_scale_range( return low, high = params if not isinstance(low, (int, float)) or not isinstance(high, (int, float)): - raise TypeError( - f"{name}: expected (low, high) to be a tuple of numbers, got {params}." - ) + raise TypeError(f"{name}: expected (low, high) to be a tuple of numbers, got {params}.") if not allow_negative and not allow_zero and low <= 0: - raise ValueError( - f"{name}: lower bound must be > 0 when using the 'scale' operation (got {low})." - ) + raise ValueError(f"{name}: lower bound must be > 0 when using the 'scale' operation (got {low}).") if not allow_negative and allow_zero and low < 0: - raise ValueError( - f"{name}: lower bound must be ≥ 0 when using the 'scale' operation (got {low})." - ) + raise ValueError(f"{name}: lower bound must be ≥ 0 when using the 'scale' operation (got {low}).") if high < low: raise ValueError(f"{name}: upper bound ({high}) must be ≥ lower bound ({low}).") diff --git a/roboverse_pack/tasks/humanoid/__init__.py b/roboverse_pack/tasks/humanoid/__init__.py new file mode 100644 index 000000000..9be6186b0 --- /dev/null +++ b/roboverse_pack/tasks/humanoid/__init__.py @@ -0,0 +1,36 @@ +"""Humanoid task package for RoboVerse. + +This package exposes environments and task wrappers used for legged robots +and humanoids within the RoboVerse ecosystem. +""" + +from __future__ import annotations + +import importlib +import traceback +from pathlib import Path + + +def _import_task_modules() -> None: + """Eagerly import task modules so @register_task runs, avoiding a full package crawl. + + We only import modules under `locomotion/` to register tasks while steering + clear of a recursive import of every helper/callback module, which was + triggering circular-import timing issues. + """ + pkg_dir = Path(__file__).resolve().parent + locomotion_dir = pkg_dir / "locomotion" + if not locomotion_dir.exists(): + return + + for py_file in sorted(locomotion_dir.glob("*.py")): + if py_file.name.startswith("_") or py_file.name == "__init__.py": + continue + try: + module_name = ".".join((__name__, "locomotion", py_file.with_suffix("").name)) + importlib.import_module(module_name) + except Exception: + traceback.print_exc() + + +_import_task_modules() diff --git a/roboverse_pack/tasks/unitree_rl/base/__init__.py b/roboverse_pack/tasks/humanoid/base/__init__.py similarity index 100% rename from roboverse_pack/tasks/unitree_rl/base/__init__.py rename to roboverse_pack/tasks/humanoid/base/__init__.py diff --git a/roboverse_pack/tasks/unitree_rl/base/base_agent.py b/roboverse_pack/tasks/humanoid/base/base_agent.py similarity index 98% rename from roboverse_pack/tasks/unitree_rl/base/base_agent.py rename to roboverse_pack/tasks/humanoid/base/base_agent.py index 0b7335107..94eca3a36 100644 --- a/roboverse_pack/tasks/unitree_rl/base/base_agent.py +++ b/roboverse_pack/tasks/humanoid/base/base_agent.py @@ -12,7 +12,7 @@ from metasim.task.rl_task import RLTaskEnv from metasim.types import Action, Reward, TensorState from metasim.utils.state import list_state_to_tensor -from roboverse_learn.rl.unitree_rl.configs.cfg_base import BaseEnvCfg, CallbacksCfg +from roboverse_pack.tasks.humanoid.cfg_base import BaseEnvCfg, CallbacksCfg class AgentTask(RLTaskEnv): diff --git a/roboverse_pack/tasks/unitree_rl/base/base_legged_robot.py b/roboverse_pack/tasks/humanoid/base/base_legged_robot.py similarity index 99% rename from roboverse_pack/tasks/unitree_rl/base/base_legged_robot.py rename to roboverse_pack/tasks/humanoid/base/base_legged_robot.py index a1191b247..9202849cb 100644 --- a/roboverse_pack/tasks/unitree_rl/base/base_legged_robot.py +++ b/roboverse_pack/tasks/humanoid/base/base_legged_robot.py @@ -9,13 +9,13 @@ from metasim.scenario.scenario import ScenarioCfg from metasim.utils.state import TensorState -from roboverse_learn.rl.unitree_rl.configs.cfg_base import BaseEnvCfg -from roboverse_learn.rl.unitree_rl.helper import ( +from roboverse_pack.robots import G1Dof12Cfg, Go2Cfg +from roboverse_pack.tasks.humanoid.cfg_base import BaseEnvCfg +from roboverse_pack.utils.humanoid_utils import ( get_axis_params, get_reward_fn, pattern_match, ) -from roboverse_pack.robots import G1Dof12Cfg, Go2Cfg from .base_agent import AgentTask diff --git a/roboverse_pack/tasks/unitree_rl/base/types.py b/roboverse_pack/tasks/humanoid/base/types.py similarity index 100% rename from roboverse_pack/tasks/unitree_rl/base/types.py rename to roboverse_pack/tasks/humanoid/base/types.py diff --git a/roboverse_learn/rl/unitree_rl/configs/cfg_base.py b/roboverse_pack/tasks/humanoid/cfg_base.py similarity index 77% rename from roboverse_learn/rl/unitree_rl/configs/cfg_base.py rename to roboverse_pack/tasks/humanoid/cfg_base.py index e42c042a4..30a3c6569 100644 --- a/roboverse_learn/rl/unitree_rl/configs/cfg_base.py +++ b/roboverse_pack/tasks/humanoid/cfg_base.py @@ -1,13 +1,15 @@ from __future__ import annotations -from typing import Callable, Literal + from dataclasses import MISSING +from typing import Callable from metasim.utils import configclass -from metasim.queries import ContactForces @configclass class CallbacksCfg: + """Callback configuration buckets used by humanoid tasks.""" + setup: dict = {} reset: dict = {} # func_name: (func(env, env_ids,**kwargs), kwargs) pre_step: dict = {} # func_name: (func(env, actions, **kwargs), kwargs) @@ -18,9 +20,7 @@ class CallbacksCfg: @configclass class BaseEnvCfg: - """ - The base class of environment configuration for legged robots. - """ + """The base class of environment configuration for legged robots.""" episode_length_s = 20.0 obs_len_history = 0 # number of past observations to include in the observation @@ -28,6 +28,8 @@ class BaseEnvCfg: @configclass class Control: + """Low-level control parameters for the robot actuators.""" + torque_limits_factor: float = 1.0 # scale torque limits from urdf soft_joint_pos_limit_factor: float = 1.0 # scale dof pos limits from urdf action_clip: float = 100.0 @@ -39,8 +41,12 @@ class Control: @configclass class Commands: + """Command sampling configuration for velocity targets.""" + @configclass class Ranges: + """Command bounds for velocity and heading.""" + lin_vel_x = [-1.0, 1.0] # min max [m/s] lin_vel_y = [-1.0, 1.0] # min max [m/s] ang_vel_yaw = [-1, 1] # min max [rad/s] @@ -59,6 +65,8 @@ class Ranges: @configclass class Curriculum: + """Curriculum schedule configuration.""" + enabled = False funcs: dict[str, Callable] = MISSING @@ -66,15 +74,19 @@ class Curriculum: @configclass class Rewards: - only_positive_rewards = False # if true negative total rewards are clipped at zero (avoids early termination problems) - functions: list[Callable] | str = ( - "roboverse_learn.rl.unitree_rl.configs.callback_funcs.reward_funcs" + """Reward configuration and scaling.""" + + only_positive_rewards = ( + False # if true negative total rewards are clipped at zero (avoids early termination problems) ) + functions: list[Callable] | str = "roboverse_pack.callback_funcs.humanoid.reward_funcs" scales: any = MISSING rewards = Rewards() class InitialStates: + """Default spawn positions for robots and objects.""" + objects = {} robots = { "g1_dof12": {"pos": [0.0, 0.0, 0.8]}, @@ -112,13 +124,12 @@ class InitialStates: def __post_init__(self): self.callbacks = CallbacksCfg() - _normalize = lambda value: {} if value is MISSING else value - self.callbacks.query = _normalize(self.callbacks_query) - self.callbacks.terminate = _normalize(self.callbacks_terminate) - self.callbacks.setup = _normalize(self.callbacks_setup) - self.callbacks.reset = _normalize(self.callbacks_reset) - self.callbacks.pre_step = _normalize(self.callbacks_pre_step) - self.callbacks.post_step = _normalize(self.callbacks_post_step) + self.callbacks.query = self._normalize(self.callbacks_query) + self.callbacks.terminate = self._normalize(self.callbacks_terminate) + self.callbacks.setup = self._normalize(self.callbacks_setup) + self.callbacks.reset = self._normalize(self.callbacks_reset) + self.callbacks.pre_step = self._normalize(self.callbacks_pre_step) + self.callbacks.post_step = self._normalize(self.callbacks_post_step) # Type check for callbacks for cb_attr in [ @@ -143,3 +154,8 @@ def __post_init__(self): raise ValueError( f"Callback {func_name} in {cb_attr} must be a callable or a tuple of (callable, dict)." ) + + @staticmethod + def _normalize(value): + """Convert MISSING sentinel to an empty dict for callback containers.""" + return {} if value is MISSING else value diff --git a/roboverse_pack/tasks/humanoid/locomotion/__init__.py b/roboverse_pack/tasks/humanoid/locomotion/__init__.py new file mode 100644 index 000000000..8682b16ca --- /dev/null +++ b/roboverse_pack/tasks/humanoid/locomotion/__init__.py @@ -0,0 +1 @@ +"""Locomotion task registrations for humanoid robots.""" diff --git a/roboverse_pack/tasks/unitree_rl/locomotion/walk_g1_dof12.py b/roboverse_pack/tasks/humanoid/locomotion/walk_g1_dof12.py similarity index 55% rename from roboverse_pack/tasks/unitree_rl/locomotion/walk_g1_dof12.py rename to roboverse_pack/tasks/humanoid/locomotion/walk_g1_dof12.py index b5cd2609b..7832c4f70 100644 --- a/roboverse_pack/tasks/unitree_rl/locomotion/walk_g1_dof12.py +++ b/roboverse_pack/tasks/humanoid/locomotion/walk_g1_dof12.py @@ -1,20 +1,170 @@ from __future__ import annotations import copy +import math import torch +from metasim.queries import ContactForces from metasim.scenario.lights import DomeLightCfg from metasim.scenario.scenario import ScenarioCfg from metasim.scenario.simulator_params import SimParamCfg from metasim.task.registry import register_task from metasim.types import TensorState +from metasim.utils import configclass from metasim.utils.math import quat_rotate_inverse -from roboverse_learn.rl.unitree_rl.configs.locomotion.walk_g1_dof12 import ( - WalkG1Dof12EnvCfg, - WalkG1Dof12RslRlTrainCfg, +from roboverse_pack.callback_funcs.humanoid import ( + reset_funcs, + reward_funcs, + step_funcs, + termination_funcs, ) -from roboverse_pack.tasks.unitree_rl.base import LeggedRobotTask +from roboverse_pack.randomization.humanoid import ( + MassRandomizer, + MaterialRandomizer, +) +from roboverse_pack.tasks.humanoid.base import LeggedRobotTask +from roboverse_pack.tasks.humanoid.cfg_base import BaseEnvCfg +from roboverse_pack.utils.curriculum_utils import lin_vel_cmd_levels + + +@configclass +class WalkG1Dof12EnvCfg(BaseEnvCfg): + """Configuration for the 12-DOF G1 walking task.""" + + episode_length_s = 20.0 + obs_len_history = 1 + priv_obs_len_history = 1 + + control = BaseEnvCfg.Control(action_scale=0.25, action_clip=100, soft_joint_pos_limit_factor=0.9) + + @configclass + class RewardsScales: + """Reward weights for gait stability and efficiency.""" + + track_lin_vel_xy = (1.0, {"std": math.sqrt(0.25)}) + track_ang_vel_z = (0.5, {"std": math.sqrt(0.25)}) + lin_vel_z = -2.0 + ang_vel_xy = -0.05 + flat_orientation = -1.0 + base_height = (-10.0, {"target_height": 0.78}) + joint_acc = -2.5e-7 + joint_vel = -0.001 + action_rate = -0.01 + joint_pos_limits = -5.0 + is_alive = 0.15 + joint_deviation_legs = ( + -1.0, + {"joint_names": (".*_hip_roll_joint", ".*_hip_yaw_joint")}, + reward_funcs.joint_deviation_l1, + ) + feet_slide = (-0.2, {"body_names": (".*ankle_roll.*")}) + # feet_swing_height = -20.0 + feet_clearance = ( + 1.0, + { + "std": math.sqrt(0.05), + "tanh_mult": 2.0, + "target_height": 0.1, + "body_names": (".*ankle_roll.*"), + }, + ) + # contact = 0.18 + feet_gait = ( + 0.18, + { + "period": 0.8, + "offset": [0.0, 0.5], + "threshold": 0.55, + "body_names": (".*ankle_roll.*"), + }, + ) + energy = -1e-5 + ######################## + + rewards = BaseEnvCfg.Rewards(scales=RewardsScales(), only_positive_rewards=True) + + commands = BaseEnvCfg.Commands( + value=None, + resample=step_funcs.resample_commands, + heading_command=True, + resampling_time=10.0, + rel_standing_envs=0.02, + ranges=BaseEnvCfg.Commands.Ranges( + lin_vel_x=(-1.0, 1.0), + lin_vel_y=(-1.0, 1.0), + ang_vel_yaw=(-1.0, 1.0), + heading=(-3.14, 3.14), + ), + limit_ranges=BaseEnvCfg.Commands.Ranges( + lin_vel_x=(-1.0, 1.0), + lin_vel_y=(-1.0, 1.0), + ang_vel_yaw=(-1.0, 1.0), + heading=(-3.14, 3.14), + ), + ) + + curriculum = BaseEnvCfg.Curriculum(enabled=False, funcs={"lin_vel_cmd_levels": lin_vel_cmd_levels}) + + callbacks_query = {"contact_forces": ContactForces(history_length=3)} + callbacks_setup = { + "material_randomizer": MaterialRandomizer( + obj_name="g1_dof12", + static_friction_range=(0.1, 1.25), + dynamic_friction_range=(0.1, 1.25), + restitution_range=(0.0, 0.0), + num_buckets=64, + ), + "mass_randomizer": MassRandomizer( + obj_name="g1_dof12", + body_names="pelvis", + mass_distribution_params=(-1.0, 3.0), + operation="add", + ), + } + callbacks_reset = { + "random_root_state": ( + reset_funcs.random_root_state_terrain_aware, + { + "pose_range": [ + [0.0, 0.0, 0, 0, 0, 0], # x, y, z_offset, roll, pitch, yaw + [0.0, 0.0, 0, 0, 0, 0], + ], + "velocity_range": [[-0.5] * 6, [0.5] * 6], + # base_height_offset is None by default, uses robot's default z position (0.8m from cfg_base.py) + }, + ), + "reset_joints_by_scale": ( + reset_funcs.reset_joints_by_scale, + {"position_range": (0.5, 1.5), "velocity_range": (1.0, 1.0)}, + ), + } + callbacks_post_step = { + "push_robot": ( + step_funcs.push_by_setting_velocity, + { + "interval_range_s": (5.0, 5.0), + "velocity_range": [[-1.5, -1.5, 0.0], [1.5, 1.5, 0.0]], + }, + ) + } + callbacks_terminate = { + "time_out": termination_funcs.time_out, + "undesired_contact": ( + termination_funcs.undesired_contact, + { + "contact_names": [ + ".*_elbow_.*", + ".*_shoulder_.*", + ".*_wrist_.*", + "pelvis", + "torso_link", + ], + "limit_range": 1.0, + }, + ), + "bad_orientation": (termination_funcs.bad_orientation, {"limit_angle": 0.8}), + } @register_task( @@ -26,7 +176,6 @@ class WalkG1Dof12Task(LeggedRobotTask): """Registered task wrapper with scenario defaults and cfg hooks.""" env_cfg_cls = WalkG1Dof12EnvCfg - train_cfg_cls = WalkG1Dof12RslRlTrainCfg task_name = "walk_g1_dof12" scenario = ScenarioCfg( diff --git a/roboverse_pack/tasks/unitree_rl/locomotion/walk_g1_dof29.py b/roboverse_pack/tasks/humanoid/locomotion/walk_g1_dof29.py similarity index 51% rename from roboverse_pack/tasks/unitree_rl/locomotion/walk_g1_dof29.py rename to roboverse_pack/tasks/humanoid/locomotion/walk_g1_dof29.py index e7bd95186..e379ef36a 100644 --- a/roboverse_pack/tasks/unitree_rl/locomotion/walk_g1_dof29.py +++ b/roboverse_pack/tasks/humanoid/locomotion/walk_g1_dof29.py @@ -1,20 +1,171 @@ from __future__ import annotations import copy +import math import torch +import roboverse_pack.utils.curriculum_utils as curr_funs +from metasim.queries import ContactForces from metasim.scenario.lights import DomeLightCfg from metasim.scenario.scenario import ScenarioCfg from metasim.scenario.simulator_params import SimParamCfg from metasim.task.registry import register_task from metasim.types import TensorState +from metasim.utils import configclass from metasim.utils.math import quat_rotate_inverse -from roboverse_learn.rl.unitree_rl.configs.locomotion.walk_g1_dof29 import ( - WalkG1Dof29EnvCfg, - WalkG1Dof29EnvRslRlTrainCfg, +from roboverse_pack.callback_funcs.humanoid import ( + reset_funcs, + reward_funcs, + step_funcs, + termination_funcs, ) -from roboverse_pack.tasks.unitree_rl.base import LeggedRobotTask +from roboverse_pack.queries.lidar import LidarPointCloud +from roboverse_pack.randomization.humanoid import ( + MassRandomizer, + MaterialRandomizer, +) +from roboverse_pack.tasks.humanoid.base import LeggedRobotTask +from roboverse_pack.tasks.humanoid.cfg_base import BaseEnvCfg + + +@configclass +class WalkG1Dof29EnvCfg(BaseEnvCfg): + """Environment configuration for humanoid walking task.""" + + obs_len_history = 5 + priv_obs_len_history = 5 + episode_length_s = 20.0 + + control = BaseEnvCfg.Control(action_scale=0.25, soft_joint_pos_limit_factor=0.9) + + @configclass + class RewardsScales: + """Reward weights for gait, posture, and energy usage.""" + + track_lin_vel_xy = (1.0, {"std": math.sqrt(0.25)}) + track_ang_vel_z = (0.5, {"std": math.sqrt(0.25)}) + is_alive = 0.15 + lin_vel_z = -2.0 + ang_vel_xy = -0.05 + joint_vel = -0.001 + joint_acc = -2.5e-7 + action_rate = -0.05 + joint_pos_limits = -5.0 + energy = -2e-5 + joint_deviation_arms = ( + -0.1, + {"joint_names": (".*_shoulder_.*_joint", ".*_elbow_joint", ".*_wrist_.*")}, + reward_funcs.joint_deviation_l1, + ) + joint_deviation_waists = ( + -1.0, + {"joint_names": "waist.*"}, + reward_funcs.joint_deviation_l1, + ) + joint_deviation_legs = ( + -1.0, + {"joint_names": (".*_hip_roll_joint", ".*_hip_yaw_joint")}, + reward_funcs.joint_deviation_l1, + ) + flat_orientation = -5.0 + base_height = (-10.0, {"target_height": 0.78}) + feet_gait = ( + 0.5, + { + "period": 0.8, + "offset": [0.0, 0.5], + "threshold": 0.55, + "body_names": (".*ankle_roll.*"), + }, + ) + feet_slide = (-0.2, {"body_names": (".*ankle_roll.*")}) + feet_clearance = ( + 1.0, + { + "std": math.sqrt(0.05), + "tanh_mult": 2.0, + "target_height": 0.1, + "body_names": (".*ankle_roll.*"), + }, + ) + undesired_contacts = (-1.0, {"threshold": 1, "body_names": ("(?!.*ankle.*).*")}) + + rewards = BaseEnvCfg.Rewards( + only_positive_rewards=False, + scales=RewardsScales(), + ) + + commands = BaseEnvCfg.Commands( + value=None, + resample=step_funcs.resample_commands, + heading_command=False, + rel_standing_envs=0.02, + ranges=BaseEnvCfg.Commands.Ranges(lin_vel_x=(-0.1, 0.1), lin_vel_y=(-0.1, 0.1), ang_vel_yaw=(-0.1, 0.1)), + limit_ranges=BaseEnvCfg.Commands.Ranges(lin_vel_x=(-0.5, 1.0), lin_vel_y=(-0.3, 0.3), ang_vel_yaw=(-0.2, 0.2)), + ) + + curriculum = BaseEnvCfg.Curriculum( + enabled=True, + funcs={ + "lin_vel_cmd_levels": curr_funs.lin_vel_cmd_levels, + # "terrain_levels": curr_funs.terrain_levels_vel + }, + ) + + callbacks_query = { + "contact_forces": ContactForces(history_length=3), + "lidar_point_cloud": LidarPointCloud(enabled=False), + } + callbacks_setup = { + "material_randomizer": MaterialRandomizer( + obj_name="g1_dof29", + static_friction_range=(0.3, 1.0), + dynamic_friction_range=(0.3, 1.0), + restitution_range=(0.0, 0.0), + num_buckets=64, + ), + "mass_randomizer": MassRandomizer( + obj_name="g1_dof29", + body_names="torso_link", + mass_distribution_params=(-1.0, 3.0), + operation="add", + ), + } + callbacks_reset = { + "random_root_state": ( + reset_funcs.random_root_state_terrain_aware, + { + "pose_range": [ + [-0.5, -0.5, 0.0, 0, 0, -3.14], # x, y, z_offset, roll, pitch, yaw + [0.5, 0.5, 0.05, 0, 0, 3.14], # z_offset can vary slightly + ], + "velocity_range": [[0] * 6, [0] * 6], + # base_height_offset is None by default, uses robot's default z position (0.8m from cfg_base.py) + }, + ), + "reset_joints_by_scale": ( + reset_funcs.reset_joints_by_scale, + {"position_range": (1.0, 1.0), "velocity_range": (-1.0, 1.0)}, + ), + } + callbacks_post_step = { + "push_robot": ( + step_funcs.push_by_setting_velocity, + { + "interval_range_s": (5.0, 5.0), + "velocity_range": [[-0.5, -0.5, 0.0], [0.5, 0.5, 0.0]], + }, + ) + } + callbacks_terminate = { + "time_out": termination_funcs.time_out, + "base_height": ( + termination_funcs.root_height_below_minimum, + {"minimum_height": 0.2}, + ), + "bad_orientation": (termination_funcs.bad_orientation, {"limit_angle": 0.8}), + } @register_task( @@ -26,7 +177,6 @@ class WalkG1Dof29Task(LeggedRobotTask): """Registered humanoid locomotion task.""" env_cfg_cls = WalkG1Dof29EnvCfg - train_cfg_cls = WalkG1Dof29EnvRslRlTrainCfg task_name = "walk_g1_dof29" scenario = ScenarioCfg( diff --git a/roboverse_pack/tasks/unitree_rl/__init__.py b/roboverse_pack/tasks/unitree_rl/__init__.py deleted file mode 100644 index 4e92f64dc..000000000 --- a/roboverse_pack/tasks/unitree_rl/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Unitree RL task package for RoboVerse. - -This package exposes environments and task wrappers used for legged robots -and humanoids within the RoboVerse ecosystem. -""" - -from __future__ import annotations - -import importlib -import traceback -from pathlib import Path - - -def _auto_import_submodules() -> None: - pkg_dir = Path(__file__).resolve().parent - pkg_name = __name__ - - for py_file in pkg_dir.rglob("*.py"): - if py_file.name == "__init__.py": - continue - try: - rel = py_file.relative_to(pkg_dir).with_suffix("") - dotted = ".".join((pkg_name, *rel.parts)) - importlib.import_module(dotted) - except Exception: - traceback.print_exc() - - -_auto_import_submodules() diff --git a/roboverse_pack/tasks/unitree_rl/loco_manipulation/catch_humanoid.py b/roboverse_pack/tasks/unitree_rl/loco_manipulation/catch_humanoid.py deleted file mode 100644 index a75367f62..000000000 --- a/roboverse_pack/tasks/unitree_rl/loco_manipulation/catch_humanoid.py +++ /dev/null @@ -1,74 +0,0 @@ -import torch - -from metasim.types import TensorState -from metasim.utils.math import quat_rotate_inverse -from roboverse_pack.tasks.unitree_rl.base import LeggedRobotTask - - -class CatchHumanoidTask(LeggedRobotTask): - """Humanoid locomotion-manipulation task for catching an object.""" - - def _init_buffers(self): - self.noise_scale_vec = self._get_noise_scale_vec() - return super()._init_buffers() - - def _get_noise_scale_vec(self): - noise_vec = torch.zeros(size=(47,), dtype=torch.float, device=self.device) - self.add_noise = self.cfg.noise.add_noise - noise_scales = self.cfg.noise.scales - noise_level = self.cfg.noise.noise_level - noise_vec[:3] = noise_scales.ang_vel * noise_level * self.cfg.normalization.obs_scales.ang_vel - noise_vec[3:6] = noise_scales.gravity * noise_level - noise_vec[6:9] = 0.0 # commands - noise_vec[9 : 9 + self.num_actions] = ( - noise_scales.dof_pos * noise_level * self.cfg.normalization.obs_scales.dof_pos - ) - noise_vec[9 + self.num_actions : 9 + 2 * self.num_actions] = ( - noise_scales.dof_vel * noise_level * self.cfg.normalization.obs_scales.dof_vel - ) - noise_vec[9 + 2 * self.num_actions : 9 + 3 * self.num_actions] = 0.0 # previous actions - noise_vec[9 + 3 * self.num_actions : 9 + 3 * self.num_actions + 2] = 0.0 # sin/cos phase - - return noise_vec - - def _compute_task_observations(self, env_states: TensorState): - robot_state = env_states.robots[self.name] - base_quat = robot_state.root_state[:, 3:7] - base_lin_vel = quat_rotate_inverse(base_quat, robot_state.root_state[:, 7:10]) - base_ang_vel = quat_rotate_inverse(base_quat, robot_state.root_state[:, 10:13]) - projected_gravity = quat_rotate_inverse(base_quat, self.gravity_vec) - - q = (env_states.robots[self.name].joint_pos - self.default_dof_pos) * self.cfg.normalization.obs_scales.dof_pos - dq = env_states.robots[self.name].joint_vel * self.cfg.normalization.obs_scales.dof_vel - - obs_buf = torch.cat( - ( - base_ang_vel * self.cfg.normalization.obs_scales.ang_vel, # 3 - projected_gravity, # 3 - self.commands[:, :3] * self.commands_scale, # 3 - q, # num_actions - dq, # num_actions - self.actions, # num_actions - # self.history_buffer['actions'][-1] # num_actions - ), - dim=-1, - ) - - # add noise if needed - if self.add_noise: - obs_buf += (2 * torch.rand_like(obs_buf) - 1) * self.noise_scale_vec - - priv_obs_buf = torch.cat( - ( - base_lin_vel * self.cfg.normalization.obs_scales.lin_vel, - base_ang_vel * self.cfg.normalization.obs_scales.ang_vel, - projected_gravity, - self.commands[:, :3] * self.commands_scale, - q, # num_actions - dq, # num_actions - self.actions, - ), - dim=-1, - ) - - return obs_buf, priv_obs_buf diff --git a/roboverse_learn/rl/unitree_rl/helper/curriculum_utils.py b/roboverse_pack/utils/curriculum_utils.py similarity index 84% rename from roboverse_learn/rl/unitree_rl/helper/curriculum_utils.py rename to roboverse_pack/utils/curriculum_utils.py index d68008f05..6349ce05e 100644 --- a/roboverse_learn/rl/unitree_rl/helper/curriculum_utils.py +++ b/roboverse_pack/utils/curriculum_utils.py @@ -1,7 +1,10 @@ from __future__ import annotations + from typing import Sequence + import torch -from roboverse_pack.tasks.unitree_rl.base.types import EnvTypes + +from roboverse_pack.tasks.humanoid.base.types import EnvTypes def lin_vel_cmd_levels( @@ -9,16 +12,14 @@ def lin_vel_cmd_levels( env_ids: list[int] | torch.Tensor, reward_term_name: str = "track_lin_vel_xy", ) -> torch.Tensor: + """Curriculum for linear velocity commands based on reward performance.""" if env.common_step_counter % env.max_episode_steps == 0: command_term = env.commands_manager ranges = command_term.ranges limit_ranges = command_term.limit_ranges reward_term_scales = env.reward_scales[reward_term_name][0] / env.step_dt - reward = ( - torch.mean(env.episode_rewards[reward_term_name][env_ids]) - / env.cfg.episode_length_s - ) + reward = torch.mean(env.episode_rewards[reward_term_name][env_ids]) / env.cfg.episode_length_s if reward > reward_term_scales * 0.8: delta_command = torch.tensor([-0.1, 0.1], device=env.device) @@ -55,18 +56,11 @@ def terrain_levels_vel(env: EnvTypes, env_ids: Sequence[int]) -> torch.Tensor: terrain = env.handler.terrain command = env.commands_manager.value # compute the distance the robot walked - distance = torch.norm( - base.root_state[env_ids, :2] - env.handler.scene.env_origins[env_ids, :2], dim=1 - ) + distance = torch.norm(base.root_state[env_ids, :2] - env.handler.scene.env_origins[env_ids, :2], dim=1) # robots that walked far enough progress to harder terrains move_up = distance > terrain.cfg.terrain_generator.size[0] / 2 # robots that walked less than half of their required distance go to simpler terrains - move_down = ( - distance - < torch.norm(command[env_ids, :2], dim=1) - * (env.max_episode_steps * env.step_dt) - * 0.5 - ) + move_down = distance < torch.norm(command[env_ids, :2], dim=1) * (env.max_episode_steps * env.step_dt) * 0.5 move_down *= ~move_up # update terrain levels terrain.update_env_origins(env_ids, move_up, move_down) diff --git a/roboverse_learn/rl/unitree_rl/helper/utils.py b/roboverse_pack/utils/humanoid_utils.py similarity index 65% rename from roboverse_learn/rl/unitree_rl/helper/utils.py rename to roboverse_pack/utils/humanoid_utils.py index 9a187e187..ced4b4093 100644 --- a/roboverse_learn/rl/unitree_rl/helper/utils.py +++ b/roboverse_pack/utils/humanoid_utils.py @@ -1,46 +1,12 @@ from __future__ import annotations -from typing import Callable import re -import os -import datetime -from loguru import logger as log from functools import lru_cache +from typing import Callable import torch -def get_log_dir(task_name: str, now=None) -> str: - """Get the log directory.""" - if now is None: - now = datetime.datetime.now().strftime("%Y_%m%d_%H%M%S") - log_dir = f"./outputs/unitree_rl/{task_name}/{now}" - if not os.path.exists(log_dir): - os.makedirs(log_dir, exist_ok=True) - log.info("Log directory: {}", log_dir) - return log_dir - - -def get_load_path(load_root: str, checkpoint: int | str = None) -> str: - """Get the path to load the model from.""" - if isinstance(checkpoint, int): - if checkpoint == -1: - models = [ - file - for file in os.listdir(load_root) - if "model" in file and file.endswith(".pt") - ] - models.sort(key=lambda m: f"{m!s:0>15}") - model = models[-1] - load_path = f"{load_root}/{model}" - else: - load_path = f"{load_root}/model_{checkpoint}.pt" - else: - load_path = f"{load_root}/{checkpoint}.pt" - log.info(f"Loading checkpoint {checkpoint} from {load_root}") - return load_path - - def get_indices_from_substring( candidates_list: list[str] | tuple[str] | str, data_base: list[str], @@ -51,7 +17,7 @@ def get_indices_from_substring( Args: candidates_list: Single pattern or list of patterns (supports regex if use_regex=True) data_base: List of names to search in - use_regex: If True, treat candidates as regex patterns. If False, use substring matching. + fullmatch: If True, require full regex match; otherwise allow substring search. Returns: Sorted tensor of matching indices @@ -65,16 +31,14 @@ def get_indices_from_substring( found_indices = [] if isinstance(candidates_list, str): candidates_list = (candidates_list,) - assert isinstance( - candidates_list, (list, tuple) - ), "candidates_list must be a list, tuple or string." + assert isinstance(candidates_list, (list, tuple)), "candidates_list must be a list, tuple or string." for candidate in candidates_list: # Compile regex pattern for efficiency try: pattern = re.compile(candidate) except re.error as e: - raise ValueError(f"Invalid regex pattern '{candidate}': {e}") + raise ValueError(f"Invalid regex pattern '{candidate}': {e}") from e for i, name in enumerate(data_base): if fullmatch and pattern.fullmatch(name): @@ -106,9 +70,7 @@ def get_reward_fn(target: str, reward_functions: list[Callable] | str) -> Callab reward_module = __import__(reward_functions, fromlist=[target]) fn = getattr(reward_module, target, None) else: - raise ValueError( - "reward_functions should be a list of functions or a string module path" - ) + raise ValueError("reward_functions should be a list of functions or a string module path") if fn is None: raise KeyError(f"No reward function named '{target}'") return fn @@ -126,10 +88,11 @@ def get_axis_params(value, axis_idx, x_value=0.0, n_dims=3): @lru_cache(maxsize=128) def hash_names(names: str | tuple[str]) -> str: + """Hash a string or tuple of strings into a stable key.""" if isinstance(names, str): names = (names,) - assert isinstance(names, tuple) and all( - isinstance(_, str) for _ in names - ), "body_names must be a string or a list of strings." + assert isinstance(names, tuple) and all(isinstance(_, str) for _ in names), ( + "body_names must be a string or a list of strings." + ) hash_key = "_".join(sorted(names)) return hash_key diff --git a/scripts/unitree_deploy/__init__.py b/scripts/unitree_deploy/__init__.py new file mode 100644 index 000000000..916fd1aa1 --- /dev/null +++ b/scripts/unitree_deploy/__init__.py @@ -0,0 +1 @@ +"""Utilities for deploying policies to Unitree humanoids.""" diff --git a/scripts/unitree_deploy/common/__init__.py b/scripts/unitree_deploy/common/__init__.py new file mode 100644 index 000000000..efb40fd14 --- /dev/null +++ b/scripts/unitree_deploy/common/__init__.py @@ -0,0 +1 @@ +"""Shared helpers for Unitree deployment stack.""" diff --git a/roboverse_learn/rl/unitree_rl/deploy/common/command_helper.py b/scripts/unitree_deploy/common/command_helper.py similarity index 85% rename from roboverse_learn/rl/unitree_rl/deploy/common/command_helper.py rename to scripts/unitree_deploy/common/command_helper.py index 1e0574017..17f5203e1 100644 --- a/roboverse_learn/rl/unitree_rl/deploy/common/command_helper.py +++ b/scripts/unitree_deploy/common/command_helper.py @@ -5,11 +5,14 @@ class MotorMode: + """Motor mode constants for Unitree actuators.""" + PR = 0 # Series Control for Pitch/Roll Joints AB = 1 # Parallel Control for A/B Joints def create_damping_cmd(cmd: LowCmdGo | LowCmdHG): + """Fill a command with damping-only gains.""" size = len(cmd.motor_cmd) for i in range(size): cmd.motor_cmd[i].q = 0 @@ -20,6 +23,7 @@ def create_damping_cmd(cmd: LowCmdGo | LowCmdHG): def create_zero_cmd(cmd: LowCmdGo | LowCmdHG): + """Zero out all fields in the command.""" size = len(cmd.motor_cmd) for i in range(size): cmd.motor_cmd[i].q = 0 @@ -30,6 +34,7 @@ def create_zero_cmd(cmd: LowCmdGo | LowCmdHG): def init_cmd_hg(cmd: LowCmdHG, mode_machine: int, mode_pr: int): + """Initialize an HG command with default motor fields.""" cmd.mode_machine = mode_machine cmd.mode_pr = mode_pr size = len(cmd.motor_cmd) @@ -43,6 +48,7 @@ def init_cmd_hg(cmd: LowCmdHG, mode_machine: int, mode_pr: int): def init_cmd_go(cmd: LowCmdGo, weak_motor: list): + """Initialize a GO command, enabling torque mode on strong motors.""" cmd.head[0] = 0xFE cmd.head[1] = 0xEF cmd.level_flag = 0xFF diff --git a/roboverse_learn/rl/unitree_rl/deploy/common/remote_controller.py b/scripts/unitree_deploy/common/remote_controller.py similarity index 81% rename from roboverse_learn/rl/unitree_rl/deploy/common/remote_controller.py rename to scripts/unitree_deploy/common/remote_controller.py index ff50308c3..53337401b 100644 --- a/roboverse_learn/rl/unitree_rl/deploy/common/remote_controller.py +++ b/scripts/unitree_deploy/common/remote_controller.py @@ -2,6 +2,8 @@ class KeyMap: + """Button indices for the wireless remote.""" + R1 = 0 L1 = 1 start = 2 @@ -21,6 +23,8 @@ class KeyMap: class RemoteController: + """Lightweight parser for Unitree wireless remote payloads.""" + def __init__(self): self.lx = 0 self.ly = 0 @@ -29,6 +33,7 @@ def __init__(self): self.button = [0] * 16 def set(self, data): + """Decode raw bytes into axis and button state.""" # wireless_remote keys = struct.unpack("H", data[2:4])[0] for i in range(16): diff --git a/roboverse_learn/rl/unitree_rl/deploy/common/rotation_helper.py b/scripts/unitree_deploy/common/rotation_helper.py similarity index 84% rename from roboverse_learn/rl/unitree_rl/deploy/common/rotation_helper.py rename to scripts/unitree_deploy/common/rotation_helper.py index bcf131c0d..047b97d85 100644 --- a/roboverse_learn/rl/unitree_rl/deploy/common/rotation_helper.py +++ b/scripts/unitree_deploy/common/rotation_helper.py @@ -3,6 +3,7 @@ def get_gravity_orientation(quaternion): + """Return gravity vector components from a quaternion (w, x, y, z).""" qw = quaternion[0] qx = quaternion[1] qy = quaternion[2] @@ -18,6 +19,7 @@ def get_gravity_orientation(quaternion): def transform_imu_data(waist_yaw, waist_yaw_omega, imu_quat, imu_omega): + """Transform torso IMU data into pelvis frame given waist yaw and rate.""" RzWaist = R.from_euler("z", waist_yaw).as_matrix() R_torso = R.from_quat([imu_quat[1], imu_quat[2], imu_quat[3], imu_quat[0]]).as_matrix() R_pelvis = np.dot(R_torso, RzWaist.T) diff --git a/roboverse_learn/rl/unitree_rl/deploy/config.py b/scripts/unitree_deploy/config.py similarity index 98% rename from roboverse_learn/rl/unitree_rl/deploy/config.py rename to scripts/unitree_deploy/config.py index 2436e0308..b27a98157 100644 --- a/roboverse_learn/rl/unitree_rl/deploy/config.py +++ b/scripts/unitree_deploy/config.py @@ -5,6 +5,8 @@ class G1Config: + """Configuration loader for Unitree G1 deployment.""" + def __init__(self, file_path) -> None: with open(file_path) as f: config = yaml.load(f, Loader=yaml.FullLoader) @@ -205,6 +207,7 @@ def __init__(self, file_path) -> None: self.init_joint_settings_from_config(joint_names) def init_joint_settings_from_config(self, joint_names): + """Reorder gains/angles according to the provided joint name list.""" body_default_sorted_idx_tuples = [] left_hand_default_sorted_idx_tuples = [] right_hand_default_sorted_idx_tuples = [] diff --git a/roboverse_learn/rl/unitree_rl/deploy/configs/g1_dof12.yaml b/scripts/unitree_deploy/configs/g1_dof12.yaml similarity index 89% rename from roboverse_learn/rl/unitree_rl/deploy/configs/g1_dof12.yaml rename to scripts/unitree_deploy/configs/g1_dof12.yaml index 65e0a2df2..8963a4b12 100644 --- a/roboverse_learn/rl/unitree_rl/deploy/configs/g1_dof12.yaml +++ b/scripts/unitree_deploy/configs/g1_dof12.yaml @@ -9,7 +9,7 @@ lowstate_topic: "rt/lowstate" enable_actions_reindex: True -policy_path: "outputs/unitree_rl/g1_dof12_dof12_walking/0000_0000_000000/exported/model_exported_jit.pt" +policy_path: "outputs/humanoid/g1_dof12_dof12_walking/0000_0000_000000/exported/model_exported_jit.pt" kps: [100, 100, 100, 150, 40, 40, 100, 100, 100, 150, 40, 40] diff --git a/roboverse_learn/rl/unitree_rl/deploy/configs/g1_dof29.yaml b/scripts/unitree_deploy/configs/g1_dof29.yaml similarity index 93% rename from roboverse_learn/rl/unitree_rl/deploy/configs/g1_dof29.yaml rename to scripts/unitree_deploy/configs/g1_dof29.yaml index 08838119f..5a16f811b 100644 --- a/roboverse_learn/rl/unitree_rl/deploy/configs/g1_dof29.yaml +++ b/scripts/unitree_deploy/configs/g1_dof29.yaml @@ -9,7 +9,7 @@ lowstate_topic: "rt/lowstate" enable_actions_reindex: True # Trained policy for 29-DoF (no hands) -policy_path: "outputs/unitree_rl/walk_g1_dof29/pretrain_noMassFrictionRandomization/exported/model_exported_jit.pt" +policy_path: "outputs/humanoid/walk_g1_dof29/pretrain_noMassFrictionRandomization/exported/model_exported_jit.pt" # should be aligned to the joint order in `joint_names` kps: [200, 150, 150, 200, 20, 20, 200, 150, 150, 200, 20, 20, 200, 200, 200, 40, 40, 40, 40, 20, 20, 20, 40, 40, 40, 40, 20, 20, 20] diff --git a/roboverse_learn/rl/unitree_rl/deploy/configs/g1_dof29_dex3.yaml b/scripts/unitree_deploy/configs/g1_dof29_dex3.yaml similarity index 95% rename from roboverse_learn/rl/unitree_rl/deploy/configs/g1_dof29_dex3.yaml rename to scripts/unitree_deploy/configs/g1_dof29_dex3.yaml index 2bbc83f0e..3eeaedd0e 100644 --- a/roboverse_learn/rl/unitree_rl/deploy/configs/g1_dof29_dex3.yaml +++ b/scripts/unitree_deploy/configs/g1_dof29_dex3.yaml @@ -8,7 +8,7 @@ lowstate_topic: "rt/lowstate" enable_actions_reindex: True -policy_path: "outputs/unitree_rl/g1_dex3_humanoid_walking/2025_0819_015311/exported/model_exported_jit.pt" +policy_path: "outputs/humanoid/g1_dex3_humanoid_walking/2025_0819_015311/exported/model_exported_jit.pt" # should be aligned to the joint order in `joint_names` kps: [200, 150, 150, 200, 20, 20, 200, 150, 150, 200, 20, 20, 200, 200, 200, 40, 40, 40, 40, 20, 20, 20, 40, 40, 40, 40, 20, 20, 20, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5] diff --git a/roboverse_learn/rl/unitree_rl/deploy/deploy_real.py b/scripts/unitree_deploy/deploy_real.py similarity index 87% rename from roboverse_learn/rl/unitree_rl/deploy/deploy_real.py rename to scripts/unitree_deploy/deploy_real.py index fb22df9bf..799dd9648 100644 --- a/roboverse_learn/rl/unitree_rl/deploy/deploy_real.py +++ b/scripts/unitree_deploy/deploy_real.py @@ -1,16 +1,33 @@ from __future__ import annotations +import logging + import rootutils rootutils.setup_root(__file__, pythonpath=True) import time +from collections import deque +from copy import deepcopy import numpy as np import rootutils import torch -from collections import deque -from copy import deepcopy + rootutils.setup_root(__file__, pythonpath=True) +from roboverse_pack.tasks.humanoid.unitree_deploy.common.command_helper import ( + MotorMode, + create_damping_cmd, + create_zero_cmd, + init_cmd_go, + init_cmd_hg, +) +from roboverse_pack.tasks.humanoid.unitree_deploy.common.remote_controller import KeyMap, RemoteController +from roboverse_pack.tasks.humanoid.unitree_deploy.common.rotation_helper import ( + get_gravity_orientation, + transform_imu_data, +) +from roboverse_pack.tasks.humanoid.unitree_deploy.config import G1Config +from roboverse_pack.tasks.humanoid.unitree_deploy.utils import get_euler_xyz from unitree_sdk2py.core.channel import ChannelFactoryInitialize, ChannelPublisher, ChannelSubscriber from unitree_sdk2py.idl.default import ( unitree_go_msg_dds__LowCmd_, @@ -24,20 +41,12 @@ from unitree_sdk2py.idl.unitree_hg.msg.dds_ import LowState_ as LowStateHG from unitree_sdk2py.utils.crc import CRC -from roboverse_learn.rl.unitree_rl.deploy.common.command_helper import ( - MotorMode, - create_damping_cmd, - create_zero_cmd, - init_cmd_go, - init_cmd_hg, -) -from roboverse_learn.rl.unitree_rl.deploy.common.remote_controller import KeyMap, RemoteController -from roboverse_learn.rl.unitree_rl.deploy.common.rotation_helper import get_gravity_orientation, transform_imu_data -from roboverse_learn.rl.unitree_rl.deploy.config import G1Config -from roboverse_learn.rl.unitree_rl.deploy.utils import get_euler_xyz +logger = logging.getLogger(__name__) class Controller: + """Runtime controller that bridges policy outputs to Unitree low-level commands.""" + def __init__(self, config: G1Config) -> None: self.config = config self.remote_controller = RemoteController() @@ -96,33 +105,39 @@ def __init__(self, config: G1Config) -> None: init_cmd_go(self.low_cmd, weak_motor=self.config.weak_motor) def LowStateHgHandler(self, msg: LowStateHG): + """Handle incoming low state messages for HG transports.""" self.low_state = msg self.mode_machine_ = self.low_state.mode_machine self.remote_controller.set(self.low_state.wireless_remote) def LowStateGoHandler(self, msg: LowStateGo): + """Handle incoming low state messages for GO transports.""" self.low_state = msg self.remote_controller.set(self.low_state.wireless_remote) def send_cmd(self, cmd: LowCmdGo | LowCmdHG): + """Send a low-level command to the robot.""" cmd.crc = CRC().Crc(cmd) self.lowcmd_publisher_.Write(cmd) def wait_for_low_state(self): + """Block until at least one low state message arrives.""" while self.low_state.tick == 0: time.sleep(self.config.control_dt) - print("Successfully connected to the robot.") + logger.info("Successfully connected to the robot.") def zero_torque_state(self): - print("Enter zero torque state.") - print("Waiting for the start signal...") + """Hold zero torque until the start signal is received.""" + logger.info("Enter zero torque state.") + logger.info("Waiting for the start signal...") while self.remote_controller.button[KeyMap.start] != 1: create_zero_cmd(self.low_cmd) self.send_cmd(self.low_cmd) time.sleep(self.config.control_dt) def move_to_default_pos(self): - print("Moving to default pos.") + """Move the robot to its default posture smoothly.""" + logger.info("Moving to default pos.") # move time 2s total_time = 2 num_step = int(total_time / self.config.control_dt) @@ -149,8 +164,9 @@ def move_to_default_pos(self): time.sleep(self.config.control_dt) def default_pos_state(self): - print("Enter default pos state.") - print("Waiting for the Button A signal...") + """Maintain default position until the A button is pressed.""" + logger.info("Enter default pos state.") + logger.info("Waiting for the Button A signal...") default_body_pos = self.config.default_body_angles.copy() body_kps = self.config.default_body_kps.copy() body_kds = self.config.default_body_kds.copy() @@ -166,6 +182,7 @@ def default_pos_state(self): time.sleep(self.config.control_dt) def run(self): + """Advance one control step using the current policy.""" self.counter += 1 # Get the current joint position and velocity for default_idx, sorted_idx in self.config.body_default_sorted_idx_tuples: @@ -252,7 +269,7 @@ def run(self): args = parser.parse_args() # # Load config - config_path = f"roboverse_learn/rl/unitree_rl/deploy/configs/{args.config}" + config_path = f"roboverse_pack/tasks/humanoid/unitree_deploy/configs/{args.config}" config = G1Config(config_path) # Initialize DDS communication @@ -280,4 +297,4 @@ def run(self): # Enter the damping state create_damping_cmd(controller.low_cmd) controller.send_cmd(controller.low_cmd) - print("Exit") + logger.info("Exit") diff --git a/roboverse_learn/rl/unitree_rl/deploy/utils.py b/scripts/unitree_deploy/utils.py similarity index 99% rename from roboverse_learn/rl/unitree_rl/deploy/utils.py rename to scripts/unitree_deploy/utils.py index e9233cb3f..4978648aa 100644 --- a/roboverse_learn/rl/unitree_rl/deploy/utils.py +++ b/scripts/unitree_deploy/utils.py @@ -3,6 +3,7 @@ def euler_xyz_from_quat(quat: np.ndarray): """Convert a single (w, x, y, z) quaternion → Euler angles (roll, pitch, yaw) in radians. + Input shape: (4,), returns a tuple of scalars. """ q = np.asarray(quat, dtype=float) @@ -31,6 +32,7 @@ def euler_xyz_from_quat(quat: np.ndarray): def get_euler_xyz(quat: np.ndarray) -> np.ndarray: """Return Euler (roll, pitch, yaw) in [-π, π] for a single quaternion. + Input: (4,), Output: (3,) """ r, p, y = euler_xyz_from_quat(quat) From b579592f0de0e43a990fbcc296f61e0835c65647 Mon Sep 17 00:00:00 2001 From: Dechen Gao Date: Mon, 8 Dec 2025 17:11:15 -0800 Subject: [PATCH 13/50] Cleanup ACT (#706) * clean up ACT * pre-commit --- .../roboverse_learn/imitation_learning/ACT.md | 4 +-- roboverse_learn/il/act/README.md | 2 +- roboverse_learn/il/act/act_eval_runner.py | 6 ++-- roboverse_learn/il/act/act_run.sh | 2 +- .../assets/bimanual_viperx_ee_insertion.xml | 0 .../bimanual_viperx_ee_transfer_cube.xml | 0 .../act/assets/bimanual_viperx_insertion.xml | 0 .../assets/bimanual_viperx_transfer_cube.xml | 0 .../il/{utils => }/act/assets/scene.xml | 0 .../il/{utils => }/act/assets/tabletop.stl | Bin .../assets/vx300s_10_custom_finger_left.stl | Bin .../assets/vx300s_10_custom_finger_right.stl | Bin .../act/assets/vx300s_10_gripper_finger.stl | Bin .../act/assets/vx300s_11_ar_tag.stl | Bin .../{utils => }/act/assets/vx300s_1_base.stl | Bin .../act/assets/vx300s_2_shoulder.stl | Bin .../act/assets/vx300s_3_upper_arm.stl | Bin .../act/assets/vx300s_4_upper_forearm.stl | Bin .../act/assets/vx300s_5_lower_forearm.stl | Bin .../{utils => }/act/assets/vx300s_6_wrist.stl | Bin .../act/assets/vx300s_7_gripper.stl | Bin .../act/assets/vx300s_8_gripper_prop.stl | Bin .../act/assets/vx300s_9_gripper_bar.stl | Bin .../act/assets/vx300s_dependencies.xml | 0 .../il/{utils => }/act/assets/vx300s_left.xml | 0 .../{utils => }/act/assets/vx300s_right.xml | 0 roboverse_learn/il/act/ckpt_dir_path.txt | 2 +- .../il/{utils => }/act/conda_env.yaml | 0 .../il/{utils => }/act/constants.py | 0 .../il/{utils => }/act/detr/LICENSE | 0 .../il/{utils => }/act/detr/README.md | 0 .../il/{utils => }/act/detr/main.py | 0 .../{utils => }/act/detr/models/__init__.py | 0 .../{utils => }/act/detr/models/backbone.py | 12 ++------ .../{utils => }/act/detr/models/detr_vae.py | 0 .../act/detr/models/position_encoding.py | 0 .../act/detr/models/transformer.py | 0 .../il/{utils => }/act/detr/setup.py | 0 .../il/{utils => }/act/detr/util/__init__.py | 0 .../il/{utils => }/act/detr/util/box_ops.py | 0 .../il/{utils => }/act/detr/util/misc.py | 0 .../{utils => }/act/detr/util/plot_utils.py | 0 .../il/{utils => }/act/imitate_episodes.py | 0 roboverse_learn/il/{utils => }/act/policy.py | 0 .../il/{utils => }/act/scripted_policy.py | 0 roboverse_learn/il/{utils => }/act/train.py | 27 +++++------------- roboverse_learn/il/{utils => }/act/utils.py | 19 ++++++------ .../il/dp/datasets/robot_image_dataset.py | 3 +- roboverse_learn/il/il_setup.sh | 2 +- 49 files changed, 30 insertions(+), 49 deletions(-) rename roboverse_learn/il/{utils => }/act/assets/bimanual_viperx_ee_insertion.xml (100%) rename roboverse_learn/il/{utils => }/act/assets/bimanual_viperx_ee_transfer_cube.xml (100%) rename roboverse_learn/il/{utils => }/act/assets/bimanual_viperx_insertion.xml (100%) rename roboverse_learn/il/{utils => }/act/assets/bimanual_viperx_transfer_cube.xml (100%) rename roboverse_learn/il/{utils => }/act/assets/scene.xml (100%) rename roboverse_learn/il/{utils => }/act/assets/tabletop.stl (100%) rename roboverse_learn/il/{utils => }/act/assets/vx300s_10_custom_finger_left.stl (100%) rename roboverse_learn/il/{utils => }/act/assets/vx300s_10_custom_finger_right.stl (100%) rename roboverse_learn/il/{utils => }/act/assets/vx300s_10_gripper_finger.stl (100%) rename roboverse_learn/il/{utils => }/act/assets/vx300s_11_ar_tag.stl (100%) rename roboverse_learn/il/{utils => }/act/assets/vx300s_1_base.stl (100%) rename roboverse_learn/il/{utils => }/act/assets/vx300s_2_shoulder.stl (100%) rename roboverse_learn/il/{utils => }/act/assets/vx300s_3_upper_arm.stl (100%) rename roboverse_learn/il/{utils => }/act/assets/vx300s_4_upper_forearm.stl (100%) rename roboverse_learn/il/{utils => }/act/assets/vx300s_5_lower_forearm.stl (100%) rename roboverse_learn/il/{utils => }/act/assets/vx300s_6_wrist.stl (100%) rename roboverse_learn/il/{utils => }/act/assets/vx300s_7_gripper.stl (100%) rename roboverse_learn/il/{utils => }/act/assets/vx300s_8_gripper_prop.stl (100%) rename roboverse_learn/il/{utils => }/act/assets/vx300s_9_gripper_bar.stl (100%) rename roboverse_learn/il/{utils => }/act/assets/vx300s_dependencies.xml (100%) rename roboverse_learn/il/{utils => }/act/assets/vx300s_left.xml (100%) rename roboverse_learn/il/{utils => }/act/assets/vx300s_right.xml (100%) rename roboverse_learn/il/{utils => }/act/conda_env.yaml (100%) rename roboverse_learn/il/{utils => }/act/constants.py (100%) rename roboverse_learn/il/{utils => }/act/detr/LICENSE (100%) rename roboverse_learn/il/{utils => }/act/detr/README.md (100%) rename roboverse_learn/il/{utils => }/act/detr/main.py (100%) rename roboverse_learn/il/{utils => }/act/detr/models/__init__.py (100%) rename roboverse_learn/il/{utils => }/act/detr/models/backbone.py (90%) rename roboverse_learn/il/{utils => }/act/detr/models/detr_vae.py (100%) rename roboverse_learn/il/{utils => }/act/detr/models/position_encoding.py (100%) rename roboverse_learn/il/{utils => }/act/detr/models/transformer.py (100%) rename roboverse_learn/il/{utils => }/act/detr/setup.py (100%) rename roboverse_learn/il/{utils => }/act/detr/util/__init__.py (100%) rename roboverse_learn/il/{utils => }/act/detr/util/box_ops.py (100%) rename roboverse_learn/il/{utils => }/act/detr/util/misc.py (100%) rename roboverse_learn/il/{utils => }/act/detr/util/plot_utils.py (100%) rename roboverse_learn/il/{utils => }/act/imitate_episodes.py (100%) rename roboverse_learn/il/{utils => }/act/policy.py (100%) rename roboverse_learn/il/{utils => }/act/scripted_policy.py (100%) rename roboverse_learn/il/{utils => }/act/train.py (91%) rename roboverse_learn/il/{utils => }/act/utils.py (95%) diff --git a/docs/source/roboverse_learn/imitation_learning/ACT.md b/docs/source/roboverse_learn/imitation_learning/ACT.md index 37e2fd815..3995b8759 100644 --- a/docs/source/roboverse_learn/imitation_learning/ACT.md +++ b/docs/source/roboverse_learn/imitation_learning/ACT.md @@ -11,7 +11,7 @@ ACT (Action Chunking with Transformers) implements a transformer-based VAE polic ## Installation ```bash -cd roboverse_learn/il/utils/act/detr +cd roboverse_learn/il/act/detr pip install -e . cd ../../../../../ @@ -51,7 +51,7 @@ pip install pandas wandb ./roboverse_learn/il/act/act_run.sh ``` -`act_run.sh` uses `roboverse_learn/il/utils/act/train.py` and the generated Zarr data, which gets stored in the `data_policy/` directory, to train the ACT model. Subsequently, `roboverse_learn/il/act/act_eval_runner.py` is utilized to evaluate the trained model. +`act_run.sh` uses `roboverse_learn/il/act/train.py` and the generated Zarr data, which gets stored in the `data_policy/` directory, to train the ACT model. Subsequently, `roboverse_learn/il/act/act_eval_runner.py` is utilized to evaluate the trained model. **Outputs**: Training result is stored in `~/RoboVerse/info/outputs/ACT`. Evaluation result is stored in `~/RoboVerse/tmp/act` diff --git a/roboverse_learn/il/act/README.md b/roboverse_learn/il/act/README.md index beb307fd5..2086b38f4 100644 --- a/roboverse_learn/il/act/README.md +++ b/roboverse_learn/il/act/README.md @@ -2,7 +2,7 @@ ## 1. Install ```bash -cd roboverse_learn/il/utils/act/detr && pip install -e . +cd roboverse_learn/il/act/detr && pip install -e . cd ../../../../../ ``` diff --git a/roboverse_learn/il/act/act_eval_runner.py b/roboverse_learn/il/act/act_eval_runner.py index 022cbbf00..e22c84e64 100755 --- a/roboverse_learn/il/act/act_eval_runner.py +++ b/roboverse_learn/il/act/act_eval_runner.py @@ -358,7 +358,7 @@ class SimpleRenderCfg: import pickle - from roboverse_learn.il.utils.act.policy import ACTPolicy + from roboverse_learn.il.act.policy import ACTPolicy ckpt_path = os.path.join(args.ckpt_path, act_ckpt_name) policy = ACTPolicy(policy_config) @@ -472,9 +472,7 @@ def post_process(a): raw_action = raw_action.squeeze(0).cpu().numpy() action = post_process(raw_action) action = action[:franka_state_dim] - - - action = torch.tensor(action, dtype=torch.float32, device="cuda") + action = torch.tensor(action, dtype=torch.float32, device="cpu") # IK solver expects original joint order, but state uses alphabetical order reorder_idx = env.handler.get_joint_reindex(args.robot) diff --git a/roboverse_learn/il/act/act_run.sh b/roboverse_learn/il/act/act_run.sh index 5af5c14af..6f1571616 100755 --- a/roboverse_learn/il/act/act_run.sh +++ b/roboverse_learn/il/act/act_run.sh @@ -35,7 +35,7 @@ fi if [ "${train_enable}" = "true" ]; then echo "=== Training ===" export CUDA_VISIBLE_DEVICES=${gpu_id} - python -m roboverse_learn.il.utils.act.train \ + python -m roboverse_learn.il.act.train \ --task_name ${task_name_set}_${extra}_chunk${chunk_size} \ --num_episodes ${expert_data_num} \ --dataset_dir data_policy/${task_name_set}FrankaL${level}_${extra}_${expert_data_num}.zarr \ diff --git a/roboverse_learn/il/utils/act/assets/bimanual_viperx_ee_insertion.xml b/roboverse_learn/il/act/assets/bimanual_viperx_ee_insertion.xml similarity index 100% rename from roboverse_learn/il/utils/act/assets/bimanual_viperx_ee_insertion.xml rename to roboverse_learn/il/act/assets/bimanual_viperx_ee_insertion.xml diff --git a/roboverse_learn/il/utils/act/assets/bimanual_viperx_ee_transfer_cube.xml b/roboverse_learn/il/act/assets/bimanual_viperx_ee_transfer_cube.xml similarity index 100% rename from roboverse_learn/il/utils/act/assets/bimanual_viperx_ee_transfer_cube.xml rename to roboverse_learn/il/act/assets/bimanual_viperx_ee_transfer_cube.xml diff --git a/roboverse_learn/il/utils/act/assets/bimanual_viperx_insertion.xml b/roboverse_learn/il/act/assets/bimanual_viperx_insertion.xml similarity index 100% rename from roboverse_learn/il/utils/act/assets/bimanual_viperx_insertion.xml rename to roboverse_learn/il/act/assets/bimanual_viperx_insertion.xml diff --git a/roboverse_learn/il/utils/act/assets/bimanual_viperx_transfer_cube.xml b/roboverse_learn/il/act/assets/bimanual_viperx_transfer_cube.xml similarity index 100% rename from roboverse_learn/il/utils/act/assets/bimanual_viperx_transfer_cube.xml rename to roboverse_learn/il/act/assets/bimanual_viperx_transfer_cube.xml diff --git a/roboverse_learn/il/utils/act/assets/scene.xml b/roboverse_learn/il/act/assets/scene.xml similarity index 100% rename from roboverse_learn/il/utils/act/assets/scene.xml rename to roboverse_learn/il/act/assets/scene.xml diff --git a/roboverse_learn/il/utils/act/assets/tabletop.stl b/roboverse_learn/il/act/assets/tabletop.stl similarity index 100% rename from roboverse_learn/il/utils/act/assets/tabletop.stl rename to roboverse_learn/il/act/assets/tabletop.stl diff --git a/roboverse_learn/il/utils/act/assets/vx300s_10_custom_finger_left.stl b/roboverse_learn/il/act/assets/vx300s_10_custom_finger_left.stl similarity index 100% rename from roboverse_learn/il/utils/act/assets/vx300s_10_custom_finger_left.stl rename to roboverse_learn/il/act/assets/vx300s_10_custom_finger_left.stl diff --git a/roboverse_learn/il/utils/act/assets/vx300s_10_custom_finger_right.stl b/roboverse_learn/il/act/assets/vx300s_10_custom_finger_right.stl similarity index 100% rename from roboverse_learn/il/utils/act/assets/vx300s_10_custom_finger_right.stl rename to roboverse_learn/il/act/assets/vx300s_10_custom_finger_right.stl diff --git a/roboverse_learn/il/utils/act/assets/vx300s_10_gripper_finger.stl b/roboverse_learn/il/act/assets/vx300s_10_gripper_finger.stl similarity index 100% rename from roboverse_learn/il/utils/act/assets/vx300s_10_gripper_finger.stl rename to roboverse_learn/il/act/assets/vx300s_10_gripper_finger.stl diff --git a/roboverse_learn/il/utils/act/assets/vx300s_11_ar_tag.stl b/roboverse_learn/il/act/assets/vx300s_11_ar_tag.stl similarity index 100% rename from roboverse_learn/il/utils/act/assets/vx300s_11_ar_tag.stl rename to roboverse_learn/il/act/assets/vx300s_11_ar_tag.stl diff --git a/roboverse_learn/il/utils/act/assets/vx300s_1_base.stl b/roboverse_learn/il/act/assets/vx300s_1_base.stl similarity index 100% rename from roboverse_learn/il/utils/act/assets/vx300s_1_base.stl rename to roboverse_learn/il/act/assets/vx300s_1_base.stl diff --git a/roboverse_learn/il/utils/act/assets/vx300s_2_shoulder.stl b/roboverse_learn/il/act/assets/vx300s_2_shoulder.stl similarity index 100% rename from roboverse_learn/il/utils/act/assets/vx300s_2_shoulder.stl rename to roboverse_learn/il/act/assets/vx300s_2_shoulder.stl diff --git a/roboverse_learn/il/utils/act/assets/vx300s_3_upper_arm.stl b/roboverse_learn/il/act/assets/vx300s_3_upper_arm.stl similarity index 100% rename from roboverse_learn/il/utils/act/assets/vx300s_3_upper_arm.stl rename to roboverse_learn/il/act/assets/vx300s_3_upper_arm.stl diff --git a/roboverse_learn/il/utils/act/assets/vx300s_4_upper_forearm.stl b/roboverse_learn/il/act/assets/vx300s_4_upper_forearm.stl similarity index 100% rename from roboverse_learn/il/utils/act/assets/vx300s_4_upper_forearm.stl rename to roboverse_learn/il/act/assets/vx300s_4_upper_forearm.stl diff --git a/roboverse_learn/il/utils/act/assets/vx300s_5_lower_forearm.stl b/roboverse_learn/il/act/assets/vx300s_5_lower_forearm.stl similarity index 100% rename from roboverse_learn/il/utils/act/assets/vx300s_5_lower_forearm.stl rename to roboverse_learn/il/act/assets/vx300s_5_lower_forearm.stl diff --git a/roboverse_learn/il/utils/act/assets/vx300s_6_wrist.stl b/roboverse_learn/il/act/assets/vx300s_6_wrist.stl similarity index 100% rename from roboverse_learn/il/utils/act/assets/vx300s_6_wrist.stl rename to roboverse_learn/il/act/assets/vx300s_6_wrist.stl diff --git a/roboverse_learn/il/utils/act/assets/vx300s_7_gripper.stl b/roboverse_learn/il/act/assets/vx300s_7_gripper.stl similarity index 100% rename from roboverse_learn/il/utils/act/assets/vx300s_7_gripper.stl rename to roboverse_learn/il/act/assets/vx300s_7_gripper.stl diff --git a/roboverse_learn/il/utils/act/assets/vx300s_8_gripper_prop.stl b/roboverse_learn/il/act/assets/vx300s_8_gripper_prop.stl similarity index 100% rename from roboverse_learn/il/utils/act/assets/vx300s_8_gripper_prop.stl rename to roboverse_learn/il/act/assets/vx300s_8_gripper_prop.stl diff --git a/roboverse_learn/il/utils/act/assets/vx300s_9_gripper_bar.stl b/roboverse_learn/il/act/assets/vx300s_9_gripper_bar.stl similarity index 100% rename from roboverse_learn/il/utils/act/assets/vx300s_9_gripper_bar.stl rename to roboverse_learn/il/act/assets/vx300s_9_gripper_bar.stl diff --git a/roboverse_learn/il/utils/act/assets/vx300s_dependencies.xml b/roboverse_learn/il/act/assets/vx300s_dependencies.xml similarity index 100% rename from roboverse_learn/il/utils/act/assets/vx300s_dependencies.xml rename to roboverse_learn/il/act/assets/vx300s_dependencies.xml diff --git a/roboverse_learn/il/utils/act/assets/vx300s_left.xml b/roboverse_learn/il/act/assets/vx300s_left.xml similarity index 100% rename from roboverse_learn/il/utils/act/assets/vx300s_left.xml rename to roboverse_learn/il/act/assets/vx300s_left.xml diff --git a/roboverse_learn/il/utils/act/assets/vx300s_right.xml b/roboverse_learn/il/act/assets/vx300s_right.xml similarity index 100% rename from roboverse_learn/il/utils/act/assets/vx300s_right.xml rename to roboverse_learn/il/act/assets/vx300s_right.xml diff --git a/roboverse_learn/il/act/ckpt_dir_path.txt b/roboverse_learn/il/act/ckpt_dir_path.txt index 155f0b63d..76e18fe3d 100644 --- a/roboverse_learn/il/act/ckpt_dir_path.txt +++ b/roboverse_learn/il/act/ckpt_dir_path.txt @@ -1 +1 @@ -info/outputs/ACT/2025.10.18/08.55.17_close_box_obs:joint_pos_act:joint_pos_chunk10_100 +info/outputs/ACT/2025.12.01/17.27.21_close_box_obs:joint_pos_act:joint_pos_chunk20_90 diff --git a/roboverse_learn/il/utils/act/conda_env.yaml b/roboverse_learn/il/act/conda_env.yaml similarity index 100% rename from roboverse_learn/il/utils/act/conda_env.yaml rename to roboverse_learn/il/act/conda_env.yaml diff --git a/roboverse_learn/il/utils/act/constants.py b/roboverse_learn/il/act/constants.py similarity index 100% rename from roboverse_learn/il/utils/act/constants.py rename to roboverse_learn/il/act/constants.py diff --git a/roboverse_learn/il/utils/act/detr/LICENSE b/roboverse_learn/il/act/detr/LICENSE similarity index 100% rename from roboverse_learn/il/utils/act/detr/LICENSE rename to roboverse_learn/il/act/detr/LICENSE diff --git a/roboverse_learn/il/utils/act/detr/README.md b/roboverse_learn/il/act/detr/README.md similarity index 100% rename from roboverse_learn/il/utils/act/detr/README.md rename to roboverse_learn/il/act/detr/README.md diff --git a/roboverse_learn/il/utils/act/detr/main.py b/roboverse_learn/il/act/detr/main.py similarity index 100% rename from roboverse_learn/il/utils/act/detr/main.py rename to roboverse_learn/il/act/detr/main.py diff --git a/roboverse_learn/il/utils/act/detr/models/__init__.py b/roboverse_learn/il/act/detr/models/__init__.py similarity index 100% rename from roboverse_learn/il/utils/act/detr/models/__init__.py rename to roboverse_learn/il/act/detr/models/__init__.py diff --git a/roboverse_learn/il/utils/act/detr/models/backbone.py b/roboverse_learn/il/act/detr/models/backbone.py similarity index 90% rename from roboverse_learn/il/utils/act/detr/models/backbone.py rename to roboverse_learn/il/act/detr/models/backbone.py index 601f2e4da..749e41df8 100644 --- a/roboverse_learn/il/utils/act/detr/models/backbone.py +++ b/roboverse_learn/il/act/detr/models/backbone.py @@ -18,6 +18,7 @@ import IPython e = IPython.embed + class FrozenBatchNorm2d(torch.nn.Module): """ BatchNorm2d where the batch statistics and the affine parameters are fixed. @@ -72,27 +73,20 @@ def __init__(self, backbone: nn.Module, train_backbone: bool, num_channels: int, self.num_channels = num_channels def forward(self, tensor): - print(f"[BackBone]: tensor shape: {tensor.shape}") xs = self.body(tensor) return xs - # out: Dict[str, NestedTensor] = {} - # for name, x in xs.items(): - # m = tensor_list.mask - # assert m is not None - # mask = F.interpolate(m[None].float(), size=x.shape[-2:]).to(torch.bool)[0] - # out[name] = NestedTensor(x, mask) - # return out class Backbone(BackboneBase): """ResNet backbone with frozen BatchNorm.""" + def __init__(self, name: str, train_backbone: bool, return_interm_layers: bool, dilation: bool): backbone = getattr(torchvision.models, name)( replace_stride_with_dilation=[False, False, dilation], - pretrained=is_main_process(), norm_layer=FrozenBatchNorm2d) # pretrained # TODO do we want frozen batch_norm?? + pretrained=is_main_process(), norm_layer=FrozenBatchNorm2d) # pretrained # TODO do we want frozen batch_norm?? num_channels = 512 if name in ('resnet18', 'resnet34') else 2048 super().__init__(backbone, train_backbone, num_channels, return_interm_layers) diff --git a/roboverse_learn/il/utils/act/detr/models/detr_vae.py b/roboverse_learn/il/act/detr/models/detr_vae.py similarity index 100% rename from roboverse_learn/il/utils/act/detr/models/detr_vae.py rename to roboverse_learn/il/act/detr/models/detr_vae.py diff --git a/roboverse_learn/il/utils/act/detr/models/position_encoding.py b/roboverse_learn/il/act/detr/models/position_encoding.py similarity index 100% rename from roboverse_learn/il/utils/act/detr/models/position_encoding.py rename to roboverse_learn/il/act/detr/models/position_encoding.py diff --git a/roboverse_learn/il/utils/act/detr/models/transformer.py b/roboverse_learn/il/act/detr/models/transformer.py similarity index 100% rename from roboverse_learn/il/utils/act/detr/models/transformer.py rename to roboverse_learn/il/act/detr/models/transformer.py diff --git a/roboverse_learn/il/utils/act/detr/setup.py b/roboverse_learn/il/act/detr/setup.py similarity index 100% rename from roboverse_learn/il/utils/act/detr/setup.py rename to roboverse_learn/il/act/detr/setup.py diff --git a/roboverse_learn/il/utils/act/detr/util/__init__.py b/roboverse_learn/il/act/detr/util/__init__.py similarity index 100% rename from roboverse_learn/il/utils/act/detr/util/__init__.py rename to roboverse_learn/il/act/detr/util/__init__.py diff --git a/roboverse_learn/il/utils/act/detr/util/box_ops.py b/roboverse_learn/il/act/detr/util/box_ops.py similarity index 100% rename from roboverse_learn/il/utils/act/detr/util/box_ops.py rename to roboverse_learn/il/act/detr/util/box_ops.py diff --git a/roboverse_learn/il/utils/act/detr/util/misc.py b/roboverse_learn/il/act/detr/util/misc.py similarity index 100% rename from roboverse_learn/il/utils/act/detr/util/misc.py rename to roboverse_learn/il/act/detr/util/misc.py diff --git a/roboverse_learn/il/utils/act/detr/util/plot_utils.py b/roboverse_learn/il/act/detr/util/plot_utils.py similarity index 100% rename from roboverse_learn/il/utils/act/detr/util/plot_utils.py rename to roboverse_learn/il/act/detr/util/plot_utils.py diff --git a/roboverse_learn/il/utils/act/imitate_episodes.py b/roboverse_learn/il/act/imitate_episodes.py similarity index 100% rename from roboverse_learn/il/utils/act/imitate_episodes.py rename to roboverse_learn/il/act/imitate_episodes.py diff --git a/roboverse_learn/il/utils/act/policy.py b/roboverse_learn/il/act/policy.py similarity index 100% rename from roboverse_learn/il/utils/act/policy.py rename to roboverse_learn/il/act/policy.py diff --git a/roboverse_learn/il/utils/act/scripted_policy.py b/roboverse_learn/il/act/scripted_policy.py similarity index 100% rename from roboverse_learn/il/utils/act/scripted_policy.py rename to roboverse_learn/il/act/scripted_policy.py diff --git a/roboverse_learn/il/utils/act/train.py b/roboverse_learn/il/act/train.py similarity index 91% rename from roboverse_learn/il/utils/act/train.py rename to roboverse_learn/il/act/train.py index 1c229499d..75b3722a8 100644 --- a/roboverse_learn/il/utils/act/train.py +++ b/roboverse_learn/il/act/train.py @@ -8,10 +8,7 @@ import json from copy import deepcopy from tqdm import tqdm -from einops import rearrange -from .constants import DT -from .constants import PUPPET_GRIPPER_JOINT_OPEN from .utils import load_data # data functions from .utils import compute_dict_mean, set_seed, detach_dict # helper functions from .policy import ACTPolicy, CNNMLPPolicy @@ -86,7 +83,13 @@ def main(args): 'data': dataset_metadata, # Add the dataset metadata to config } - train_dataloader, val_dataloader, stats, _ = load_data(dataset_dir, num_episodes, camera_names, batch_size_train, batch_size_val) + train_dataloader, val_dataloader, stats, _ = load_data( + dataset_dir, + num_episodes, + camera_names, + batch_size_train, + batch_size_val + ) # save dataset stats if not os.path.isdir(ckpt_dir): @@ -175,22 +178,6 @@ def train_bc(train_dataloader, val_dataloader, config): summary_string += f'{k}: {v.item():.3f} ' print(summary_string) - # training - - # policy.train() - # optimizer.zero_grad() - # for batch_idx, data in enumerate(train_dataloader): - # forward_dict = forward_pass(data, policy) - # # backward - # loss = forward_dict['loss'] - # loss.backward() - # optimizer.step() - # optimizer.zero_grad() - # train_history.append(detach_dict(forward_dict)) - # epoch_summary = compute_dict_mean(train_history[(batch_idx+1)*epoch:(batch_idx+1)*(epoch+1)]) - # epoch_train_loss = epoch_summary['loss'] - # print(f'Train loss: {epoch_train_loss:.5f}') - policy.train() optimizer.zero_grad() epoch_train_dicts = [] diff --git a/roboverse_learn/il/utils/act/utils.py b/roboverse_learn/il/act/utils.py similarity index 95% rename from roboverse_learn/il/utils/act/utils.py rename to roboverse_learn/il/act/utils.py index dbd5d2505..5d7022d1f 100644 --- a/roboverse_learn/il/utils/act/utils.py +++ b/roboverse_learn/il/act/utils.py @@ -6,10 +6,7 @@ import h5py import json from torch.utils.data import TensorDataset, DataLoader -from PIL import Image -module_path = os.path.abspath(os.path.join(__file__, "../../diffusion_policy/diffusion_policy/common")) -sys.path.append(module_path) -from replay_buffer import * +from roboverse_learn.il.utils.common.replay_buffer import ReplayBuffer import IPython e = IPython.embed @@ -159,6 +156,11 @@ def get_norm_stats(dataset_dir, num_episodes): zarr_path, keys=["state", "action"] ) + actual_episodes = replay_buffer.n_episodes + if num_episodes > actual_episodes: + print(f"Warning: Requested {num_episodes} episodes, but dataset only has {actual_episodes}") + print(f"Using all {actual_episodes} available episodes") + num_episodes = actual_episodes # Calculate max episode length max_episode_len = int(np.max(replay_buffer.episode_lengths)) @@ -168,7 +170,7 @@ def get_norm_stats(dataset_dir, num_episodes): all_action_data = [] # Process each episode up to num_episodes - for episode_idx in range(min(num_episodes, replay_buffer.n_episodes)): + for episode_idx in range(num_episodes): episode_slice = replay_buffer.get_episode_slice(episode_idx) state = replay_buffer["state"][episode_slice] action = replay_buffer["action"][episode_slice] @@ -198,20 +200,21 @@ def get_norm_stats(dataset_dir, num_episodes): "max_episode_len": max_episode_len } - return stats + return stats, num_episodes def load_data(dataset_dir, num_episodes, camera_names, batch_size_train, batch_size_val): print(f'\nData from: {dataset_dir}\n') + + norm_stats, num_episodes = get_norm_stats(dataset_dir, num_episodes) + # obtain train test split train_ratio = 0.8 shuffled_indices = np.random.permutation(num_episodes) train_indices = shuffled_indices[:int(train_ratio * num_episodes)] val_indices = shuffled_indices[int(train_ratio * num_episodes):] - # obtain normalization stats for state and action - norm_stats = get_norm_stats(dataset_dir, num_episodes) # construct dataset and dataloader train_dataset = ZarrEpisodicRoboVerseDataset(train_indices, dataset_dir, camera_names, norm_stats) val_dataset = ZarrEpisodicRoboVerseDataset(val_indices, dataset_dir, camera_names, norm_stats) diff --git a/roboverse_learn/il/dp/datasets/robot_image_dataset.py b/roboverse_learn/il/dp/datasets/robot_image_dataset.py index 3e6df9e38..9d3596f39 100644 --- a/roboverse_learn/il/dp/datasets/robot_image_dataset.py +++ b/roboverse_learn/il/dp/datasets/robot_image_dataset.py @@ -31,8 +31,7 @@ def __init__( max_train_episodes=None, ): super().__init__() - # cprint(zarr_path, "red") - # cprint(batch_size, "red") + self.replay_buffer = ReplayBuffer.copy_from_path( zarr_path, # keys=['head_camera', 'front_camera', 'left_camera', 'right_camera', 'state', 'action'], diff --git a/roboverse_learn/il/il_setup.sh b/roboverse_learn/il/il_setup.sh index 4547cb3b6..098ddc73d 100644 --- a/roboverse_learn/il/il_setup.sh +++ b/roboverse_learn/il/il_setup.sh @@ -8,7 +8,7 @@ pip install -e . # Install act echo "Install act..." cd ../../../../ -cd roboverse_learn/il/utils/act/detr || { echo "detr do not exit"; exit 1; } +cd roboverse_learn/il/act/detr || { echo "detr do not exit"; exit 1; } pip install -e . # Install additional dependencies From 55042a5faea2327abb1f266560acf0fc769c237d Mon Sep 17 00:00:00 2001 From: HanchuZhou Date: Mon, 8 Dec 2025 17:22:11 -0800 Subject: [PATCH 14/50] Develop (#717) * Update pick_place configs and tasks * Fix bug that success rate is more than 100% in evaluate_lift.py; Fixed hard-coded path in track.py * add contributor * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .gitignore | 4 ++++ CONTRIBUTORS.md | 1 + .../rl/fast_td3/configs/pick_place.yaml | 2 +- roboverse_learn/rl/fast_td3/configs/track.yaml | 4 ++-- roboverse_learn/rl/fast_td3/evaluate_lift.py | 17 ++++++++++++++++- roboverse_pack/tasks/pick_place/README.md | 11 +++++------ roboverse_pack/tasks/pick_place/track.py | 2 +- 7 files changed, 30 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index a6e1e0bf5..8ccc3c121 100644 --- a/.gitignore +++ b/.gitignore @@ -104,3 +104,7 @@ id_rsa* # for test cache .pytest_cache/ + +*.pkl +*.pt +output diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index fc6f553bb..528f39029 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -47,6 +47,7 @@ Guidelines for modifications: * Yutong Liang * Yuyang Li * Zhigen Zhao +* Hanchu Zhou ## Acknowledgements diff --git a/roboverse_learn/rl/fast_td3/configs/pick_place.yaml b/roboverse_learn/rl/fast_td3/configs/pick_place.yaml index 004f1f2d2..065d7cd8c 100644 --- a/roboverse_learn/rl/fast_td3/configs/pick_place.yaml +++ b/roboverse_learn/rl/fast_td3/configs/pick_place.yaml @@ -4,7 +4,7 @@ # ------------------------------------------------------------------------------- # Environment # ------------------------------------------------------------------------------- -sim: "isaacsim" +sim: "isaacgym" robots: ["franka"] task: "pick_place.approach_grasp_simple" decimation: 4 diff --git a/roboverse_learn/rl/fast_td3/configs/track.yaml b/roboverse_learn/rl/fast_td3/configs/track.yaml index f7aacae09..b06adffb0 100644 --- a/roboverse_learn/rl/fast_td3/configs/track.yaml +++ b/roboverse_learn/rl/fast_td3/configs/track.yaml @@ -5,7 +5,7 @@ # ------------------------------------------------------------------------------- # Environment # ------------------------------------------------------------------------------- -sim: "isaacsim" +sim: "isaacgym" robots: ["franka"] task: "pick_place.track" decimation: 4 @@ -14,7 +14,7 @@ headless: True # State file path for track task (pkl file path for grasp states) # If null, uses default path or env var PICK_PLACE_TRACK_STATE_FILE -state_file_path: "eval_states/pick_place.approach_grasp_simple_franka_lift_states_101states_20251122_180651.pkl" +state_file_path: "eval_states/pick_place.approach_grasp_simple_franka_lift_states_100states_20251126_170312.pkl" # ------------------------------------------------------------------------------- # Seeds & Device diff --git a/roboverse_learn/rl/fast_td3/evaluate_lift.py b/roboverse_learn/rl/fast_td3/evaluate_lift.py index a7e7e5ae8..f83180311 100644 --- a/roboverse_learn/rl/fast_td3/evaluate_lift.py +++ b/roboverse_learn/rl/fast_td3/evaluate_lift.py @@ -183,6 +183,11 @@ def evaluate_lift_collection( current_episode_init_state[i] = None episodes_completed = 0 + # Track how many episodes produced at least one successful lift + successful_episodes_count = 0 + # Per-env flag indicating whether the current episode already had a success + success_in_episode = {i: False for i in range(num_eval_envs)} + current_returns = torch.zeros(num_eval_envs, device=device) current_lengths = torch.zeros(num_eval_envs, device=device) done_masks = torch.zeros(num_eval_envs, dtype=torch.bool, device=device) @@ -263,6 +268,11 @@ def evaluate_lift_collection( collected_trajs.append(traj_data_serializable) collected_states.append(state_data_serializable) + # Mark episode as successful (count at most once per episode) + if not success_in_episode[i]: + success_in_episode[i] = True + successful_episodes_count += 1 + log.info( f"[Env {i}] Collected trajectory {len(collected_trajs)} " f"(lift maintained {lift_frame_count[i]} frames, total steps: {len(current_episode_actions[i])})" @@ -300,6 +310,8 @@ def evaluate_lift_collection( current_episode_init_state[i] = None current_returns[i] = 0 current_lengths[i] = 0 + # reset per-episode success flag for next episode + success_in_episode[i] = False done_masks = torch.logical_or(done_masks, dones) @@ -315,6 +327,8 @@ def evaluate_lift_collection( current_episode_actions[i] = [] current_episode_states[i] = [] current_episode_init_state[i] = extract_state_dict(env, scenario, env_idx=i) + # reset per-episode success flag after full reset + success_in_episode[i] = False else: obs = next_obs @@ -347,7 +361,8 @@ def evaluate_lift_collection( "collected_count": len(collected_trajs), "target_count": target_count, "episodes_completed": episodes_completed, - "success_rate": len(collected_trajs) / episodes_completed if episodes_completed > 0 else 0.0, + # success_rate = successful simulations / total simulations (episodes) + "success_rate": successful_episodes_count / episodes_completed if episodes_completed > 0 else 0.0, } return stats diff --git a/roboverse_pack/tasks/pick_place/README.md b/roboverse_pack/tasks/pick_place/README.md index 877acf81e..9c0457e25 100644 --- a/roboverse_pack/tasks/pick_place/README.md +++ b/roboverse_pack/tasks/pick_place/README.md @@ -15,8 +15,7 @@ The task is split into two stages: Train the first stage to learn approach and grasp: ```bash -cd roboverse_learn/rl/fast_td3 -python train.py --config pick_place.yaml +python -m roboverse_learn.rl.fast_td3.train --config pick_place.yaml ``` This will generate checkpoints in the output directory. Note the checkpoint path for the next step. @@ -26,8 +25,8 @@ This will generate checkpoints in the output directory. Note the checkpoint path Evaluate the trained model and collect stable grasp states and first-half trajectories: ```bash -python evaluate_lift.py \ - --checkpoint models/pick_place.approach_grasp_simple_1210000.pt \ +python -m roboverse_learn.rl.fast_td3.evaluate_lift \ + --checkpoint models/pick_place.approach_grasp_simple_65000.pt \ --target_count 100 \ --state_dir eval_states \ --traj_dir eval_trajs @@ -42,7 +41,7 @@ This generates: Load the collected states as initial states for track training: ```bash -python train.py --config track.yaml +python -m roboverse_learn.rl.fast_td3.train --config track.yaml ``` Make sure `track.yaml` has the correct `state_file_path` pointing to the states file from Stage 2: @@ -56,7 +55,7 @@ state_file_path: "eval_states/pick_place.approach_grasp_simple_franka_lift_state Evaluate the track task to get second-half trajectories: ```bash -python evaluate.py --checkpoint models/pick_place.track_*.pt +python -m roboverse_learn.rl.fast_td3.evaluate --checkpoint models/pick_place.track_*.pt ``` ### Stage 5: Merge Trajectories diff --git a/roboverse_pack/tasks/pick_place/track.py b/roboverse_pack/tasks/pick_place/track.py index 9bd7012d9..0fb2e33a8 100644 --- a/roboverse_pack/tasks/pick_place/track.py +++ b/roboverse_pack/tasks/pick_place/track.py @@ -230,7 +230,7 @@ class PickPlaceTrack(PickPlaceBase): def __init__(self, scenario, device=None): self.state_file_path = ( - "eval_states/pick_place.approach_grasp_simple_franka_lift_states_101states_20251122_180651.pkl" + "eval_states/pick_place.approach_grasp_simple_franka_lift_states_100states_20251126_170312.pkl" ) self._loaded_states = None From 3980ece18361be8e82a60011c7bfb8e30ea4eb32 Mon Sep 17 00:00:00 2001 From: Dechen Gao Date: Mon, 8 Dec 2025 17:25:07 -0800 Subject: [PATCH 15/50] Refactor DP (#711) * refactor DP/FM/VITA * pre-commit * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- roboverse_learn/il/README.md | 48 + .../bet/libraries/mingpt => }/__init__.py | 0 roboverse_learn/il/act/utils.py | 2 +- roboverse_learn/il/base/__init__.py | 0 .../dataset => base}/base_dataset.py | 3 +- .../il/{dp => }/base/base_eval_runner.py | 4 +- .../policy => base}/base_image_policy.py | 4 +- .../il/{dp => }/base/base_model.py | 0 .../il/{dp => }/base/base_runner.py | 0 roboverse_learn/il/configs/__init__.py | 0 .../dataset_config/robot_image_dataset.yaml | 2 +- .../il/{dp => }/configs/dp_runner.yaml | 4 +- .../eval_config/diffusion_policy_eval.yaml | 2 +- .../configs/model_config/ddim_unet_model.yaml | 6 +- .../configs/model_config/ddpm_dit_model.yaml | 6 +- .../configs/model_config/ddpm_unet_model.yaml | 6 +- .../configs/model_config/fm_dit_model.yaml | 6 +- .../configs/model_config/fm_unet_model.yaml | 6 +- .../configs/model_config/score_model.yaml | 6 +- .../configs/model_config/vita_model.yaml | 8 +- .../train_config/diffusion_policy_train.yaml | 2 +- roboverse_learn/il/datasets/__init__.py | 0 .../il/{dp/base => datasets}/base_dataset.py | 3 +- .../{dp => }/datasets/robot_image_dataset.py | 16 +- .../{utils/diffusion_policy => dp}/.gitignore | 0 roboverse_learn/il/dp/README.md | 33 +- roboverse_learn/il/dp/__init__.py | 0 .../models}/bet/action_ae/__init__.py | 2 +- .../bet/action_ae/discretizers/k_means.py | 2 +- .../bet/latent_generators/latent_generator.py | 2 +- .../models}/bet/latent_generators/mingpt.py | 8 +- .../bet/latent_generators/transformer.py | 6 +- .../models}/bet/libraries/loss_fn.py | 0 .../models}/bet/libraries/mingpt/LICENSE | 0 .../models/bet/libraries/mingpt/__init__.py | 0 .../models}/bet/libraries/mingpt/model.py | 0 .../models}/bet/libraries/mingpt/trainer.py | 0 .../models}/bet/libraries/mingpt/utils.py | 0 .../model => dp/models}/bet/utils.py | 0 .../models}/diffusion/action_ae.py | 2 +- .../models}/diffusion/conditional_unet1d.py | 4 +- .../models}/diffusion/conv1d_components.py | 0 .../models}/diffusion/ema_model.py | 0 .../model => dp/models}/diffusion/flow_net.py | 4 +- .../model => dp/models}/diffusion/layers.py | 0 .../models}/diffusion/mask_generator.py | 2 +- .../models}/diffusion/positional_embedding.py | 0 .../diffusion/transformer_for_diffusion.py | 4 +- .../models}/vision/crop_randomizer.py | 2 +- .../models}/vision/model_getter.py | 0 .../models}/vision/multi_image_obs_encoder.py | 6 +- .../ddim_unet_image_policy.py | 14 +- .../ddpm_dit_image_policy.py | 6 +- .../{models => policies}/ddpm_image_policy.py | 10 +- .../ddpm_unet_image_policy.py | 6 +- .../score_unet_image_policy.py | 12 +- roboverse_learn/il/dp/requirements.txt | 18 + .../shared_memory/shared_memory_queue.py | 4 +- .../shared_memory_ring_buffer.py | 4 +- .../shared_memory/shared_memory_util.py | 0 .../shared_memory/shared_ndarray.py | 2 +- roboverse_learn/il/eval_runner/__init__.py | 0 .../il/{dp => }/eval_runner/dp_eval_runner.py | 6 +- roboverse_learn/il/fm/README.md | 36 + .../policies}/fm_dit_image_policy.py | 12 +- .../policies}/fm_unet_image_policy.py | 12 +- roboverse_learn/il/fm/requirements.txt | 19 + roboverse_learn/il/il_run.sh | 174 ++-- roboverse_learn/il/il_setup.sh | 17 - roboverse_learn/il/runner/__init__.py | 0 .../il/{dp => }/runner/base_policy.py | 0 .../il/{dp => }/runner/dp_runner.py | 16 +- roboverse_learn/il/{dp/main.py => train.py} | 2 +- .../il/utils/{common => }/checkpoint_util.py | 0 .../il/utils/common/normalize_util.py | 198 ---- roboverse_learn/il/utils/common/normalizer.py | 368 ------- .../il/utils/common/replay_buffer.py | 622 ----------- roboverse_learn/il/utils/common/sampler.py | 166 --- .../il/utils/{common => }/cv2_util.py | 0 .../{common => }/dict_of_tensor_mixin.py | 0 .../diffusion_policy/__init__.py | 2 - .../common/checkpoint_util.py | 60 -- .../diffusion_policy/common/cv2_util.py | 151 --- .../diffusion_policy/common/env_util.py | 28 - .../diffusion_policy/common/json_logger.py | 115 --- .../common/nested_dict_util.py | 34 - .../common/pose_trajectory_interpolator.py | 208 ---- .../diffusion_policy/common/precise_sleep.py | 27 - .../common/pymunk_override.py | 244 ----- .../diffusion_policy/common/pymunk_util.py | 51 - .../diffusion_policy/common/pytorch_util.py | 82 -- .../common/robomimic_config_util.py | 41 - .../diffusion_policy/common/robomimic_util.py | 169 --- .../common/timestamp_accumulator.py | 219 ---- .../diffusion_policy/config/robot_dp.yaml | 164 --- .../config/task/default_task.yaml | 41 - .../dataset/robot_image_dataset.py | 177 ---- .../model/common/dict_of_tensor_mixin.py | 46 - .../model/common/lr_scheduler.py | 55 - .../model/common/module_attr_mixin.py | 15 - .../model/common/rotation_transformer.py | 98 -- .../model/common/shape_util.py | 23 - .../model/common/tensor_util.py | 973 ------------------ .../policy/diffusion_unet_image_policy.py | 262 ----- .../workspace/base_workspace.py | 140 --- .../workspace/robotworkspace.py | 358 ------- .../il/utils/diffusion_policy/pyproject.toml | 27 - .../scripts/prune_and_rename.py | 42 - .../il/utils/diffusion_policy/train.py | 37 - .../il/utils/diffusion_policy/train_dp.sh | 52 - .../il/utils/{common => }/env_util.py | 0 .../il/utils/{common => }/eval_args.py | 0 .../utils/{common => }/eval_runner_getter.py | 2 +- .../common => }/flow_matchers.py | 0 .../il/utils/{common => }/json_logger.py | 0 .../il/utils/{common => }/lr_scheduler.py | 0 .../utils/{common => }/module_attr_mixin.py | 0 .../il/utils/{common => }/nested_dict_util.py | 0 .../common => }/normalize_util.py | 4 +- .../model/common => }/normalizer.py | 4 +- .../pose_trajectory_interpolator.py | 0 .../il/utils/{common => }/precise_sleep.py | 0 .../il/utils/{common => }/pymunk_override.py | 0 .../il/utils/{common => }/pymunk_util.py | 0 .../il/utils/{common => }/pytorch_util.py | 0 .../common => }/replay_buffer.py | 0 .../{common => }/robomimic_config_util.py | 0 .../il/utils/{common => }/robomimic_util.py | 0 .../{common => }/rotation_transformer.py | 0 .../diffusion_policy/common => }/sampler.py | 2 +- .../il/utils/{common => }/shape_util.py | 0 .../il/utils/{common => }/tensor_util.py | 0 .../{common => }/timestamp_accumulator.py | 0 roboverse_learn/il/vita/README.md | 34 + .../models => vita/policies}/vita_policy.py | 15 +- roboverse_learn/il/vita/requirements.txt | 19 + roboverse_learn/vla/OpenVLA/vla_eval.py | 2 +- roboverse_learn/vla/SmolVLA/smolvla_eval.py | 2 +- roboverse_learn/vla/pi0/pi_eval.py | 2 +- 139 files changed, 426 insertions(+), 5536 deletions(-) create mode 100644 roboverse_learn/il/README.md rename roboverse_learn/il/{utils/diffusion_policy/diffusion_policy/model/bet/libraries/mingpt => }/__init__.py (100%) create mode 100644 roboverse_learn/il/base/__init__.py rename roboverse_learn/il/{utils/diffusion_policy/diffusion_policy/dataset => base}/base_dataset.py (94%) rename roboverse_learn/il/{dp => }/base/base_eval_runner.py (99%) rename roboverse_learn/il/{utils/diffusion_policy/diffusion_policy/policy => base}/base_image_policy.py (81%) rename roboverse_learn/il/{dp => }/base/base_model.py (100%) rename roboverse_learn/il/{dp => }/base/base_runner.py (100%) create mode 100644 roboverse_learn/il/configs/__init__.py rename roboverse_learn/il/{dp => }/configs/dataset_config/robot_image_dataset.yaml (72%) rename roboverse_learn/il/{dp => }/configs/dp_runner.yaml (90%) rename roboverse_learn/il/{dp => }/configs/eval_config/diffusion_policy_eval.yaml (86%) rename roboverse_learn/il/{dp => }/configs/model_config/ddim_unet_model.yaml (79%) rename roboverse_learn/il/{dp => }/configs/model_config/ddpm_dit_model.yaml (78%) rename roboverse_learn/il/{dp => }/configs/model_config/ddpm_unet_model.yaml (79%) rename roboverse_learn/il/{dp => }/configs/model_config/fm_dit_model.yaml (68%) rename roboverse_learn/il/{dp => }/configs/model_config/fm_unet_model.yaml (68%) rename roboverse_learn/il/{dp => }/configs/model_config/score_model.yaml (79%) rename roboverse_learn/il/{dp => }/configs/model_config/vita_model.yaml (76%) rename roboverse_learn/il/{dp => }/configs/train_config/diffusion_policy_train.yaml (93%) create mode 100644 roboverse_learn/il/datasets/__init__.py rename roboverse_learn/il/{dp/base => datasets}/base_dataset.py (94%) rename roboverse_learn/il/{dp => }/datasets/robot_image_dataset.py (93%) rename roboverse_learn/il/{utils/diffusion_policy => dp}/.gitignore (100%) create mode 100644 roboverse_learn/il/dp/__init__.py rename roboverse_learn/il/{utils/diffusion_policy/diffusion_policy/model => dp/models}/bet/action_ae/__init__.py (96%) rename roboverse_learn/il/{utils/diffusion_policy/diffusion_policy/model => dp/models}/bet/action_ae/discretizers/k_means.py (98%) rename roboverse_learn/il/{utils/diffusion_policy/diffusion_policy/model => dp/models}/bet/latent_generators/latent_generator.py (97%) rename roboverse_learn/il/{utils/diffusion_policy/diffusion_policy/model => dp/models}/bet/latent_generators/mingpt.py (95%) rename roboverse_learn/il/{utils/diffusion_policy/diffusion_policy/model => dp/models}/bet/latent_generators/transformer.py (93%) rename roboverse_learn/il/{utils/diffusion_policy/diffusion_policy/model => dp/models}/bet/libraries/loss_fn.py (100%) rename roboverse_learn/il/{utils/diffusion_policy/diffusion_policy/model => dp/models}/bet/libraries/mingpt/LICENSE (100%) create mode 100644 roboverse_learn/il/dp/models/bet/libraries/mingpt/__init__.py rename roboverse_learn/il/{utils/diffusion_policy/diffusion_policy/model => dp/models}/bet/libraries/mingpt/model.py (100%) rename roboverse_learn/il/{utils/diffusion_policy/diffusion_policy/model => dp/models}/bet/libraries/mingpt/trainer.py (100%) rename roboverse_learn/il/{utils/diffusion_policy/diffusion_policy/model => dp/models}/bet/libraries/mingpt/utils.py (100%) rename roboverse_learn/il/{utils/diffusion_policy/diffusion_policy/model => dp/models}/bet/utils.py (100%) rename roboverse_learn/il/{utils/diffusion_policy/diffusion_policy/model => dp/models}/diffusion/action_ae.py (98%) rename roboverse_learn/il/{utils/diffusion_policy/diffusion_policy/model => dp/models}/diffusion/conditional_unet1d.py (98%) rename roboverse_learn/il/{utils/diffusion_policy/diffusion_policy/model => dp/models}/diffusion/conv1d_components.py (100%) rename roboverse_learn/il/{utils/diffusion_policy/diffusion_policy/model => dp/models}/diffusion/ema_model.py (100%) rename roboverse_learn/il/{utils/diffusion_policy/diffusion_policy/model => dp/models}/diffusion/flow_net.py (98%) rename roboverse_learn/il/{utils/diffusion_policy/diffusion_policy/model => dp/models}/diffusion/layers.py (100%) rename roboverse_learn/il/{utils/diffusion_policy/diffusion_policy/model => dp/models}/diffusion/mask_generator.py (99%) rename roboverse_learn/il/{utils/diffusion_policy/diffusion_policy/model => dp/models}/diffusion/positional_embedding.py (100%) rename roboverse_learn/il/{utils/diffusion_policy/diffusion_policy/model => dp/models}/diffusion/transformer_for_diffusion.py (98%) rename roboverse_learn/il/{utils/diffusion_policy/diffusion_policy/model => dp/models}/vision/crop_randomizer.py (99%) rename roboverse_learn/il/{utils/diffusion_policy/diffusion_policy/model => dp/models}/vision/model_getter.py (100%) rename roboverse_learn/il/{utils/diffusion_policy/diffusion_policy/model => dp/models}/vision/multi_image_obs_encoder.py (96%) rename roboverse_learn/il/dp/{models => policies}/ddim_unet_image_policy.py (95%) rename roboverse_learn/il/dp/{models => policies}/ddpm_dit_image_policy.py (89%) rename roboverse_learn/il/dp/{models => policies}/ddpm_image_policy.py (96%) rename roboverse_learn/il/dp/{models => policies}/ddpm_unet_image_policy.py (88%) rename roboverse_learn/il/dp/{models => policies}/score_unet_image_policy.py (94%) create mode 100644 roboverse_learn/il/dp/requirements.txt rename roboverse_learn/il/{utils/diffusion_policy/diffusion_policy => dp}/shared_memory/shared_memory_queue.py (97%) rename roboverse_learn/il/{utils/diffusion_policy/diffusion_policy => dp}/shared_memory/shared_memory_ring_buffer.py (98%) rename roboverse_learn/il/{utils/diffusion_policy/diffusion_policy => dp}/shared_memory/shared_memory_util.py (100%) rename roboverse_learn/il/{utils/diffusion_policy/diffusion_policy => dp}/shared_memory/shared_ndarray.py (98%) create mode 100644 roboverse_learn/il/eval_runner/__init__.py rename roboverse_learn/il/{dp => }/eval_runner/dp_eval_runner.py (95%) create mode 100644 roboverse_learn/il/fm/README.md rename roboverse_learn/il/{dp/models => fm/policies}/fm_dit_image_policy.py (95%) rename roboverse_learn/il/{dp/models => fm/policies}/fm_unet_image_policy.py (95%) create mode 100644 roboverse_learn/il/fm/requirements.txt create mode 100644 roboverse_learn/il/runner/__init__.py rename roboverse_learn/il/{dp => }/runner/base_policy.py (100%) rename roboverse_learn/il/{dp => }/runner/dp_runner.py (98%) rename roboverse_learn/il/{dp/main.py => train.py} (91%) rename roboverse_learn/il/utils/{common => }/checkpoint_util.py (100%) delete mode 100644 roboverse_learn/il/utils/common/normalize_util.py delete mode 100644 roboverse_learn/il/utils/common/normalizer.py delete mode 100644 roboverse_learn/il/utils/common/replay_buffer.py delete mode 100644 roboverse_learn/il/utils/common/sampler.py rename roboverse_learn/il/utils/{common => }/cv2_util.py (100%) rename roboverse_learn/il/utils/{common => }/dict_of_tensor_mixin.py (100%) delete mode 100644 roboverse_learn/il/utils/diffusion_policy/diffusion_policy/__init__.py delete mode 100644 roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/checkpoint_util.py delete mode 100644 roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/cv2_util.py delete mode 100644 roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/env_util.py delete mode 100644 roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/json_logger.py delete mode 100644 roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/nested_dict_util.py delete mode 100644 roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/pose_trajectory_interpolator.py delete mode 100644 roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/precise_sleep.py delete mode 100644 roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/pymunk_override.py delete mode 100644 roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/pymunk_util.py delete mode 100644 roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/pytorch_util.py delete mode 100644 roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/robomimic_config_util.py delete mode 100644 roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/robomimic_util.py delete mode 100644 roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/timestamp_accumulator.py delete mode 100644 roboverse_learn/il/utils/diffusion_policy/diffusion_policy/config/robot_dp.yaml delete mode 100644 roboverse_learn/il/utils/diffusion_policy/diffusion_policy/config/task/default_task.yaml delete mode 100644 roboverse_learn/il/utils/diffusion_policy/diffusion_policy/dataset/robot_image_dataset.py delete mode 100644 roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/common/dict_of_tensor_mixin.py delete mode 100644 roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/common/lr_scheduler.py delete mode 100644 roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/common/module_attr_mixin.py delete mode 100644 roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/common/rotation_transformer.py delete mode 100644 roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/common/shape_util.py delete mode 100644 roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/common/tensor_util.py delete mode 100644 roboverse_learn/il/utils/diffusion_policy/diffusion_policy/policy/diffusion_unet_image_policy.py delete mode 100644 roboverse_learn/il/utils/diffusion_policy/diffusion_policy/workspace/base_workspace.py delete mode 100644 roboverse_learn/il/utils/diffusion_policy/diffusion_policy/workspace/robotworkspace.py delete mode 100644 roboverse_learn/il/utils/diffusion_policy/pyproject.toml delete mode 100644 roboverse_learn/il/utils/diffusion_policy/scripts/prune_and_rename.py delete mode 100644 roboverse_learn/il/utils/diffusion_policy/train.py delete mode 100644 roboverse_learn/il/utils/diffusion_policy/train_dp.sh rename roboverse_learn/il/utils/{common => }/env_util.py (100%) rename roboverse_learn/il/utils/{common => }/eval_args.py (100%) rename roboverse_learn/il/utils/{common => }/eval_runner_getter.py (79%) rename roboverse_learn/il/utils/{diffusion_policy/diffusion_policy/common => }/flow_matchers.py (100%) rename roboverse_learn/il/utils/{common => }/json_logger.py (100%) rename roboverse_learn/il/utils/{common => }/lr_scheduler.py (100%) rename roboverse_learn/il/utils/{common => }/module_attr_mixin.py (100%) rename roboverse_learn/il/utils/{common => }/nested_dict_util.py (100%) rename roboverse_learn/il/utils/{diffusion_policy/diffusion_policy/common => }/normalize_util.py (98%) rename roboverse_learn/il/utils/{diffusion_policy/diffusion_policy/model/common => }/normalizer.py (98%) rename roboverse_learn/il/utils/{common => }/pose_trajectory_interpolator.py (100%) rename roboverse_learn/il/utils/{common => }/precise_sleep.py (100%) rename roboverse_learn/il/utils/{common => }/pymunk_override.py (100%) rename roboverse_learn/il/utils/{common => }/pymunk_util.py (100%) rename roboverse_learn/il/utils/{common => }/pytorch_util.py (100%) rename roboverse_learn/il/utils/{diffusion_policy/diffusion_policy/common => }/replay_buffer.py (100%) rename roboverse_learn/il/utils/{common => }/robomimic_config_util.py (100%) rename roboverse_learn/il/utils/{common => }/robomimic_util.py (100%) rename roboverse_learn/il/utils/{common => }/rotation_transformer.py (100%) rename roboverse_learn/il/utils/{diffusion_policy/diffusion_policy/common => }/sampler.py (98%) rename roboverse_learn/il/utils/{common => }/shape_util.py (100%) rename roboverse_learn/il/utils/{common => }/tensor_util.py (100%) rename roboverse_learn/il/utils/{common => }/timestamp_accumulator.py (100%) create mode 100644 roboverse_learn/il/vita/README.md rename roboverse_learn/il/{dp/models => vita/policies}/vita_policy.py (93%) create mode 100644 roboverse_learn/il/vita/requirements.txt diff --git a/roboverse_learn/il/README.md b/roboverse_learn/il/README.md new file mode 100644 index 000000000..11be6f589 --- /dev/null +++ b/roboverse_learn/il/README.md @@ -0,0 +1,48 @@ +# RoboVerse Imitation Learning (IL) Policies + +## Example Usage + +Pick a policy folder and follow its README for setup and usage. + +Example: + +```bash +# From the repo root +cd roboverse_learn/il/dp # or fm/, vita/ depending on the policy +pip install -r requirements.txt +cd ../../.. + +# Run policy training and evaluation (example: diffusion policy, DiT backbone) +bash roboverse_learn/il/il_run.sh --task_name_set close_box --algo_choose ddpm_dit +``` + +We keep each policy as self-contained as possible (code, dependencies, docs) and only share the minimum common abstractions. + +## Troubleshooting + +```bash +# Fix potential package version issues +bash roboverse_learn/il/il_setup.sh +``` + +## Supported Algorithms + +| Name | Policy | Backbone | Model Config | Ref | +| --- | --- | --- | --- | --- | +| `ddpm_dit` | Diffusion Policy (DDPM) | DiT | `model_config/ddpm_dit_model.yaml` | [1], [5] | +| `fm_dit` | Flow Matching | DiT | `model_config/fm_dit_model.yaml` | [6], [5] | +| `vita` | VITA Policy | MLP | `model_config/vita_model.yaml` | [7] | +| `ddpm_unet` | Diffusion Policy (DDPM) | UNet | `model_config/ddpm_model.yaml` | [1], [4] | +| `ddim_unet` | Diffusion Policy (DDIM) | UNet | `model_config/ddim_model.yaml` | [2], [4] | +| `fm_unet` | Flow Matching | UNet | `model_config/fm_unet_model.yaml` | [6] | +| `score_unet` | Score-Based Model | UNet | `model_config/score_model.yaml` | [3], [4] | + +### References + +1. Ho, Jonathan, Ajay Jain, and Pieter Abbeel. "Denoising Diffusion Probabilistic Models." (2020). +2. Song, Jiaming, Chenlin Meng, and Stefano Ermon. "Denoising Diffusion Implicit Models." (2021). +3. Song, Yang, et al. "Score-Based Generative Modeling through Stochastic Differential Equations." (2021). +4. Chi, Cheng, et al. "Diffusion Policy: Diffusion Models for Robotic Manipulation." (2023). +5. Peebles, William, and Jun-Yan Zhu. "DiT: Diffusion Models with Transformers." (2023). +6. Lipman, Yaron, et al. "Flow Matching for Generative Modeling." (2023). +7. Gao, Dechen, et al. "VITA: Vision-to-Action Flow Matching Policy." (2025). diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/bet/libraries/mingpt/__init__.py b/roboverse_learn/il/__init__.py similarity index 100% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/bet/libraries/mingpt/__init__.py rename to roboverse_learn/il/__init__.py diff --git a/roboverse_learn/il/act/utils.py b/roboverse_learn/il/act/utils.py index 5d7022d1f..66e15b33b 100644 --- a/roboverse_learn/il/act/utils.py +++ b/roboverse_learn/il/act/utils.py @@ -6,7 +6,7 @@ import h5py import json from torch.utils.data import TensorDataset, DataLoader -from roboverse_learn.il.utils.common.replay_buffer import ReplayBuffer +from roboverse_learn.il.utils.replay_buffer import ReplayBuffer import IPython e = IPython.embed diff --git a/roboverse_learn/il/base/__init__.py b/roboverse_learn/il/base/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/dataset/base_dataset.py b/roboverse_learn/il/base/base_dataset.py similarity index 94% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/dataset/base_dataset.py rename to roboverse_learn/il/base/base_dataset.py index 79ce1e528..68830efde 100644 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/dataset/base_dataset.py +++ b/roboverse_learn/il/base/base_dataset.py @@ -2,7 +2,8 @@ import torch import torch.nn -from diffusion_policy.model.common.normalizer import LinearNormalizer + +from roboverse_learn.il.utils.normalizer import LinearNormalizer class BaseLowdimDataset(torch.utils.data.Dataset): diff --git a/roboverse_learn/il/dp/base/base_eval_runner.py b/roboverse_learn/il/base/base_eval_runner.py similarity index 99% rename from roboverse_learn/il/dp/base/base_eval_runner.py rename to roboverse_learn/il/base/base_eval_runner.py index 3ffe8c714..9f2b24407 100644 --- a/roboverse_learn/il/dp/base/base_eval_runner.py +++ b/roboverse_learn/il/base/base_eval_runner.py @@ -1,4 +1,4 @@ -from dp.runner.base_policy import BasePolicyCfg +from roboverse_learn.il.runner.base_policy import BasePolicyCfg try: from curobo.types.math import Pose @@ -12,7 +12,7 @@ import torch from loguru import logger as log from metasim.scenario.scenario import ScenarioCfg -from roboverse_learn.il.utils.common.pytorch_util import dict_apply +from roboverse_learn.il.utils.pytorch_util import dict_apply class BaseEvalRunner: diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/policy/base_image_policy.py b/roboverse_learn/il/base/base_image_policy.py similarity index 81% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/policy/base_image_policy.py rename to roboverse_learn/il/base/base_image_policy.py index fa5a4751f..beafab037 100644 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/policy/base_image_policy.py +++ b/roboverse_learn/il/base/base_image_policy.py @@ -1,8 +1,8 @@ from typing import Dict import torch -from diffusion_policy.model.common.module_attr_mixin import ModuleAttrMixin -from diffusion_policy.model.common.normalizer import LinearNormalizer +from roboverse_learn.il.utils.module_attr_mixin import ModuleAttrMixin +from roboverse_learn.il.utils.normalizer import LinearNormalizer class BaseImagePolicy(ModuleAttrMixin): diff --git a/roboverse_learn/il/dp/base/base_model.py b/roboverse_learn/il/base/base_model.py similarity index 100% rename from roboverse_learn/il/dp/base/base_model.py rename to roboverse_learn/il/base/base_model.py diff --git a/roboverse_learn/il/dp/base/base_runner.py b/roboverse_learn/il/base/base_runner.py similarity index 100% rename from roboverse_learn/il/dp/base/base_runner.py rename to roboverse_learn/il/base/base_runner.py diff --git a/roboverse_learn/il/configs/__init__.py b/roboverse_learn/il/configs/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/roboverse_learn/il/dp/configs/dataset_config/robot_image_dataset.yaml b/roboverse_learn/il/configs/dataset_config/robot_image_dataset.yaml similarity index 72% rename from roboverse_learn/il/dp/configs/dataset_config/robot_image_dataset.yaml rename to roboverse_learn/il/configs/dataset_config/robot_image_dataset.yaml index 1b15af977..6012c131c 100644 --- a/roboverse_learn/il/dp/configs/dataset_config/robot_image_dataset.yaml +++ b/roboverse_learn/il/configs/dataset_config/robot_image_dataset.yaml @@ -1,4 +1,4 @@ -_target_: dp.datasets.robot_image_dataset.RobotImageDataset +_target_: roboverse_learn.il.datasets.robot_image_dataset.RobotImageDataset zarr_path: data_policy/useless.zarr horizon: ${horizon} pad_before: ${eval:'${n_obs_steps}-1'} diff --git a/roboverse_learn/il/dp/configs/dp_runner.yaml b/roboverse_learn/il/configs/dp_runner.yaml similarity index 90% rename from roboverse_learn/il/dp/configs/dp_runner.yaml rename to roboverse_learn/il/configs/dp_runner.yaml index a5febd94d..d18623539 100644 --- a/roboverse_learn/il/dp/configs/dp_runner.yaml +++ b/roboverse_learn/il/configs/dp_runner.yaml @@ -1,13 +1,13 @@ defaults: - _self_ - dataset_config: robot_image_dataset - - model_config: ${oc.env:algo_model,ddpm_model} # diffusion_policy_model/fm_model/DDIM_model + - model_config: ${oc.env:algo_model,ddpm_dit_model} - eval_config: diffusion_policy_eval - train_config: diffusion_policy_train task_name: placeholder name: robot_${task_name} -_target_: dp.runner.dp_runner.DPRunner +_target_: roboverse_learn.il.runner.dp_runner.DPRunner image_shape: &image_shape [3, 256, 256] diff --git a/roboverse_learn/il/dp/configs/eval_config/diffusion_policy_eval.yaml b/roboverse_learn/il/configs/eval_config/diffusion_policy_eval.yaml similarity index 86% rename from roboverse_learn/il/dp/configs/eval_config/diffusion_policy_eval.yaml rename to roboverse_learn/il/configs/eval_config/diffusion_policy_eval.yaml index 25768449f..8633a210f 100644 --- a/roboverse_learn/il/dp/configs/eval_config/diffusion_policy_eval.yaml +++ b/roboverse_learn/il/configs/eval_config/diffusion_policy_eval.yaml @@ -1,5 +1,5 @@ eval_args: - _target_: roboverse_learn.il.utils.common.eval_args.Args + _target_: roboverse_learn.il.utils.eval_args.Args # random: # _target_: metasim.cfg.randomization.RandomizationCfg # level: 0 diff --git a/roboverse_learn/il/dp/configs/model_config/ddim_unet_model.yaml b/roboverse_learn/il/configs/model_config/ddim_unet_model.yaml similarity index 79% rename from roboverse_learn/il/dp/configs/model_config/ddim_unet_model.yaml rename to roboverse_learn/il/configs/model_config/ddim_unet_model.yaml index ef0fd5da5..05286fd9b 100644 --- a/roboverse_learn/il/dp/configs/model_config/ddim_unet_model.yaml +++ b/roboverse_learn/il/configs/model_config/ddim_unet_model.yaml @@ -1,4 +1,4 @@ -_target_: dp.models.ddim_unet_image_policy.DiffusionUnetImagePolicy +_target_: roboverse_learn.il.dp.policies.ddim_unet_image_policy.DiffusionUnetImagePolicy shape_meta: ${shape_meta} @@ -13,10 +13,10 @@ noise_scheduler: prediction_type: epsilon # or sample obs_encoder: - _target_: diffusion_policy.model.vision.multi_image_obs_encoder.MultiImageObsEncoder + _target_: roboverse_learn.il.dp.models.vision.multi_image_obs_encoder.MultiImageObsEncoder shape_meta: ${shape_meta} rgb_model: - _target_: diffusion_policy.model.vision.model_getter.get_resnet + _target_: roboverse_learn.il.dp.models.vision.model_getter.get_resnet name: resnet18 weights: null resize_shape: null diff --git a/roboverse_learn/il/dp/configs/model_config/ddpm_dit_model.yaml b/roboverse_learn/il/configs/model_config/ddpm_dit_model.yaml similarity index 78% rename from roboverse_learn/il/dp/configs/model_config/ddpm_dit_model.yaml rename to roboverse_learn/il/configs/model_config/ddpm_dit_model.yaml index f3806a9c4..bb453cb7c 100644 --- a/roboverse_learn/il/dp/configs/model_config/ddpm_dit_model.yaml +++ b/roboverse_learn/il/configs/model_config/ddpm_dit_model.yaml @@ -1,4 +1,4 @@ -_target_: dp.models.ddpm_dit_image_policy.DiffusionDiTImagePolicy +_target_: roboverse_learn.il.dp.policies.ddpm_dit_image_policy.DiffusionDiTImagePolicy shape_meta: ${shape_meta} @@ -13,10 +13,10 @@ noise_scheduler: prediction_type: epsilon # or sample obs_encoder: - _target_: diffusion_policy.model.vision.multi_image_obs_encoder.MultiImageObsEncoder + _target_: roboverse_learn.il.dp.models.vision.multi_image_obs_encoder.MultiImageObsEncoder shape_meta: ${shape_meta} rgb_model: - _target_: diffusion_policy.model.vision.model_getter.get_resnet + _target_: roboverse_learn.il.dp.models.vision.model_getter.get_resnet name: resnet18 weights: null resize_shape: null diff --git a/roboverse_learn/il/dp/configs/model_config/ddpm_unet_model.yaml b/roboverse_learn/il/configs/model_config/ddpm_unet_model.yaml similarity index 79% rename from roboverse_learn/il/dp/configs/model_config/ddpm_unet_model.yaml rename to roboverse_learn/il/configs/model_config/ddpm_unet_model.yaml index 4d14c065c..575fd912b 100644 --- a/roboverse_learn/il/dp/configs/model_config/ddpm_unet_model.yaml +++ b/roboverse_learn/il/configs/model_config/ddpm_unet_model.yaml @@ -1,4 +1,4 @@ -_target_: dp.models.ddpm_unet_image_policy.DiffusionUnetImagePolicy +_target_: roboverse_learn.il.dp.policies.ddpm_unet_image_policy.DiffusionUnetImagePolicy shape_meta: ${shape_meta} @@ -13,10 +13,10 @@ noise_scheduler: prediction_type: epsilon # or sample obs_encoder: - _target_: diffusion_policy.model.vision.multi_image_obs_encoder.MultiImageObsEncoder + _target_: roboverse_learn.il.dp.models.vision.multi_image_obs_encoder.MultiImageObsEncoder shape_meta: ${shape_meta} rgb_model: - _target_: diffusion_policy.model.vision.model_getter.get_resnet + _target_: roboverse_learn.il.dp.models.vision.model_getter.get_resnet name: resnet18 weights: null resize_shape: null diff --git a/roboverse_learn/il/dp/configs/model_config/fm_dit_model.yaml b/roboverse_learn/il/configs/model_config/fm_dit_model.yaml similarity index 68% rename from roboverse_learn/il/dp/configs/model_config/fm_dit_model.yaml rename to roboverse_learn/il/configs/model_config/fm_dit_model.yaml index aa2417783..4779783c2 100644 --- a/roboverse_learn/il/dp/configs/model_config/fm_dit_model.yaml +++ b/roboverse_learn/il/configs/model_config/fm_dit_model.yaml @@ -1,13 +1,13 @@ -_target_: dp.models.fm_dit_image_policy.FlowMatchingDiTImagePolicy +_target_: roboverse_learn.il.fm.policies.fm_dit_image_policy.FlowMatchingDiTImagePolicy shape_meta: ${shape_meta} obs_encoder: - _target_: diffusion_policy.model.vision.multi_image_obs_encoder.MultiImageObsEncoder + _target_: roboverse_learn.il.dp.models.vision.multi_image_obs_encoder.MultiImageObsEncoder shape_meta: ${shape_meta} rgb_model: - _target_: diffusion_policy.model.vision.model_getter.get_resnet + _target_: roboverse_learn.il.dp.models.vision.model_getter.get_resnet name: resnet18 weights: null resize_shape: null diff --git a/roboverse_learn/il/dp/configs/model_config/fm_unet_model.yaml b/roboverse_learn/il/configs/model_config/fm_unet_model.yaml similarity index 68% rename from roboverse_learn/il/dp/configs/model_config/fm_unet_model.yaml rename to roboverse_learn/il/configs/model_config/fm_unet_model.yaml index 61306a6de..41bb00e90 100644 --- a/roboverse_learn/il/dp/configs/model_config/fm_unet_model.yaml +++ b/roboverse_learn/il/configs/model_config/fm_unet_model.yaml @@ -1,13 +1,13 @@ -_target_: dp.models.fm_unet_image_policy.FlowMatchingUnetImagePolicy +_target_: roboverse_learn.il.fm.policies.fm_unet_image_policy.FlowMatchingUnetImagePolicy shape_meta: ${shape_meta} obs_encoder: - _target_: diffusion_policy.model.vision.multi_image_obs_encoder.MultiImageObsEncoder + _target_: roboverse_learn.il.dp.models.vision.multi_image_obs_encoder.MultiImageObsEncoder shape_meta: ${shape_meta} rgb_model: - _target_: diffusion_policy.model.vision.model_getter.get_resnet + _target_: roboverse_learn.il.dp.models.vision.model_getter.get_resnet name: resnet18 weights: null resize_shape: null diff --git a/roboverse_learn/il/dp/configs/model_config/score_model.yaml b/roboverse_learn/il/configs/model_config/score_model.yaml similarity index 79% rename from roboverse_learn/il/dp/configs/model_config/score_model.yaml rename to roboverse_learn/il/configs/model_config/score_model.yaml index 211bcac1b..60e21dcaa 100644 --- a/roboverse_learn/il/dp/configs/model_config/score_model.yaml +++ b/roboverse_learn/il/configs/model_config/score_model.yaml @@ -1,4 +1,4 @@ -_target_: dp.models.score_unet_image_policy.ScoreMatchingUnetImagePolicy +_target_: roboverse_learn.il.dp.policies.score_unet_image_policy.ScoreMatchingUnetImagePolicy shape_meta: ${shape_meta} @@ -13,10 +13,10 @@ noise_scheduler: prediction_type: epsilon # or sample obs_encoder: - _target_: diffusion_policy.model.vision.multi_image_obs_encoder.MultiImageObsEncoder + _target_: roboverse_learn.il.dp.models.vision.multi_image_obs_encoder.MultiImageObsEncoder shape_meta: ${shape_meta} rgb_model: - _target_: diffusion_policy.model.vision.model_getter.get_resnet + _target_: roboverse_learn.il.dp.models.vision.model_getter.get_resnet name: resnet18 weights: null resize_shape: null diff --git a/roboverse_learn/il/dp/configs/model_config/vita_model.yaml b/roboverse_learn/il/configs/model_config/vita_model.yaml similarity index 76% rename from roboverse_learn/il/dp/configs/model_config/vita_model.yaml rename to roboverse_learn/il/configs/model_config/vita_model.yaml index e55c04258..378dc73ae 100644 --- a/roboverse_learn/il/dp/configs/model_config/vita_model.yaml +++ b/roboverse_learn/il/configs/model_config/vita_model.yaml @@ -1,13 +1,13 @@ -_target_: dp.models.vita_policy.VITAImagePolicy +_target_: roboverse_learn.il.vita.policies.vita_policy.VITAImagePolicy shape_meta: ${shape_meta} obs_encoder: - _target_: diffusion_policy.model.vision.multi_image_obs_encoder.MultiImageObsEncoder + _target_: roboverse_learn.il.dp.models.vision.multi_image_obs_encoder.MultiImageObsEncoder shape_meta: ${shape_meta} rgb_model: - _target_: diffusion_policy.model.vision.model_getter.get_resnet + _target_: roboverse_learn.il.dp.models.vision.model_getter.get_resnet name: resnet18 weights: null resize_shape: null @@ -31,7 +31,7 @@ latent_dim: 512 # Flow matcher parameters flow_matcher: - _target_: diffusion_policy.common.flow_matchers.ExactOptimalTransportConditionalFlowMatcher + _target_: roboverse_learn.il.utils.flow_matchers.ExactOptimalTransportConditionalFlowMatcher sigma: 0.0 num_sampling_steps: 6 diff --git a/roboverse_learn/il/dp/configs/train_config/diffusion_policy_train.yaml b/roboverse_learn/il/configs/train_config/diffusion_policy_train.yaml similarity index 93% rename from roboverse_learn/il/dp/configs/train_config/diffusion_policy_train.yaml rename to roboverse_learn/il/configs/train_config/diffusion_policy_train.yaml index 92e5426e0..edb213072 100644 --- a/roboverse_learn/il/dp/configs/train_config/diffusion_policy_train.yaml +++ b/roboverse_learn/il/configs/train_config/diffusion_policy_train.yaml @@ -46,7 +46,7 @@ val_dataloader: persistent_workers: False ema: - _target_: diffusion_policy.model.diffusion.ema_model.EMAModel + _target_: roboverse_learn.il.dp.models.diffusion.ema_model.EMAModel update_after_step: 0 inv_gamma: 1.0 power: 0.75 diff --git a/roboverse_learn/il/datasets/__init__.py b/roboverse_learn/il/datasets/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/roboverse_learn/il/dp/base/base_dataset.py b/roboverse_learn/il/datasets/base_dataset.py similarity index 94% rename from roboverse_learn/il/dp/base/base_dataset.py rename to roboverse_learn/il/datasets/base_dataset.py index e29862e47..e2d04a803 100644 --- a/roboverse_learn/il/dp/base/base_dataset.py +++ b/roboverse_learn/il/datasets/base_dataset.py @@ -2,8 +2,7 @@ import torch import torch.nn - -from roboverse_learn.il.utils.common.normalizer import LinearNormalizer +from roboverse_learn.il.utils.normalizer import LinearNormalizer class BaseLowdimDataset(torch.utils.data.Dataset): diff --git a/roboverse_learn/il/dp/datasets/robot_image_dataset.py b/roboverse_learn/il/datasets/robot_image_dataset.py similarity index 93% rename from roboverse_learn/il/dp/datasets/robot_image_dataset.py rename to roboverse_learn/il/datasets/robot_image_dataset.py index 9d3596f39..f8be9376c 100644 --- a/roboverse_learn/il/dp/datasets/robot_image_dataset.py +++ b/roboverse_learn/il/datasets/robot_image_dataset.py @@ -4,18 +4,17 @@ import numba import numpy as np import torch -from termcolor import cprint - -from dp.base.base_dataset import BaseImageDataset -from roboverse_learn.il.utils.common.normalize_util import get_image_range_normalizer -from roboverse_learn.il.utils.common.normalizer import LinearNormalizer -from roboverse_learn.il.utils.common.pytorch_util import dict_apply -from roboverse_learn.il.utils.common.replay_buffer import ReplayBuffer -from roboverse_learn.il.utils.common.sampler import ( +from roboverse_learn.il.utils.normalize_util import get_image_range_normalizer +from roboverse_learn.il.utils.pytorch_util import dict_apply +from roboverse_learn.il.utils.replay_buffer import ReplayBuffer +from roboverse_learn.il.utils.sampler import ( SequenceSampler, downsample_mask, get_val_mask, ) +from roboverse_learn.il.base.base_dataset import BaseImageDataset +from roboverse_learn.il.utils.normalizer import LinearNormalizer +from termcolor import cprint class RobotImageDataset(BaseImageDataset): @@ -30,6 +29,7 @@ def __init__( batch_size=64, max_train_episodes=None, ): + super().__init__() self.replay_buffer = ReplayBuffer.copy_from_path( diff --git a/roboverse_learn/il/utils/diffusion_policy/.gitignore b/roboverse_learn/il/dp/.gitignore similarity index 100% rename from roboverse_learn/il/utils/diffusion_policy/.gitignore rename to roboverse_learn/il/dp/.gitignore diff --git a/roboverse_learn/il/dp/README.md b/roboverse_learn/il/dp/README.md index 674bb722a..62ddb4734 100644 --- a/roboverse_learn/il/dp/README.md +++ b/roboverse_learn/il/dp/README.md @@ -1,15 +1,10 @@ -# Flow Matching and Diffusion Based IL Policies +# Diffusion Policy ## 1. Install ```bash -cd roboverse_learn/il/utils/diffusion_policy - -pip install -e . - -cd ../../../../ - -pip install pandas wandb +cd roboverse_learn/il/dp +pip install -r requirements.txt ``` Register for a Weights & Biases (wandb) account to obtain an API key. @@ -39,25 +34,3 @@ eval_enable=False train_enable=False eval_enable=True ``` - -## Supported Algorithms - -| Algorithm | Backbone | Model Config | Ref | -| --- | --- | --- | --- | -| Diffusion Policy (DDPM) | DiT | `model_config/ddpm_dit_model.yaml` | [1], [5] | -| Flow Matching | DiT | `model_config/fm_dit_model.yaml` | [6], [5] | -| VITA Policy | MLP | `model_config/vita_model.yaml` | [7] | -| Diffusion Policy (DDPM) | UNet | `model_config/ddpm_model.yaml` | [1], [4] | -| Diffusion Policy (DDIM) | UNet | `model_config/ddim_model.yaml` | [2], [4] | -| Flow Matching | UNet | `model_config/fm_unet_model.yaml` | [6] | -| Score-Based Model | UNet | `model_config/score_model.yaml` | [3], [4] | - -### References - -1. Ho, Jonathan, Ajay Jain, and Pieter Abbeel. "Denoising Diffusion Probabilistic Models." (2020). -2. Song, Jiaming, Chenlin Meng, and Stefano Ermon. "Denoising Diffusion Implicit Models." (2021). -3. Song, Yang, et al. "Score-Based Generative Modeling through Stochastic Differential Equations." (2021). -4. Chi, Cheng, et al. "Diffusion Policy: Diffusion Models for Robotic Manipulation." (2023). -5. Peebles, William, and Jun-Yan Zhu. "DiT: Diffusion Models with Transformers." (2023). -6. Lipman, Yaron, et al. "Flow Matching for Generative Modeling." (2023). -7. Gao, Dechen, et al. "VITA: Vision-to-Action Flow Matching Policy." (2025). diff --git a/roboverse_learn/il/dp/__init__.py b/roboverse_learn/il/dp/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/bet/action_ae/__init__.py b/roboverse_learn/il/dp/models/bet/action_ae/__init__.py similarity index 96% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/bet/action_ae/__init__.py rename to roboverse_learn/il/dp/models/bet/action_ae/__init__.py index bfaa76a40..6808f9035 100644 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/bet/action_ae/__init__.py +++ b/roboverse_learn/il/dp/models/bet/action_ae/__init__.py @@ -1,7 +1,7 @@ import abc from typing import Optional, Union -import diffusion_policy.model.bet.utils as utils +import roboverse_learn.il.dp.models.bet.utils as utils import torch import torch.nn as nn from torch.utils.data import DataLoader diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/bet/action_ae/discretizers/k_means.py b/roboverse_learn/il/dp/models/bet/action_ae/discretizers/k_means.py similarity index 98% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/bet/action_ae/discretizers/k_means.py rename to roboverse_learn/il/dp/models/bet/action_ae/discretizers/k_means.py index bae365596..e9a5effde 100644 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/bet/action_ae/discretizers/k_means.py +++ b/roboverse_learn/il/dp/models/bet/action_ae/discretizers/k_means.py @@ -3,7 +3,7 @@ import numpy as np import torch import tqdm -from diffusion_policy.model.common.dict_of_tensor_mixin import DictOfTensorMixin +from roboverse_learn.il.utils.dict_of_tensor_mixin import DictOfTensorMixin class KMeansDiscretizer(DictOfTensorMixin): diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/bet/latent_generators/latent_generator.py b/roboverse_learn/il/dp/models/bet/latent_generators/latent_generator.py similarity index 97% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/bet/latent_generators/latent_generator.py rename to roboverse_learn/il/dp/models/bet/latent_generators/latent_generator.py index 89120c660..36fbf4399 100644 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/bet/latent_generators/latent_generator.py +++ b/roboverse_learn/il/dp/models/bet/latent_generators/latent_generator.py @@ -1,7 +1,7 @@ import abc from typing import Optional, Tuple -import diffusion_policy.model.bet.utils as utils +import roboverse_learn.il.dp.models.bet.utils as utils import torch diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/bet/latent_generators/mingpt.py b/roboverse_learn/il/dp/models/bet/latent_generators/mingpt.py similarity index 95% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/bet/latent_generators/mingpt.py rename to roboverse_learn/il/dp/models/bet/latent_generators/mingpt.py index 242bebdea..55b6d7748 100644 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/bet/latent_generators/mingpt.py +++ b/roboverse_learn/il/dp/models/bet/latent_generators/mingpt.py @@ -1,13 +1,13 @@ from typing import Optional, Tuple -import diffusion_policy.model.bet.latent_generators.latent_generator as latent_generator -import diffusion_policy.model.bet.libraries.mingpt.model as mingpt_model -import diffusion_policy.model.bet.libraries.mingpt.trainer as mingpt_trainer +import roboverse_learn.il.dp.models.bet.latent_generators.latent_generator as latent_generator +import roboverse_learn.il.dp.models.bet.libraries.mingpt.model as mingpt_model +import roboverse_learn.il.dp.models.bet.libraries.mingpt.trainer as mingpt_trainer import einops import torch import torch.nn as nn import torch.nn.functional as F -from diffusion_policy.model.bet.libraries.loss_fn import FocalLoss, soft_cross_entropy +from roboverse_learn.il.dp.models.bet.libraries.loss_fn import FocalLoss, soft_cross_entropy class MinGPT(latent_generator.AbstractLatentGenerator): diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/bet/latent_generators/transformer.py b/roboverse_learn/il/dp/models/bet/latent_generators/transformer.py similarity index 93% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/bet/latent_generators/transformer.py rename to roboverse_learn/il/dp/models/bet/latent_generators/transformer.py index 96c093374..b9baf4f48 100644 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/bet/latent_generators/transformer.py +++ b/roboverse_learn/il/dp/models/bet/latent_generators/transformer.py @@ -1,12 +1,12 @@ from typing import Optional, Tuple -import diffusion_policy.model.bet.latent_generators.latent_generator as latent_generator +import roboverse_learn.il.dp.models.bet.latent_generators.latent_generator as latent_generator import einops import torch import torch.nn as nn import torch.nn.functional as F -from diffusion_policy.model.bet.libraries.loss_fn import FocalLoss, soft_cross_entropy -from diffusion_policy.model.diffusion.transformer_for_diffusion import ( +from roboverse_learn.il.dp.models.bet.libraries.loss_fn import FocalLoss, soft_cross_entropy +from roboverse_learn.il.dp.models.diffusion.transformer_for_diffusion import ( TransformerForDiffusion, ) diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/bet/libraries/loss_fn.py b/roboverse_learn/il/dp/models/bet/libraries/loss_fn.py similarity index 100% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/bet/libraries/loss_fn.py rename to roboverse_learn/il/dp/models/bet/libraries/loss_fn.py diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/bet/libraries/mingpt/LICENSE b/roboverse_learn/il/dp/models/bet/libraries/mingpt/LICENSE similarity index 100% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/bet/libraries/mingpt/LICENSE rename to roboverse_learn/il/dp/models/bet/libraries/mingpt/LICENSE diff --git a/roboverse_learn/il/dp/models/bet/libraries/mingpt/__init__.py b/roboverse_learn/il/dp/models/bet/libraries/mingpt/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/bet/libraries/mingpt/model.py b/roboverse_learn/il/dp/models/bet/libraries/mingpt/model.py similarity index 100% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/bet/libraries/mingpt/model.py rename to roboverse_learn/il/dp/models/bet/libraries/mingpt/model.py diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/bet/libraries/mingpt/trainer.py b/roboverse_learn/il/dp/models/bet/libraries/mingpt/trainer.py similarity index 100% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/bet/libraries/mingpt/trainer.py rename to roboverse_learn/il/dp/models/bet/libraries/mingpt/trainer.py diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/bet/libraries/mingpt/utils.py b/roboverse_learn/il/dp/models/bet/libraries/mingpt/utils.py similarity index 100% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/bet/libraries/mingpt/utils.py rename to roboverse_learn/il/dp/models/bet/libraries/mingpt/utils.py diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/bet/utils.py b/roboverse_learn/il/dp/models/bet/utils.py similarity index 100% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/bet/utils.py rename to roboverse_learn/il/dp/models/bet/utils.py diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/diffusion/action_ae.py b/roboverse_learn/il/dp/models/diffusion/action_ae.py similarity index 98% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/diffusion/action_ae.py rename to roboverse_learn/il/dp/models/diffusion/action_ae.py index 1672e1438..9b2262bb7 100644 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/diffusion/action_ae.py +++ b/roboverse_learn/il/dp/models/diffusion/action_ae.py @@ -1,6 +1,6 @@ import torch.nn as nn import torch.nn.functional as F -from diffusion_policy.model.diffusion.layers import Mlp +from roboverse_learn.il.dp.models.diffusion.layers import Mlp def weights_init_encoder(m): diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/diffusion/conditional_unet1d.py b/roboverse_learn/il/dp/models/diffusion/conditional_unet1d.py similarity index 98% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/diffusion/conditional_unet1d.py rename to roboverse_learn/il/dp/models/diffusion/conditional_unet1d.py index 630c6d2f5..8c5228110 100644 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/diffusion/conditional_unet1d.py +++ b/roboverse_learn/il/dp/models/diffusion/conditional_unet1d.py @@ -4,12 +4,12 @@ import einops import torch import torch.nn as nn -from diffusion_policy.model.diffusion.conv1d_components import ( +from roboverse_learn.il.dp.models.diffusion.conv1d_components import ( Conv1dBlock, Downsample1d, Upsample1d, ) -from diffusion_policy.model.diffusion.positional_embedding import SinusoidalPosEmb +from roboverse_learn.il.dp.models.diffusion.positional_embedding import SinusoidalPosEmb from einops.layers.torch import Rearrange logger = logging.getLogger(__name__) diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/diffusion/conv1d_components.py b/roboverse_learn/il/dp/models/diffusion/conv1d_components.py similarity index 100% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/diffusion/conv1d_components.py rename to roboverse_learn/il/dp/models/diffusion/conv1d_components.py diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/diffusion/ema_model.py b/roboverse_learn/il/dp/models/diffusion/ema_model.py similarity index 100% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/diffusion/ema_model.py rename to roboverse_learn/il/dp/models/diffusion/ema_model.py diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/diffusion/flow_net.py b/roboverse_learn/il/dp/models/diffusion/flow_net.py similarity index 98% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/diffusion/flow_net.py rename to roboverse_learn/il/dp/models/diffusion/flow_net.py index d3447406a..1f0b446b6 100644 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/diffusion/flow_net.py +++ b/roboverse_learn/il/dp/models/diffusion/flow_net.py @@ -2,8 +2,8 @@ import torch.nn as nn import torch.nn.functional as F -from diffusion_policy.model.diffusion.positional_embedding import RotaryPosEmb, SinusoidalPosEmb -from diffusion_policy.model.diffusion.layers import Mlp +from roboverse_learn.il.dp.models.diffusion.positional_embedding import RotaryPosEmb, SinusoidalPosEmb +from roboverse_learn.il.dp.models.diffusion.layers import Mlp class Attention(nn.Module): diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/diffusion/layers.py b/roboverse_learn/il/dp/models/diffusion/layers.py similarity index 100% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/diffusion/layers.py rename to roboverse_learn/il/dp/models/diffusion/layers.py diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/diffusion/mask_generator.py b/roboverse_learn/il/dp/models/diffusion/mask_generator.py similarity index 99% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/diffusion/mask_generator.py rename to roboverse_learn/il/dp/models/diffusion/mask_generator.py index e6c8d6a3c..314cc18d6 100644 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/diffusion/mask_generator.py +++ b/roboverse_learn/il/dp/models/diffusion/mask_generator.py @@ -1,7 +1,7 @@ from typing import Optional, Sequence import torch -from diffusion_policy.model.common.module_attr_mixin import ModuleAttrMixin +from roboverse_learn.il.utils.module_attr_mixin import ModuleAttrMixin from torch import nn diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/diffusion/positional_embedding.py b/roboverse_learn/il/dp/models/diffusion/positional_embedding.py similarity index 100% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/diffusion/positional_embedding.py rename to roboverse_learn/il/dp/models/diffusion/positional_embedding.py diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/diffusion/transformer_for_diffusion.py b/roboverse_learn/il/dp/models/diffusion/transformer_for_diffusion.py similarity index 98% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/diffusion/transformer_for_diffusion.py rename to roboverse_learn/il/dp/models/diffusion/transformer_for_diffusion.py index 5e7e89634..29c164a79 100644 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/diffusion/transformer_for_diffusion.py +++ b/roboverse_learn/il/dp/models/diffusion/transformer_for_diffusion.py @@ -3,8 +3,8 @@ import torch import torch.nn as nn -from diffusion_policy.model.common.module_attr_mixin import ModuleAttrMixin -from diffusion_policy.model.diffusion.positional_embedding import SinusoidalPosEmb +from roboverse_learn.il.utils.module_attr_mixin import ModuleAttrMixin +from roboverse_learn.il.dp.models.diffusion.positional_embedding import SinusoidalPosEmb logger = logging.getLogger(__name__) diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/vision/crop_randomizer.py b/roboverse_learn/il/dp/models/vision/crop_randomizer.py similarity index 99% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/vision/crop_randomizer.py rename to roboverse_learn/il/dp/models/vision/crop_randomizer.py index 218e88dde..0817216aa 100644 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/vision/crop_randomizer.py +++ b/roboverse_learn/il/dp/models/vision/crop_randomizer.py @@ -1,4 +1,4 @@ -import diffusion_policy.model.common.tensor_util as tu +import roboverse_learn.il.utils.tensor_util as tu import torch import torch.nn as nn import torchvision.transforms.functional as ttf diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/vision/model_getter.py b/roboverse_learn/il/dp/models/vision/model_getter.py similarity index 100% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/vision/model_getter.py rename to roboverse_learn/il/dp/models/vision/model_getter.py diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/vision/multi_image_obs_encoder.py b/roboverse_learn/il/dp/models/vision/multi_image_obs_encoder.py similarity index 96% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/vision/multi_image_obs_encoder.py rename to roboverse_learn/il/dp/models/vision/multi_image_obs_encoder.py index 0214d673b..912518709 100644 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/vision/multi_image_obs_encoder.py +++ b/roboverse_learn/il/dp/models/vision/multi_image_obs_encoder.py @@ -4,9 +4,9 @@ import torch import torch.nn as nn import torchvision -from diffusion_policy.common.pytorch_util import dict_apply, replace_submodules -from diffusion_policy.model.common.module_attr_mixin import ModuleAttrMixin -from diffusion_policy.model.vision.crop_randomizer import CropRandomizer +from roboverse_learn.il.utils.pytorch_util import dict_apply, replace_submodules +from roboverse_learn.il.utils.module_attr_mixin import ModuleAttrMixin +from roboverse_learn.il.dp.models.vision.crop_randomizer import CropRandomizer class MultiImageObsEncoder(ModuleAttrMixin): diff --git a/roboverse_learn/il/dp/models/ddim_unet_image_policy.py b/roboverse_learn/il/dp/policies/ddim_unet_image_policy.py similarity index 95% rename from roboverse_learn/il/dp/models/ddim_unet_image_policy.py rename to roboverse_learn/il/dp/policies/ddim_unet_image_policy.py index f195e8ad8..69efd3822 100644 --- a/roboverse_learn/il/dp/models/ddim_unet_image_policy.py +++ b/roboverse_learn/il/dp/policies/ddim_unet_image_policy.py @@ -4,16 +4,16 @@ import torch.nn as nn import torch.nn.functional as F from diffusers.schedulers.scheduling_ddim import DDIMScheduler -from diffusion_policy.model.diffusion.conditional_unet1d import ConditionalUnet1D -from diffusion_policy.model.diffusion.mask_generator import LowdimMaskGenerator -from diffusion_policy.model.vision.multi_image_obs_encoder import MultiImageObsEncoder +from roboverse_learn.il.dp.models.diffusion.conditional_unet1d import ConditionalUnet1D +from roboverse_learn.il.dp.models.diffusion.mask_generator import LowdimMaskGenerator +from roboverse_learn.il.dp.models.vision.multi_image_obs_encoder import MultiImageObsEncoder from einops import rearrange, reduce from loguru import logger as log -from roboverse_learn.il.utils.common.module_attr_mixin import ModuleAttrMixin -from roboverse_learn.il.utils.common.normalizer import LinearNormalizer -from roboverse_learn.il.utils.common.pytorch_util import dict_apply -from diffusion_policy.policy.base_image_policy import BaseImagePolicy +from roboverse_learn.il.utils.module_attr_mixin import ModuleAttrMixin +from roboverse_learn.il.utils.normalizer import LinearNormalizer +from roboverse_learn.il.utils.pytorch_util import dict_apply +from roboverse_learn.il.base.base_image_policy import BaseImagePolicy class BaseImagePolicy(ModuleAttrMixin): diff --git a/roboverse_learn/il/dp/models/ddpm_dit_image_policy.py b/roboverse_learn/il/dp/policies/ddpm_dit_image_policy.py similarity index 89% rename from roboverse_learn/il/dp/models/ddpm_dit_image_policy.py rename to roboverse_learn/il/dp/policies/ddpm_dit_image_policy.py index 07f777d3b..f533931b0 100644 --- a/roboverse_learn/il/dp/models/ddpm_dit_image_policy.py +++ b/roboverse_learn/il/dp/policies/ddpm_dit_image_policy.py @@ -3,11 +3,11 @@ from typing import Any, Dict, Mapping, Optional from diffusers.schedulers.scheduling_ddpm import DDPMScheduler -from diffusion_policy.model.diffusion.flow_net import FlowTransformer -from diffusion_policy.model.vision.multi_image_obs_encoder import MultiImageObsEncoder +from roboverse_learn.il.dp.models.diffusion.flow_net import FlowTransformer +from roboverse_learn.il.dp.models.vision.multi_image_obs_encoder import MultiImageObsEncoder import torch -from roboverse_learn.il.dp.models.ddpm_image_policy import DiffusionDenoisingImagePolicy +from roboverse_learn.il.dp.policies.ddpm_image_policy import DiffusionDenoisingImagePolicy class DiffusionDiTImagePolicy(DiffusionDenoisingImagePolicy): diff --git a/roboverse_learn/il/dp/models/ddpm_image_policy.py b/roboverse_learn/il/dp/policies/ddpm_image_policy.py similarity index 96% rename from roboverse_learn/il/dp/models/ddpm_image_policy.py rename to roboverse_learn/il/dp/policies/ddpm_image_policy.py index 159139f81..ccfdf5868 100644 --- a/roboverse_learn/il/dp/models/ddpm_image_policy.py +++ b/roboverse_learn/il/dp/policies/ddpm_image_policy.py @@ -5,13 +5,13 @@ import torch import torch.nn.functional as F from diffusers.schedulers.scheduling_ddpm import DDPMScheduler -from diffusion_policy.model.diffusion.mask_generator import LowdimMaskGenerator -from diffusion_policy.model.vision.multi_image_obs_encoder import MultiImageObsEncoder +from roboverse_learn.il.dp.models.diffusion.mask_generator import LowdimMaskGenerator +from roboverse_learn.il.dp.models.vision.multi_image_obs_encoder import MultiImageObsEncoder from einops import reduce -from roboverse_learn.il.utils.common.normalizer import LinearNormalizer -from roboverse_learn.il.utils.common.pytorch_util import dict_apply -from diffusion_policy.policy.base_image_policy import BaseImagePolicy +from roboverse_learn.il.utils.normalizer import LinearNormalizer +from roboverse_learn.il.utils.pytorch_util import dict_apply +from roboverse_learn.il.base.base_image_policy import BaseImagePolicy class DiffusionDenoisingImagePolicy(BaseImagePolicy): diff --git a/roboverse_learn/il/dp/models/ddpm_unet_image_policy.py b/roboverse_learn/il/dp/policies/ddpm_unet_image_policy.py similarity index 88% rename from roboverse_learn/il/dp/models/ddpm_unet_image_policy.py rename to roboverse_learn/il/dp/policies/ddpm_unet_image_policy.py index b09a5c1f7..e8e5ddb91 100644 --- a/roboverse_learn/il/dp/models/ddpm_unet_image_policy.py +++ b/roboverse_learn/il/dp/policies/ddpm_unet_image_policy.py @@ -3,11 +3,11 @@ from typing import Any, Dict, Mapping, Optional, Sequence from diffusers.schedulers.scheduling_ddpm import DDPMScheduler -from diffusion_policy.model.diffusion.conditional_unet1d import ConditionalUnet1D -from diffusion_policy.model.vision.multi_image_obs_encoder import MultiImageObsEncoder +from roboverse_learn.il.dp.models.diffusion.conditional_unet1d import ConditionalUnet1D +from roboverse_learn.il.dp.models.vision.multi_image_obs_encoder import MultiImageObsEncoder import torch -from roboverse_learn.il.dp.models.ddpm_image_policy import DiffusionDenoisingImagePolicy +from roboverse_learn.il.dp.policies.ddpm_image_policy import DiffusionDenoisingImagePolicy class DiffusionUnetImagePolicy(DiffusionDenoisingImagePolicy): diff --git a/roboverse_learn/il/dp/models/score_unet_image_policy.py b/roboverse_learn/il/dp/policies/score_unet_image_policy.py similarity index 94% rename from roboverse_learn/il/dp/models/score_unet_image_policy.py rename to roboverse_learn/il/dp/policies/score_unet_image_policy.py index a00ffa242..e6e787553 100644 --- a/roboverse_learn/il/dp/models/score_unet_image_policy.py +++ b/roboverse_learn/il/dp/policies/score_unet_image_policy.py @@ -4,15 +4,15 @@ import torch.nn as nn import torch.nn.functional as F from diffusers.schedulers.scheduling_ddpm import DDPMScheduler -from diffusion_policy.model.diffusion.conditional_unet1d import ConditionalUnet1D -from diffusion_policy.model.diffusion.mask_generator import LowdimMaskGenerator -from diffusion_policy.model.vision.multi_image_obs_encoder import MultiImageObsEncoder +from roboverse_learn.il.dp.models.diffusion.conditional_unet1d import ConditionalUnet1D +from roboverse_learn.il.dp.models.diffusion.mask_generator import LowdimMaskGenerator +from roboverse_learn.il.dp.models.vision.multi_image_obs_encoder import MultiImageObsEncoder from einops import rearrange, reduce from loguru import logger as log -from roboverse_learn.il.utils.common.normalizer import LinearNormalizer -from roboverse_learn.il.utils.common.pytorch_util import dict_apply -from diffusion_policy.policy.base_image_policy import BaseImagePolicy +from roboverse_learn.il.utils.normalizer import LinearNormalizer +from roboverse_learn.il.utils.pytorch_util import dict_apply +from roboverse_learn.il.base.base_image_policy import BaseImagePolicy class ScoreMatchingUnetImagePolicy(BaseImagePolicy): diff --git a/roboverse_learn/il/dp/requirements.txt b/roboverse_learn/il/dp/requirements.txt new file mode 100644 index 000000000..f881747b6 --- /dev/null +++ b/roboverse_learn/il/dp/requirements.txt @@ -0,0 +1,18 @@ +zarr==2.12.0 +ipdb +gpustat +omegaconf +hydra-core==1.2.0 +dill==0.3.5.1 +einops==0.4.1 +diffusers +numba +moviepy +imageio +av +matplotlib +termcolor +huggingface_hub +pillow +pandas +wandb diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/shared_memory/shared_memory_queue.py b/roboverse_learn/il/dp/shared_memory/shared_memory_queue.py similarity index 97% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/shared_memory/shared_memory_queue.py rename to roboverse_learn/il/dp/shared_memory/shared_memory_queue.py index b6d609f30..b9196c205 100644 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/shared_memory/shared_memory_queue.py +++ b/roboverse_learn/il/dp/shared_memory/shared_memory_queue.py @@ -4,11 +4,11 @@ from typing import Dict, List, Union import numpy as np -from diffusion_policy.shared_memory.shared_memory_util import ( +from roboverse_learn.il.dp.shared_memory.shared_memory_util import ( ArraySpec, SharedAtomicCounter, ) -from diffusion_policy.shared_memory.shared_ndarray import SharedNDArray +from roboverse_learn.il.dp.shared_memory.shared_ndarray import SharedNDArray class SharedMemoryQueue: diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/shared_memory/shared_memory_ring_buffer.py b/roboverse_learn/il/dp/shared_memory/shared_memory_ring_buffer.py similarity index 98% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/shared_memory/shared_memory_ring_buffer.py rename to roboverse_learn/il/dp/shared_memory/shared_memory_ring_buffer.py index 09c9fc20f..5fceea8e9 100644 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/shared_memory/shared_memory_ring_buffer.py +++ b/roboverse_learn/il/dp/shared_memory/shared_memory_ring_buffer.py @@ -5,11 +5,11 @@ from typing import Dict, List, Union import numpy as np -from diffusion_policy.shared_memory.shared_memory_util import ( +from roboverse_learn.il.dp.shared_memory.shared_memory_util import ( ArraySpec, SharedAtomicCounter, ) -from diffusion_policy.shared_memory.shared_ndarray import SharedNDArray +from roboverse_learn.il.dp.shared_memory.shared_ndarray import SharedNDArray class SharedMemoryRingBuffer: diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/shared_memory/shared_memory_util.py b/roboverse_learn/il/dp/shared_memory/shared_memory_util.py similarity index 100% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/shared_memory/shared_memory_util.py rename to roboverse_learn/il/dp/shared_memory/shared_memory_util.py diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/shared_memory/shared_ndarray.py b/roboverse_learn/il/dp/shared_memory/shared_ndarray.py similarity index 98% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/shared_memory/shared_ndarray.py rename to roboverse_learn/il/dp/shared_memory/shared_ndarray.py index f741cbe19..be9266a1f 100644 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/shared_memory/shared_ndarray.py +++ b/roboverse_learn/il/dp/shared_memory/shared_ndarray.py @@ -8,7 +8,7 @@ import numpy as np import numpy.typing as npt -from diffusion_policy.common.nested_dict_util import nested_dict_check, nested_dict_map +from roboverse_learn.il.utils.nested_dict_util import nested_dict_check, nested_dict_map SharedMemoryLike = Union[str, SharedMemory] # shared memory or name of shared memory SharedT = TypeVar("SharedT", bound=np.generic) diff --git a/roboverse_learn/il/eval_runner/__init__.py b/roboverse_learn/il/eval_runner/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/roboverse_learn/il/dp/eval_runner/dp_eval_runner.py b/roboverse_learn/il/eval_runner/dp_eval_runner.py similarity index 95% rename from roboverse_learn/il/dp/eval_runner/dp_eval_runner.py rename to roboverse_learn/il/eval_runner/dp_eval_runner.py index 36cf14250..8b71e6288 100644 --- a/roboverse_learn/il/dp/eval_runner/dp_eval_runner.py +++ b/roboverse_learn/il/eval_runner/dp_eval_runner.py @@ -4,9 +4,9 @@ import hydra import numpy as np import torch -from dp.runner.base_policy import DiffusionPolicyCfg -from dp.base.base_eval_runner import BaseEvalRunner -from dp.runner.dp_runner import DPRunner +from roboverse_learn.il.runner.base_policy import DiffusionPolicyCfg +from roboverse_learn.il.base.base_eval_runner import BaseEvalRunner +from roboverse_learn.il.runner.dp_runner import DPRunner class DPEvalRunner(BaseEvalRunner): diff --git a/roboverse_learn/il/fm/README.md b/roboverse_learn/il/fm/README.md new file mode 100644 index 000000000..264b4bbe3 --- /dev/null +++ b/roboverse_learn/il/fm/README.md @@ -0,0 +1,36 @@ +# Flow Matching Policies (IL) + +Flow Matching variants (UNet and DiT) live here and use the shared IL runners under `il/dp/`. + +## Install + +```bash +cd roboverse_learn/il/dp +pip install -r requirements.txt +``` + +Create a Weights & Biases account to obtain an API key for logging. + +## Collect and process data + +```bash +./roboverse_learn/il/collect_demo.sh +``` + +## Train and eval + +Use the shared driver and point it at a Flow Matching model: + +```bash +# Choose one: fm_dit_model (DiT backbone) or fm_unet_model (UNet backbone) +export algo_model="fm_dit_model" + +./roboverse_learn/il/dp/dp_run.sh +``` + +Inside `dp_run.sh` you can toggle `train_enable` / `eval_enable`, set task names, seeds, GPU id, and checkpoint paths for evaluation. + +## References + +- Yaron Lipman et al., "Flow Matching for Generative Modeling." (2023). +- William Peebles and Jun-Yan Zhu, "DiT: Diffusion Models with Transformers." (2023). diff --git a/roboverse_learn/il/dp/models/fm_dit_image_policy.py b/roboverse_learn/il/fm/policies/fm_dit_image_policy.py similarity index 95% rename from roboverse_learn/il/dp/models/fm_dit_image_policy.py rename to roboverse_learn/il/fm/policies/fm_dit_image_policy.py index 555be1624..36325c582 100644 --- a/roboverse_learn/il/dp/models/fm_dit_image_policy.py +++ b/roboverse_learn/il/fm/policies/fm_dit_image_policy.py @@ -2,14 +2,14 @@ import torch import torch.nn.functional as F -from diffusion_policy.model.diffusion.flow_net import FlowTransformer -from diffusion_policy.model.diffusion.mask_generator import LowdimMaskGenerator -from diffusion_policy.model.vision.multi_image_obs_encoder import MultiImageObsEncoder from einops import reduce -from roboverse_learn.il.utils.common.normalizer import LinearNormalizer -from roboverse_learn.il.utils.common.pytorch_util import dict_apply -from diffusion_policy.policy.base_image_policy import BaseImagePolicy +from roboverse_learn.il.dp.models.diffusion.flow_net import FlowTransformer +from roboverse_learn.il.dp.models.diffusion.mask_generator import LowdimMaskGenerator +from roboverse_learn.il.dp.models.vision.multi_image_obs_encoder import MultiImageObsEncoder +from roboverse_learn.il.utils.normalizer import LinearNormalizer +from roboverse_learn.il.utils.pytorch_util import dict_apply +from roboverse_learn.il.base.base_image_policy import BaseImagePolicy class FlowMatchingDiTImagePolicy(BaseImagePolicy): diff --git a/roboverse_learn/il/dp/models/fm_unet_image_policy.py b/roboverse_learn/il/fm/policies/fm_unet_image_policy.py similarity index 95% rename from roboverse_learn/il/dp/models/fm_unet_image_policy.py rename to roboverse_learn/il/fm/policies/fm_unet_image_policy.py index 7fff8d436..5134064dc 100644 --- a/roboverse_learn/il/dp/models/fm_unet_image_policy.py +++ b/roboverse_learn/il/fm/policies/fm_unet_image_policy.py @@ -2,14 +2,14 @@ import torch import torch.nn.functional as F -from diffusion_policy.model.diffusion.conditional_unet1d import ConditionalUnet1D -from diffusion_policy.model.diffusion.mask_generator import LowdimMaskGenerator -from diffusion_policy.model.vision.multi_image_obs_encoder import MultiImageObsEncoder from einops import reduce -from roboverse_learn.il.utils.common.normalizer import LinearNormalizer -from roboverse_learn.il.utils.common.pytorch_util import dict_apply -from diffusion_policy.policy.base_image_policy import BaseImagePolicy +from roboverse_learn.il.dp.models.diffusion.conditional_unet1d import ConditionalUnet1D +from roboverse_learn.il.dp.models.diffusion.mask_generator import LowdimMaskGenerator +from roboverse_learn.il.dp.models.vision.multi_image_obs_encoder import MultiImageObsEncoder +from roboverse_learn.il.utils.normalizer import LinearNormalizer +from roboverse_learn.il.utils.pytorch_util import dict_apply +from roboverse_learn.il.base.base_image_policy import BaseImagePolicy class FlowMatchingUnetImagePolicy(BaseImagePolicy): diff --git a/roboverse_learn/il/fm/requirements.txt b/roboverse_learn/il/fm/requirements.txt new file mode 100644 index 000000000..fb7e62adf --- /dev/null +++ b/roboverse_learn/il/fm/requirements.txt @@ -0,0 +1,19 @@ +zarr==2.12.0 +ipdb +gpustat +omegaconf +hydra-core==1.2.0 +dill==0.3.5.1 +einops==0.4.1 +diffusers +numba +moviepy +imageio +av +matplotlib +termcolor +huggingface_hub +pillow +pandas +wandb +torchcfm diff --git a/roboverse_learn/il/il_run.sh b/roboverse_learn/il/il_run.sh index 475e7b772..76e4eb8d8 100644 --- a/roboverse_learn/il/il_run.sh +++ b/roboverse_learn/il/il_run.sh @@ -1,12 +1,27 @@ #!/bin/bash -# Try: bash roboverse_learn/il/il_run.sh --task_name_set close_box --algo_choose dp_DDPM --demo_num 100 --sim_set mujoco +# Usage: bash roboverse_learn/il/il_run.sh --task_name_set close_box --algo_choose ddpm_dit --demo_num 100 --sim_set mujoco -task_name_set="close_box" # Tasks, opts: close_box, stack_cube pick_cube -algo_choose="dp_DDPM" # IL algorithm, opts: act, dp_DDPM, dp_DDIM, dp_FM_UNet, dp_FM_DiT, dp_Score, dp_VITA -sim_set="mujoco" # Simulator, opts: mujoco, isaacsim -demo_num=100 # Number of demonstration to collect, train, and eval +task_name_set="close_box" # Tasks, e.g., close_box, stack_cube, pick_cube +algo_choose="ddpm_dit" # IL algorithm, opts: ddpm_unet, ddpm_dit, ddim_unet, fm_unet, fm_dit, vita, act, score +sim_set="mujoco" # Simulator, e.g., mujoco, isaacsim +demo_num=90 # Number of demonstrations to collect, train, and eval -# parse parameters +# Training/eval control +train_enable=True +eval_enable=False + +# Training parameters +level=0 +num_epochs=100 +seed=42 +gpu=0 +obs_space=joint_pos +act_space=joint_pos +delta_ee=0 +eval_num_envs=1 +eval_max_step=300 + +# Parse parameters while [[ $# -gt 0 ]]; do case "$1" in --task_name_set) @@ -25,14 +40,31 @@ while [[ $# -gt 0 ]]; do demo_num="$2" shift 2 ;; + --train_enable) + train_enable="$2" + shift 2 + ;; + --eval_enable) + eval_enable="$2" + shift 2 + ;; + --num_epochs) + num_epochs="$2" + shift 2 + ;; + --gpu) + gpu="$2" + shift 2 + ;; *) - echo "Unknown parameter: $1,optional parameter:--task_name_set --algo_choose --sim_set --demo_num" + echo "Unknown parameter: $1" + echo "Optional parameters: --task_name_set --algo_choose --sim_set --demo_num --train_enable --eval_enable --num_epochs --gpu" exit 1 ;; esac done -# 1. collect_demo +# Collect demo echo "=== Running collect_demo.sh ===" sed -i "s/^task_name_set=.*/task_name_set=$task_name_set/" ./roboverse_learn/il/collect_demo.sh sed -i "s/^sim_set=.*/sim_set=$sim_set/" ./roboverse_learn/il/collect_demo.sh @@ -40,73 +72,97 @@ sed -i "s/^num_demo_success=.*/num_demo_success=$demo_num/" ./roboverse_learn/il sed -i "s/^expert_data_num=.*/expert_data_num=$demo_num/" ./roboverse_learn/il/collect_demo.sh bash ./roboverse_learn/il/collect_demo.sh -# 2. il algorithm +# Map algo_choose to model config case "$algo_choose" in - "dp_DDPM") - echo "=== Running dp_run.sh ===" - sed -i "s/^task_name_set=.*/task_name_set=$task_name_set/" ./roboverse_learn/il/dp/dp_run.sh - sed -i "s/^sim_set=.*/sim_set=$sim_set/" ./roboverse_learn/il/dp/dp_run.sh - sed -i "s/^expert_data_num=.*/expert_data_num=$demo_num/" ./roboverse_learn/il/dp/dp_run.sh - sed -i "s/^eval_ckpt_name=.*/eval_ckpt_name=$demo_num/" ./roboverse_learn/il/dp/dp_run.sh - sed -i "s/^algo_choose=.*/algo_choose=0/" ./roboverse_learn/il/dp/dp_run.sh - bash ./roboverse_learn/il/dp/dp_run.sh + "ddpm_unet") + algo_model="ddpm_unet_model" + config_name="dp_runner" + main_script="./roboverse_learn/il/train.py" + output_dir="DP" ;; - "dp_DDIM") - echo "=== Running dp_run.sh ===" - sed -i "s/^task_name_set=.*/task_name_set=$task_name_set/" ./roboverse_learn/il/dp/dp_run.sh - sed -i "s/^sim_set=.*/sim_set=$sim_set/" ./roboverse_learn/il/dp/dp_run.sh - sed -i "s/^expert_data_num=.*/expert_data_num=$demo_num/" ./roboverse_learn/il/dp/dp_run.sh - sed -i "s/^eval_ckpt_name=.*/eval_ckpt_name=$demo_num/" ./roboverse_learn/il/dp/dp_run.sh - sed -i "s/^algo_choose=.*/algo_choose=1/" ./roboverse_learn/il/dp/dp_run.sh - bash ./roboverse_learn/il/dp/dp_run.sh + "ddpm_dit") + algo_model="ddpm_dit_model" + config_name="dp_runner" + main_script="./roboverse_learn/il/train.py" + output_dir="DP" ;; - "dp_FM_UNet") - echo "=== Running dp_run.sh ===" - sed -i "s/^task_name_set=.*/task_name_set=$task_name_set/" ./roboverse_learn/il/dp/dp_run.sh - sed -i "s/^sim_set=.*/sim_set=$sim_set/" ./roboverse_learn/il/dp/dp_run.sh - sed -i "s/^expert_data_num=.*/expert_data_num=$demo_num/" ./roboverse_learn/il/dp/dp_run.sh - sed -i "s/^eval_ckpt_name=.*/eval_ckpt_name=$demo_num/" ./roboverse_learn/il/dp/dp_run.sh - sed -i "s/^algo_choose=.*/algo_choose=2/" ./roboverse_learn/il/dp/dp_run.sh - bash ./roboverse_learn/il/dp/dp_run.sh + "ddim_unet") + algo_model="ddim_unet_model" + config_name="dp_runner" + main_script="./roboverse_learn/il/train.py" + output_dir="DP" ;; - "dp_FM_DiT") - echo "=== Running dp_run.sh ===" - sed -i "s/^task_name_set=.*/task_name_set=$task_name_set/" ./roboverse_learn/il/dp/dp_run.sh - sed -i "s/^sim_set=.*/sim_set=$sim_set/" ./roboverse_learn/il/dp/dp_run.sh - sed -i "s/^expert_data_num=.*/expert_data_num=$demo_num/" ./roboverse_learn/il/dp/dp_run.sh - sed -i "s/^eval_ckpt_name=.*/eval_ckpt_name=$demo_num/" ./roboverse_learn/il/dp/dp_run.sh - sed -i "s/^algo_choose=.*/algo_choose=3/" ./roboverse_learn/il/dp/dp_run.sh - bash ./roboverse_learn/il/dp/dp_run.sh + "fm_unet") + algo_model="fm_unet_model" + config_name="dp_runner" + main_script="./roboverse_learn/il/train.py" + output_dir="FM" ;; - "dp_Score") - echo "=== Running dp_run.sh ===" - sed -i "s/^task_name_set=.*/task_name_set=$task_name_set/" ./roboverse_learn/il/dp/dp_run.sh - sed -i "s/^sim_set=.*/sim_set=$sim_set/" ./roboverse_learn/il/dp/dp_run.sh - sed -i "s/^expert_data_num=.*/expert_data_num=$demo_num/" ./roboverse_learn/il/dp/dp_run.sh - sed -i "s/^eval_ckpt_name=.*/eval_ckpt_name=$demo_num/" ./roboverse_learn/il/dp/dp_run.sh - sed -i "s/^algo_choose=.*/algo_choose=4/" ./roboverse_learn/il/dp/dp_run.sh - bash ./roboverse_learn/il/dp/dp_run.sh + "fm_dit") + algo_model="fm_dit_model" + config_name="dp_runner" + main_script="./roboverse_learn/il/train.py" + output_dir="FM" ;; - "dp_VITA") - echo "=== Running dp_run.sh ===" - sed -i "s/^task_name_set=.*/task_name_set=$task_name_set/" ./roboverse_learn/il/dp/dp_run.sh - sed -i "s/^sim_set=.*/sim_set=$sim_set/" ./roboverse_learn/il/dp/dp_run.sh - sed -i "s/^expert_data_num=.*/expert_data_num=$demo_num/" ./roboverse_learn/il/dp/dp_run.sh - sed -i "s/^eval_ckpt_name=.*/eval_ckpt_name=$demo_num/" ./roboverse_learn/il/dp/dp_run.sh - sed -i "s/^algo_choose=.*/algo_choose=5/" ./roboverse_learn/il/dp/dp_run.sh - bash ./roboverse_learn/il/dp/dp_run.sh + "score") + algo_model="score_model" + config_name="dp_runner" + main_script="./roboverse_learn/il/train.py" + output_dir="DP" + ;; + "vita") + algo_model="vita_model" + config_name="dp_runner" + main_script="./roboverse_learn/il/train.py" + output_dir="VITA" ;; "act") - echo "=== Running act_run.sh ===" + echo "=== Running ACT training ===" sed -i "s/^task_name_set=.*/task_name_set=$task_name_set/" ./roboverse_learn/il/act/act_run.sh sed -i "s/^sim_set=.*/sim_set=$sim_set/" ./roboverse_learn/il/act/act_run.sh sed -i "s/^expert_data_num=.*/expert_data_num=$demo_num/" ./roboverse_learn/il/act/act_run.sh bash ./roboverse_learn/il/act/act_run.sh + echo "=== Completed all data collection, training, and evaluation ===" + exit 0 ;; *) - echo "Unavailable chose: $algo_choose, optional options: act, dp_DDPM, dp_DDIM, dp_FM_UNet, dp_FM_DiT, dp_Score, dp_VITA" + echo "Unsupported algorithm: $algo_choose" + echo "Available options: act, ddpm_unet, ddpm_dit, ddim_unet, fm_unet, fm_dit, score, vita" exit 1 ;; esac +# Run training/evaluation for DP/FM/VITA policies +echo "=== Running ${algo_choose} (${algo_model}) ===" +echo "Selected model: $algo_model" + +eval_ckpt_name=$demo_num +eval_path="./info/outputs/${output_dir}/${task_name_set}/checkpoints/${eval_ckpt_name}.ckpt" + +echo "Checkpoint path: $eval_path" + +extra="obs:${obs_space}_act:${act_space}" +if [ "${delta_ee}" = 1 ]; then + extra="${extra}_delta" +fi + +export algo_model +python ${main_script} --config-name=${config_name}.yaml \ +task_name=${task_name_set} \ +"dataset_config.zarr_path=./data_policy/${task_name_set}FrankaL${level}_${extra}_${demo_num}.zarr" \ +train_config.training_params.seed=${seed} \ +train_config.training_params.num_epochs=${num_epochs} \ +train_config.training_params.device=${gpu} \ +eval_config.policy_runner.obs.obs_type=${obs_space} \ +eval_config.policy_runner.action.action_type=${act_space} \ +eval_config.policy_runner.action.delta=${delta_ee} \ +eval_config.eval_args.task=${task_name_set} \ +eval_config.eval_args.max_step=${eval_max_step} \ +eval_config.eval_args.num_envs=${eval_num_envs} \ +eval_config.eval_args.sim=${sim_set} \ ++eval_config.eval_args.max_demo=${demo_num} \ +train_enable=${train_enable} \ +eval_enable=${eval_enable} \ +eval_path=${eval_path} + echo "=== Completed all data collection, training, and evaluation ===" diff --git a/roboverse_learn/il/il_setup.sh b/roboverse_learn/il/il_setup.sh index 098ddc73d..f87f9aaa8 100644 --- a/roboverse_learn/il/il_setup.sh +++ b/roboverse_learn/il/il_setup.sh @@ -1,27 +1,10 @@ #!/bin/bash -# Install diffusion_policy -echo "Install diffusion_policy..." -cd ./roboverse_learn/il/utils/diffusion_policy || { echo "diffusion_policy do not exit"; exit 1; } -pip install -e . - -# Install act -echo "Install act..." -cd ../../../../ -cd roboverse_learn/il/act/detr || { echo "detr do not exit"; exit 1; } -pip install -e . - # Install additional dependencies echo "Install additional dependencies..." -cd ../../../../../ -pip install pandas wandb - # Fix .zarr issue pip install zarr==2.16.1 blosc==1.11.1 pip install numcodecs==0.11.0 # Fix hydra issue pip install --upgrade hydra-core - -# dp-VITA additional dependency -pip install torchcfm diff --git a/roboverse_learn/il/runner/__init__.py b/roboverse_learn/il/runner/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/roboverse_learn/il/dp/runner/base_policy.py b/roboverse_learn/il/runner/base_policy.py similarity index 100% rename from roboverse_learn/il/dp/runner/base_policy.py rename to roboverse_learn/il/runner/base_policy.py diff --git a/roboverse_learn/il/dp/runner/dp_runner.py b/roboverse_learn/il/runner/dp_runner.py similarity index 98% rename from roboverse_learn/il/dp/runner/dp_runner.py rename to roboverse_learn/il/runner/dp_runner.py index 68a4f0b84..b1997caff 100644 --- a/roboverse_learn/il/dp/runner/dp_runner.py +++ b/roboverse_learn/il/runner/dp_runner.py @@ -14,20 +14,20 @@ import torch import tqdm import wandb -from diffusion_policy.model.diffusion.ema_model import EMAModel +from roboverse_learn.il.dp.models.diffusion.ema_model import EMAModel from loguru import logger as log from metasim.scenario.scenario import ScenarioCfg from metasim.scenario.cameras import PinholeCameraCfg from metasim.constants import SimType from metasim.utils.demo_util import get_traj from metasim.utils.setup_util import get_robot -from dp.base.base_eval_runner import BaseEvalRunner -from dp.base.base_runner import BaseRunner -from roboverse_learn.il.utils.common.eval_args import Args -from roboverse_learn.il.utils.common.eval_runner_getter import get_runner -from roboverse_learn.il.utils.common.json_logger import JsonLogger -from roboverse_learn.il.utils.common.lr_scheduler import get_scheduler -from roboverse_learn.il.utils.common.pytorch_util import dict_apply, optimizer_to +from roboverse_learn.il.base.base_eval_runner import BaseEvalRunner +from roboverse_learn.il.base.base_runner import BaseRunner +from roboverse_learn.il.utils.eval_args import Args +from roboverse_learn.il.utils.eval_runner_getter import get_runner +from roboverse_learn.il.utils.json_logger import JsonLogger +from roboverse_learn.il.utils.lr_scheduler import get_scheduler +from roboverse_learn.il.utils.pytorch_util import dict_apply, optimizer_to from torch.utils.data import DataLoader from metasim.task.registry import get_task_class diff --git a/roboverse_learn/il/dp/main.py b/roboverse_learn/il/train.py similarity index 91% rename from roboverse_learn/il/dp/main.py rename to roboverse_learn/il/train.py index b1adf2706..6306cc9e5 100644 --- a/roboverse_learn/il/dp/main.py +++ b/roboverse_learn/il/train.py @@ -8,7 +8,7 @@ here = os.path.dirname(os.path.abspath(__file__)) project_root = os.path.dirname(here) sys.path.insert(0, project_root) -from dp.base.base_runner import BaseRunner +from roboverse_learn.il.base.base_runner import BaseRunner abs_config_path = str(pathlib.Path(__file__).resolve().parent.joinpath("configs").absolute()) OmegaConf.register_new_resolver("eval", eval, replace=True) diff --git a/roboverse_learn/il/utils/common/checkpoint_util.py b/roboverse_learn/il/utils/checkpoint_util.py similarity index 100% rename from roboverse_learn/il/utils/common/checkpoint_util.py rename to roboverse_learn/il/utils/checkpoint_util.py diff --git a/roboverse_learn/il/utils/common/normalize_util.py b/roboverse_learn/il/utils/common/normalize_util.py deleted file mode 100644 index ba8e32097..000000000 --- a/roboverse_learn/il/utils/common/normalize_util.py +++ /dev/null @@ -1,198 +0,0 @@ -import numpy as np -from diffusion_policy.model.common.normalizer import SingleFieldLinearNormalizer - - -from roboverse_learn.il.utils.common.pytorch_util import ( - dict_apply_reduce, - dict_apply_split, -) - - -def get_range_normalizer_from_stat(stat, output_max=1, output_min=-1, range_eps=1e-7): - # -1, 1 normalization - input_max = stat["max"] - input_min = stat["min"] - input_range = input_max - input_min - ignore_dim = input_range < range_eps - input_range[ignore_dim] = output_max - output_min - scale = (output_max - output_min) / input_range - offset = output_min - scale * input_min - offset[ignore_dim] = (output_max + output_min) / 2 - input_min[ignore_dim] - - return SingleFieldLinearNormalizer.create_manual(scale=scale, offset=offset, input_stats_dict=stat) - - -def get_image_range_normalizer(): - scale = np.array([2], dtype=np.float32) - offset = np.array([-1], dtype=np.float32) - stat = { - "min": np.array([0], dtype=np.float32), - "max": np.array([1], dtype=np.float32), - "mean": np.array([0.5], dtype=np.float32), - "std": np.array([np.sqrt(1 / 12)], dtype=np.float32), - } - return SingleFieldLinearNormalizer.create_manual(scale=scale, offset=offset, input_stats_dict=stat) - - -def get_identity_normalizer_from_stat(stat): - scale = np.ones_like(stat["min"]) - offset = np.zeros_like(stat["min"]) - return SingleFieldLinearNormalizer.create_manual(scale=scale, offset=offset, input_stats_dict=stat) - - -def robomimic_abs_action_normalizer_from_stat(stat, rotation_transformer): - result = dict_apply_split(stat, lambda x: {"pos": x[..., :3], "rot": x[..., 3:6], "gripper": x[..., 6:]}) - - def get_pos_param_info(stat, output_max=1, output_min=-1, range_eps=1e-7): - # -1, 1 normalization - input_max = stat["max"] - input_min = stat["min"] - input_range = input_max - input_min - ignore_dim = input_range < range_eps - input_range[ignore_dim] = output_max - output_min - scale = (output_max - output_min) / input_range - offset = output_min - scale * input_min - offset[ignore_dim] = (output_max + output_min) / 2 - input_min[ignore_dim] - - return {"scale": scale, "offset": offset}, stat - - def get_rot_param_info(stat): - example = rotation_transformer.forward(stat["mean"]) - scale = np.ones_like(example) - offset = np.zeros_like(example) - info = { - "max": np.ones_like(example), - "min": np.full_like(example, -1), - "mean": np.zeros_like(example), - "std": np.ones_like(example), - } - return {"scale": scale, "offset": offset}, info - - def get_gripper_param_info(stat): - example = stat["max"] - scale = np.ones_like(example) - offset = np.zeros_like(example) - info = { - "max": np.ones_like(example), - "min": np.full_like(example, -1), - "mean": np.zeros_like(example), - "std": np.ones_like(example), - } - return {"scale": scale, "offset": offset}, info - - pos_param, pos_info = get_pos_param_info(result["pos"]) - rot_param, rot_info = get_rot_param_info(result["rot"]) - gripper_param, gripper_info = get_gripper_param_info(result["gripper"]) - - param = dict_apply_reduce([pos_param, rot_param, gripper_param], lambda x: np.concatenate(x, axis=-1)) - info = dict_apply_reduce([pos_info, rot_info, gripper_info], lambda x: np.concatenate(x, axis=-1)) - - return SingleFieldLinearNormalizer.create_manual( - scale=param["scale"], offset=param["offset"], input_stats_dict=info - ) - - -def robomimic_abs_action_only_normalizer_from_stat(stat): - result = dict_apply_split(stat, lambda x: {"pos": x[..., :3], "other": x[..., 3:]}) - - def get_pos_param_info(stat, output_max=1, output_min=-1, range_eps=1e-7): - # -1, 1 normalization - input_max = stat["max"] - input_min = stat["min"] - input_range = input_max - input_min - ignore_dim = input_range < range_eps - input_range[ignore_dim] = output_max - output_min - scale = (output_max - output_min) / input_range - offset = output_min - scale * input_min - offset[ignore_dim] = (output_max + output_min) / 2 - input_min[ignore_dim] - - return {"scale": scale, "offset": offset}, stat - - def get_other_param_info(stat): - example = stat["max"] - scale = np.ones_like(example) - offset = np.zeros_like(example) - info = { - "max": np.ones_like(example), - "min": np.full_like(example, -1), - "mean": np.zeros_like(example), - "std": np.ones_like(example), - } - return {"scale": scale, "offset": offset}, info - - pos_param, pos_info = get_pos_param_info(result["pos"]) - other_param, other_info = get_other_param_info(result["other"]) - - param = dict_apply_reduce([pos_param, other_param], lambda x: np.concatenate(x, axis=-1)) - info = dict_apply_reduce([pos_info, other_info], lambda x: np.concatenate(x, axis=-1)) - - return SingleFieldLinearNormalizer.create_manual( - scale=param["scale"], offset=param["offset"], input_stats_dict=info - ) - - -def robomimic_abs_action_only_dual_arm_normalizer_from_stat(stat): - Da = stat["max"].shape[-1] - Dah = Da // 2 - result = dict_apply_split( - stat, - lambda x: { - "pos0": x[..., :3], - "other0": x[..., 3:Dah], - "pos1": x[..., Dah : Dah + 3], - "other1": x[..., Dah + 3 :], - }, - ) - - def get_pos_param_info(stat, output_max=1, output_min=-1, range_eps=1e-7): - # -1, 1 normalization - input_max = stat["max"] - input_min = stat["min"] - input_range = input_max - input_min - ignore_dim = input_range < range_eps - input_range[ignore_dim] = output_max - output_min - scale = (output_max - output_min) / input_range - offset = output_min - scale * input_min - offset[ignore_dim] = (output_max + output_min) / 2 - input_min[ignore_dim] - - return {"scale": scale, "offset": offset}, stat - - def get_other_param_info(stat): - example = stat["max"] - scale = np.ones_like(example) - offset = np.zeros_like(example) - info = { - "max": np.ones_like(example), - "min": np.full_like(example, -1), - "mean": np.zeros_like(example), - "std": np.ones_like(example), - } - return {"scale": scale, "offset": offset}, info - - pos0_param, pos0_info = get_pos_param_info(result["pos0"]) - pos1_param, pos1_info = get_pos_param_info(result["pos1"]) - other0_param, other0_info = get_other_param_info(result["other0"]) - other1_param, other1_info = get_other_param_info(result["other1"]) - - param = dict_apply_reduce( - [pos0_param, other0_param, pos1_param, other1_param], - lambda x: np.concatenate(x, axis=-1), - ) - info = dict_apply_reduce( - [pos0_info, other0_info, pos1_info, other1_info], - lambda x: np.concatenate(x, axis=-1), - ) - - return SingleFieldLinearNormalizer.create_manual( - scale=param["scale"], offset=param["offset"], input_stats_dict=info - ) - - -def array_to_stats(arr: np.ndarray): - stat = { - "min": np.min(arr, axis=0), - "max": np.max(arr, axis=0), - "mean": np.mean(arr, axis=0), - "std": np.std(arr, axis=0), - } - return stat diff --git a/roboverse_learn/il/utils/common/normalizer.py b/roboverse_learn/il/utils/common/normalizer.py deleted file mode 100644 index 62f45b4ef..000000000 --- a/roboverse_learn/il/utils/common/normalizer.py +++ /dev/null @@ -1,368 +0,0 @@ -import unittest -from typing import Dict, Union - -import numpy as np -import torch -import torch.nn as nn -import zarr -from roboverse_learn.il.utils.common.pytorch_util import dict_apply -from roboverse_learn.il.utils.common.dict_of_tensor_mixin import DictOfTensorMixin - - -class LinearNormalizer(DictOfTensorMixin): - avaliable_modes = ["limits", "gaussian"] - - @torch.no_grad() - def fit( - self, - data: Union[Dict, torch.Tensor, np.ndarray, zarr.Array], - last_n_dims=1, - dtype=torch.float32, - mode="limits", - output_max=1.0, - output_min=-1.0, - range_eps=1e-4, - fit_offset=True, - ): - if isinstance(data, dict): - for key, value in data.items(): - self.params_dict[key] = _fit( - value, - last_n_dims=last_n_dims, - dtype=dtype, - mode=mode, - output_max=output_max, - output_min=output_min, - range_eps=range_eps, - fit_offset=fit_offset, - ) - else: - self.params_dict["_default"] = _fit( - data, - last_n_dims=last_n_dims, - dtype=dtype, - mode=mode, - output_max=output_max, - output_min=output_min, - range_eps=range_eps, - fit_offset=fit_offset, - ) - - def __call__(self, x: Union[Dict, torch.Tensor, np.ndarray]) -> torch.Tensor: - return self.normalize(x) - - def __getitem__(self, key: str): - return SingleFieldLinearNormalizer(self.params_dict[key]) - - def __setitem__(self, key: str, value: "SingleFieldLinearNormalizer"): - self.params_dict[key] = value.params_dict - - def _normalize_impl(self, x, forward=True): - if isinstance(x, dict): - result = dict() - for key, value in x.items(): - params = self.params_dict[key] - try: - result[key] = _normalize(value, params, forward=forward) - except: - import pdb - - pdb.set_trace() - return result - else: - if "_default" not in self.params_dict: - raise RuntimeError("Not initialized") - params = self.params_dict["_default"] - return _normalize(x, params, forward=forward) - - def normalize(self, x: Union[Dict, torch.Tensor, np.ndarray]) -> torch.Tensor: - return self._normalize_impl(x, forward=True) - - def unnormalize(self, x: Union[Dict, torch.Tensor, np.ndarray]) -> torch.Tensor: - return self._normalize_impl(x, forward=False) - - def get_input_stats(self) -> Dict: - if len(self.params_dict) == 0: - raise RuntimeError("Not initialized") - if len(self.params_dict) == 1 and "_default" in self.params_dict: - return self.params_dict["_default"]["input_stats"] - - result = dict() - for key, value in self.params_dict.items(): - if key != "_default": - result[key] = value["input_stats"] - return result - - def get_output_stats(self, key="_default"): - input_stats = self.get_input_stats() - if "min" in input_stats: - # no dict - return dict_apply(input_stats, self.normalize) - - result = dict() - for key, group in input_stats.items(): - this_dict = dict() - for name, value in group.items(): - this_dict[name] = self.normalize({key: value})[key] - result[key] = this_dict - return result - - -class SingleFieldLinearNormalizer(DictOfTensorMixin): - avaliable_modes = ["limits", "gaussian"] - - @torch.no_grad() - def fit( - self, - data: Union[torch.Tensor, np.ndarray, zarr.Array], - last_n_dims=1, - dtype=torch.float32, - mode="limits", - output_max=1.0, - output_min=-1.0, - range_eps=1e-4, - fit_offset=True, - ): - self.params_dict = _fit( - data, - last_n_dims=last_n_dims, - dtype=dtype, - mode=mode, - output_max=output_max, - output_min=output_min, - range_eps=range_eps, - fit_offset=fit_offset, - ) - - @classmethod - def create_fit(cls, data: Union[torch.Tensor, np.ndarray, zarr.Array], **kwargs): - obj = cls() - obj.fit(data, **kwargs) - return obj - - @classmethod - def create_manual( - cls, - scale: Union[torch.Tensor, np.ndarray], - offset: Union[torch.Tensor, np.ndarray], - input_stats_dict: Dict[str, Union[torch.Tensor, np.ndarray]], - ): - def to_tensor(x): - if not isinstance(x, torch.Tensor): - x = torch.from_numpy(x) - x = x.flatten() - return x - - # check - for x in [offset] + list(input_stats_dict.values()): - assert x.shape == scale.shape - assert x.dtype == scale.dtype - - params_dict = nn.ParameterDict({ - "scale": to_tensor(scale), - "offset": to_tensor(offset), - "input_stats": nn.ParameterDict(dict_apply(input_stats_dict, to_tensor)), - }) - return cls(params_dict) - - @classmethod - def create_identity(cls, dtype=torch.float32): - scale = torch.tensor([1], dtype=dtype) - offset = torch.tensor([0], dtype=dtype) - input_stats_dict = { - "min": torch.tensor([-1], dtype=dtype), - "max": torch.tensor([1], dtype=dtype), - "mean": torch.tensor([0], dtype=dtype), - "std": torch.tensor([1], dtype=dtype), - } - return cls.create_manual(scale, offset, input_stats_dict) - - def normalize(self, x: Union[torch.Tensor, np.ndarray]) -> torch.Tensor: - return _normalize(x, self.params_dict, forward=True) - - def unnormalize(self, x: Union[torch.Tensor, np.ndarray]) -> torch.Tensor: - return _normalize(x, self.params_dict, forward=False) - - def get_input_stats(self): - return self.params_dict["input_stats"] - - def get_output_stats(self): - return dict_apply(self.params_dict["input_stats"], self.normalize) - - def __call__(self, x: Union[torch.Tensor, np.ndarray]) -> torch.Tensor: - return self.normalize(x) - - -def _fit( - data: Union[torch.Tensor, np.ndarray, zarr.Array], - last_n_dims=1, - dtype=torch.float32, - mode="limits", - output_max=1.0, - output_min=-1.0, - range_eps=1e-4, - fit_offset=True, -): - assert mode in ["limits", "gaussian"] - assert last_n_dims >= 0 - assert output_max > output_min - - # convert data to torch and type - if isinstance(data, zarr.Array): - data = data[:] - if isinstance(data, np.ndarray): - data = torch.from_numpy(data) - if dtype is not None: - data = data.type(dtype) - - # convert shape - dim = 1 - if last_n_dims > 0: - dim = np.prod(data.shape[-last_n_dims:]) - data = data.reshape(-1, dim) - - # compute input stats min max mean std - input_min, _ = data.min(axis=0) - input_max, _ = data.max(axis=0) - input_mean = data.mean(axis=0) - input_std = data.std(axis=0) - - # compute scale and offset - if mode == "limits": - if fit_offset: - # unit scale - input_range = input_max - input_min - ignore_dim = input_range < range_eps - input_range[ignore_dim] = output_max - output_min - scale = (output_max - output_min) / input_range - offset = output_min - scale * input_min - offset[ignore_dim] = (output_max + output_min) / 2 - input_min[ignore_dim] - # ignore dims scaled to mean of output max and min - else: - # use this when data is pre-zero-centered. - assert output_max > 0 - assert output_min < 0 - # unit abs - output_abs = min(abs(output_min), abs(output_max)) - input_abs = torch.maximum(torch.abs(input_min), torch.abs(input_max)) - ignore_dim = input_abs < range_eps - input_abs[ignore_dim] = output_abs - # don't scale constant channels - scale = output_abs / input_abs - offset = torch.zeros_like(input_mean) - elif mode == "gaussian": - ignore_dim = input_std < range_eps - scale = input_std.clone() - scale[ignore_dim] = 1 - scale = 1 / scale - - if fit_offset: - offset = -input_mean * scale - else: - offset = torch.zeros_like(input_mean) - - # save - this_params = nn.ParameterDict({ - "scale": scale, - "offset": offset, - "input_stats": nn.ParameterDict({ - "min": input_min, - "max": input_max, - "mean": input_mean, - "std": input_std, - }), - }) - for p in this_params.parameters(): - p.requires_grad_(False) - return this_params - - -def _normalize(x, params, forward=True): - assert "scale" in params - if isinstance(x, np.ndarray): - x = torch.from_numpy(x) - scale = params["scale"] - offset = params["offset"] - x = x.to(device=scale.device, dtype=scale.dtype) - src_shape = x.shape - x = x.reshape(-1, scale.shape[0]) - if forward: - x = x * scale + offset - else: - x = (x - offset) / scale - x = x.reshape(src_shape) - return x - - -def test(): - data = torch.zeros((100, 10, 9, 2)).uniform_() - data[..., 0, 0] = 0 - - normalizer = SingleFieldLinearNormalizer() - normalizer.fit(data, mode="limits", last_n_dims=2) - datan = normalizer.normalize(data) - assert datan.shape == data.shape - assert np.allclose(datan.max(), 1.0) - assert np.allclose(datan.min(), -1.0) - dataun = normalizer.unnormalize(datan) - assert torch.allclose(data, dataun, atol=1e-7) - - input_stats = normalizer.get_input_stats() - output_stats = normalizer.get_output_stats() - - normalizer = SingleFieldLinearNormalizer() - normalizer.fit(data, mode="limits", last_n_dims=1, fit_offset=False) - datan = normalizer.normalize(data) - assert datan.shape == data.shape - assert np.allclose(datan.max(), 1.0, atol=1e-3) - assert np.allclose(datan.min(), 0.0, atol=1e-3) - dataun = normalizer.unnormalize(datan) - assert torch.allclose(data, dataun, atol=1e-7) - - data = torch.zeros((100, 10, 9, 2)).uniform_() - normalizer = SingleFieldLinearNormalizer() - normalizer.fit(data, mode="gaussian", last_n_dims=0) - datan = normalizer.normalize(data) - assert datan.shape == data.shape - assert np.allclose(datan.mean(), 0.0, atol=1e-3) - assert np.allclose(datan.std(), 1.0, atol=1e-3) - dataun = normalizer.unnormalize(datan) - assert torch.allclose(data, dataun, atol=1e-7) - - # dict - data = torch.zeros((100, 10, 9, 2)).uniform_() - data[..., 0, 0] = 0 - - normalizer = LinearNormalizer() - normalizer.fit(data, mode="limits", last_n_dims=2) - datan = normalizer.normalize(data) - assert datan.shape == data.shape - assert np.allclose(datan.max(), 1.0) - assert np.allclose(datan.min(), -1.0) - dataun = normalizer.unnormalize(datan) - assert torch.allclose(data, dataun, atol=1e-7) - - input_stats = normalizer.get_input_stats() - output_stats = normalizer.get_output_stats() - - data = { - "obs": torch.zeros((1000, 128, 9, 2)).uniform_() * 512, - "action": torch.zeros((1000, 128, 2)).uniform_() * 512, - } - normalizer = LinearNormalizer() - normalizer.fit(data) - datan = normalizer.normalize(data) - dataun = normalizer.unnormalize(datan) - for key in data: - assert torch.allclose(data[key], dataun[key], atol=1e-4) - - input_stats = normalizer.get_input_stats() - output_stats = normalizer.get_output_stats() - - state_dict = normalizer.state_dict() - n = LinearNormalizer() - n.load_state_dict(state_dict) - datan = n.normalize(data) - dataun = n.unnormalize(datan) - for key in data: - assert torch.allclose(data[key], dataun[key], atol=1e-4) diff --git a/roboverse_learn/il/utils/common/replay_buffer.py b/roboverse_learn/il/utils/common/replay_buffer.py deleted file mode 100644 index c832cd578..000000000 --- a/roboverse_learn/il/utils/common/replay_buffer.py +++ /dev/null @@ -1,622 +0,0 @@ -import math -import numbers -import os -from functools import cached_property -from typing import Dict, Optional, Union - -import numcodecs -import numpy as np -import zarr - - -def check_chunks_compatible(chunks: tuple, shape: tuple): - assert len(shape) == len(chunks) - for c in chunks: - assert isinstance(c, numbers.Integral) - assert c > 0 - - -def rechunk_recompress_array(group, name, chunks=None, chunk_length=None, compressor=None, tmp_key="_temp"): - old_arr = group[name] - if chunks is None: - if chunk_length is not None: - chunks = (chunk_length,) + old_arr.chunks[1:] - else: - chunks = old_arr.chunks - check_chunks_compatible(chunks, old_arr.shape) - - if compressor is None: - compressor = old_arr.compressor - - if (chunks == old_arr.chunks) and (compressor == old_arr.compressor): - # no change - return old_arr - - # rechunk recompress - group.move(name, tmp_key) - old_arr = group[tmp_key] - n_copied, n_skipped, n_bytes_copied = zarr.copy( - source=old_arr, - dest=group, - name=name, - chunks=chunks, - compressor=compressor, - ) - del group[tmp_key] - arr = group[name] - return arr - - -def get_optimal_chunks(shape, dtype, target_chunk_bytes=2e6, max_chunk_length=None): - """ - Common shapes - T,D - T,N,D - T,H,W,C - T,N,H,W,C - """ - itemsize = np.dtype(dtype).itemsize - # reversed - rshape = list(shape[::-1]) - if max_chunk_length is not None: - rshape[-1] = int(max_chunk_length) - split_idx = len(shape) - 1 - for i in range(len(shape) - 1): - this_chunk_bytes = itemsize * np.prod(rshape[:i]) - next_chunk_bytes = itemsize * np.prod(rshape[: i + 1]) - if this_chunk_bytes <= target_chunk_bytes and next_chunk_bytes > target_chunk_bytes: - split_idx = i - - rchunks = rshape[:split_idx] - item_chunk_bytes = itemsize * np.prod(rshape[:split_idx]) - this_max_chunk_length = rshape[split_idx] - next_chunk_length = min(this_max_chunk_length, math.ceil(target_chunk_bytes / item_chunk_bytes)) - rchunks.append(next_chunk_length) - len_diff = len(shape) - len(rchunks) - rchunks.extend([1] * len_diff) - chunks = tuple(rchunks[::-1]) - # print(np.prod(chunks) * itemsize / target_chunk_bytes) - return chunks - - -class ReplayBuffer: - """ - Zarr-based temporal datastructure. - Assumes first dimension to be time. Only chunk in time dimension. - """ - - def __init__(self, root: Union[zarr.Group, Dict[str, dict]]): - """ - Dummy constructor. Use copy_from* and create_from* class methods instead. - """ - assert "data" in root - assert "meta" in root - assert "episode_ends" in root["meta"] - for key, value in root["data"].items(): - assert value.shape[0] == root["meta"]["episode_ends"][-1] - self.root = root - - # ============= create constructors =============== - @classmethod - def create_empty_zarr(cls, storage=None, root=None): - if root is None: - if storage is None: - storage = zarr.MemoryStore() - root = zarr.group(store=storage) - data = root.require_group("data", overwrite=False) - meta = root.require_group("meta", overwrite=False) - if "episode_ends" not in meta: - episode_ends = meta.zeros( - "episode_ends", - shape=(0,), - dtype=np.int64, - compressor=None, - overwrite=False, - ) - return cls(root=root) - - @classmethod - def create_empty_numpy(cls): - root = { - "data": dict(), - "meta": {"episode_ends": np.zeros((0,), dtype=np.int64)}, - } - return cls(root=root) - - @classmethod - def create_from_group(cls, group, **kwargs): - if "data" not in group: - # create from stratch - buffer = cls.create_empty_zarr(root=group, **kwargs) - else: - # already exist - buffer = cls(root=group, **kwargs) - return buffer - - @classmethod - def create_from_path(cls, zarr_path, mode="r", **kwargs): - """ - Open a on-disk zarr directly (for dataset larger than memory). - Slower. - """ - group = zarr.open(os.path.expanduser(zarr_path), mode) - return cls.create_from_group(group, **kwargs) - - # ============= copy constructors =============== - @classmethod - def copy_from_store( - cls, - src_store, - store=None, - keys=None, - chunks: Dict[str, tuple] = dict(), - compressors: Union[dict, str, numcodecs.abc.Codec] = dict(), - if_exists="replace", - **kwargs, - ): - """ - Load to memory. - """ - src_root = zarr.group(src_store) - root = None - if store is None: - # numpy backend - meta = dict() - for key, value in src_root["meta"].items(): - if len(value.shape) == 0: - meta[key] = np.array(value) - else: - meta[key] = value[:] - - if keys is None: - keys = src_root["data"].keys() - data = dict() - for key in keys: - arr = src_root["data"][key] - data[key] = arr[:] - - root = {"meta": meta, "data": data} - else: - root = zarr.group(store=store) - # copy without recompression - n_copied, n_skipped, n_bytes_copied = zarr.copy_store( - source=src_store, - dest=store, - source_path="/meta", - dest_path="/meta", - if_exists=if_exists, - ) - data_group = root.create_group("data", overwrite=True) - if keys is None: - keys = src_root["data"].keys() - for key in keys: - value = src_root["data"][key] - cks = cls._resolve_array_chunks(chunks=chunks, key=key, array=value) - cpr = cls._resolve_array_compressor(compressors=compressors, key=key, array=value) - if cks == value.chunks and cpr == value.compressor: - # copy without recompression - this_path = "/data/" + key - n_copied, n_skipped, n_bytes_copied = zarr.copy_store( - source=src_store, - dest=store, - source_path=this_path, - dest_path=this_path, - if_exists=if_exists, - ) - else: - # copy with recompression - n_copied, n_skipped, n_bytes_copied = zarr.copy( - source=value, - dest=data_group, - name=key, - chunks=cks, - compressor=cpr, - if_exists=if_exists, - ) - buffer = cls(root=root) - return buffer - - @classmethod - def copy_from_path( - cls, - zarr_path, - backend=None, - store=None, - keys=None, - chunks: Dict[str, tuple] = dict(), - compressors: Union[dict, str, numcodecs.abc.Codec] = dict(), - if_exists="replace", - **kwargs, - ): - """ - Copy a on-disk zarr to in-memory compressed. - Recommended - """ - if backend == "numpy": - print("backend argument is deprecated!") - store = None - - group = zarr.open(store=os.path.expanduser(zarr_path), mode="r") - return cls.copy_from_store( - src_store=group.store, - store=store, - keys=keys, - chunks=chunks, - compressors=compressors, - if_exists=if_exists, - **kwargs, - ) - - # ============= save methods =============== - def save_to_store( - self, - store, - chunks: Optional[Dict[str, tuple]] = dict(), - compressors: Union[str, numcodecs.abc.Codec, dict] = dict(), - if_exists="replace", - **kwargs, - ): - - root = zarr.group(store) - if self.backend == "zarr": - # recompression free copy - n_copied, n_skipped, n_bytes_copied = zarr.copy_store( - source=self.root.store, - dest=store, - source_path="/meta", - dest_path="/meta", - if_exists=if_exists, - ) - else: - meta_group = root.create_group("meta", overwrite=True) - # save meta, no chunking - for key, value in self.root["meta"].items(): - _ = meta_group.array(name=key, data=value, shape=value.shape, chunks=value.shape) - - # save data, chunk - data_group = root.create_group("data", overwrite=True) - for key, value in self.root["data"].items(): - cks = self._resolve_array_chunks(chunks=chunks, key=key, array=value) - cpr = self._resolve_array_compressor(compressors=compressors, key=key, array=value) - if isinstance(value, zarr.Array): - if cks == value.chunks and cpr == value.compressor: - # copy without recompression - this_path = "/data/" + key - n_copied, n_skipped, n_bytes_copied = zarr.copy_store( - source=self.root.store, - dest=store, - source_path=this_path, - dest_path=this_path, - if_exists=if_exists, - ) - else: - # copy with recompression - n_copied, n_skipped, n_bytes_copied = zarr.copy( - source=value, - dest=data_group, - name=key, - chunks=cks, - compressor=cpr, - if_exists=if_exists, - ) - else: - # numpy - _ = data_group.array(name=key, data=value, chunks=cks, compressor=cpr) - return store - - def save_to_path( - self, - zarr_path, - chunks: Optional[Dict[str, tuple]] = dict(), - compressors: Union[str, numcodecs.abc.Codec, dict] = dict(), - if_exists="replace", - **kwargs, - ): - store = zarr.DirectoryStore(os.path.expanduser(zarr_path)) - return self.save_to_store(store, chunks=chunks, compressors=compressors, if_exists=if_exists, **kwargs) - - @staticmethod - def resolve_compressor(compressor="default"): - if compressor == "default": - compressor = numcodecs.Blosc(cname="lz4", clevel=5, shuffle=numcodecs.Blosc.NOSHUFFLE) - elif compressor == "disk": - compressor = numcodecs.Blosc("zstd", clevel=5, shuffle=numcodecs.Blosc.BITSHUFFLE) - return compressor - - @classmethod - def _resolve_array_compressor(cls, compressors: Union[dict, str, numcodecs.abc.Codec], key, array): - # allows compressor to be explicitly set to None - cpr = "nil" - if isinstance(compressors, dict): - if key in compressors: - cpr = cls.resolve_compressor(compressors[key]) - elif isinstance(array, zarr.Array): - cpr = array.compressor - else: - cpr = cls.resolve_compressor(compressors) - # backup default - if cpr == "nil": - cpr = cls.resolve_compressor("default") - return cpr - - @classmethod - def _resolve_array_chunks(cls, chunks: Union[dict, tuple], key, array): - cks = None - if isinstance(chunks, dict): - if key in chunks: - cks = chunks[key] - elif isinstance(array, zarr.Array): - cks = array.chunks - elif isinstance(chunks, tuple): - cks = chunks - else: - raise TypeError(f"Unsupported chunks type {type(chunks)}") - # backup default - if cks is None: - cks = get_optimal_chunks(shape=array.shape, dtype=array.dtype) - # check - check_chunks_compatible(chunks=cks, shape=array.shape) - return cks - - # ============= properties ================= - @cached_property - def data(self): - return self.root["data"] - - @cached_property - def meta(self): - return self.root["meta"] - - def update_meta(self, data): - # sanitize data - np_data = dict() - for key, value in data.items(): - if isinstance(value, np.ndarray): - np_data[key] = value - else: - arr = np.array(value) - if arr.dtype == object: - raise TypeError(f"Invalid value type {type(value)}") - np_data[key] = arr - - meta_group = self.meta - if self.backend == "zarr": - for key, value in np_data.items(): - _ = meta_group.array( - name=key, - data=value, - shape=value.shape, - chunks=value.shape, - overwrite=True, - ) - else: - meta_group.update(np_data) - - return meta_group - - @property - def episode_ends(self): - return self.meta["episode_ends"] - - def get_episode_idxs(self): - import numba - - numba.jit(nopython=True) - - def _get_episode_idxs(episode_ends): - result = np.zeros((episode_ends[-1],), dtype=np.int64) - for i in range(len(episode_ends)): - start = 0 - if i > 0: - start = episode_ends[i - 1] - end = episode_ends[i] - for idx in range(start, end): - result[idx] = i - return result - - return _get_episode_idxs(self.episode_ends) - - @property - def backend(self): - backend = "numpy" - if isinstance(self.root, zarr.Group): - backend = "zarr" - return backend - - # =========== dict-like API ============== - def __repr__(self) -> str: - if self.backend == "zarr": - return str(self.root.tree()) - else: - return super().__repr__() - - def keys(self): - return self.data.keys() - - def values(self): - return self.data.values() - - def items(self): - return self.data.items() - - def __getitem__(self, key): - return self.data[key] - - def __contains__(self, key): - return key in self.data - - # =========== our API ============== - @property - def n_steps(self): - if len(self.episode_ends) == 0: - return 0 - return self.episode_ends[-1] - - @property - def n_episodes(self): - return len(self.episode_ends) - - @property - def chunk_size(self): - if self.backend == "zarr": - return next(iter(self.data.arrays()))[-1].chunks[0] - return None - - @property - def episode_lengths(self): - ends = self.episode_ends[:] - ends = np.insert(ends, 0, 0) - lengths = np.diff(ends) - return lengths - - def add_episode( - self, - data: Dict[str, np.ndarray], - chunks: Optional[Dict[str, tuple]] = dict(), - compressors: Union[str, numcodecs.abc.Codec, dict] = dict(), - ): - assert len(data) > 0 - is_zarr = self.backend == "zarr" - - curr_len = self.n_steps - episode_length = None - for key, value in data.items(): - assert len(value.shape) >= 1 - if episode_length is None: - episode_length = len(value) - else: - assert episode_length == len(value) - new_len = curr_len + episode_length - - for key, value in data.items(): - new_shape = (new_len,) + value.shape[1:] - # create array - if key not in self.data: - if is_zarr: - cks = self._resolve_array_chunks(chunks=chunks, key=key, array=value) - cpr = self._resolve_array_compressor(compressors=compressors, key=key, array=value) - arr = self.data.zeros( - name=key, - shape=new_shape, - chunks=cks, - dtype=value.dtype, - compressor=cpr, - ) - else: - # copy data to prevent modify - arr = np.zeros(shape=new_shape, dtype=value.dtype) - self.data[key] = arr - else: - arr = self.data[key] - assert value.shape[1:] == arr.shape[1:] - # same method for both zarr and numpy - if is_zarr: - arr.resize(new_shape) - else: - arr.resize(new_shape, refcheck=False) - # copy data - arr[-value.shape[0] :] = value - - # append to episode ends - episode_ends = self.episode_ends - if is_zarr: - episode_ends.resize(episode_ends.shape[0] + 1) - else: - episode_ends.resize(episode_ends.shape[0] + 1, refcheck=False) - episode_ends[-1] = new_len - - # rechunk - if is_zarr: - if episode_ends.chunks[0] < episode_ends.shape[0]: - rechunk_recompress_array( - self.meta, - "episode_ends", - chunk_length=int(episode_ends.shape[0] * 1.5), - ) - - def drop_episode(self): - is_zarr = self.backend == "zarr" - episode_ends = self.episode_ends[:].copy() - assert len(episode_ends) > 0 - start_idx = 0 - if len(episode_ends) > 1: - start_idx = episode_ends[-2] - for key, value in self.data.items(): - new_shape = (start_idx,) + value.shape[1:] - if is_zarr: - value.resize(new_shape) - else: - value.resize(new_shape, refcheck=False) - if is_zarr: - self.episode_ends.resize(len(episode_ends) - 1) - else: - self.episode_ends.resize(len(episode_ends) - 1, refcheck=False) - - def pop_episode(self): - assert self.n_episodes > 0 - episode = self.get_episode(self.n_episodes - 1, copy=True) - self.drop_episode() - return episode - - def extend(self, data): - self.add_episode(data) - - def get_episode(self, idx, copy=False): - idx = list(range(len(self.episode_ends)))[idx] - start_idx = 0 - if idx > 0: - start_idx = self.episode_ends[idx - 1] - end_idx = self.episode_ends[idx] - result = self.get_steps_slice(start_idx, end_idx, copy=copy) - return result - - def get_episode_slice(self, idx): - start_idx = 0 - if idx > 0: - start_idx = self.episode_ends[idx - 1] - end_idx = self.episode_ends[idx] - return slice(start_idx, end_idx) - - def get_steps_slice(self, start, stop, step=None, copy=False): - _slice = slice(start, stop, step) - - result = dict() - for key, value in self.data.items(): - x = value[_slice] - if copy and isinstance(value, np.ndarray): - x = x.copy() - result[key] = x - return result - - # =========== chunking ============= - def get_chunks(self) -> dict: - assert self.backend == "zarr" - chunks = dict() - for key, value in self.data.items(): - chunks[key] = value.chunks - return chunks - - def set_chunks(self, chunks: dict): - assert self.backend == "zarr" - for key, value in chunks.items(): - if key in self.data: - arr = self.data[key] - if value != arr.chunks: - check_chunks_compatible(chunks=value, shape=arr.shape) - rechunk_recompress_array(self.data, key, chunks=value) - - def get_compressors(self) -> dict: - assert self.backend == "zarr" - compressors = dict() - for key, value in self.data.items(): - compressors[key] = value.compressor - return compressors - - def set_compressors(self, compressors: dict): - assert self.backend == "zarr" - for key, value in compressors.items(): - if key in self.data: - arr = self.data[key] - compressor = self.resolve_compressor(value) - if compressor != arr.compressor: - rechunk_recompress_array(self.data, key, compressor=compressor) diff --git a/roboverse_learn/il/utils/common/sampler.py b/roboverse_learn/il/utils/common/sampler.py deleted file mode 100644 index 5f58624bd..000000000 --- a/roboverse_learn/il/utils/common/sampler.py +++ /dev/null @@ -1,166 +0,0 @@ -from __future__ import annotations - -import numba -import numpy as np - -from roboverse_learn.il.utils.common.replay_buffer import ReplayBuffer - - -@numba.jit(nopython=True) -def create_indices( - episode_ends: np.ndarray, - sequence_length: int, - episode_mask: np.ndarray, - pad_before: int = 0, - pad_after: int = 0, - debug: bool = True, -) -> np.ndarray: - pad_before = min(max(pad_before, 0), sequence_length - 1) - pad_after = min(max(pad_after, 0), sequence_length - 1) - - indices = list() - for i in range(len(episode_ends)): - if not episode_mask[i]: - # skip episode - continue - start_idx = 0 - if i > 0: - start_idx = episode_ends[i - 1] - end_idx = episode_ends[i] - episode_length = end_idx - start_idx - - min_start = -pad_before - max_start = episode_length - sequence_length + pad_after - - # range stops one idx before end - for idx in range(min_start, max_start + 1): - buffer_start_idx = max(idx, 0) + start_idx - buffer_end_idx = min(idx + sequence_length, episode_length) + start_idx - start_offset = buffer_start_idx - (idx + start_idx) - end_offset = (idx + sequence_length + start_idx) - buffer_end_idx - sample_start_idx = 0 + start_offset - sample_end_idx = sequence_length - end_offset - if debug: - assert start_offset >= 0 - assert end_offset >= 0 - assert (sample_end_idx - sample_start_idx) == (buffer_end_idx - buffer_start_idx) - indices.append([buffer_start_idx, buffer_end_idx, sample_start_idx, sample_end_idx]) - indices = np.array(indices) - return indices - - -def get_val_mask(n_episodes, val_ratio, seed=0): - val_mask = np.zeros(n_episodes, dtype=bool) - if val_ratio <= 0: - return val_mask - - # have at least 1 episode for validation, and at least 1 episode for train - n_val = min(max(1, round(n_episodes * val_ratio)), n_episodes - 1) - rng = np.random.default_rng(seed=seed) - # val_idxs = rng.choice(n_episodes, size=n_val, replace=False) - val_idxs = -1 - val_mask[val_idxs] = True - return val_mask - - -def downsample_mask(mask, max_n, seed=0): - # subsample training data - train_mask = mask - if (max_n is not None) and (np.sum(train_mask) > max_n): - n_train = int(max_n) - curr_train_idxs = np.nonzero(train_mask)[0] - rng = np.random.default_rng(seed=seed) - train_idxs_idx = rng.choice(len(curr_train_idxs), size=n_train, replace=False) - train_idxs = curr_train_idxs[train_idxs_idx] - train_mask = np.zeros_like(train_mask) - train_mask[train_idxs] = True - assert np.sum(train_mask) == n_train - return train_mask - - -class SequenceSampler: - def __init__( - self, - replay_buffer: ReplayBuffer, - sequence_length: int, - pad_before: int = 0, - pad_after: int = 0, - keys=None, - key_first_k=None, - episode_mask: np.ndarray | None = None, - ): - """ - key_first_k: dict str: int - Only take first k data from these keys (to improve perf) - """ - - super().__init__() - if key_first_k is None: - key_first_k = dict() - assert sequence_length >= 1 - if keys is None: - keys = list(replay_buffer.keys()) - - episode_ends = replay_buffer.episode_ends[:] - if episode_mask is None: - episode_mask = np.ones(episode_ends.shape, dtype=bool) - - if np.any(episode_mask): - indices = create_indices( - episode_ends, - sequence_length=sequence_length, - pad_before=pad_before, - pad_after=pad_after, - episode_mask=episode_mask, - ) - else: - indices = np.zeros((0, 4), dtype=np.int64) - - # (buffer_start_idx, buffer_end_idx, sample_start_idx, sample_end_idx) - self.indices = indices - self.keys = list(keys) # prevent OmegaConf list performance problem - self.sequence_length = sequence_length - self.replay_buffer = replay_buffer - self.key_first_k = key_first_k - - def __len__(self): - return len(self.indices) - - def sample_sequence(self, idx): - buffer_start_idx, buffer_end_idx, sample_start_idx, sample_end_idx = self.indices[idx] - result = dict() - for key in self.keys: - input_arr = self.replay_buffer[key] - # performance optimization, avoid small allocation if possible - if key not in self.key_first_k: - sample = input_arr[buffer_start_idx:buffer_end_idx] - else: - # performance optimization, only load used obs steps - n_data = buffer_end_idx - buffer_start_idx - k_data = min(self.key_first_k[key], n_data) - # fill value with Nan to catch bugs - # the non-loaded region should never be used - sample = np.full( - (n_data,) + input_arr.shape[1:], - fill_value=np.nan, - dtype=input_arr.dtype, - ) - try: - sample[:k_data] = input_arr[buffer_start_idx : buffer_start_idx + k_data] - except Exception as e: - import pdb - - pdb.set_trace() - data = sample - if (sample_start_idx > 0) or (sample_end_idx < self.sequence_length): - data = np.zeros( - shape=(self.sequence_length,) + input_arr.shape[1:], - dtype=input_arr.dtype, - ) - if sample_start_idx > 0: - data[:sample_start_idx] = sample[0] - if sample_end_idx < self.sequence_length: - data[sample_end_idx:] = sample[-1] - data[sample_start_idx:sample_end_idx] = sample - result[key] = data - return result diff --git a/roboverse_learn/il/utils/common/cv2_util.py b/roboverse_learn/il/utils/cv2_util.py similarity index 100% rename from roboverse_learn/il/utils/common/cv2_util.py rename to roboverse_learn/il/utils/cv2_util.py diff --git a/roboverse_learn/il/utils/common/dict_of_tensor_mixin.py b/roboverse_learn/il/utils/dict_of_tensor_mixin.py similarity index 100% rename from roboverse_learn/il/utils/common/dict_of_tensor_mixin.py rename to roboverse_learn/il/utils/dict_of_tensor_mixin.py diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/__init__.py b/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/__init__.py deleted file mode 100644 index 7394cce83..000000000 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .workspace.robotworkspace import RobotWorkspace -from .workspace.base_workspace import BaseWorkspace diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/checkpoint_util.py b/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/checkpoint_util.py deleted file mode 100644 index 049b6ceb2..000000000 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/checkpoint_util.py +++ /dev/null @@ -1,60 +0,0 @@ -import os -from typing import Dict, Optional - - -class TopKCheckpointManager: - def __init__( - self, - save_dir, - monitor_key: str, - mode="min", - k=1, - format_str="epoch={epoch:03d}-train_loss={train_loss:.3f}.ckpt", - ): - assert mode in ["max", "min"] - assert k >= 0 - - self.save_dir = save_dir - self.monitor_key = monitor_key - self.mode = mode - self.k = k - self.format_str = format_str - self.path_value_map = dict() - - def get_ckpt_path(self, data: Dict[str, float]) -> Optional[str]: - if self.k == 0: - return None - - value = data[self.monitor_key] - ckpt_path = os.path.join(self.save_dir, self.format_str.format(**data)) - - if len(self.path_value_map) < self.k: - # under-capacity - self.path_value_map[ckpt_path] = value - return ckpt_path - - # at capacity - sorted_map = sorted(self.path_value_map.items(), key=lambda x: x[1]) - min_path, min_value = sorted_map[0] - max_path, max_value = sorted_map[-1] - - delete_path = None - if self.mode == "max": - if value > min_value: - delete_path = min_path - else: - if value < max_value: - delete_path = max_path - - if delete_path is None: - return None - else: - del self.path_value_map[delete_path] - self.path_value_map[ckpt_path] = value - - if not os.path.exists(self.save_dir): - os.mkdir(self.save_dir) - - if os.path.exists(delete_path): - os.remove(delete_path) - return ckpt_path diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/cv2_util.py b/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/cv2_util.py deleted file mode 100644 index c6c9e6446..000000000 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/cv2_util.py +++ /dev/null @@ -1,151 +0,0 @@ -import math -from typing import Tuple - -import cv2 -import numpy as np - - -def draw_reticle(img, u, v, label_color): - """ - Draws a reticle (cross-hair) on the image at the given position on top of - the original image. - @param img (In/Out) uint8 3 channel image - @param u X coordinate (width) - @param v Y coordinate (height) - @param label_color tuple of 3 ints for RGB color used for drawing. - """ - # Cast to int. - u = int(u) - v = int(v) - - white = (255, 255, 255) - cv2.circle(img, (u, v), 10, label_color, 1) - cv2.circle(img, (u, v), 11, white, 1) - cv2.circle(img, (u, v), 12, label_color, 1) - cv2.line(img, (u, v + 1), (u, v + 3), white, 1) - cv2.line(img, (u + 1, v), (u + 3, v), white, 1) - cv2.line(img, (u, v - 1), (u, v - 3), white, 1) - cv2.line(img, (u - 1, v), (u - 3, v), white, 1) - - -def draw_text( - img, - *, - text, - uv_top_left, - color=(255, 255, 255), - fontScale=0.5, - thickness=1, - fontFace=cv2.FONT_HERSHEY_SIMPLEX, - outline_color=(0, 0, 0), - line_spacing=1.5, -): - """ - Draws multiline with an outline. - """ - assert isinstance(text, str) - - uv_top_left = np.array(uv_top_left, dtype=float) - assert uv_top_left.shape == (2,) - - for line in text.splitlines(): - (w, h), _ = cv2.getTextSize( - text=line, - fontFace=fontFace, - fontScale=fontScale, - thickness=thickness, - ) - uv_bottom_left_i = uv_top_left + [0, h] - org = tuple(uv_bottom_left_i.astype(int)) - - if outline_color is not None: - cv2.putText( - img, - text=line, - org=org, - fontFace=fontFace, - fontScale=fontScale, - color=outline_color, - thickness=thickness * 3, - lineType=cv2.LINE_AA, - ) - cv2.putText( - img, - text=line, - org=org, - fontFace=fontFace, - fontScale=fontScale, - color=color, - thickness=thickness, - lineType=cv2.LINE_AA, - ) - - uv_top_left += [0, h * line_spacing] - - -def get_image_transform( - input_res: Tuple[int, int] = (1280, 720), - output_res: Tuple[int, int] = (640, 480), - bgr_to_rgb: bool = False, -): - - iw, ih = input_res - ow, oh = output_res - rw, rh = None, None - interp_method = cv2.INTER_AREA - - if (iw / ih) >= (ow / oh): - # input is wider - rh = oh - rw = math.ceil(rh / ih * iw) - if oh > ih: - interp_method = cv2.INTER_LINEAR - else: - rw = ow - rh = math.ceil(rw / iw * ih) - if ow > iw: - interp_method = cv2.INTER_LINEAR - - w_slice_start = (rw - ow) // 2 - w_slice = slice(w_slice_start, w_slice_start + ow) - h_slice_start = (rh - oh) // 2 - h_slice = slice(h_slice_start, h_slice_start + oh) - c_slice = slice(None) - if bgr_to_rgb: - c_slice = slice(None, None, -1) - - def transform(img: np.ndarray): - assert img.shape == ((ih, iw, 3)) - # resize - img = cv2.resize(img, (rw, rh), interpolation=interp_method) - # crop - img = img[h_slice, w_slice, c_slice] - return img - - return transform - - -def optimal_row_cols(n_cameras, in_wh_ratio, max_resolution=(1920, 1080)): - out_w, out_h = max_resolution - out_wh_ratio = out_w / out_h - - n_rows = np.arange(n_cameras, dtype=np.int64) + 1 - n_cols = np.ceil(n_cameras / n_rows).astype(np.int64) - cat_wh_ratio = in_wh_ratio * (n_cols / n_rows) - ratio_diff = np.abs(out_wh_ratio - cat_wh_ratio) - best_idx = np.argmin(ratio_diff) - best_n_row = n_rows[best_idx] - best_n_col = n_cols[best_idx] - best_cat_wh_ratio = cat_wh_ratio[best_idx] - - rw, rh = None, None - if best_cat_wh_ratio >= out_wh_ratio: - # cat is wider - rw = math.floor(out_w / best_n_col) - rh = math.floor(rw / in_wh_ratio) - else: - rh = math.floor(out_h / best_n_row) - rw = math.floor(rh * in_wh_ratio) - - # crop_resolution = (rw, rh) - return rw, rh, best_n_col, best_n_row diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/env_util.py b/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/env_util.py deleted file mode 100644 index 30622fac6..000000000 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/env_util.py +++ /dev/null @@ -1,28 +0,0 @@ -import cv2 -import numpy as np - - -def render_env_video(env, states, actions=None): - observations = states - imgs = list() - for i in range(len(observations)): - state = observations[i] - env.set_state(state) - if i == 0: - env.set_state(state) - img = env.render() - # draw action - if actions is not None: - action = actions[i] - coord = (action / 512 * 96).astype(np.int32) - cv2.drawMarker( - img, - coord, - color=(255, 0, 0), - markerType=cv2.MARKER_CROSS, - markerSize=8, - thickness=1, - ) - imgs.append(img) - imgs = np.array(imgs) - return imgs diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/json_logger.py b/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/json_logger.py deleted file mode 100644 index 9bd7f2973..000000000 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/json_logger.py +++ /dev/null @@ -1,115 +0,0 @@ -import copy -import json -import numbers -import os -from typing import Any, Callable, Optional, Sequence - -import pandas as pd - - -def read_json_log(path: str, required_keys: Sequence[str] = tuple(), **kwargs) -> pd.DataFrame: - """ - Read json-per-line file, with potentially incomplete lines. - kwargs passed to pd.read_json - """ - lines = list() - with open(path, "r") as f: - while True: - # one json per line - line = f.readline() - if len(line) == 0: - # EOF - break - elif not line.endswith("\n"): - # incomplete line - break - is_relevant = False - for k in required_keys: - if k in line: - is_relevant = True - break - if is_relevant: - lines.append(line) - if len(lines) < 1: - return pd.DataFrame() - json_buf = f'[{",".join([line for line in (line.strip() for line in lines) if line])}]' - df = pd.read_json(json_buf, **kwargs) - return df - - -class JsonLogger: - def __init__(self, path: str, filter_fn: Optional[Callable[[str, Any], bool]] = None): - if filter_fn is None: - filter_fn = lambda k, v: isinstance(v, numbers.Number) - - # default to append mode - self.path = path - self.filter_fn = filter_fn - self.file = None - self.last_log = None - - def start(self): - # use line buffering - try: - self.file = file = open(self.path, "r+", buffering=1) - except FileNotFoundError: - self.file = file = open(self.path, "w+", buffering=1) - - # Move the pointer (similar to a cursor in a text editor) to the end of the file - pos = file.seek(0, os.SEEK_END) - - # Read each character in the file one at a time from the last - # character going backwards, searching for a newline character - # If we find a new line, exit the search - while pos > 0 and file.read(1) != "\n": - pos -= 1 - file.seek(pos, os.SEEK_SET) - # now the file pointer is at one past the last '\n' - # and pos is at the last '\n'. - last_line_end = file.tell() - - # find the start of second last line - pos = max(0, pos - 1) - file.seek(pos, os.SEEK_SET) - while pos > 0 and file.read(1) != "\n": - pos -= 1 - file.seek(pos, os.SEEK_SET) - # now the file pointer is at one past the second last '\n' - last_line_start = file.tell() - - if last_line_start < last_line_end: - # has last line of json - last_line = file.readline() - self.last_log = json.loads(last_line) - - # remove the last incomplete line - file.seek(last_line_end) - file.truncate() - - def stop(self): - self.file.close() - self.file = None - - def __enter__(self): - self.start() - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.stop() - - def log(self, data: dict): - filtered_data = dict(filter(lambda x: self.filter_fn(*x), data.items())) - # save current as last log - self.last_log = filtered_data - for k, v in filtered_data.items(): - if isinstance(v, numbers.Integral): - filtered_data[k] = int(v) - elif isinstance(v, numbers.Number): - filtered_data[k] = float(v) - buf = json.dumps(filtered_data) - # ensure one line per json - buf = buf.replace("\n", "") + "\n" - self.file.write(buf) - - def get_last_log(self): - return copy.deepcopy(self.last_log) diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/nested_dict_util.py b/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/nested_dict_util.py deleted file mode 100644 index 013bd0bd8..000000000 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/nested_dict_util.py +++ /dev/null @@ -1,34 +0,0 @@ -import functools - - -def nested_dict_map(f, x): - """ - Map f over all leaf of nested dict x - """ - - if not isinstance(x, dict): - return f(x) - y = dict() - for key, value in x.items(): - y[key] = nested_dict_map(f, value) - return y - - -def nested_dict_reduce(f, x): - """ - Map f over all values of nested dict x, and reduce to a single value - """ - if not isinstance(x, dict): - return x - - reduced_values = list() - for value in x.values(): - reduced_values.append(nested_dict_reduce(f, value)) - y = functools.reduce(f, reduced_values) - return y - - -def nested_dict_check(f, x): - bool_dict = nested_dict_map(f, x) - result = nested_dict_reduce(lambda x, y: x and y, bool_dict) - return result diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/pose_trajectory_interpolator.py b/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/pose_trajectory_interpolator.py deleted file mode 100644 index e87bc3d98..000000000 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/pose_trajectory_interpolator.py +++ /dev/null @@ -1,208 +0,0 @@ -import numbers -from typing import Union - -import numpy as np -import scipy.interpolate as si -import scipy.spatial.transform as st - - -def rotation_distance(a: st.Rotation, b: st.Rotation) -> float: - return (b * a.inv()).magnitude() - - -def pose_distance(start_pose, end_pose): - start_pose = np.array(start_pose) - end_pose = np.array(end_pose) - start_pos = start_pose[:3] - end_pos = end_pose[:3] - start_rot = st.Rotation.from_rotvec(start_pose[3:]) - end_rot = st.Rotation.from_rotvec(end_pose[3:]) - pos_dist = np.linalg.norm(end_pos - start_pos) - rot_dist = rotation_distance(start_rot, end_rot) - return pos_dist, rot_dist - - -class PoseTrajectoryInterpolator: - def __init__(self, times: np.ndarray, poses: np.ndarray): - assert len(times) >= 1 - assert len(poses) == len(times) - if not isinstance(times, np.ndarray): - times = np.array(times) - if not isinstance(poses, np.ndarray): - poses = np.array(poses) - - if len(times) == 1: - # special treatment for single step interpolation - self.single_step = True - self._times = times - self._poses = poses - else: - self.single_step = False - assert np.all(times[1:] >= times[:-1]) - - pos = poses[:, :3] - rot = st.Rotation.from_rotvec(poses[:, 3:]) - - self.pos_interp = si.interp1d(times, pos, axis=0, assume_sorted=True) - self.rot_interp = st.Slerp(times, rot) - - @property - def times(self) -> np.ndarray: - if self.single_step: - return self._times - else: - return self.pos_interp.x - - @property - def poses(self) -> np.ndarray: - if self.single_step: - return self._poses - else: - n = len(self.times) - poses = np.zeros((n, 6)) - poses[:, :3] = self.pos_interp.y - poses[:, 3:] = self.rot_interp(self.times).as_rotvec() - return poses - - def trim(self, start_t: float, end_t: float) -> "PoseTrajectoryInterpolator": - assert start_t <= end_t - times = self.times - should_keep = (start_t < times) & (times < end_t) - keep_times = times[should_keep] - all_times = np.concatenate([[start_t], keep_times, [end_t]]) - # remove duplicates, Slerp requires strictly increasing x - all_times = np.unique(all_times) - # interpolate - all_poses = self(all_times) - return PoseTrajectoryInterpolator(times=all_times, poses=all_poses) - - def drive_to_waypoint( - self, pose, time, curr_time, max_pos_speed=np.inf, max_rot_speed=np.inf - ) -> "PoseTrajectoryInterpolator": - assert max_pos_speed > 0 - assert max_rot_speed > 0 - time = max(time, curr_time) - - curr_pose = self(curr_time) - pos_dist, rot_dist = pose_distance(curr_pose, pose) - pos_min_duration = pos_dist / max_pos_speed - rot_min_duration = rot_dist / max_rot_speed - duration = time - curr_time - duration = max(duration, max(pos_min_duration, rot_min_duration)) - assert duration >= 0 - last_waypoint_time = curr_time + duration - - # insert new pose - trimmed_interp = self.trim(curr_time, curr_time) - times = np.append(trimmed_interp.times, [last_waypoint_time], axis=0) - poses = np.append(trimmed_interp.poses, [pose], axis=0) - - # create new interpolator - final_interp = PoseTrajectoryInterpolator(times, poses) - return final_interp - - def schedule_waypoint( - self, - pose, - time, - max_pos_speed=np.inf, - max_rot_speed=np.inf, - curr_time=None, - last_waypoint_time=None, - ) -> "PoseTrajectoryInterpolator": - assert max_pos_speed > 0 - assert max_rot_speed > 0 - if last_waypoint_time is not None: - assert curr_time is not None - - # trim current interpolator to between curr_time and last_waypoint_time - start_time = self.times[0] - end_time = self.times[-1] - assert start_time <= end_time - - if curr_time is not None: - if time <= curr_time: - # if insert time is earlier than current time - # no effect should be done to the interpolator - return self - # now, curr_time < time - start_time = max(curr_time, start_time) - - if last_waypoint_time is not None: - # if last_waypoint_time is earlier than start_time - # use start_time - if time <= last_waypoint_time: - end_time = curr_time - else: - end_time = max(last_waypoint_time, curr_time) - else: - end_time = curr_time - - end_time = min(end_time, time) - start_time = min(start_time, end_time) - # end time should be the latest of all times except time - # after this we can assume order (proven by zhenjia, due to the 2 min operations) - - # Constraints: - # start_time <= end_time <= time (proven by zhenjia) - # curr_time <= start_time (proven by zhenjia) - # curr_time <= time (proven by zhenjia) - - # time can't change - # last_waypoint_time can't change - # curr_time can't change - assert start_time <= end_time - assert end_time <= time - if last_waypoint_time is not None: - if time <= last_waypoint_time: - assert end_time == curr_time - else: - assert end_time == max(last_waypoint_time, curr_time) - - if curr_time is not None: - assert curr_time <= start_time - assert curr_time <= time - - trimmed_interp = self.trim(start_time, end_time) - # after this, all waypoints in trimmed_interp is within start_time and end_time - # and is earlier than time - - # determine speed - duration = time - end_time - end_pose = trimmed_interp(end_time) - pos_dist, rot_dist = pose_distance(pose, end_pose) - pos_min_duration = pos_dist / max_pos_speed - rot_min_duration = rot_dist / max_rot_speed - duration = max(duration, max(pos_min_duration, rot_min_duration)) - assert duration >= 0 - last_waypoint_time = end_time + duration - - # insert new pose - times = np.append(trimmed_interp.times, [last_waypoint_time], axis=0) - poses = np.append(trimmed_interp.poses, [pose], axis=0) - - # create new interpolator - final_interp = PoseTrajectoryInterpolator(times, poses) - return final_interp - - def __call__(self, t: Union[numbers.Number, np.ndarray]) -> np.ndarray: - is_single = False - if isinstance(t, numbers.Number): - is_single = True - t = np.array([t]) - - pose = np.zeros((len(t), 6)) - if self.single_step: - pose[:] = self._poses[0] - else: - start_time = self.times[0] - end_time = self.times[-1] - t = np.clip(t, start_time, end_time) - - pose = np.zeros((len(t), 6)) - pose[:, :3] = self.pos_interp(t) - pose[:, 3:] = self.rot_interp(t).as_rotvec() - - if is_single: - pose = pose[0] - return pose diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/precise_sleep.py b/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/precise_sleep.py deleted file mode 100644 index 04d783ee7..000000000 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/precise_sleep.py +++ /dev/null @@ -1,27 +0,0 @@ -import time - - -def precise_sleep(dt: float, slack_time: float = 0.001, time_func=time.monotonic): - """ - Use hybrid of time.sleep and spinning to minimize jitter. - Sleep dt - slack_time seconds first, then spin for the rest. - """ - t_start = time_func() - if dt > slack_time: - time.sleep(dt - slack_time) - t_end = t_start + dt - while time_func() < t_end: - pass - return - - -def precise_wait(t_end: float, slack_time: float = 0.001, time_func=time.monotonic): - t_start = time_func() - t_wait = t_end - t_start - if t_wait > 0: - t_sleep = t_wait - slack_time - if t_sleep > 0: - time.sleep(t_sleep) - while time_func() < t_end: - pass - return diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/pymunk_override.py b/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/pymunk_override.py deleted file mode 100644 index 4b9dd3b45..000000000 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/pymunk_override.py +++ /dev/null @@ -1,244 +0,0 @@ -# ---------------------------------------------------------------------------- -# pymunk -# Copyright (c) 2007-2016 Victor Blomqvist -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -# ---------------------------------------------------------------------------- - -"""This submodule contains helper functions to help with quick prototyping -using pymunk together with pygame. - -Intended to help with debugging and prototyping, not for actual production use -in a full application. The methods contained in this module is opinionated -about your coordinate system and not in any way optimized. -""" - -__docformat__ = "reStructuredText" - -__all__ = [ - "DrawOptions", - "get_mouse_pos", - "to_pygame", - "from_pygame", - "lighten", - "positive_y_is_up", -] - -from typing import List, Sequence, Tuple - -import numpy as np -import pygame -import pymunk -from pymunk.space_debug_draw_options import SpaceDebugColor -from pymunk.vec2d import Vec2d - -positive_y_is_up: bool = False -"""Make increasing values of y point upwards. - -When True:: - - y - ^ - | . (3, 3) - | - | . (2, 2) - | - +------ > x - -When False:: - - +------ > x - | - | . (2, 2) - | - | . (3, 3) - v - y - -""" - - -class DrawOptions(pymunk.SpaceDebugDrawOptions): - def __init__(self, surface: pygame.Surface) -> None: - """Draw a pymunk.Space on a pygame.Surface object. - - Typical usage:: - - >>> import pymunk - >>> surface = pygame.Surface((10,10)) - >>> space = pymunk.Space() - >>> options = pymunk.pygame_util.DrawOptions(surface) - >>> space.debug_draw(options) - - You can control the color of a shape by setting shape.color to the color - you want it drawn in:: - - >>> c = pymunk.Circle(None, 10) - >>> c.color = pygame.Color("pink") - - See pygame_util.demo.py for a full example - - Since pygame uses a coordinate system where y points down (in contrast - to many other cases), you either have to make the physics simulation - with Pymunk also behave in that way, or flip everything when you draw. - - The easiest is probably to just make the simulation behave the same - way as Pygame does. In that way all coordinates used are in the same - orientation and easy to reason about:: - - >>> space = pymunk.Space() - >>> space.gravity = (0, -1000) - >>> body = pymunk.Body() - >>> body.position = (0, 0) # will be positioned in the top left corner - >>> space.debug_draw(options) - - To flip the drawing its possible to set the module property - :py:data:`positive_y_is_up` to True. Then the pygame drawing will flip - the simulation upside down before drawing:: - - >>> positive_y_is_up = True - >>> body = pymunk.Body() - >>> body.position = (0, 0) - >>> # Body will be position in bottom left corner - - :Parameters: - surface : pygame.Surface - Surface that the objects will be drawn on - """ - self.surface = surface - super(DrawOptions, self).__init__() - - def draw_circle( - self, - pos: Vec2d, - angle: float, - radius: float, - outline_color: SpaceDebugColor, - fill_color: SpaceDebugColor, - ) -> None: - p = to_pygame(pos, self.surface) - - pygame.draw.circle(self.surface, fill_color.as_int(), p, round(radius), 0) - pygame.draw.circle(self.surface, light_color(fill_color).as_int(), p, round(radius - 4), 0) - - circle_edge = pos + Vec2d(radius, 0).rotated(angle) - p2 = to_pygame(circle_edge, self.surface) - line_r = 2 if radius > 20 else 1 - # pygame.draw.lines(self.surface, outline_color.as_int(), False, [p, p2], line_r) - - def draw_segment(self, a: Vec2d, b: Vec2d, color: SpaceDebugColor) -> None: - p1 = to_pygame(a, self.surface) - p2 = to_pygame(b, self.surface) - - pygame.draw.aalines(self.surface, color.as_int(), False, [p1, p2]) - - def draw_fat_segment( - self, - a: Tuple[float, float], - b: Tuple[float, float], - radius: float, - outline_color: SpaceDebugColor, - fill_color: SpaceDebugColor, - ) -> None: - p1 = to_pygame(a, self.surface) - p2 = to_pygame(b, self.surface) - - r = round(max(1, radius * 2)) - pygame.draw.lines(self.surface, fill_color.as_int(), False, [p1, p2], r) - if r > 2: - orthog = [abs(p2[1] - p1[1]), abs(p2[0] - p1[0])] - if orthog[0] == 0 and orthog[1] == 0: - return - scale = radius / (orthog[0] * orthog[0] + orthog[1] * orthog[1]) ** 0.5 - orthog[0] = round(orthog[0] * scale) - orthog[1] = round(orthog[1] * scale) - points = [ - (p1[0] - orthog[0], p1[1] - orthog[1]), - (p1[0] + orthog[0], p1[1] + orthog[1]), - (p2[0] + orthog[0], p2[1] + orthog[1]), - (p2[0] - orthog[0], p2[1] - orthog[1]), - ] - pygame.draw.polygon(self.surface, fill_color.as_int(), points) - pygame.draw.circle( - self.surface, - fill_color.as_int(), - (round(p1[0]), round(p1[1])), - round(radius), - ) - pygame.draw.circle( - self.surface, - fill_color.as_int(), - (round(p2[0]), round(p2[1])), - round(radius), - ) - - def draw_polygon( - self, - verts: Sequence[Tuple[float, float]], - radius: float, - outline_color: SpaceDebugColor, - fill_color: SpaceDebugColor, - ) -> None: - ps = [to_pygame(v, self.surface) for v in verts] - ps += [ps[0]] - - radius = 2 - pygame.draw.polygon(self.surface, light_color(fill_color).as_int(), ps) - - if radius > 0: - for i in range(len(verts)): - a = verts[i] - b = verts[(i + 1) % len(verts)] - self.draw_fat_segment(a, b, radius, fill_color, fill_color) - - def draw_dot(self, size: float, pos: Tuple[float, float], color: SpaceDebugColor) -> None: - p = to_pygame(pos, self.surface) - pygame.draw.circle(self.surface, color.as_int(), p, round(size), 0) - - -def get_mouse_pos(surface: pygame.Surface) -> Tuple[int, int]: - """Get position of the mouse pointer in pymunk coordinates.""" - p = pygame.mouse.get_pos() - return from_pygame(p, surface) - - -def to_pygame(p: Tuple[float, float], surface: pygame.Surface) -> Tuple[int, int]: - """Convenience method to convert pymunk coordinates to pygame surface - local coordinates. - - Note that in case positive_y_is_up is False, this function won't actually do - anything except converting the point to integers. - """ - if positive_y_is_up: - return round(p[0]), surface.get_height() - round(p[1]) - else: - return round(p[0]), round(p[1]) - - -def from_pygame(p: Tuple[float, float], surface: pygame.Surface) -> Tuple[int, int]: - """Convenience method to convert pygame surface local coordinates to - pymunk coordinates - """ - return to_pygame(p, surface) - - -def light_color(color: SpaceDebugColor): - color = np.minimum(1.2 * np.float32([color.r, color.g, color.b, color.a]), np.float32([255])) - color = SpaceDebugColor(r=color[0], g=color[1], b=color[2], a=color[3]) - return color diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/pymunk_util.py b/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/pymunk_util.py deleted file mode 100644 index 9fb2b5d6e..000000000 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/pymunk_util.py +++ /dev/null @@ -1,51 +0,0 @@ -import numpy as np -import pygame -import pymunk -import pymunk.pygame_util - -COLLTYPE_DEFAULT = 0 -COLLTYPE_MOUSE = 1 -COLLTYPE_BALL = 2 - - -def get_body_type(static=False): - body_type = pymunk.Body.DYNAMIC - if static: - body_type = pymunk.Body.STATIC - return body_type - - -def create_rectangle(space, pos_x, pos_y, width, height, density=3, static=False): - body = pymunk.Body(body_type=get_body_type(static)) - body.position = (pos_x, pos_y) - shape = pymunk.Poly.create_box(body, (width, height)) - shape.density = density - space.add(body, shape) - return body, shape - - -def create_rectangle_bb(space, left, bottom, right, top, **kwargs): - pos_x = (left + right) / 2 - pos_y = (top + bottom) / 2 - height = top - bottom - width = right - left - return create_rectangle(space, pos_x, pos_y, width, height, **kwargs) - - -def create_circle(space, pos_x, pos_y, radius, density=3, static=False): - body = pymunk.Body(body_type=get_body_type(static)) - body.position = (pos_x, pos_y) - shape = pymunk.Circle(body, radius=radius) - shape.density = density - shape.collision_type = COLLTYPE_BALL - space.add(body, shape) - return body, shape - - -def get_body_state(body): - state = np.zeros(6, dtype=np.float32) - state[:2] = body.position - state[2] = body.angle - state[3:5] = body.velocity - state[5] = body.angular_velocity - return state diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/pytorch_util.py b/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/pytorch_util.py deleted file mode 100644 index fc64fa564..000000000 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/pytorch_util.py +++ /dev/null @@ -1,82 +0,0 @@ -import collections -from typing import Callable, Dict, List - -import torch -import torch.nn as nn - - -def dict_apply(x: Dict[str, torch.Tensor], func: Callable[[torch.Tensor], torch.Tensor]) -> Dict[str, torch.Tensor]: - result = dict() - for key, value in x.items(): - if isinstance(value, dict): - result[key] = dict_apply(value, func) - else: - result[key] = func(value) - return result - - -def pad_remaining_dims(x, target): - assert x.shape == target.shape[: len(x.shape)] - return x.reshape(x.shape + (1,) * (len(target.shape) - len(x.shape))) - - -def dict_apply_split( - x: Dict[str, torch.Tensor], - split_func: Callable[[torch.Tensor], Dict[str, torch.Tensor]], -) -> Dict[str, torch.Tensor]: - results = collections.defaultdict(dict) - for key, value in x.items(): - result = split_func(value) - for k, v in result.items(): - results[k][key] = v - return results - - -def dict_apply_reduce( - x: List[Dict[str, torch.Tensor]], - reduce_func: Callable[[List[torch.Tensor]], torch.Tensor], -) -> Dict[str, torch.Tensor]: - result = dict() - for key in x[0].keys(): - result[key] = reduce_func([x_[key] for x_ in x]) - return result - - -def replace_submodules( - root_module: nn.Module, - predicate: Callable[[nn.Module], bool], - func: Callable[[nn.Module], nn.Module], -) -> nn.Module: - """ - predicate: Return true if the module is to be replaced. - func: Return new module to use. - """ - if predicate(root_module): - return func(root_module) - - bn_list = [k.split(".") for k, m in root_module.named_modules(remove_duplicate=True) if predicate(m)] - for *parent, k in bn_list: - parent_module = root_module - if len(parent) > 0: - parent_module = root_module.get_submodule(".".join(parent)) - if isinstance(parent_module, nn.Sequential): - src_module = parent_module[int(k)] - else: - src_module = getattr(parent_module, k) - tgt_module = func(src_module) - if isinstance(parent_module, nn.Sequential): - parent_module[int(k)] = tgt_module - else: - setattr(parent_module, k, tgt_module) - # verify that all BN are replaced - bn_list = [k.split(".") for k, m in root_module.named_modules(remove_duplicate=True) if predicate(m)] - assert len(bn_list) == 0 - return root_module - - -def optimizer_to(optimizer, device): - for state in optimizer.state.values(): - for k, v in state.items(): - if isinstance(v, torch.Tensor): - state[k] = v.to(device=device) - return optimizer diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/robomimic_config_util.py b/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/robomimic_config_util.py deleted file mode 100644 index b992b15aa..000000000 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/robomimic_config_util.py +++ /dev/null @@ -1,41 +0,0 @@ -import robomimic.scripts.generate_paper_configs as gpc -from omegaconf import OmegaConf -from robomimic.config import config_factory -from robomimic.scripts.generate_paper_configs import ( - modify_config_for_dataset, - modify_config_for_default_image_exp, - modify_config_for_default_low_dim_exp, -) - - -def get_robomimic_config(algo_name="bc_rnn", hdf5_type="low_dim", task_name="square", dataset_type="ph"): - base_dataset_dir = "/tmp/null" - filter_key = None - - # decide whether to use low-dim or image training defaults - modifier_for_obs = modify_config_for_default_image_exp - if hdf5_type in ["low_dim", "low_dim_sparse", "low_dim_dense"]: - modifier_for_obs = modify_config_for_default_low_dim_exp - - algo_config_name = "bc" if algo_name == "bc_rnn" else algo_name - config = config_factory(algo_name=algo_config_name) - # turn into default config for observation modalities (e.g.: low-dim or rgb) - config = modifier_for_obs(config) - # add in config based on the dataset - config = modify_config_for_dataset( - config=config, - task_name=task_name, - dataset_type=dataset_type, - hdf5_type=hdf5_type, - base_dataset_dir=base_dataset_dir, - filter_key=filter_key, - ) - # add in algo hypers based on dataset - algo_config_modifier = getattr(gpc, f"modify_{algo_name}_config_for_dataset") - config = algo_config_modifier( - config=config, - task_name=task_name, - dataset_type=dataset_type, - hdf5_type=hdf5_type, - ) - return config diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/robomimic_util.py b/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/robomimic_util.py deleted file mode 100644 index 652afb8bf..000000000 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/robomimic_util.py +++ /dev/null @@ -1,169 +0,0 @@ -import copy - -import h5py -import numpy as np -import robomimic.utils.env_utils as EnvUtils -import robomimic.utils.file_utils as FileUtils -import robomimic.utils.obs_utils as ObsUtils -from robomimic.config import config_factory -from scipy.spatial.transform import Rotation - - -class RobomimicAbsoluteActionConverter: - def __init__(self, dataset_path, algo_name="bc"): - # default BC config - config = config_factory(algo_name=algo_name) - - # read config to set up metadata for observation modalities (e.g. detecting rgb observations) - # must ran before create dataset - ObsUtils.initialize_obs_utils_with_config(config) - - env_meta = FileUtils.get_env_metadata_from_dataset(dataset_path) - abs_env_meta = copy.deepcopy(env_meta) - abs_env_meta["env_kwargs"]["controller_configs"]["control_delta"] = False - - env = EnvUtils.create_env_from_metadata( - env_meta=env_meta, - render=False, - render_offscreen=False, - use_image_obs=False, - ) - assert len(env.env.robots) in (1, 2) - - abs_env = EnvUtils.create_env_from_metadata( - env_meta=abs_env_meta, - render=False, - render_offscreen=False, - use_image_obs=False, - ) - assert not abs_env.env.robots[0].controller.use_delta - - self.env = env - self.abs_env = abs_env - self.file = h5py.File(dataset_path, "r") - - def __len__(self): - return len(self.file["data"]) - - def convert_actions(self, states: np.ndarray, actions: np.ndarray) -> np.ndarray: - """ - Given state and delta action sequence - generate equivalent goal position and orientation for each step - keep the original gripper action intact. - """ - # in case of multi robot - # reshape (N,14) to (N,2,7) - # or (N,7) to (N,1,7) - stacked_actions = actions.reshape(*actions.shape[:-1], -1, 7) - - env = self.env - # generate abs actions - action_goal_pos = np.zeros(stacked_actions.shape[:-1] + (3,), dtype=stacked_actions.dtype) - action_goal_ori = np.zeros(stacked_actions.shape[:-1] + (3,), dtype=stacked_actions.dtype) - action_gripper = stacked_actions[..., [-1]] - for i in range(len(states)): - _ = env.reset_to({"states": states[i]}) - - # taken from robot_env.py L#454 - for idx, robot in enumerate(env.env.robots): - # run controller goal generator - robot.control(stacked_actions[i, idx], policy_step=True) - - # read pos and ori from robots - controller = robot.controller - action_goal_pos[i, idx] = controller.goal_pos - action_goal_ori[i, idx] = Rotation.from_matrix(controller.goal_ori).as_rotvec() - - stacked_abs_actions = np.concatenate([action_goal_pos, action_goal_ori, action_gripper], axis=-1) - abs_actions = stacked_abs_actions.reshape(actions.shape) - return abs_actions - - def convert_idx(self, idx): - file = self.file - demo = file[f"data/demo_{idx}"] - # input - states = demo["states"][:] - actions = demo["actions"][:] - - # generate abs actions - abs_actions = self.convert_actions(states, actions) - return abs_actions - - def convert_and_eval_idx(self, idx): - env = self.env - abs_env = self.abs_env - file = self.file - # first step have high error for some reason, not representative - eval_skip_steps = 1 - - demo = file[f"data/demo_{idx}"] - # input - states = demo["states"][:] - actions = demo["actions"][:] - - # generate abs actions - abs_actions = self.convert_actions(states, actions) - - # verify - robot0_eef_pos = demo["obs"]["robot0_eef_pos"][:] - robot0_eef_quat = demo["obs"]["robot0_eef_quat"][:] - - delta_error_info = self.evaluate_rollout_error( - env, - states, - actions, - robot0_eef_pos, - robot0_eef_quat, - metric_skip_steps=eval_skip_steps, - ) - abs_error_info = self.evaluate_rollout_error( - abs_env, - states, - abs_actions, - robot0_eef_pos, - robot0_eef_quat, - metric_skip_steps=eval_skip_steps, - ) - - info = {"delta_max_error": delta_error_info, "abs_max_error": abs_error_info} - return abs_actions, info - - @staticmethod - def evaluate_rollout_error(env, states, actions, robot0_eef_pos, robot0_eef_quat, metric_skip_steps=1): - # first step have high error for some reason, not representative - - # evaluate abs actions - rollout_next_states = list() - rollout_next_eef_pos = list() - rollout_next_eef_quat = list() - obs = env.reset_to({"states": states[0]}) - for i in range(len(states)): - obs = env.reset_to({"states": states[i]}) - obs, reward, done, info = env.step(actions[i]) - obs = env.get_observation() - rollout_next_states.append(env.get_state()["states"]) - rollout_next_eef_pos.append(obs["robot0_eef_pos"]) - rollout_next_eef_quat.append(obs["robot0_eef_quat"]) - rollout_next_states = np.array(rollout_next_states) - rollout_next_eef_pos = np.array(rollout_next_eef_pos) - rollout_next_eef_quat = np.array(rollout_next_eef_quat) - - next_state_diff = states[1:] - rollout_next_states[:-1] - max_next_state_diff = np.max(np.abs(next_state_diff[metric_skip_steps:])) - - next_eef_pos_diff = robot0_eef_pos[1:] - rollout_next_eef_pos[:-1] - next_eef_pos_dist = np.linalg.norm(next_eef_pos_diff, axis=-1) - max_next_eef_pos_dist = next_eef_pos_dist[metric_skip_steps:].max() - - next_eef_rot_diff = ( - Rotation.from_quat(robot0_eef_quat[1:]) * Rotation.from_quat(rollout_next_eef_quat[:-1]).inv() - ) - next_eef_rot_dist = next_eef_rot_diff.magnitude() - max_next_eef_rot_dist = next_eef_rot_dist[metric_skip_steps:].max() - - info = { - "state": max_next_state_diff, - "pos": max_next_eef_pos_dist, - "rot": max_next_eef_rot_dist, - } - return info diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/timestamp_accumulator.py b/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/timestamp_accumulator.py deleted file mode 100644 index 7934c71d3..000000000 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/timestamp_accumulator.py +++ /dev/null @@ -1,219 +0,0 @@ -import math -from typing import Dict, List, Optional, Tuple - -import numpy as np - - -def get_accumulate_timestamp_idxs( - timestamps: List[float], - start_time: float, - dt: float, - eps: float = 1e-5, - next_global_idx: Optional[int] = 0, - allow_negative=False, -) -> Tuple[List[int], List[int], int]: - """ - For each dt window, choose the first timestamp in the window. - Assumes timestamps sorted. One timestamp might be chosen multiple times due to dropped frames. - next_global_idx should start at 0 normally, and then use the returned next_global_idx. - However, when overwiting previous values are desired, set last_global_idx to None. - - Returns: - local_idxs: which index in the given timestamps array to chose from - global_idxs: the global index of each chosen timestamp - next_global_idx: used for next call. - """ - local_idxs = list() - global_idxs = list() - for local_idx, ts in enumerate(timestamps): - # add eps * dt to timestamps so that when ts == start_time + k * dt - # is always recorded as kth element (avoiding floating point errors) - global_idx = math.floor((ts - start_time) / dt + eps) - if (not allow_negative) and (global_idx < 0): - continue - if next_global_idx is None: - next_global_idx = global_idx - - n_repeats = max(0, global_idx - next_global_idx + 1) - for i in range(n_repeats): - local_idxs.append(local_idx) - global_idxs.append(next_global_idx + i) - next_global_idx += n_repeats - return local_idxs, global_idxs, next_global_idx - - -def align_timestamps( - timestamps: List[float], - target_global_idxs: List[int], - start_time: float, - dt: float, - eps: float = 1e-5, -): - if isinstance(target_global_idxs, np.ndarray): - target_global_idxs = target_global_idxs.tolist() - assert len(target_global_idxs) > 0 - - local_idxs, global_idxs, _ = get_accumulate_timestamp_idxs( - timestamps=timestamps, - start_time=start_time, - dt=dt, - eps=eps, - next_global_idx=target_global_idxs[0], - allow_negative=True, - ) - if len(global_idxs) > len(target_global_idxs): - # if more steps available, truncate - global_idxs = global_idxs[: len(target_global_idxs)] - local_idxs = local_idxs[: len(target_global_idxs)] - - if len(global_idxs) == 0: - import pdb - - pdb.set_trace() - - for i in range(len(target_global_idxs) - len(global_idxs)): - # if missing, repeat - local_idxs.append(len(timestamps) - 1) - global_idxs.append(global_idxs[-1] + 1) - assert global_idxs == target_global_idxs - assert len(local_idxs) == len(global_idxs) - return local_idxs - - -class TimestampObsAccumulator: - def __init__(self, start_time: float, dt: float, eps: float = 1e-5): - self.start_time = start_time - self.dt = dt - self.eps = eps - self.obs_buffer = dict() - self.timestamp_buffer = None - self.next_global_idx = 0 - - def __len__(self): - return self.next_global_idx - - @property - def data(self): - if self.timestamp_buffer is None: - return dict() - result = dict() - for key, value in self.obs_buffer.items(): - result[key] = value[: len(self)] - return result - - @property - def actual_timestamps(self): - if self.timestamp_buffer is None: - return np.array([]) - return self.timestamp_buffer[: len(self)] - - @property - def timestamps(self): - if self.timestamp_buffer is None: - return np.array([]) - return self.start_time + np.arange(len(self)) * self.dt - - def put(self, data: Dict[str, np.ndarray], timestamps: np.ndarray): - """ - data: - key: T,* - """ - - local_idxs, global_idxs, self.next_global_idx = get_accumulate_timestamp_idxs( - timestamps=timestamps, - start_time=self.start_time, - dt=self.dt, - eps=self.eps, - next_global_idx=self.next_global_idx, - ) - - if len(global_idxs) > 0: - if self.timestamp_buffer is None: - # first allocation - self.obs_buffer = dict() - for key, value in data.items(): - self.obs_buffer[key] = np.zeros_like(value) - self.timestamp_buffer = np.zeros((len(timestamps),), dtype=np.float64) - - this_max_size = global_idxs[-1] + 1 - if this_max_size > len(self.timestamp_buffer): - # reallocate - new_size = max(this_max_size, len(self.timestamp_buffer) * 2) - for key in list(self.obs_buffer.keys()): - new_shape = (new_size,) + self.obs_buffer[key].shape[1:] - self.obs_buffer[key] = np.resize(self.obs_buffer[key], new_shape) - self.timestamp_buffer = np.resize(self.timestamp_buffer, (new_size)) - - # write data - for key, value in self.obs_buffer.items(): - value[global_idxs] = data[key][local_idxs] - self.timestamp_buffer[global_idxs] = timestamps[local_idxs] - - -class TimestampActionAccumulator: - def __init__(self, start_time: float, dt: float, eps: float = 1e-5): - """ - Different from Obs accumulator, the action accumulator - allows overwriting previous values. - """ - self.start_time = start_time - self.dt = dt - self.eps = eps - self.action_buffer = None - self.timestamp_buffer = None - self.size = 0 - - def __len__(self): - return self.size - - @property - def actions(self): - if self.action_buffer is None: - return np.array([]) - return self.action_buffer[: len(self)] - - @property - def actual_timestamps(self): - if self.timestamp_buffer is None: - return np.array([]) - return self.timestamp_buffer[: len(self)] - - @property - def timestamps(self): - if self.timestamp_buffer is None: - return np.array([]) - return self.start_time + np.arange(len(self)) * self.dt - - def put(self, actions: np.ndarray, timestamps: np.ndarray): - """ - Note: timestamps is the time when the action will be issued, - not when the action will be completed (target_timestamp) - """ - - local_idxs, global_idxs, _ = get_accumulate_timestamp_idxs( - timestamps=timestamps, - start_time=self.start_time, - dt=self.dt, - eps=self.eps, - # allows overwriting previous actions - next_global_idx=None, - ) - - if len(global_idxs) > 0: - if self.timestamp_buffer is None: - # first allocation - self.action_buffer = np.zeros_like(actions) - self.timestamp_buffer = np.zeros((len(actions),), dtype=np.float64) - - this_max_size = global_idxs[-1] + 1 - if this_max_size > len(self.timestamp_buffer): - # reallocate - new_size = max(this_max_size, len(self.timestamp_buffer) * 2) - new_shape = (new_size,) + self.action_buffer.shape[1:] - self.action_buffer = np.resize(self.action_buffer, new_shape) - self.timestamp_buffer = np.resize(self.timestamp_buffer, (new_size,)) - - # potentially rewrite old data (as expected) - self.action_buffer[global_idxs] = actions[local_idxs] - self.timestamp_buffer[global_idxs] = timestamps[local_idxs] - self.size = max(self.size, this_max_size) diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/config/robot_dp.yaml b/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/config/robot_dp.yaml deleted file mode 100644 index 11aa2e5ca..000000000 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/config/robot_dp.yaml +++ /dev/null @@ -1,164 +0,0 @@ -defaults: - - _self_ - - task: default_task - -name: robot_${task.name} -_target_: diffusion_policy.workspace.robotworkspace.RobotWorkspace - -task_name: ${task.name} -shape_meta: ${task.shape_meta} -exp_name: "default" - -horizon: 8 -n_obs_steps: 3 -n_action_steps: 4 -n_latency_steps: 0 -dataset_obs_steps: ${n_obs_steps} -past_action_visible: False -keypoint_visible_rate: 1.0 -obs_as_global_cond: True - -policy_runner: - action: - action_type: "ee" - ee_cfg: - rotation_rep: "quaternion" - gripper_rep: "q_pos" - delta: False - obs: - obs_type: "ee" - ee_cfg: - rotation_rep: "quaternion" - gripper_rep: "q_pos" -policy: - _target_: diffusion_policy.policy.diffusion_unet_image_policy.DiffusionUnetImagePolicy - - shape_meta: ${shape_meta} - - noise_scheduler: - _target_: diffusers.schedulers.scheduling_ddpm.DDPMScheduler - num_train_timesteps: 100 - beta_start: 0.0001 - beta_end: 0.02 - beta_schedule: squaredcos_cap_v2 - variance_type: fixed_small # Yilun's paper uses fixed_small_log instead, but easy to cause Nan - clip_sample: True # required when predict_epsilon=False - prediction_type: epsilon # or sample - - obs_encoder: - _target_: diffusion_policy.model.vision.multi_image_obs_encoder.MultiImageObsEncoder - shape_meta: ${shape_meta} - rgb_model: - _target_: diffusion_policy.model.vision.model_getter.get_resnet - name: resnet18 - weights: null - resize_shape: null - crop_shape: null - # constant center crop - random_crop: True - use_group_norm: True - share_rgb_model: False - imagenet_norm: True - - horizon: ${horizon} - n_action_steps: ${eval:'${n_action_steps}+${n_latency_steps}'} - n_obs_steps: ${n_obs_steps} - num_inference_steps: 100 - obs_as_global_cond: ${obs_as_global_cond} - # crop_shape: null - diffusion_step_embed_dim: 128 - # down_dims: [512, 1024, 2048] - down_dims: [256, 512, 1024] - kernel_size: 5 - n_groups: 8 - cond_predict_scale: True - - # scheduler.step params - # predict_epsilon: True - -ema: - _target_: diffusion_policy.model.diffusion.ema_model.EMAModel - update_after_step: 0 - inv_gamma: 1.0 - power: 0.75 - min_value: 0.0 - max_value: 0.9999 - -dataloader: - batch_size: 32 - num_workers: 0 - shuffle: True - pin_memory: True - persistent_workers: False - -val_dataloader: - batch_size: 32 - num_workers: 0 - shuffle: False - pin_memory: True - persistent_workers: False - -optimizer: - _target_: torch.optim.AdamW - lr: 1.0e-4 - betas: [0.95, 0.999] - eps: 1.0e-8 - weight_decay: 1.0e-6 - -training: - device: "cuda:0" - seed: 42 - debug: False - resume: True - # optimization - lr_scheduler: cosine - lr_warmup_steps: 500 - num_epochs: 1000 - gradient_accumulate_every: 1 - # EMA destroys performance when used with BatchNorm - # replace BatchNorm with GroupNorm. - use_ema: True - freeze_encoder: False - # training loop control - # in epochs - rollout_every: 50 - checkpoint_every: 50 - val_every: 5 - sample_every: 5 - # steps per epoch - max_train_steps: 250 - max_val_steps: 250 - # misc - tqdm_interval_sec: 1.0 - -logging: - project: RoboVerse_DP - resume: True - mode: online - name: ${now:%Y.%m.%d-%H.%M.%S}_${task_name} - tags: ${now:%Y.%m.%d-%H.%M.%S}_${task_name} - id: null - group: null - -checkpoint: - topk: - monitor_key: test_mean_score - mode: max - k: 5 - format_str: 'epoch={epoch:04d}-test_mean_score={test_mean_score:.3f}.ckpt' - save_last_ckpt: True - save_last_snapshot: False - save_root_dir: info/outputs/DP/${now:%Y.%m.%d}/${now:%H.%M.%S}_${task_name} - -multi_run: - run_dir: info/outputs/DP/${now:%Y.%m.%d}/${now:%H.%M.%S}_${task_name} - wandb_name_base: ${now:%Y.%m.%d-%H.%M.%S}_${task_name} - -hydra: - job: - override_dirname: ${name} - run: - dir: info/outputs/DP/${now:%Y.%m.%d}/${now:%H.%M.%S}_${task_name} - sweep: - dir: info/outputs/DP/${now:%Y.%m.%d}/${now:%H.%M.%S}_${task_name} - subdir: ${hydra.job.num} diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/config/task/default_task.yaml b/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/config/task/default_task.yaml deleted file mode 100644 index 094cd81f7..000000000 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/config/task/default_task.yaml +++ /dev/null @@ -1,41 +0,0 @@ -name: task_config - -image_shape: &image_shape [3, 256, 256] -shape_meta: &shape_meta - # acceptable types: rgb, low_dim - obs: - head_cam: - shape: *image_shape - type: rgb - agent_pos: - shape: [9] - type: low_dim - action: - shape: [9] - -env_runner: - _target_: diffusion_policy.env_runner.pusht_image_runner.PushTImageRunner - n_train: 6 - n_train_vis: 2 - train_start_seed: 0 - n_test: 50 - n_test_vis: 4 - legacy_test: True - test_start_seed: 100000 - max_steps: 300 - n_obs_steps: ${n_obs_steps} - n_action_steps: ${n_action_steps} - fps: 10 - past_action: ${past_action_visible} - n_envs: null - -dataset: - _target_: diffusion_policy.dataset.robot_image_dataset.RobotImageDataset - zarr_path: diffusion_policy/data/useless.zarr - horizon: ${horizon} - pad_before: ${eval:'${n_obs_steps}-1'} - pad_after: ${eval:'${n_action_steps}-1'} - seed: 42 - val_ratio: 0.02 - batch_size: 32 - max_train_episodes: null diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/dataset/robot_image_dataset.py b/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/dataset/robot_image_dataset.py deleted file mode 100644 index d0de35ba2..000000000 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/dataset/robot_image_dataset.py +++ /dev/null @@ -1,177 +0,0 @@ -import copy -from typing import Dict - -import numba -import numpy as np -import torch -from diffusion_policy.common.normalize_util import get_image_range_normalizer -from diffusion_policy.common.pytorch_util import dict_apply -from diffusion_policy.common.replay_buffer import ReplayBuffer -from diffusion_policy.common.sampler import ( - SequenceSampler, - downsample_mask, - get_val_mask, -) -from diffusion_policy.dataset.base_dataset import BaseImageDataset -from diffusion_policy.model.common.normalizer import LinearNormalizer -from termcolor import cprint - - -class RobotImageDataset(BaseImageDataset): - def __init__( - self, - zarr_path, - horizon=1, - pad_before=0, - pad_after=0, - seed=42, - val_ratio=0.0, - batch_size=64, - max_train_episodes=None, - ): - - super().__init__() - # cprint(zarr_path, "red") - # cprint(batch_size, "red") - self.replay_buffer = ReplayBuffer.copy_from_path( - zarr_path, - # keys=['head_camera', 'front_camera', 'left_camera', 'right_camera', 'state', 'action'], - keys=["head_camera", "state", "action"], - ) - - val_mask = get_val_mask(n_episodes=self.replay_buffer.n_episodes, val_ratio=val_ratio, seed=seed) - train_mask = ~val_mask - train_mask = downsample_mask(mask=train_mask, max_n=max_train_episodes, seed=seed) - - self.sampler = SequenceSampler( - replay_buffer=self.replay_buffer, - sequence_length=horizon, - pad_before=pad_before, - pad_after=pad_after, - episode_mask=train_mask, - ) - self.train_mask = train_mask - self.horizon = horizon - self.pad_before = pad_before - self.pad_after = pad_after - - self.batch_size = batch_size - sequence_length = self.sampler.sequence_length - self.buffers = { - k: np.zeros((batch_size, sequence_length, *v.shape[1:]), dtype=v.dtype) - for k, v in self.sampler.replay_buffer.items() - } - self.buffers_torch = {k: torch.from_numpy(v) for k, v in self.buffers.items()} - for v in self.buffers_torch.values(): - v.pin_memory() - - def get_validation_dataset(self): - val_set = copy.copy(self) - val_set.sampler = SequenceSampler( - replay_buffer=self.replay_buffer, - sequence_length=self.horizon, - pad_before=self.pad_before, - pad_after=self.pad_after, - episode_mask=~self.train_mask, - ) - val_set.train_mask = ~self.train_mask - return val_set - - def get_normalizer(self, mode="limits", **kwargs): - data = { - "action": self.replay_buffer["action"], - "agent_pos": self.replay_buffer["state"], - } - normalizer = LinearNormalizer() - normalizer.fit(data=data, last_n_dims=1, mode=mode, **kwargs) - normalizer["head_cam"] = get_image_range_normalizer() - normalizer["front_cam"] = get_image_range_normalizer() - normalizer["left_cam"] = get_image_range_normalizer() - normalizer["right_cam"] = get_image_range_normalizer() - return normalizer - - def __len__(self) -> int: - return len(self.sampler) - - def _sample_to_data(self, sample): - agent_pos = sample["state"].astype(np.float32) # (agent_posx2, block_posex3) - head_cam = np.moveaxis(sample["head_camera"], -1, 1) / 255.0 - - data = { - "obs": { - "head_cam": head_cam, # T, 3, H, W - "agent_pos": agent_pos, # T, D - }, - "action": sample["action"].astype(np.float32), # T, D - } - return data - - def __getitem__(self, idx) -> Dict[str, torch.Tensor]: - if isinstance(idx, slice): - raise NotImplementedError # Specialized - elif isinstance(idx, int): - sample = self.sampler.sample_sequence(idx) - sample = dict_apply(sample, torch.from_numpy) - return sample - elif isinstance(idx, np.ndarray): - # print(idx, len(idx)) - # print(self.batch_size) - assert len(idx) == self.batch_size - for k, v in self.sampler.replay_buffer.items(): - batch_sample_sequence( - self.buffers[k], - v, - self.sampler.indices, - idx, - self.sampler.sequence_length, - ) - return self.buffers_torch - else: - raise ValueError(idx) - - def postprocess(self, samples, device): - agent_pos = samples["state"].to(device, non_blocking=True) - head_cam = samples["head_camera"].to(device, non_blocking=True) / 255.0 - action = samples["action"].to(device, non_blocking=True) - return { - "obs": { - "head_cam": head_cam, # B, T, 3, H, W - "agent_pos": agent_pos, # B, T, D - }, - "action": action, # B, T, D - } - - -def _batch_sample_sequence( - data: np.ndarray, - input_arr: np.ndarray, - indices: np.ndarray, - idx: np.ndarray, - sequence_length: int, -): - for i in numba.prange(len(idx)): - buffer_start_idx, buffer_end_idx, sample_start_idx, sample_end_idx = indices[idx[i]] - data[i, sample_start_idx:sample_end_idx] = input_arr[buffer_start_idx:buffer_end_idx] - if sample_start_idx > 0: - data[i, :sample_start_idx] = data[i, sample_start_idx] - if sample_end_idx < sequence_length: - data[i, sample_end_idx:] = data[i, sample_end_idx - 1] - - -_batch_sample_sequence_sequential = numba.jit(_batch_sample_sequence, nopython=True, parallel=False) -_batch_sample_sequence_parallel = numba.jit(_batch_sample_sequence, nopython=True, parallel=True) - - -def batch_sample_sequence( - data: np.ndarray, - input_arr: np.ndarray, - indices: np.ndarray, - idx: np.ndarray, - sequence_length: int, -): - batch_size = len(idx) - assert data.shape == (batch_size, sequence_length, *input_arr.shape[1:]) - if batch_size >= 16 and data.nbytes // batch_size >= 2**16: - _batch_sample_sequence_parallel(data, input_arr, indices, idx, sequence_length) - else: - _batch_sample_sequence_sequential(data, input_arr, indices, idx, sequence_length) diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/common/dict_of_tensor_mixin.py b/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/common/dict_of_tensor_mixin.py deleted file mode 100644 index 2ba358d2b..000000000 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/common/dict_of_tensor_mixin.py +++ /dev/null @@ -1,46 +0,0 @@ -import torch -import torch.nn as nn - - -class DictOfTensorMixin(nn.Module): - def __init__(self, params_dict=None): - super().__init__() - if params_dict is None: - params_dict = nn.ParameterDict() - self.params_dict = params_dict - - @property - def device(self): - return next(iter(self.parameters())).device - - def _load_from_state_dict( - self, - state_dict, - prefix, - local_metadata, - strict, - missing_keys, - unexpected_keys, - error_msgs, - ): - def dfs_add(dest, keys, value: torch.Tensor): - if len(keys) == 1: - dest[keys[0]] = value - return - - if keys[0] not in dest: - dest[keys[0]] = nn.ParameterDict() - dfs_add(dest[keys[0]], keys[1:], value) - - def load_dict(state_dict, prefix): - out_dict = nn.ParameterDict() - for key, value in state_dict.items(): - value: torch.Tensor - if key.startswith(prefix): - param_keys = key[len(prefix) :].split(".")[1:] - dfs_add(out_dict, param_keys, value.clone()) - return out_dict - - self.params_dict = load_dict(state_dict, prefix + "params_dict") - self.params_dict.requires_grad_(False) - return diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/common/lr_scheduler.py b/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/common/lr_scheduler.py deleted file mode 100644 index 1c653b3ba..000000000 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/common/lr_scheduler.py +++ /dev/null @@ -1,55 +0,0 @@ -from diffusers.optimization import ( - TYPE_TO_SCHEDULER_FUNCTION, - Optimizer, - Optional, - SchedulerType, - Union, -) - - -def get_scheduler( - name: Union[str, SchedulerType], - optimizer: Optimizer, - num_warmup_steps: Optional[int] = None, - num_training_steps: Optional[int] = None, - **kwargs, -): - """ - Added kwargs vs diffuser's original implementation - - Unified API to get any scheduler from its name. - - Args: - name (`str` or `SchedulerType`): - The name of the scheduler to use. - optimizer (`torch.optim.Optimizer`): - The optimizer that will be used during training. - num_warmup_steps (`int`, *optional*): - The number of warmup steps to do. This is not required by all schedulers (hence the argument being - optional), the function will raise an error if it's unset and the scheduler type requires it. - num_training_steps (`int``, *optional*): - The number of training steps to do. This is not required by all schedulers (hence the argument being - optional), the function will raise an error if it's unset and the scheduler type requires it. - """ - name = SchedulerType(name) - schedule_func = TYPE_TO_SCHEDULER_FUNCTION[name] - if name == SchedulerType.CONSTANT: - return schedule_func(optimizer, **kwargs) - - # All other schedulers require `num_warmup_steps` - if num_warmup_steps is None: - raise ValueError(f"{name} requires `num_warmup_steps`, please provide that argument.") - - if name == SchedulerType.CONSTANT_WITH_WARMUP: - return schedule_func(optimizer, num_warmup_steps=num_warmup_steps, **kwargs) - - # All other schedulers require `num_training_steps` - if num_training_steps is None: - raise ValueError(f"{name} requires `num_training_steps`, please provide that argument.") - - return schedule_func( - optimizer, - num_warmup_steps=num_warmup_steps, - num_training_steps=num_training_steps, - **kwargs, - ) diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/common/module_attr_mixin.py b/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/common/module_attr_mixin.py deleted file mode 100644 index 5d2cf4ea9..000000000 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/common/module_attr_mixin.py +++ /dev/null @@ -1,15 +0,0 @@ -import torch.nn as nn - - -class ModuleAttrMixin(nn.Module): - def __init__(self): - super().__init__() - self._dummy_variable = nn.Parameter() - - @property - def device(self): - return next(iter(self.parameters())).device - - @property - def dtype(self): - return next(iter(self.parameters())).dtype diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/common/rotation_transformer.py b/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/common/rotation_transformer.py deleted file mode 100644 index 24ed1015c..000000000 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/common/rotation_transformer.py +++ /dev/null @@ -1,98 +0,0 @@ -import functools -from typing import Union - -import numpy as np -import pytorch3d.transforms as pt -import torch - - -class RotationTransformer: - valid_reps = ["axis_angle", "euler_angles", "quaternion", "rotation_6d", "matrix"] - - def __init__( - self, - from_rep="axis_angle", - to_rep="rotation_6d", - from_convention=None, - to_convention=None, - ): - """ - Valid representations - - Always use matrix as intermediate representation. - """ - assert from_rep != to_rep - assert from_rep in self.valid_reps - assert to_rep in self.valid_reps - if from_rep == "euler_angles": - assert from_convention is not None - if to_rep == "euler_angles": - assert to_convention is not None - - forward_funcs = list() - inverse_funcs = list() - - if from_rep != "matrix": - funcs = [ - getattr(pt, f"{from_rep}_to_matrix"), - getattr(pt, f"matrix_to_{from_rep}"), - ] - if from_convention is not None: - funcs = [functools.partial(func, convention=from_convention) for func in funcs] - forward_funcs.append(funcs[0]) - inverse_funcs.append(funcs[1]) - - if to_rep != "matrix": - funcs = [ - getattr(pt, f"matrix_to_{to_rep}"), - getattr(pt, f"{to_rep}_to_matrix"), - ] - if to_convention is not None: - funcs = [functools.partial(func, convention=to_convention) for func in funcs] - forward_funcs.append(funcs[0]) - inverse_funcs.append(funcs[1]) - - inverse_funcs = inverse_funcs[::-1] - - self.forward_funcs = forward_funcs - self.inverse_funcs = inverse_funcs - - @staticmethod - def _apply_funcs(x: Union[np.ndarray, torch.Tensor], funcs: list) -> Union[np.ndarray, torch.Tensor]: - x_ = x - if isinstance(x, np.ndarray): - x_ = torch.from_numpy(x) - x_: torch.Tensor - for func in funcs: - x_ = func(x_) - y = x_ - if isinstance(x, np.ndarray): - y = x_.numpy() - return y - - def forward(self, x: Union[np.ndarray, torch.Tensor]) -> Union[np.ndarray, torch.Tensor]: - return self._apply_funcs(x, self.forward_funcs) - - def inverse(self, x: Union[np.ndarray, torch.Tensor]) -> Union[np.ndarray, torch.Tensor]: - return self._apply_funcs(x, self.inverse_funcs) - - -def test(): - tf = RotationTransformer() - - rotvec = np.random.uniform(-2 * np.pi, 2 * np.pi, size=(1000, 3)) - rot6d = tf.forward(rotvec) - new_rotvec = tf.inverse(rot6d) - - from scipy.spatial.transform import Rotation - - diff = Rotation.from_rotvec(rotvec) * Rotation.from_rotvec(new_rotvec).inv() - dist = diff.magnitude() - assert dist.max() < 1e-7 - - tf = RotationTransformer("rotation_6d", "matrix") - rot6d_wrong = rot6d + np.random.normal(scale=0.1, size=rot6d.shape) - mat = tf.forward(rot6d_wrong) - mat_det = np.linalg.det(mat) - assert np.allclose(mat_det, 1) - # rotaiton_6d will be normalized to rotation matrix diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/common/shape_util.py b/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/common/shape_util.py deleted file mode 100644 index eec4b66e0..000000000 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/common/shape_util.py +++ /dev/null @@ -1,23 +0,0 @@ -from typing import Callable, Dict, List, Tuple - -import torch -import torch.nn as nn - - -def get_module_device(m: nn.Module): - device = torch.device("cpu") - try: - param = next(iter(m.parameters())) - device = param.device - except StopIteration: - pass - return device - - -@torch.no_grad() -def get_output_shape(input_shape: Tuple[int], net: Callable[[torch.Tensor], torch.Tensor]): - device = get_module_device(net) - test_input = torch.zeros((1,) + tuple(input_shape), device=device) - test_output = net(test_input) - output_shape = tuple(test_output.shape[1:]) - return output_shape diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/common/tensor_util.py b/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/common/tensor_util.py deleted file mode 100644 index 4cb7b9ac8..000000000 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/common/tensor_util.py +++ /dev/null @@ -1,973 +0,0 @@ -""" -A collection of utilities for working with nested tensor structures consisting -of numpy arrays and torch tensors. -""" - -import collections - -import numpy as np -import torch - - -def recursive_dict_list_tuple_apply(x, type_func_dict): - """ - Recursively apply functions to a nested dictionary or list or tuple, given a dictionary of - {data_type: function_to_apply}. - - Args: - x (dict or list or tuple): a possibly nested dictionary or list or tuple - type_func_dict (dict): a mapping from data types to the functions to be - applied for each data type. - - Returns: - y (dict or list or tuple): new nested dict-list-tuple - """ - assert list not in type_func_dict - assert tuple not in type_func_dict - assert dict not in type_func_dict - - if isinstance(x, (dict, collections.OrderedDict)): - new_x = collections.OrderedDict() if isinstance(x, collections.OrderedDict) else dict() - for k, v in x.items(): - new_x[k] = recursive_dict_list_tuple_apply(v, type_func_dict) - return new_x - elif isinstance(x, (list, tuple)): - ret = [recursive_dict_list_tuple_apply(v, type_func_dict) for v in x] - if isinstance(x, tuple): - ret = tuple(ret) - return ret - else: - for t, f in type_func_dict.items(): - if isinstance(x, t): - return f(x) - else: - raise NotImplementedError("Cannot handle data type %s" % str(type(x))) - - -def map_tensor(x, func): - """ - Apply function @func to torch.Tensor objects in a nested dictionary or - list or tuple. - - Args: - x (dict or list or tuple): a possibly nested dictionary or list or tuple - func (function): function to apply to each tensor - - Returns: - y (dict or list or tuple): new nested dict-list-tuple - """ - return recursive_dict_list_tuple_apply( - x, - { - torch.Tensor: func, - type(None): lambda x: x, - }, - ) - - -def map_ndarray(x, func): - """ - Apply function @func to np.ndarray objects in a nested dictionary or - list or tuple. - - Args: - x (dict or list or tuple): a possibly nested dictionary or list or tuple - func (function): function to apply to each array - - Returns: - y (dict or list or tuple): new nested dict-list-tuple - """ - return recursive_dict_list_tuple_apply( - x, - { - np.ndarray: func, - type(None): lambda x: x, - }, - ) - - -def map_tensor_ndarray(x, tensor_func, ndarray_func): - """ - Apply function @tensor_func to torch.Tensor objects and @ndarray_func to - np.ndarray objects in a nested dictionary or list or tuple. - - Args: - x (dict or list or tuple): a possibly nested dictionary or list or tuple - tensor_func (function): function to apply to each tensor - ndarray_Func (function): function to apply to each array - - Returns: - y (dict or list or tuple): new nested dict-list-tuple - """ - return recursive_dict_list_tuple_apply( - x, - { - torch.Tensor: tensor_func, - np.ndarray: ndarray_func, - type(None): lambda x: x, - }, - ) - - -def clone(x): - """ - Clones all torch tensors and numpy arrays in nested dictionary or list - or tuple and returns a new nested structure. - - Args: - x (dict or list or tuple): a possibly nested dictionary or list or tuple - - Returns: - y (dict or list or tuple): new nested dict-list-tuple - """ - return recursive_dict_list_tuple_apply( - x, - { - torch.Tensor: lambda x: x.clone(), - np.ndarray: lambda x: x.copy(), - type(None): lambda x: x, - }, - ) - - -def detach(x): - """ - Detaches all torch tensors in nested dictionary or list - or tuple and returns a new nested structure. - - Args: - x (dict or list or tuple): a possibly nested dictionary or list or tuple - - Returns: - y (dict or list or tuple): new nested dict-list-tuple - """ - return recursive_dict_list_tuple_apply( - x, - { - torch.Tensor: lambda x: x.detach(), - }, - ) - - -def to_batch(x): - """ - Introduces a leading batch dimension of 1 for all torch tensors and numpy - arrays in nested dictionary or list or tuple and returns a new nested structure. - - Args: - x (dict or list or tuple): a possibly nested dictionary or list or tuple - - Returns: - y (dict or list or tuple): new nested dict-list-tuple - """ - return recursive_dict_list_tuple_apply( - x, - { - torch.Tensor: lambda x: x[None, ...], - np.ndarray: lambda x: x[None, ...], - type(None): lambda x: x, - }, - ) - - -def to_sequence(x): - """ - Introduces a time dimension of 1 at dimension 1 for all torch tensors and numpy - arrays in nested dictionary or list or tuple and returns a new nested structure. - - Args: - x (dict or list or tuple): a possibly nested dictionary or list or tuple - - Returns: - y (dict or list or tuple): new nested dict-list-tuple - """ - return recursive_dict_list_tuple_apply( - x, - { - torch.Tensor: lambda x: x[:, None, ...], - np.ndarray: lambda x: x[:, None, ...], - type(None): lambda x: x, - }, - ) - - -def index_at_time(x, ind): - """ - Indexes all torch tensors and numpy arrays in dimension 1 with index @ind in - nested dictionary or list or tuple and returns a new nested structure. - - Args: - x (dict or list or tuple): a possibly nested dictionary or list or tuple - ind (int): index - - Returns: - y (dict or list or tuple): new nested dict-list-tuple - """ - return recursive_dict_list_tuple_apply( - x, - { - torch.Tensor: lambda x: x[:, ind, ...], - np.ndarray: lambda x: x[:, ind, ...], - type(None): lambda x: x, - }, - ) - - -def unsqueeze(x, dim): - """ - Adds dimension of size 1 at dimension @dim in all torch tensors and numpy arrays - in nested dictionary or list or tuple and returns a new nested structure. - - Args: - x (dict or list or tuple): a possibly nested dictionary or list or tuple - dim (int): dimension - - Returns: - y (dict or list or tuple): new nested dict-list-tuple - """ - return recursive_dict_list_tuple_apply( - x, - { - torch.Tensor: lambda x: x.unsqueeze(dim=dim), - np.ndarray: lambda x: np.expand_dims(x, axis=dim), - type(None): lambda x: x, - }, - ) - - -def contiguous(x): - """ - Makes all torch tensors and numpy arrays contiguous in nested dictionary or - list or tuple and returns a new nested structure. - - Args: - x (dict or list or tuple): a possibly nested dictionary or list or tuple - - Returns: - y (dict or list or tuple): new nested dict-list-tuple - """ - return recursive_dict_list_tuple_apply( - x, - { - torch.Tensor: lambda x: x.contiguous(), - np.ndarray: lambda x: np.ascontiguousarray(x), - type(None): lambda x: x, - }, - ) - - -def to_device(x, device): - """ - Sends all torch tensors in nested dictionary or list or tuple to device - @device, and returns a new nested structure. - - Args: - x (dict or list or tuple): a possibly nested dictionary or list or tuple - device (torch.Device): device to send tensors to - - Returns: - y (dict or list or tuple): new nested dict-list-tuple - """ - return recursive_dict_list_tuple_apply( - x, - { - torch.Tensor: lambda x, d=device: x.to(d), - type(None): lambda x: x, - }, - ) - - -def to_tensor(x): - """ - Converts all numpy arrays in nested dictionary or list or tuple to - torch tensors (and leaves existing torch Tensors as-is), and returns - a new nested structure. - - Args: - x (dict or list or tuple): a possibly nested dictionary or list or tuple - - Returns: - y (dict or list or tuple): new nested dict-list-tuple - """ - return recursive_dict_list_tuple_apply( - x, - { - torch.Tensor: lambda x: x, - np.ndarray: lambda x: torch.from_numpy(x), - type(None): lambda x: x, - }, - ) - - -def to_numpy(x): - """ - Converts all torch tensors in nested dictionary or list or tuple to - numpy (and leaves existing numpy arrays as-is), and returns - a new nested structure. - - Args: - x (dict or list or tuple): a possibly nested dictionary or list or tuple - - Returns: - y (dict or list or tuple): new nested dict-list-tuple - """ - - def f(tensor): - if tensor.is_cuda: - return tensor.detach().cpu().numpy() - else: - return tensor.detach().numpy() - - return recursive_dict_list_tuple_apply( - x, - { - torch.Tensor: f, - np.ndarray: lambda x: x, - type(None): lambda x: x, - }, - ) - - -def to_list(x): - """ - Converts all torch tensors and numpy arrays in nested dictionary or list - or tuple to a list, and returns a new nested structure. Useful for - json encoding. - - Args: - x (dict or list or tuple): a possibly nested dictionary or list or tuple - - Returns: - y (dict or list or tuple): new nested dict-list-tuple - """ - - def f(tensor): - if tensor.is_cuda: - return tensor.detach().cpu().numpy().tolist() - else: - return tensor.detach().numpy().tolist() - - return recursive_dict_list_tuple_apply( - x, - { - torch.Tensor: f, - np.ndarray: lambda x: x.tolist(), - type(None): lambda x: x, - }, - ) - - -def to_float(x): - """ - Converts all torch tensors and numpy arrays in nested dictionary or list - or tuple to float type entries, and returns a new nested structure. - - Args: - x (dict or list or tuple): a possibly nested dictionary or list or tuple - - Returns: - y (dict or list or tuple): new nested dict-list-tuple - """ - return recursive_dict_list_tuple_apply( - x, - { - torch.Tensor: lambda x: x.float(), - np.ndarray: lambda x: x.astype(np.float32), - type(None): lambda x: x, - }, - ) - - -def to_uint8(x): - """ - Converts all torch tensors and numpy arrays in nested dictionary or list - or tuple to uint8 type entries, and returns a new nested structure. - - Args: - x (dict or list or tuple): a possibly nested dictionary or list or tuple - - Returns: - y (dict or list or tuple): new nested dict-list-tuple - """ - return recursive_dict_list_tuple_apply( - x, - { - torch.Tensor: lambda x: x.byte(), - np.ndarray: lambda x: x.astype(np.uint8), - type(None): lambda x: x, - }, - ) - - -def to_torch(x, device): - """ - Converts all numpy arrays and torch tensors in nested dictionary or list or tuple to - torch tensors on device @device and returns a new nested structure. - - Args: - x (dict or list or tuple): a possibly nested dictionary or list or tuple - device (torch.Device): device to send tensors to - - Returns: - y (dict or list or tuple): new nested dict-list-tuple - """ - return to_device(to_float(to_tensor(x)), device) - - -def to_one_hot_single(tensor, num_class): - """ - Convert tensor to one-hot representation, assuming a certain number of total class labels. - - Args: - tensor (torch.Tensor): tensor containing integer labels - num_class (int): number of classes - - Returns: - x (torch.Tensor): tensor containing one-hot representation of labels - """ - x = torch.zeros(tensor.size() + (num_class,)).to(tensor.device) - x.scatter_(-1, tensor.unsqueeze(-1), 1) - return x - - -def to_one_hot(tensor, num_class): - """ - Convert all tensors in nested dictionary or list or tuple to one-hot representation, - assuming a certain number of total class labels. - - Args: - tensor (dict or list or tuple): a possibly nested dictionary or list or tuple - num_class (int): number of classes - - Returns: - y (dict or list or tuple): new nested dict-list-tuple - """ - return map_tensor(tensor, func=lambda x, nc=num_class: to_one_hot_single(x, nc)) - - -def flatten_single(x, begin_axis=1): - """ - Flatten a tensor in all dimensions from @begin_axis onwards. - - Args: - x (torch.Tensor): tensor to flatten - begin_axis (int): which axis to flatten from - - Returns: - y (torch.Tensor): flattened tensor - """ - fixed_size = x.size()[:begin_axis] - _s = list(fixed_size) + [-1] - return x.reshape(*_s) - - -def flatten(x, begin_axis=1): - """ - Flatten all tensors in nested dictionary or list or tuple, from @begin_axis onwards. - - Args: - x (dict or list or tuple): a possibly nested dictionary or list or tuple - begin_axis (int): which axis to flatten from - - Returns: - y (dict or list or tuple): new nested dict-list-tuple - """ - return recursive_dict_list_tuple_apply( - x, - { - torch.Tensor: lambda x, b=begin_axis: flatten_single(x, begin_axis=b), - }, - ) - - -def reshape_dimensions_single(x, begin_axis, end_axis, target_dims): - """ - Reshape selected dimensions in a tensor to a target dimension. - - Args: - x (torch.Tensor): tensor to reshape - begin_axis (int): begin dimension - end_axis (int): end dimension - target_dims (tuple or list): target shape for the range of dimensions - (@begin_axis, @end_axis) - - Returns: - y (torch.Tensor): reshaped tensor - """ - assert begin_axis <= end_axis - assert begin_axis >= 0 - assert end_axis < len(x.shape) - assert isinstance(target_dims, (tuple, list)) - s = x.shape - final_s = [] - for i in range(len(s)): - if i == begin_axis: - final_s.extend(target_dims) - elif i < begin_axis or i > end_axis: - final_s.append(s[i]) - return x.reshape(*final_s) - - -def reshape_dimensions(x, begin_axis, end_axis, target_dims): - """ - Reshape selected dimensions for all tensors in nested dictionary or list or tuple - to a target dimension. - - Args: - x (dict or list or tuple): a possibly nested dictionary or list or tuple - begin_axis (int): begin dimension - end_axis (int): end dimension - target_dims (tuple or list): target shape for the range of dimensions - (@begin_axis, @end_axis) - - Returns: - y (dict or list or tuple): new nested dict-list-tuple - """ - return recursive_dict_list_tuple_apply( - x, - { - torch.Tensor: lambda x, b=begin_axis, e=end_axis, t=target_dims: reshape_dimensions_single( - x, begin_axis=b, end_axis=e, target_dims=t - ), - np.ndarray: lambda x, b=begin_axis, e=end_axis, t=target_dims: reshape_dimensions_single( - x, begin_axis=b, end_axis=e, target_dims=t - ), - type(None): lambda x: x, - }, - ) - - -def join_dimensions(x, begin_axis, end_axis): - """ - Joins all dimensions between dimensions (@begin_axis, @end_axis) into a flat dimension, for - all tensors in nested dictionary or list or tuple. - - Args: - x (dict or list or tuple): a possibly nested dictionary or list or tuple - begin_axis (int): begin dimension - end_axis (int): end dimension - - Returns: - y (dict or list or tuple): new nested dict-list-tuple - """ - return recursive_dict_list_tuple_apply( - x, - { - torch.Tensor: lambda x, b=begin_axis, e=end_axis: reshape_dimensions_single( - x, begin_axis=b, end_axis=e, target_dims=[-1] - ), - np.ndarray: lambda x, b=begin_axis, e=end_axis: reshape_dimensions_single( - x, begin_axis=b, end_axis=e, target_dims=[-1] - ), - type(None): lambda x: x, - }, - ) - - -def expand_at_single(x, size, dim): - """ - Expand a tensor at a single dimension @dim by @size - - Args: - x (torch.Tensor): input tensor - size (int): size to expand - dim (int): dimension to expand - - Returns: - y (torch.Tensor): expanded tensor - """ - assert dim < x.ndimension() - assert x.shape[dim] == 1 - expand_dims = [-1] * x.ndimension() - expand_dims[dim] = size - return x.expand(*expand_dims) - - -def expand_at(x, size, dim): - """ - Expand all tensors in nested dictionary or list or tuple at a single - dimension @dim by @size. - - Args: - x (dict or list or tuple): a possibly nested dictionary or list or tuple - size (int): size to expand - dim (int): dimension to expand - - Returns: - y (dict or list or tuple): new nested dict-list-tuple - """ - return map_tensor(x, lambda t, s=size, d=dim: expand_at_single(t, s, d)) - - -def unsqueeze_expand_at(x, size, dim): - """ - Unsqueeze and expand a tensor at a dimension @dim by @size. - - Args: - x (dict or list or tuple): a possibly nested dictionary or list or tuple - size (int): size to expand - dim (int): dimension to unsqueeze and expand - - Returns: - y (dict or list or tuple): new nested dict-list-tuple - """ - x = unsqueeze(x, dim) - return expand_at(x, size, dim) - - -def repeat_by_expand_at(x, repeats, dim): - """ - Repeat a dimension by combining expand and reshape operations. - - Args: - x (dict or list or tuple): a possibly nested dictionary or list or tuple - repeats (int): number of times to repeat the target dimension - dim (int): dimension to repeat on - - Returns: - y (dict or list or tuple): new nested dict-list-tuple - """ - x = unsqueeze_expand_at(x, repeats, dim + 1) - return join_dimensions(x, dim, dim + 1) - - -def named_reduce_single(x, reduction, dim): - """ - Reduce tensor at a dimension by named reduction functions. - - Args: - x (torch.Tensor): tensor to be reduced - reduction (str): one of ["sum", "max", "mean", "flatten"] - dim (int): dimension to be reduced (or begin axis for flatten) - - Returns: - y (torch.Tensor): reduced tensor - """ - assert x.ndimension() > dim - assert reduction in ["sum", "max", "mean", "flatten"] - if reduction == "flatten": - x = flatten(x, begin_axis=dim) - elif reduction == "max": - x = torch.max(x, dim=dim)[0] # [B, D] - elif reduction == "sum": - x = torch.sum(x, dim=dim) - else: - x = torch.mean(x, dim=dim) - return x - - -def named_reduce(x, reduction, dim): - """ - Reduces all tensors in nested dictionary or list or tuple at a dimension - using a named reduction function. - - Args: - x (dict or list or tuple): a possibly nested dictionary or list or tuple - reduction (str): one of ["sum", "max", "mean", "flatten"] - dim (int): dimension to be reduced (or begin axis for flatten) - - Returns: - y (dict or list or tuple): new nested dict-list-tuple - """ - return map_tensor(x, func=lambda t, r=reduction, d=dim: named_reduce_single(t, r, d)) - - -def gather_along_dim_with_dim_single(x, target_dim, source_dim, indices): - """ - This function indexes out a target dimension of a tensor in a structured way, - by allowing a different value to be selected for each member of a flat index - tensor (@indices) corresponding to a source dimension. This can be interpreted - as moving along the source dimension, using the corresponding index value - in @indices to select values for all other dimensions outside of the - source and target dimensions. A common use case is to gather values - in target dimension 1 for each batch member (target dimension 0). - - Args: - x (torch.Tensor): tensor to gather values for - target_dim (int): dimension to gather values along - source_dim (int): dimension to hold constant and use for gathering values - from the other dimensions - indices (torch.Tensor): flat index tensor with same shape as tensor @x along - @source_dim - - Returns: - y (torch.Tensor): gathered tensor, with dimension @target_dim indexed out - """ - assert len(indices.shape) == 1 - assert x.shape[source_dim] == indices.shape[0] - - # unsqueeze in all dimensions except the source dimension - new_shape = [1] * x.ndimension() - new_shape[source_dim] = -1 - indices = indices.reshape(*new_shape) - - # repeat in all dimensions - but preserve shape of source dimension, - # and make sure target_dimension has singleton dimension - expand_shape = list(x.shape) - expand_shape[source_dim] = -1 - expand_shape[target_dim] = 1 - indices = indices.expand(*expand_shape) - - out = x.gather(dim=target_dim, index=indices) - return out.squeeze(target_dim) - - -def gather_along_dim_with_dim(x, target_dim, source_dim, indices): - """ - Apply @gather_along_dim_with_dim_single to all tensors in a nested - dictionary or list or tuple. - - Args: - x (dict or list or tuple): a possibly nested dictionary or list or tuple - target_dim (int): dimension to gather values along - source_dim (int): dimension to hold constant and use for gathering values - from the other dimensions - indices (torch.Tensor): flat index tensor with same shape as tensor @x along - @source_dim - - Returns: - y (dict or list or tuple): new nested dict-list-tuple - """ - return map_tensor( - x, - lambda y, t=target_dim, s=source_dim, i=indices: gather_along_dim_with_dim_single(y, t, s, i), - ) - - -def gather_sequence_single(seq, indices): - """ - Given a tensor with leading dimensions [B, T, ...], gather an element from each sequence in - the batch given an index for each sequence. - - Args: - seq (torch.Tensor): tensor with leading dimensions [B, T, ...] - indices (torch.Tensor): tensor indices of shape [B] - - Return: - y (torch.Tensor): indexed tensor of shape [B, ....] - """ - return gather_along_dim_with_dim_single(seq, target_dim=1, source_dim=0, indices=indices) - - -def gather_sequence(seq, indices): - """ - Given a nested dictionary or list or tuple, gathers an element from each sequence of the batch - for tensors with leading dimensions [B, T, ...]. - - Args: - seq (dict or list or tuple): a possibly nested dictionary or list or tuple with tensors - of leading dimensions [B, T, ...] - indices (torch.Tensor): tensor indices of shape [B] - - Returns: - y (dict or list or tuple): new nested dict-list-tuple with tensors of shape [B, ...] - """ - return gather_along_dim_with_dim(seq, target_dim=1, source_dim=0, indices=indices) - - -def pad_sequence_single(seq, padding, batched=False, pad_same=True, pad_values=None): - """ - Pad input tensor or array @seq in the time dimension (dimension 1). - - Args: - seq (np.ndarray or torch.Tensor): sequence to be padded - padding (tuple): begin and end padding, e.g. [1, 1] pads both begin and end of the sequence by 1 - batched (bool): if sequence has the batch dimension - pad_same (bool): if pad by duplicating - pad_values (scalar or (ndarray, Tensor)): values to be padded if not pad_same - - Returns: - padded sequence (np.ndarray or torch.Tensor) - """ - assert isinstance(seq, (np.ndarray, torch.Tensor)) - assert pad_same or pad_values is not None - if pad_values is not None: - assert isinstance(pad_values, float) - repeat_func = np.repeat if isinstance(seq, np.ndarray) else torch.repeat_interleave - concat_func = np.concatenate if isinstance(seq, np.ndarray) else torch.cat - ones_like_func = np.ones_like if isinstance(seq, np.ndarray) else torch.ones_like - seq_dim = 1 if batched else 0 - - begin_pad = [] - end_pad = [] - - if padding[0] > 0: - pad = seq[[0]] if pad_same else ones_like_func(seq[[0]]) * pad_values - begin_pad.append(repeat_func(pad, padding[0], seq_dim)) - if padding[1] > 0: - pad = seq[[-1]] if pad_same else ones_like_func(seq[[-1]]) * pad_values - end_pad.append(repeat_func(pad, padding[1], seq_dim)) - - return concat_func(begin_pad + [seq] + end_pad, seq_dim) - - -def pad_sequence(seq, padding, batched=False, pad_same=True, pad_values=None): - """ - Pad a nested dictionary or list or tuple of sequence tensors in the time dimension (dimension 1). - - Args: - seq (dict or list or tuple): a possibly nested dictionary or list or tuple with tensors - of leading dimensions [B, T, ...] - padding (tuple): begin and end padding, e.g. [1, 1] pads both begin and end of the sequence by 1 - batched (bool): if sequence has the batch dimension - pad_same (bool): if pad by duplicating - pad_values (scalar or (ndarray, Tensor)): values to be padded if not pad_same - - Returns: - padded sequence (dict or list or tuple) - """ - return recursive_dict_list_tuple_apply( - seq, - { - torch.Tensor: lambda x, p=padding, b=batched, ps=pad_same, pv=pad_values: pad_sequence_single( - x, p, b, ps, pv - ), - np.ndarray: lambda x, p=padding, b=batched, ps=pad_same, pv=pad_values: pad_sequence_single( - x, p, b, ps, pv - ), - type(None): lambda x: x, - }, - ) - - -def assert_size_at_dim_single(x, size, dim, msg): - """ - Ensure that array or tensor @x has size @size in dim @dim. - - Args: - x (np.ndarray or torch.Tensor): input array or tensor - size (int): size that tensors should have at @dim - dim (int): dimension to check - msg (str): text to display if assertion fails - """ - assert x.shape[dim] == size, msg - - -def assert_size_at_dim(x, size, dim, msg): - """ - Ensure that arrays and tensors in nested dictionary or list or tuple have - size @size in dim @dim. - - Args: - x (dict or list or tuple): a possibly nested dictionary or list or tuple - size (int): size that tensors should have at @dim - dim (int): dimension to check - """ - map_tensor(x, lambda t, s=size, d=dim, m=msg: assert_size_at_dim_single(t, s, d, m)) - - -def get_shape(x): - """ - Get all shapes of arrays and tensors in nested dictionary or list or tuple. - - Args: - x (dict or list or tuple): a possibly nested dictionary or list or tuple - - Returns: - y (dict or list or tuple): new nested dict-list-tuple that contains each array or - tensor's shape - """ - return recursive_dict_list_tuple_apply( - x, - { - torch.Tensor: lambda x: x.shape, - np.ndarray: lambda x: x.shape, - type(None): lambda x: x, - }, - ) - - -def list_of_flat_dict_to_dict_of_list(list_of_dict): - """ - Helper function to go from a list of flat dictionaries to a dictionary of lists. - By "flat" we mean that none of the values are dictionaries, but are numpy arrays, - floats, etc. - - Args: - list_of_dict (list): list of flat dictionaries - - Returns: - dict_of_list (dict): dictionary of lists - """ - assert isinstance(list_of_dict, list) - dic = collections.OrderedDict() - for i in range(len(list_of_dict)): - for k in list_of_dict[i]: - if k not in dic: - dic[k] = [] - dic[k].append(list_of_dict[i][k]) - return dic - - -def flatten_nested_dict_list(d, parent_key="", sep="_", item_key=""): - """ - Flatten a nested dict or list to a list. - - For example, given a dict - { - a: 1 - b: { - c: 2 - } - c: 3 - } - - the function would return [(a, 1), (b_c, 2), (c, 3)] - - Args: - d (dict, list): a nested dict or list to be flattened - parent_key (str): recursion helper - sep (str): separator for nesting keys - item_key (str): recursion helper - Returns: - list: a list of (key, value) tuples - """ - items = [] - if isinstance(d, (tuple, list)): - new_key = parent_key + sep + item_key if len(parent_key) > 0 else item_key - for i, v in enumerate(d): - items.extend(flatten_nested_dict_list(v, new_key, sep=sep, item_key=str(i))) - return items - elif isinstance(d, dict): - new_key = parent_key + sep + item_key if len(parent_key) > 0 else item_key - for k, v in d.items(): - assert isinstance(k, str) - items.extend(flatten_nested_dict_list(v, new_key, sep=sep, item_key=k)) - return items - else: - new_key = parent_key + sep + item_key if len(parent_key) > 0 else item_key - return [(new_key, d)] - - -def time_distributed(inputs, op, activation=None, inputs_as_kwargs=False, inputs_as_args=False, **kwargs): - """ - Apply function @op to all tensors in nested dictionary or list or tuple @inputs in both the - batch (B) and time (T) dimension, where the tensors are expected to have shape [B, T, ...]. - Will do this by reshaping tensors to [B * T, ...], passing through the op, and then reshaping - outputs to [B, T, ...]. - - Args: - inputs (list or tuple or dict): a possibly nested dictionary or list or tuple with tensors - of leading dimensions [B, T, ...] - op: a layer op that accepts inputs - activation: activation to apply at the output - inputs_as_kwargs (bool): whether to feed input as a kwargs dict to the op - inputs_as_args (bool) whether to feed input as a args list to the op - kwargs (dict): other kwargs to supply to the op - - Returns: - outputs (dict or list or tuple): new nested dict-list-tuple with tensors of leading dimension [B, T]. - """ - batch_size, seq_len = flatten_nested_dict_list(inputs)[0][1].shape[:2] - inputs = join_dimensions(inputs, 0, 1) - if inputs_as_kwargs: - outputs = op(**inputs, **kwargs) - elif inputs_as_args: - outputs = op(*inputs, **kwargs) - else: - outputs = op(inputs, **kwargs) - - if activation is not None: - outputs = map_tensor(outputs, activation) - outputs = reshape_dimensions(outputs, begin_axis=0, end_axis=0, target_dims=(batch_size, seq_len)) - return outputs diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/policy/diffusion_unet_image_policy.py b/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/policy/diffusion_unet_image_policy.py deleted file mode 100644 index d763e45b7..000000000 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/policy/diffusion_unet_image_policy.py +++ /dev/null @@ -1,262 +0,0 @@ -from typing import Dict - -import torch -import torch.nn as nn -import torch.nn.functional as F -from diffusers.schedulers.scheduling_ddpm import DDPMScheduler -from diffusion_policy.common.pytorch_util import dict_apply -from diffusion_policy.model.common.normalizer import LinearNormalizer -from diffusion_policy.model.diffusion.conditional_unet1d import ConditionalUnet1D -from diffusion_policy.model.diffusion.mask_generator import LowdimMaskGenerator -from diffusion_policy.model.vision.multi_image_obs_encoder import MultiImageObsEncoder -from diffusion_policy.policy.base_image_policy import BaseImagePolicy -from einops import rearrange, reduce - - -class DiffusionUnetImagePolicy(BaseImagePolicy): - def __init__( - self, - shape_meta: dict, - noise_scheduler: DDPMScheduler, - obs_encoder: MultiImageObsEncoder, - horizon, - n_action_steps, - n_obs_steps, - num_inference_steps=None, - obs_as_global_cond=True, - diffusion_step_embed_dim=256, - down_dims=(256, 512, 1024), - kernel_size=5, - n_groups=8, - cond_predict_scale=True, - # parameters passed to step - **kwargs, - ): - super().__init__() - - # parse shapes - action_shape = shape_meta["action"]["shape"] - assert len(action_shape) == 1 - action_dim = action_shape[0] - # get feature dim - obs_feature_dim = obs_encoder.output_shape()[0] - - # create diffusion model - input_dim = action_dim + obs_feature_dim - global_cond_dim = None - if obs_as_global_cond: - input_dim = action_dim - global_cond_dim = obs_feature_dim * n_obs_steps - - model = ConditionalUnet1D( - input_dim=input_dim, - local_cond_dim=None, - global_cond_dim=global_cond_dim, - diffusion_step_embed_dim=diffusion_step_embed_dim, - down_dims=down_dims, - kernel_size=kernel_size, - n_groups=n_groups, - cond_predict_scale=cond_predict_scale, - ) - - self.obs_encoder = obs_encoder - self.model = model - self.noise_scheduler = noise_scheduler - self.mask_generator = LowdimMaskGenerator( - action_dim=action_dim, - obs_dim=0 if obs_as_global_cond else obs_feature_dim, - max_n_obs_steps=n_obs_steps, - fix_obs_steps=True, - action_visible=False, - ) - self.normalizer = LinearNormalizer() - self.horizon = horizon - self.obs_feature_dim = obs_feature_dim - self.action_dim = action_dim - self.n_action_steps = n_action_steps - self.n_obs_steps = n_obs_steps - self.obs_as_global_cond = obs_as_global_cond - self.kwargs = kwargs - - if num_inference_steps is None: - num_inference_steps = noise_scheduler.config.num_train_timesteps - self.num_inference_steps = num_inference_steps - - # ========= inference ============ - def conditional_sample( - self, - condition_data, - condition_mask, - local_cond=None, - global_cond=None, - generator=None, - # keyword arguments to scheduler.step - **kwargs, - ): - model = self.model - scheduler = self.noise_scheduler - - trajectory = torch.randn( - size=condition_data.shape, - dtype=condition_data.dtype, - device=condition_data.device, - generator=generator, - ) - - # set step values - scheduler.set_timesteps(self.num_inference_steps) - - for t in scheduler.timesteps: - # 1. apply conditioning - trajectory[condition_mask] = condition_data[condition_mask] - - # 2. predict model output - model_output = model(trajectory, t, local_cond=local_cond, global_cond=global_cond) - - # 3. compute previous image: x_t -> x_t-1 - trajectory = scheduler.step(model_output, t, trajectory, generator=generator, **kwargs).prev_sample - - # finally make sure conditioning is enforced - trajectory[condition_mask] = condition_data[condition_mask] - - return trajectory - - def predict_action(self, obs_dict: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]: - """ - obs_dict: must include "obs" key - result: must include "action" key - """ - assert "past_action" not in obs_dict # not implemented yet - # print("!!obs_dict", obs_dict["head_cam"].shape) - # normalize input - nobs = self.normalizer.normalize(obs_dict) - # print("!!nobs", nobs["head_cam"].shape) - value = next(iter(nobs.values())) - B, To = value.shape[:2] - T = self.horizon - Da = self.action_dim - Do = self.obs_feature_dim - To = self.n_obs_steps - - # build input - device = self.device - dtype = self.dtype - - # handle different ways of passing observation - local_cond = None - global_cond = None - if self.obs_as_global_cond: - # condition through global feature - this_nobs = dict_apply(nobs, lambda x: x[:, :To, ...].reshape(-1, *x.shape[2:])) - # print("!!To", To) - # print(this_nobs["head_cam"].shape, this_nobs["agent_pos"].shape) - nobs_features = self.obs_encoder(this_nobs) - # reshape back to B, Do - # print("!!if", nobs_features.shape) - global_cond = nobs_features.reshape(B, -1) - # empty data for action - cond_data = torch.zeros(size=(B, T, Da), device=device, dtype=dtype) - cond_mask = torch.zeros_like(cond_data, dtype=torch.bool) - else: - # condition through impainting - this_nobs = dict_apply(nobs, lambda x: x[:, :To, ...].reshape(-1, *x.shape[2:])) - nobs_features = self.obs_encoder(this_nobs) - # reshape back to B, T, Do - nobs_features = nobs_features.reshape(B, To, -1) - cond_data = torch.zeros(size=(B, T, Da + Do), device=device, dtype=dtype) - cond_mask = torch.zeros_like(cond_data, dtype=torch.bool) - cond_data[:, :To, Da:] = nobs_features - cond_mask[:, :To, Da:] = True - - # run sampling - nsample = self.conditional_sample( - cond_data, - cond_mask, - local_cond=local_cond, - global_cond=global_cond, - **self.kwargs, - ) - - # unnormalize prediction - naction_pred = nsample[..., :Da] - action_pred = self.normalizer["action"].unnormalize(naction_pred) - - # get action - start = To - 1 - end = start + self.n_action_steps - action = action_pred[:, start:end] - - result = {"action": action, "action_pred": action_pred} - return result - - # ========= training ============ - def set_normalizer(self, normalizer: LinearNormalizer): - self.normalizer.load_state_dict(normalizer.state_dict()) - - def compute_loss(self, batch): - # normalize input - assert "valid_mask" not in batch - nobs = self.normalizer.normalize(batch["obs"]) - nactions = self.normalizer["action"].normalize(batch["action"]) - batch_size = nactions.shape[0] - horizon = nactions.shape[1] - - # handle different ways of passing observation - local_cond = None - global_cond = None - trajectory = nactions - cond_data = trajectory - if self.obs_as_global_cond: - # reshape B, T, ... to B*T - this_nobs = dict_apply(nobs, lambda x: x[:, : self.n_obs_steps, ...].reshape(-1, *x.shape[2:])) - nobs_features = self.obs_encoder(this_nobs) - # reshape back to B, Do - global_cond = nobs_features.reshape(batch_size, -1) - else: - # reshape B, T, ... to B*T - this_nobs = dict_apply(nobs, lambda x: x.reshape(-1, *x.shape[2:])) - nobs_features = self.obs_encoder(this_nobs) - # reshape back to B, T, Do - nobs_features = nobs_features.reshape(batch_size, horizon, -1) - cond_data = torch.cat([nactions, nobs_features], dim=-1) - trajectory = cond_data.detach() - - # generate impainting mask - condition_mask = self.mask_generator(trajectory.shape) - - # Sample noise that we'll add to the images - noise = torch.randn(trajectory.shape, device=trajectory.device) - bsz = trajectory.shape[0] - # Sample a random timestep for each image - timesteps = torch.randint( - 0, - self.noise_scheduler.config.num_train_timesteps, - (bsz,), - device=trajectory.device, - ).long() - # Add noise to the clean images according to the noise magnitude at each timestep - # (this is the forward diffusion process) - noisy_trajectory = self.noise_scheduler.add_noise(trajectory, noise, timesteps) - - # compute loss mask - loss_mask = ~condition_mask - - # apply conditioning - noisy_trajectory[condition_mask] = cond_data[condition_mask] - - # Predict the noise residual - pred = self.model(noisy_trajectory, timesteps, local_cond=local_cond, global_cond=global_cond) - - pred_type = self.noise_scheduler.config.prediction_type - if pred_type == "epsilon": - target = noise - elif pred_type == "sample": - target = trajectory - else: - raise ValueError(f"Unsupported prediction type {pred_type}") - - loss = F.mse_loss(pred, target, reduction="none") - loss = loss * loss_mask.type(loss.dtype) - loss = reduce(loss, "b ... -> b (...)", "mean") - loss = loss.mean() - return loss diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/workspace/base_workspace.py b/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/workspace/base_workspace.py deleted file mode 100644 index abd1b40d7..000000000 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/workspace/base_workspace.py +++ /dev/null @@ -1,140 +0,0 @@ -import copy -import os -import pathlib -import threading -from typing import Optional - -import dill -import hydra -import torch -from hydra.core.hydra_config import HydraConfig -from omegaconf import OmegaConf - - -class BaseWorkspace: - include_keys = tuple() - exclude_keys = tuple() - - def __init__(self, cfg: OmegaConf, output_dir: Optional[str] = None): - self.cfg = cfg - self._output_dir = output_dir - self._saving_thread = None - - @property - def output_dir(self): - output_dir = self._output_dir - if output_dir is None: - output_dir = HydraConfig.get().runtime.output_dir - return output_dir - - def run(self): - """ - Create any resource shouldn't be serialized as local variables - """ - pass - - def save_checkpoint( - self, - path=None, - tag="latest", - exclude_keys=None, - include_keys=None, - use_thread=True, - ): - if path is None: - path = pathlib.Path(self.output_dir).joinpath("checkpoints", f"{tag}.ckpt") - else: - path = pathlib.Path(path) - if exclude_keys is None: - exclude_keys = tuple(self.exclude_keys) - if include_keys is None: - include_keys = tuple(self.include_keys) + ("_output_dir",) - - path.parent.mkdir(parents=True, exist_ok=True) - payload = {"cfg": self.cfg, "state_dicts": dict(), "pickles": dict()} - - for key, value in self.__dict__.items(): - if hasattr(value, "state_dict") and hasattr(value, "load_state_dict"): - # modules, optimizers and samplers etc - if key not in exclude_keys: - if use_thread: - payload["state_dicts"][key] = _copy_to_cpu(value.state_dict()) - else: - payload["state_dicts"][key] = value.state_dict() - elif key in include_keys: - payload["pickles"][key] = dill.dumps(value) - if use_thread: - self._saving_thread = threading.Thread( - target=lambda: torch.save(payload, path.open("wb"), pickle_module=dill) - ) - self._saving_thread.start() - else: - torch.save(payload, path.open("wb"), pickle_module=dill) - return str(path.absolute()) - - def get_checkpoint_path(self, tag="latest"): - return pathlib.Path(self.output_dir).joinpath("checkpoints", f"{tag}.ckpt") - - def load_payload(self, payload, exclude_keys=None, include_keys=None, **kwargs): - if exclude_keys is None: - exclude_keys = tuple() - if include_keys is None: - include_keys = payload["pickles"].keys() - - for key, value in payload["state_dicts"].items(): - if key not in exclude_keys: - self.__dict__[key].load_state_dict(value, **kwargs) - for key in include_keys: - if key in payload["pickles"]: - self.__dict__[key] = dill.loads(payload["pickles"][key]) - - def load_checkpoint(self, path=None, tag="latest", exclude_keys=None, include_keys=None, **kwargs): - if path is None: - path = self.get_checkpoint_path(tag=tag) - else: - path = pathlib.Path(path) - payload = torch.load(path.open("rb"), pickle_module=dill, **kwargs) - self.load_payload(payload, exclude_keys=exclude_keys, include_keys=include_keys) - return payload - - @classmethod - def create_from_checkpoint(cls, path, exclude_keys=None, include_keys=None, **kwargs): - payload = torch.load(open(path, "rb"), pickle_module=dill) - instance = cls(payload["cfg"]) - instance.load_payload( - payload=payload, - exclude_keys=exclude_keys, - include_keys=include_keys, - **kwargs, - ) - return instance - - def save_snapshot(self, tag="latest"): - """ - Quick loading and saving for reserach, saves full state of the workspace. - - However, loading a snapshot assumes the code stays exactly the same. - Use save_checkpoint for long-term storage. - """ - path = pathlib.Path(self.output_dir).joinpath("snapshots", f"{tag}.pkl") - path.parent.mkdir(parents=False, exist_ok=True) - torch.save(self, path.open("wb"), pickle_module=dill) - return str(path.absolute()) - - @classmethod - def create_from_snapshot(cls, path): - return torch.load(open(path, "rb"), pickle_module=dill) - - -def _copy_to_cpu(x): - if isinstance(x, torch.Tensor): - return x.detach().to("cpu") - elif isinstance(x, dict): - result = dict() - for k, v in x.items(): - result[k] = _copy_to_cpu(v) - return result - elif isinstance(x, list): - return [_copy_to_cpu(k) for k in x] - else: - return copy.deepcopy(x) diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/workspace/robotworkspace.py b/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/workspace/robotworkspace.py deleted file mode 100644 index 0871c51e2..000000000 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/workspace/robotworkspace.py +++ /dev/null @@ -1,358 +0,0 @@ -import copy -import os -import pathlib -import random - -import hydra -import numpy as np -import torch -import tqdm -import wandb -from diffusion_policy.common.checkpoint_util import TopKCheckpointManager -from diffusion_policy.common.json_logger import JsonLogger -from diffusion_policy.common.pytorch_util import dict_apply, optimizer_to -from diffusion_policy.dataset.base_dataset import BaseImageDataset -from diffusion_policy.model.common.lr_scheduler import get_scheduler -from diffusion_policy.model.diffusion.ema_model import EMAModel -from diffusion_policy.policy.diffusion_unet_image_policy import DiffusionUnetImagePolicy -from diffusion_policy.workspace.base_workspace import BaseWorkspace -from omegaconf import OmegaConf -from torch.utils.data import DataLoader - -OmegaConf.register_new_resolver("eval", eval, replace=True) - - -class RobotWorkspace(BaseWorkspace): - include_keys = ["global_step", "epoch"] - - def __init__(self, cfg: OmegaConf, output_dir=None): - super().__init__(cfg, output_dir=output_dir) - - # set seed - seed = cfg.training.seed - torch.manual_seed(seed) - np.random.seed(seed) - random.seed(seed) - - # configure model - self.model: DiffusionUnetImagePolicy = hydra.utils.instantiate(cfg.policy) - - self.ema_model: DiffusionUnetImagePolicy = None - if cfg.training.use_ema: - self.ema_model = copy.deepcopy(self.model) - - # configure training state - self.optimizer = hydra.utils.instantiate(cfg.optimizer, params=self.model.parameters()) - - # configure training state - self.global_step = 0 - self.epoch = 0 - - def run(self): - cfg = copy.deepcopy(self.cfg) - - # resume training - if cfg.training.resume: - lastest_ckpt_path = self.get_checkpoint_path() - if lastest_ckpt_path.is_file(): - print(f"Resuming from checkpoint {lastest_ckpt_path}") - self.load_checkpoint(path=lastest_ckpt_path) - - # configure dataset - dataset: BaseImageDataset - dataset = hydra.utils.instantiate(cfg.task.dataset) - assert isinstance(dataset, BaseImageDataset) - train_dataloader = create_dataloader(dataset, **cfg.dataloader) - normalizer = dataset.get_normalizer() - - # configure validation dataset - val_dataset = dataset.get_validation_dataset() - val_dataloader = create_dataloader(val_dataset, **cfg.val_dataloader) - - self.model.set_normalizer(normalizer) - if cfg.training.use_ema: - self.ema_model.set_normalizer(normalizer) - - # configure lr scheduler - lr_scheduler = get_scheduler( - cfg.training.lr_scheduler, - optimizer=self.optimizer, - num_warmup_steps=cfg.training.lr_warmup_steps, - num_training_steps=(len(train_dataloader) * cfg.training.num_epochs) - // cfg.training.gradient_accumulate_every, - # pytorch assumes stepping LRScheduler every epoch - # however huggingface diffusers steps it every batch - last_epoch=self.global_step - 1, - ) - - # configure ema - ema: EMAModel = None - if cfg.training.use_ema: - ema = hydra.utils.instantiate(cfg.ema, model=self.ema_model) - - # configure env - # env_runner: BaseImageRunner - # env_runner = hydra.utils.instantiate( - # cfg.task.env_runner, - # output_dir=self.output_dir) - # assert isinstance(env_runner, BaseImageRunner) - env_runner = None - wandb_run = None - - # configure logging - if cfg.logging.mode == "online": - wandb_run = wandb.init( - dir=str(self.output_dir), - config=OmegaConf.to_container(cfg, resolve=True), - **cfg.logging, - ) - wandb.config.update({ - "output_dir": self.output_dir, - }) - - # configure checkpoint - topk_manager = TopKCheckpointManager( - save_dir=os.path.join(self.output_dir, "checkpoints"), **cfg.checkpoint.topk - ) - - # device transfer - device = torch.device(cfg.training.device) - self.model.to(device) - if self.ema_model is not None: - self.ema_model.to(device) - optimizer_to(self.optimizer, device) - - # save batch for sampling - train_sampling_batch = None - - if cfg.training.debug: - cfg.training.num_epochs = 2 - cfg.training.max_train_steps = 3 - cfg.training.max_val_steps = 3 - cfg.training.rollout_every = 1 - cfg.training.checkpoint_every = 1 - cfg.training.val_every = 1 - cfg.training.sample_every = 1 - - # training loop - log_path = os.path.join(self.output_dir, "logs.json.txt") - with JsonLogger(log_path) as json_logger: - for local_epoch_idx in range(cfg.training.num_epochs): - step_log = dict() - # ========= train for this epoch ========== - if cfg.training.freeze_encoder: - self.model.obs_encoder.eval() - self.model.obs_encoder.requires_grad_(False) - - train_losses = list() - with tqdm.tqdm( - train_dataloader, - desc=f"Training epoch {self.epoch}", - leave=False, - mininterval=cfg.training.tqdm_interval_sec, - ) as tepoch: - for batch_idx, batch in enumerate(tepoch): - batch = dataset.postprocess(batch, device) - if train_sampling_batch is None: - train_sampling_batch = batch - # print("obs_dict:", batch) - # print("dict_keys:", batch.keys()) - # print("dict_items:", batch.items()) - # print() - # from pprint import pprint - - # pprint(batch) - # compute loss - raw_loss = self.model.compute_loss(batch) - loss = raw_loss / cfg.training.gradient_accumulate_every - loss.backward() - - # step optimizer - if self.global_step % cfg.training.gradient_accumulate_every == 0: - self.optimizer.step() - self.optimizer.zero_grad() - lr_scheduler.step() - - # update ema - if cfg.training.use_ema: - ema.step(self.model) - - # logging - raw_loss_cpu = raw_loss.item() - tepoch.set_postfix(loss=raw_loss_cpu, refresh=False) - train_losses.append(raw_loss_cpu) - step_log = { - "train_loss": raw_loss_cpu, - "global_step": self.global_step, - "epoch": self.epoch, - "lr": lr_scheduler.get_last_lr()[0], - } - - is_last_batch = batch_idx == (len(train_dataloader) - 1) - if not is_last_batch: - # log of last step is combined with validation and rollout - if wandb_run is not None: - wandb_run.log(step_log, step=self.global_step) - json_logger.log(step_log) - self.global_step += 1 - - if (cfg.training.max_train_steps is not None) and batch_idx >= ( - cfg.training.max_train_steps - 1 - ): - break - - # at the end of each epoch - # replace train_loss with epoch average - train_loss = np.mean(train_losses) - step_log["train_loss"] = train_loss - - # ========= eval for this epoch ========== - policy = self.model - if cfg.training.use_ema: - policy = self.ema_model - policy.eval() - - # run rollout - # if (self.epoch % cfg.training.rollout_every) == 0: - # runner_log = env_runner.run(policy) - # # log all - # step_log.update(runner_log) - - # run validation - if (self.epoch % cfg.training.val_every) == 0: - with torch.no_grad(): - val_losses = list() - with tqdm.tqdm( - val_dataloader, - desc=f"Validation epoch {self.epoch}", - leave=False, - mininterval=cfg.training.tqdm_interval_sec, - ) as tepoch: - for batch_idx, batch in enumerate(tepoch): - batch = dataset.postprocess(batch, device) - loss = self.model.compute_loss(batch) - val_losses.append(loss) - if (cfg.training.max_val_steps is not None) and batch_idx >= ( - cfg.training.max_val_steps - 1 - ): - break - if len(val_losses) > 0: - val_loss = torch.mean(torch.tensor(val_losses)).item() - # log epoch average validation loss - step_log["val_loss"] = val_loss - - # run diffusion sampling on a training batch - if (self.epoch % cfg.training.sample_every) == 0: - with torch.no_grad(): - # sample trajectory from training set, and evaluate difference - batch = train_sampling_batch - obs_dict = batch["obs"] - # print("obs_dict:", obs_dict) - # print("dict_keys:", obs_dict.keys()) - # print("dict_items:", obs_dict.items()) - # print() - # from pprint import pprint - # pprint(obs_dict) - gt_action = batch["action"] - - result = policy.predict_action(obs_dict) - pred_action = result["action_pred"] - mse = torch.nn.functional.mse_loss(pred_action, gt_action) - step_log["train_action_mse_error"] = mse.item() - del batch - del obs_dict - del gt_action - del result - del pred_action - del mse - - # checkpoint - if ((self.epoch + 1) % cfg.training.checkpoint_every) == 0: - # checkpointing - save_name = pathlib.Path(self.cfg.task.dataset.zarr_path).stem - self.save_checkpoint(cfg.checkpoint.save_root_dir + f"/checkpoints/{self.epoch + 1}.ckpt") # TODO - - # ========= eval end for this epoch ========== - policy.train() - - # end of epoch - # log of last step is combined with validation and rollout - json_logger.log(step_log) - if wandb_run is not None: - wandb_run.log(step_log, step=self.global_step) - self.global_step += 1 - self.epoch += 1 - - -class BatchSampler: - def __init__( - self, - data_size: int, - batch_size: int, - shuffle: bool = False, - seed: int = 0, - drop_last: bool = True, - ): - assert drop_last - self.data_size = data_size - self.batch_size = batch_size - self.num_batch = data_size // batch_size - self.discard = data_size - batch_size * self.num_batch - self.shuffle = shuffle - self.rng = np.random.default_rng(seed) if shuffle else None - - def __iter__(self): - if self.shuffle: - perm = self.rng.permutation(self.data_size) - else: - perm = np.arange(self.data_size) - if self.discard > 0: - perm = perm[: -self.discard] - perm = perm.reshape(self.num_batch, self.batch_size) - for i in range(self.num_batch): - yield perm[i] - - def __len__(self): - return self.num_batch - - -def create_dataloader( - dataset, - *, - batch_size: int, - shuffle: bool, - num_workers: int, - pin_memory: bool, - persistent_workers: bool, - seed: int = 0, -): - # print("create_dataloader_batch_size", batch_size) - batch_sampler = BatchSampler(len(dataset), batch_size, shuffle=shuffle, seed=seed, drop_last=True) - - def collate(x): - assert len(x) == 1 - return x[0] - - dataloader = DataLoader( - dataset, - collate_fn=collate, - sampler=batch_sampler, - num_workers=num_workers, - pin_memory=False, - persistent_workers=persistent_workers, - ) - return dataloader - - -@hydra.main( - version_base=None, - config_path=str(pathlib.Path(__file__).parent.parent.joinpath("config")), - config_name=pathlib.Path(__file__).stem, -) -def main(cfg): - workspace = RobotWorkspace(cfg) - workspace.run() - - -if __name__ == "__main__": - main() diff --git a/roboverse_learn/il/utils/diffusion_policy/pyproject.toml b/roboverse_learn/il/utils/diffusion_policy/pyproject.toml deleted file mode 100644 index c951d2d06..000000000 --- a/roboverse_learn/il/utils/diffusion_policy/pyproject.toml +++ /dev/null @@ -1,27 +0,0 @@ -[build-system] -requires = ["flit_core >=3.7,<4"] -build-backend = "flit_core.buildapi" - -[project] -name = "diffusion_policy" -version = "0.1.0" -description = "Diffusion policy for RoboVerse" -requires-python = ">=3.8" -dependencies = [ - "zarr==2.12.0", - "ipdb", - "gpustat", - "omegaconf", - "hydra-core==1.2.0", - "dill==0.3.5.1", - "einops==0.4.1", - "diffusers", - "numba", - "moviepy", - "imageio", - "av", - "matplotlib", - "termcolor", - "huggingface_hub", - "pillow", -] diff --git a/roboverse_learn/il/utils/diffusion_policy/scripts/prune_and_rename.py b/roboverse_learn/il/utils/diffusion_policy/scripts/prune_and_rename.py deleted file mode 100644 index 8cf5e12de..000000000 --- a/roboverse_learn/il/utils/diffusion_policy/scripts/prune_and_rename.py +++ /dev/null @@ -1,42 +0,0 @@ -import os -import sys -import shutil - -def main(): - if len(sys.argv) != 2: - print(f"Usage: {sys.argv[0]} ") - sys.exit(1) - - root_dir = sys.argv[1] - if not os.path.isdir(root_dir): - print(f"Error: '{root_dir}' is not a directory.") - sys.exit(1) - - # List subdirectories matching 'demo_XXXX' - subdirs = [d for d in os.listdir(root_dir) - if os.path.isdir(os.path.join(root_dir, d)) and d.startswith('demo_')] - # Sort by numeric suffix - subdirs.sort(key=lambda x: int(x.split('_')[1])) - - valid_dirs = [] - # Identify and remove empty ones - for d in subdirs: - path = os.path.join(root_dir, d) - metadata_path = os.path.join(path, 'metadata.json') - if not os.path.isfile(metadata_path): - print(f"Removing empty folder: {d}") - shutil.rmtree(path) - else: - valid_dirs.append(d) - - # Renumber remaining directories - for new_idx, old_name in enumerate(valid_dirs): - new_name = f"demo_{new_idx:04d}" - if old_name != new_name: - old_path = os.path.join(root_dir, old_name) - new_path = os.path.join(root_dir, new_name) - print(f"Renaming {old_name} -> {new_name}") - os.rename(old_path, new_path) - -if __name__ == '__main__': - main() diff --git a/roboverse_learn/il/utils/diffusion_policy/train.py b/roboverse_learn/il/utils/diffusion_policy/train.py deleted file mode 100644 index 0f36df59d..000000000 --- a/roboverse_learn/il/utils/diffusion_policy/train.py +++ /dev/null @@ -1,37 +0,0 @@ -import sys - -# use line-buffering for both stdout and stderr -sys.stdout = open(sys.stdout.fileno(), mode="w", buffering=1) -sys.stderr = open(sys.stderr.fileno(), mode="w", buffering=1) - -import pathlib - -import hydra -from diffusion_policy.workspace.base_workspace import BaseWorkspace -from omegaconf import OmegaConf - -import rootutils -rootutils.setup_root(__file__, pythonpath=True) - -# allows arbitrary python code execution in configs using the ${eval:''} resolver -OmegaConf.register_new_resolver("eval", eval, replace=True) - - -abs_config_path = str(pathlib.Path(__file__).resolve().parent.joinpath("diffusion_policy", "config").absolute()) - -@hydra.main( - version_base=None, - config_path=abs_config_path, -) -def main(cfg: OmegaConf): - - OmegaConf.resolve(cfg) - - cls = hydra.utils.get_class(cfg._target_) - - workspace: BaseWorkspace = cls(cfg) - workspace.run() - - -if __name__ == "__main__": - main() diff --git a/roboverse_learn/il/utils/diffusion_policy/train_dp.sh b/roboverse_learn/il/utils/diffusion_policy/train_dp.sh deleted file mode 100644 index c0b143423..000000000 --- a/roboverse_learn/il/utils/diffusion_policy/train_dp.sh +++ /dev/null @@ -1,52 +0,0 @@ -# Examples: -# bash roboverse_learn/algorithms/diffusion_policy/train_dp.sh roboverse_demo/demo_isaaclab/CloseBox-Level0/robot-franka CloseBoxFrankaL0 100 0 200 joint_pos joint_pos - -# 'metadata_dir' means path to metadata directory. e.g. roboverse_demo/demo_isaaclab/CloseBox-Level0/robot-franka -# 'task_name' gives a name to the policy, which can include the task robot and level ie CloseBoxFrankaL0 -# 'expert_data_num' means number of training data. e.g.100 -# 'gpu_id' means single gpu id, e.g.0 - -metadata_dir=${1} -task_name=${2} -expert_data_num=${3} -gpu_id=${4} -num_epochs=${5} -obs_space=${6} # joint_pos or ee -act_space=${7} # joint_pos or ee -delta_ee=${8:-0} # 0 or 1 (only matters if act_space is ee, 0 means absolute 1 means delta control ) - -config_name=robot_dp -horizon=8 -n_obs_steps=3 -n_action_steps=4 -seed=42 - -# adding the obs and action space as additional info -extra="obs:${obs_space}_act:${act_space}" -if [ "${delta_ee}" = 1 ]; then - extra="${extra}_delta" -fi - -python roboverse_learn/algorithms/data2zarr_dp.py \ ---task_name ${task_name}_${extra} \ ---expert_data_num ${expert_data_num} \ ---metadata_dir ${metadata_dir} \ ---action_space ${act_space} \ ---observation_space ${obs_space} \ ---delta_ee ${delta_ee} - -echo -e "\033[33mgpu id (to use): ${gpu_id}\033[0m" -export HYDRA_FULL_ERROR=1 -export CUDA_VISIBLE_DEVICES=${gpu_id} -python roboverse_learn/algorithms/diffusion_policy/train.py --config-name=${config_name}.yaml \ -task.name=${task_name}_${extra} \ -task.dataset.zarr_path="data_policy/${task_name}_${extra}_${expert_data_num}.zarr" \ -training.seed=${seed} \ -horizon=${horizon} \ -n_obs_steps=${n_obs_steps} \ -n_action_steps=${n_action_steps} \ -training.num_epochs=${num_epochs} \ -policy_runner.obs.obs_type=${obs_space} \ -policy_runner.action.action_type=${act_space} \ -policy_runner.action.delta=${delta_ee} \ -training.device="cuda:${gpu_id}" diff --git a/roboverse_learn/il/utils/common/env_util.py b/roboverse_learn/il/utils/env_util.py similarity index 100% rename from roboverse_learn/il/utils/common/env_util.py rename to roboverse_learn/il/utils/env_util.py diff --git a/roboverse_learn/il/utils/common/eval_args.py b/roboverse_learn/il/utils/eval_args.py similarity index 100% rename from roboverse_learn/il/utils/common/eval_args.py rename to roboverse_learn/il/utils/eval_args.py diff --git a/roboverse_learn/il/utils/common/eval_runner_getter.py b/roboverse_learn/il/utils/eval_runner_getter.py similarity index 79% rename from roboverse_learn/il/utils/common/eval_runner_getter.py rename to roboverse_learn/il/utils/eval_runner_getter.py index 1d6ddb015..17ffacd8a 100644 --- a/roboverse_learn/il/utils/common/eval_runner_getter.py +++ b/roboverse_learn/il/utils/eval_runner_getter.py @@ -1,6 +1,6 @@ def get_runner(algo): if algo == "diffusion_policy": - from roboverse_learn.il.dp.eval_runner.dp_eval_runner import DPEvalRunner + from roboverse_learn.il.eval_runner.dp_eval_runner import DPEvalRunner return DPEvalRunner else: diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/flow_matchers.py b/roboverse_learn/il/utils/flow_matchers.py similarity index 100% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/flow_matchers.py rename to roboverse_learn/il/utils/flow_matchers.py diff --git a/roboverse_learn/il/utils/common/json_logger.py b/roboverse_learn/il/utils/json_logger.py similarity index 100% rename from roboverse_learn/il/utils/common/json_logger.py rename to roboverse_learn/il/utils/json_logger.py diff --git a/roboverse_learn/il/utils/common/lr_scheduler.py b/roboverse_learn/il/utils/lr_scheduler.py similarity index 100% rename from roboverse_learn/il/utils/common/lr_scheduler.py rename to roboverse_learn/il/utils/lr_scheduler.py diff --git a/roboverse_learn/il/utils/common/module_attr_mixin.py b/roboverse_learn/il/utils/module_attr_mixin.py similarity index 100% rename from roboverse_learn/il/utils/common/module_attr_mixin.py rename to roboverse_learn/il/utils/module_attr_mixin.py diff --git a/roboverse_learn/il/utils/common/nested_dict_util.py b/roboverse_learn/il/utils/nested_dict_util.py similarity index 100% rename from roboverse_learn/il/utils/common/nested_dict_util.py rename to roboverse_learn/il/utils/nested_dict_util.py diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/normalize_util.py b/roboverse_learn/il/utils/normalize_util.py similarity index 98% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/normalize_util.py rename to roboverse_learn/il/utils/normalize_util.py index 2d7fe825e..8221cb346 100644 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/normalize_util.py +++ b/roboverse_learn/il/utils/normalize_util.py @@ -1,10 +1,10 @@ import numpy as np -from diffusion_policy.common.pytorch_util import ( +from roboverse_learn.il.utils.pytorch_util import ( dict_apply, dict_apply_reduce, dict_apply_split, ) -from diffusion_policy.model.common.normalizer import SingleFieldLinearNormalizer +from roboverse_learn.il.utils.normalizer import SingleFieldLinearNormalizer def get_range_normalizer_from_stat(stat, output_max=1, output_min=-1, range_eps=1e-7): diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/common/normalizer.py b/roboverse_learn/il/utils/normalizer.py similarity index 98% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/common/normalizer.py rename to roboverse_learn/il/utils/normalizer.py index 499a5e100..0fa75e2a5 100644 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/model/common/normalizer.py +++ b/roboverse_learn/il/utils/normalizer.py @@ -5,8 +5,8 @@ import torch import torch.nn as nn import zarr -from diffusion_policy.common.pytorch_util import dict_apply -from diffusion_policy.model.common.dict_of_tensor_mixin import DictOfTensorMixin +from roboverse_learn.il.utils.pytorch_util import dict_apply +from roboverse_learn.il.utils.dict_of_tensor_mixin import DictOfTensorMixin class LinearNormalizer(DictOfTensorMixin): diff --git a/roboverse_learn/il/utils/common/pose_trajectory_interpolator.py b/roboverse_learn/il/utils/pose_trajectory_interpolator.py similarity index 100% rename from roboverse_learn/il/utils/common/pose_trajectory_interpolator.py rename to roboverse_learn/il/utils/pose_trajectory_interpolator.py diff --git a/roboverse_learn/il/utils/common/precise_sleep.py b/roboverse_learn/il/utils/precise_sleep.py similarity index 100% rename from roboverse_learn/il/utils/common/precise_sleep.py rename to roboverse_learn/il/utils/precise_sleep.py diff --git a/roboverse_learn/il/utils/common/pymunk_override.py b/roboverse_learn/il/utils/pymunk_override.py similarity index 100% rename from roboverse_learn/il/utils/common/pymunk_override.py rename to roboverse_learn/il/utils/pymunk_override.py diff --git a/roboverse_learn/il/utils/common/pymunk_util.py b/roboverse_learn/il/utils/pymunk_util.py similarity index 100% rename from roboverse_learn/il/utils/common/pymunk_util.py rename to roboverse_learn/il/utils/pymunk_util.py diff --git a/roboverse_learn/il/utils/common/pytorch_util.py b/roboverse_learn/il/utils/pytorch_util.py similarity index 100% rename from roboverse_learn/il/utils/common/pytorch_util.py rename to roboverse_learn/il/utils/pytorch_util.py diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/replay_buffer.py b/roboverse_learn/il/utils/replay_buffer.py similarity index 100% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/replay_buffer.py rename to roboverse_learn/il/utils/replay_buffer.py diff --git a/roboverse_learn/il/utils/common/robomimic_config_util.py b/roboverse_learn/il/utils/robomimic_config_util.py similarity index 100% rename from roboverse_learn/il/utils/common/robomimic_config_util.py rename to roboverse_learn/il/utils/robomimic_config_util.py diff --git a/roboverse_learn/il/utils/common/robomimic_util.py b/roboverse_learn/il/utils/robomimic_util.py similarity index 100% rename from roboverse_learn/il/utils/common/robomimic_util.py rename to roboverse_learn/il/utils/robomimic_util.py diff --git a/roboverse_learn/il/utils/common/rotation_transformer.py b/roboverse_learn/il/utils/rotation_transformer.py similarity index 100% rename from roboverse_learn/il/utils/common/rotation_transformer.py rename to roboverse_learn/il/utils/rotation_transformer.py diff --git a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/sampler.py b/roboverse_learn/il/utils/sampler.py similarity index 98% rename from roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/sampler.py rename to roboverse_learn/il/utils/sampler.py index 2525de5e7..74663c9e9 100644 --- a/roboverse_learn/il/utils/diffusion_policy/diffusion_policy/common/sampler.py +++ b/roboverse_learn/il/utils/sampler.py @@ -2,7 +2,7 @@ import numba import numpy as np -from diffusion_policy.common.replay_buffer import ReplayBuffer +from roboverse_learn.il.utils.replay_buffer import ReplayBuffer @numba.jit(nopython=True) diff --git a/roboverse_learn/il/utils/common/shape_util.py b/roboverse_learn/il/utils/shape_util.py similarity index 100% rename from roboverse_learn/il/utils/common/shape_util.py rename to roboverse_learn/il/utils/shape_util.py diff --git a/roboverse_learn/il/utils/common/tensor_util.py b/roboverse_learn/il/utils/tensor_util.py similarity index 100% rename from roboverse_learn/il/utils/common/tensor_util.py rename to roboverse_learn/il/utils/tensor_util.py diff --git a/roboverse_learn/il/utils/common/timestamp_accumulator.py b/roboverse_learn/il/utils/timestamp_accumulator.py similarity index 100% rename from roboverse_learn/il/utils/common/timestamp_accumulator.py rename to roboverse_learn/il/utils/timestamp_accumulator.py diff --git a/roboverse_learn/il/vita/README.md b/roboverse_learn/il/vita/README.md new file mode 100644 index 000000000..ebdefd8fa --- /dev/null +++ b/roboverse_learn/il/vita/README.md @@ -0,0 +1,34 @@ +# VITA Policy (IL) + +VITA is a vision-to-action Flow Matching policy built on the shared IL runners under `il/dp/`. + +## Install + +```bash +cd roboverse_learn/il/dp +pip install -r requirements.txt +``` + +Create a Weights & Biases account to obtain an API key for logging. + +## Collect and process data + +```bash +./roboverse_learn/il/collect_demo.sh +``` + +## Train and eval + +Use the shared driver and select the VITA model: + +```bash +export algo_model="vita_model" +./roboverse_learn/il/dp/dp_run.sh +``` + +Inside `dp_run.sh` you can toggle `train_enable` / `eval_enable`, set task names, seeds, GPU id, and checkpoint paths for evaluation. + +## References + +- Dechen Gao et al., "VITA: Vision-to-Action Flow Matching Policy." (2025). +- Yaron Lipman et al., "Flow Matching for Generative Modeling." (2023). diff --git a/roboverse_learn/il/dp/models/vita_policy.py b/roboverse_learn/il/vita/policies/vita_policy.py similarity index 93% rename from roboverse_learn/il/dp/models/vita_policy.py rename to roboverse_learn/il/vita/policies/vita_policy.py index 367ffe80d..b47e3f225 100644 --- a/roboverse_learn/il/dp/models/vita_policy.py +++ b/roboverse_learn/il/vita/policies/vita_policy.py @@ -3,16 +3,15 @@ import torch import torch.nn as nn import torch.nn.functional as F -from einops import reduce -from roboverse_learn.il.utils.common.normalizer import LinearNormalizer -from roboverse_learn.il.utils.common.pytorch_util import dict_apply -from diffusion_policy.policy.base_image_policy import BaseImagePolicy +from roboverse_learn.il.utils.normalizer import LinearNormalizer +from roboverse_learn.il.utils.pytorch_util import dict_apply +from roboverse_learn.il.base.base_image_policy import BaseImagePolicy -from diffusion_policy.model.diffusion.flow_net import SimpleFlowNet -from diffusion_policy.model.diffusion.action_ae import CNNActionEncoder, SimpleActionDecoder -from diffusion_policy.model.vision.multi_image_obs_encoder import MultiImageObsEncoder -from diffusion_policy.common.flow_matchers import TorchFlowMatcher +from roboverse_learn.il.dp.models.diffusion.flow_net import SimpleFlowNet +from roboverse_learn.il.dp.models.diffusion.action_ae import CNNActionEncoder, SimpleActionDecoder +from roboverse_learn.il.dp.models.vision.multi_image_obs_encoder import MultiImageObsEncoder +from roboverse_learn.il.utils.flow_matchers import TorchFlowMatcher class VITAImagePolicy(BaseImagePolicy): diff --git a/roboverse_learn/il/vita/requirements.txt b/roboverse_learn/il/vita/requirements.txt new file mode 100644 index 000000000..fb7e62adf --- /dev/null +++ b/roboverse_learn/il/vita/requirements.txt @@ -0,0 +1,19 @@ +zarr==2.12.0 +ipdb +gpustat +omegaconf +hydra-core==1.2.0 +dill==0.3.5.1 +einops==0.4.1 +diffusers +numba +moviepy +imageio +av +matplotlib +termcolor +huggingface_hub +pillow +pandas +wandb +torchcfm diff --git a/roboverse_learn/vla/OpenVLA/vla_eval.py b/roboverse_learn/vla/OpenVLA/vla_eval.py index acae067ee..b400b84c0 100644 --- a/roboverse_learn/vla/OpenVLA/vla_eval.py +++ b/roboverse_learn/vla/OpenVLA/vla_eval.py @@ -24,7 +24,7 @@ from metasim.utils.demo_util import get_traj from metasim.utils.setup_util import get_robot from metasim.randomization import DomainRandomizationManager, DRConfig -from roboverse_learn.il.dp.runner.base_policy import BasePolicyCfg, ActionCfg, ObsCfg, EndEffectorCfg +from roboverse_learn.il.runner.base_policy import BasePolicyCfg, ActionCfg, ObsCfg, EndEffectorCfg @configclass diff --git a/roboverse_learn/vla/SmolVLA/smolvla_eval.py b/roboverse_learn/vla/SmolVLA/smolvla_eval.py index a2f4dd4df..97cd78209 100755 --- a/roboverse_learn/vla/SmolVLA/smolvla_eval.py +++ b/roboverse_learn/vla/SmolVLA/smolvla_eval.py @@ -31,7 +31,7 @@ from metasim.utils.demo_util import get_traj from metasim.utils.setup_util import get_robot from metasim.randomization import DomainRandomizationManager, DRConfig -from roboverse_learn.il.dp.runner.base_policy import BasePolicyCfg, ActionCfg, ObsCfg, EndEffectorCfg +from roboverse_learn.il.runner.base_policy import BasePolicyCfg, ActionCfg, ObsCfg, EndEffectorCfg @configclass diff --git a/roboverse_learn/vla/pi0/pi_eval.py b/roboverse_learn/vla/pi0/pi_eval.py index 4437c211f..9d8687c23 100644 --- a/roboverse_learn/vla/pi0/pi_eval.py +++ b/roboverse_learn/vla/pi0/pi_eval.py @@ -26,7 +26,7 @@ from openpi_client import image_tools, websocket_client_policy -from roboverse_learn.il.dp.runner.base_policy import ActionCfg, BasePolicyCfg, ObsCfg +from roboverse_learn.il.runner.base_policy import ActionCfg, BasePolicyCfg, ObsCfg @configclass From d8ed8e0c1bd58e0effac63be9a57a75c993b9235 Mon Sep 17 00:00:00 2001 From: Dechen Gao Date: Tue, 9 Dec 2025 10:38:25 -0800 Subject: [PATCH 16/50] dev/Add MeanFlow, Improved MeanFlow, consistency flow (#719) * Add MeanFlow, Improved MeanFlow, consistency flow * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * update readme --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- roboverse_learn/il/README.md | 13 +- .../il/configs/model_config/vita_model.yaml | 5 +- .../train_config/diffusion_policy_train.yaml | 2 +- .../il/dp/policies/ddpm_dit_image_policy.py | 2 +- .../il/fm/policies/fm_dit_image_policy.py | 2 +- roboverse_learn/il/runner/dp_runner.py | 2 +- .../models/diffusion => utils}/ema_model.py | 0 roboverse_learn/il/utils/flow/__init__.py | 0 .../il/utils/flow/base_flow_matcher.py | 6 + .../il/utils/flow/consistency_flow_matcher.py | 128 +++++++++++++ .../il/utils/{ => flow}/flow_matchers.py | 43 ++++- .../il/utils/flow/mean_flow_matcher.py | 169 ++++++++++++++++++ .../diffusion => utils/models}/flow_net.py | 2 +- .../diffusion => utils/models}/layers.py | 0 .../diffusion => vita/policies}/action_ae.py | 2 +- .../il/vita/policies/vita_policy.py | 6 +- 16 files changed, 364 insertions(+), 18 deletions(-) rename roboverse_learn/il/{dp/models/diffusion => utils}/ema_model.py (100%) create mode 100644 roboverse_learn/il/utils/flow/__init__.py create mode 100644 roboverse_learn/il/utils/flow/base_flow_matcher.py create mode 100644 roboverse_learn/il/utils/flow/consistency_flow_matcher.py rename roboverse_learn/il/utils/{ => flow}/flow_matchers.py (65%) create mode 100644 roboverse_learn/il/utils/flow/mean_flow_matcher.py rename roboverse_learn/il/{dp/models/diffusion => utils/models}/flow_net.py (99%) rename roboverse_learn/il/{dp/models/diffusion => utils/models}/layers.py (100%) rename roboverse_learn/il/{dp/models/diffusion => vita/policies}/action_ae.py (98%) diff --git a/roboverse_learn/il/README.md b/roboverse_learn/il/README.md index 11be6f589..ee9dec7fc 100644 --- a/roboverse_learn/il/README.md +++ b/roboverse_learn/il/README.md @@ -37,7 +37,7 @@ bash roboverse_learn/il/il_setup.sh | `fm_unet` | Flow Matching | UNet | `model_config/fm_unet_model.yaml` | [6] | | `score_unet` | Score-Based Model | UNet | `model_config/score_model.yaml` | [3], [4] | -### References +**References** 1. Ho, Jonathan, Ajay Jain, and Pieter Abbeel. "Denoising Diffusion Probabilistic Models." (2020). 2. Song, Jiaming, Chenlin Meng, and Stefano Ermon. "Denoising Diffusion Implicit Models." (2021). @@ -46,3 +46,14 @@ bash roboverse_learn/il/il_setup.sh 5. Peebles, William, and Jun-Yan Zhu. "DiT: Diffusion Models with Transformers." (2023). 6. Lipman, Yaron, et al. "Flow Matching for Generative Modeling." (2023). 7. Gao, Dechen, et al. "VITA: Vision-to-Action Flow Matching Policy." (2025). + +### 1-NFE Generation + +We also include multiple classic 1-NFE (1 number of function evaluations) generation methods for FM, including MeanFlow [1, 2], Improved MeanFlow (iMF) [3], and Consistency Flow Matching (CFM) [4]. + +**References** + +[1] Geng, Zhengyang, et al. "Mean flows for one-step generative modeling." arXiv preprint arXiv:2505.13447 (2025). +[2] Sheng, Juyi, et al. "MP1: MeanFlow Tames Policy Learning in 1-step for Robotic Manipulation." arXiv preprint arXiv:2507.10543 (2025). +[3] Geng, Zhengyang, et al. "Improved Mean Flows: On the Challenges of Fastforward Generative Models." arXiv preprint arXiv:2512.02012 (2025). +[4] Yang, Ling, et al. "Consistency flow matching: Defining straight flows with velocity consistency." arXiv preprint arXiv:2407.02398 (2024). diff --git a/roboverse_learn/il/configs/model_config/vita_model.yaml b/roboverse_learn/il/configs/model_config/vita_model.yaml index 378dc73ae..afee69d6f 100644 --- a/roboverse_learn/il/configs/model_config/vita_model.yaml +++ b/roboverse_learn/il/configs/model_config/vita_model.yaml @@ -31,9 +31,12 @@ latent_dim: 512 # Flow matcher parameters flow_matcher: - _target_: roboverse_learn.il.utils.flow_matchers.ExactOptimalTransportConditionalFlowMatcher + _target_: roboverse_learn.il.utils.flow.flow_matchers.ExactOptimalTransportConditionalFlowMatcher sigma: 0.0 num_sampling_steps: 6 + # MeanFlow + # _target_: roboverse_learn.il.utils.flow.flow_matchers.MeanFlowMatcher + # use_imf: true # Whether to use iMF (Improved MeanFlow) # Flow network parameters flow_net: diff --git a/roboverse_learn/il/configs/train_config/diffusion_policy_train.yaml b/roboverse_learn/il/configs/train_config/diffusion_policy_train.yaml index edb213072..9e4d5eaa0 100644 --- a/roboverse_learn/il/configs/train_config/diffusion_policy_train.yaml +++ b/roboverse_learn/il/configs/train_config/diffusion_policy_train.yaml @@ -46,7 +46,7 @@ val_dataloader: persistent_workers: False ema: - _target_: roboverse_learn.il.dp.models.diffusion.ema_model.EMAModel + _target_: roboverse_learn.il.utils.ema_model.EMAModel update_after_step: 0 inv_gamma: 1.0 power: 0.75 diff --git a/roboverse_learn/il/dp/policies/ddpm_dit_image_policy.py b/roboverse_learn/il/dp/policies/ddpm_dit_image_policy.py index f533931b0..6b6d3986c 100644 --- a/roboverse_learn/il/dp/policies/ddpm_dit_image_policy.py +++ b/roboverse_learn/il/dp/policies/ddpm_dit_image_policy.py @@ -3,7 +3,7 @@ from typing import Any, Dict, Mapping, Optional from diffusers.schedulers.scheduling_ddpm import DDPMScheduler -from roboverse_learn.il.dp.models.diffusion.flow_net import FlowTransformer +from roboverse_learn.il.utils.models.flow_net import FlowTransformer from roboverse_learn.il.dp.models.vision.multi_image_obs_encoder import MultiImageObsEncoder import torch diff --git a/roboverse_learn/il/fm/policies/fm_dit_image_policy.py b/roboverse_learn/il/fm/policies/fm_dit_image_policy.py index 36325c582..cca803b22 100644 --- a/roboverse_learn/il/fm/policies/fm_dit_image_policy.py +++ b/roboverse_learn/il/fm/policies/fm_dit_image_policy.py @@ -4,7 +4,7 @@ import torch.nn.functional as F from einops import reduce -from roboverse_learn.il.dp.models.diffusion.flow_net import FlowTransformer +from roboverse_learn.il.utils.models.flow_net import FlowTransformer from roboverse_learn.il.dp.models.diffusion.mask_generator import LowdimMaskGenerator from roboverse_learn.il.dp.models.vision.multi_image_obs_encoder import MultiImageObsEncoder from roboverse_learn.il.utils.normalizer import LinearNormalizer diff --git a/roboverse_learn/il/runner/dp_runner.py b/roboverse_learn/il/runner/dp_runner.py index b1997caff..0bacf5d27 100644 --- a/roboverse_learn/il/runner/dp_runner.py +++ b/roboverse_learn/il/runner/dp_runner.py @@ -14,7 +14,7 @@ import torch import tqdm import wandb -from roboverse_learn.il.dp.models.diffusion.ema_model import EMAModel +from roboverse_learn.il.utils.ema_model import EMAModel from loguru import logger as log from metasim.scenario.scenario import ScenarioCfg from metasim.scenario.cameras import PinholeCameraCfg diff --git a/roboverse_learn/il/dp/models/diffusion/ema_model.py b/roboverse_learn/il/utils/ema_model.py similarity index 100% rename from roboverse_learn/il/dp/models/diffusion/ema_model.py rename to roboverse_learn/il/utils/ema_model.py diff --git a/roboverse_learn/il/utils/flow/__init__.py b/roboverse_learn/il/utils/flow/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/roboverse_learn/il/utils/flow/base_flow_matcher.py b/roboverse_learn/il/utils/flow/base_flow_matcher.py new file mode 100644 index 000000000..f3ce1975d --- /dev/null +++ b/roboverse_learn/il/utils/flow/base_flow_matcher.py @@ -0,0 +1,6 @@ +class BaseFlowMatcher(): + def compute_loss(self, model, target, **kwargs): + raise NotImplementedError + + def sample(self, model, shape, device, num_steps, return_traces=False, **kwargs): + raise NotImplementedError diff --git a/roboverse_learn/il/utils/flow/consistency_flow_matcher.py b/roboverse_learn/il/utils/flow/consistency_flow_matcher.py new file mode 100644 index 000000000..e744ddfb5 --- /dev/null +++ b/roboverse_learn/il/utils/flow/consistency_flow_matcher.py @@ -0,0 +1,128 @@ +import torch +import numpy as np +import torchcfm.conditional_flow_matching as cfm + +from roboverse_learn.il.utils.flow.base_flow_matcher import BaseFlowMatcher + + +class ConsistencyFlowMatcher(BaseFlowMatcher): + def __init__( + self, + eps=1e-2, + num_segments=2, + boundary=1, + delta=1e-3, + alpha=1e-5, + noise_scale=1.0, + sigma_var=1.0, + ode_tol=1e-5, + num_sampling_steps=1, + ): + super().__init__() + self.eps = eps + self.num_segments = num_segments + self.boundary = boundary + self.delta = delta + self.alpha = alpha + self.noise_scale = noise_scale + self.sigma_var = sigma_var + self.ode_tol = ode_tol + self.sigma_t = lambda t: (1. - t) * sigma_var + self.num_sampling_steps = num_sampling_steps + + def compute_loss(self, model, target, start=None, **kwargs): + """Compute the CFM loss for training.""" + batch_size = target.shape[0] + device = target.device + + if start is None: + a0 = torch.randn_like(target) + else: + a0 = start + t = torch.rand(batch_size, device=device) * (1 - self.eps) + self.eps + r = torch.clamp(t + self.delta, max=1.0) + + t_expand = t.view(-1, 1, 1).repeat(1, target.shape[1], target.shape[2]) + r_expand = r.view(-1, 1, 1).repeat(1, target.shape[1], target.shape[2]) + xt = t_expand * target + (1 - t_expand) * a0 + xr = r_expand * target + (1 - r_expand) * a0 + + segments = torch.linspace(0, 1, self.num_segments + 1, device=device) + seg_indices = torch.searchsorted(segments, t, side="left").clamp(min=1) + segment_ends = segments[seg_indices] + segment_ends_expand = segment_ends.view(-1, 1, 1).repeat(1, target.shape[1], target.shape[2]) + x_at_segment_ends = segment_ends_expand * target + (1 - segment_ends_expand) * a0 + + vt = model(xt, t, **kwargs) + vr = model(xr, r, **kwargs) + vr = torch.nan_to_num(vr) + + ft = self._f_euler(t_expand, segment_ends_expand, xt, vt) + fr = self._threshold_based_f_euler(r_expand, segment_ends_expand, xr, vr, self.boundary, x_at_segment_ends) + + losses_f = torch.mean(torch.square(ft - fr).reshape(batch_size, -1), dim=-1) + losses_v = self._masked_losses_v(vt, vr, self.boundary, segment_ends, t, batch_size) + + loss = torch.mean(losses_f + self.alpha * losses_v) + return loss, { + 'loss': loss.item(), + 'flow_loss': torch.mean(losses_f).item(), + 'velocity_loss': torch.mean(losses_v).item() + } + + def sample(self, model, shape, device, num_steps=None, return_traces=False, start=None, **kwargs): + """Generate samples, optionally returning traces.""" + if num_steps is None: + num_steps = self.num_sampling_steps + if start is None: + noise = torch.randn(shape, device=device) + else: + noise = start + z = noise.detach().clone() + dt = 1.0 / num_steps + eps = self.eps + + if return_traces: + traj_history = [] + vel_history = [] + + for i in range(num_steps): + num_t = i / num_steps * (1 - eps) + eps + t = torch.ones(shape[0], device=device) * num_t + vt = model(z, t, **kwargs) + sigma_t = self.sigma_t(num_t) + if sigma_t > 0: + pred_sigma = vt + (sigma_t**2) / (2 * (self.noise_scale**2) * ((1-num_t)**2)) * \ + (0.5 * num_t * (1-num_t) * vt - 0.5 * (2-num_t) * z.detach().clone()) + z = z.detach().clone() + pred_sigma * dt + sigma_t * np.sqrt(dt) * torch.randn_like(pred_sigma) + else: + z = z.detach().clone() + vt * dt + + if return_traces: + traj_history.append(z.detach().clone().cpu()) + vel_history.append(vt.detach().clone().cpu()) + + if return_traces: + return z, (traj_history, vel_history) + return z + + def _f_euler(self, t_expand, segment_ends_expand, xt, vt): + return xt + (segment_ends_expand - t_expand) * vt + + def _threshold_based_f_euler(self, t_expand, segment_ends_expand, xt, vt, threshold, x_at_segment_ends): + if isinstance(threshold, int) and threshold == 0: + return x_at_segment_ends + less_than_threshold = t_expand < threshold + return less_than_threshold * self._f_euler(t_expand, segment_ends_expand, xt, vt) + \ + (~less_than_threshold) * x_at_segment_ends + + def _masked_losses_v(self, vt, vr, threshold, segment_ends, t, batch_size): + if isinstance(threshold, int) and threshold == 0: + return torch.tensor(0.0, device=vt.device) + t_expand = t.view(-1, 1, 1).repeat(1, vt.shape[1], vt.shape[2]) + less_than_threshold = t_expand < threshold + far_from_segment_ends = (segment_ends - t) > 1.01 * self.delta + far_from_segment_ends = far_from_segment_ends.view(-1, 1, 1).repeat(1, vt.shape[1], vt.shape[2]) + losses_v = torch.square(vt - vr) + losses_v = less_than_threshold * far_from_segment_ends * losses_v + return torch.mean(losses_v.reshape(batch_size, -1), dim=-1) diff --git a/roboverse_learn/il/utils/flow_matchers.py b/roboverse_learn/il/utils/flow/flow_matchers.py similarity index 65% rename from roboverse_learn/il/utils/flow_matchers.py rename to roboverse_learn/il/utils/flow/flow_matchers.py index 2309f49df..483730d91 100644 --- a/roboverse_learn/il/utils/flow_matchers.py +++ b/roboverse_learn/il/utils/flow/flow_matchers.py @@ -2,13 +2,9 @@ import numpy as np import torchcfm.conditional_flow_matching as cfm - -class BaseFlowMatcher(): - def compute_loss(self, model, target, **kwargs): - raise NotImplementedError - - def sample(self, model, shape, device, num_steps, return_traces=False, **kwargs): - raise NotImplementedError +from roboverse_learn.il.utils.flow.base_flow_matcher import BaseFlowMatcher +from roboverse_learn.il.utils.flow.mean_flow_matcher import MeanFlowMatcher +from roboverse_learn.il.utils.flow.consistency_flow_matcher import ConsistencyFlowMatcher class TorchFlowMatcher(BaseFlowMatcher): @@ -101,3 +97,36 @@ def __init__(self, num_sampling_steps=6, **kwargs): class ExactOptimalTransportConditionalFlowMatcher(TorchFlowMatcher): def __init__(self, num_sampling_steps=6, **kwargs): super().__init__(cfm.ExactOptimalTransportConditionalFlowMatcher(**kwargs), num_sampling_steps) + + +class MeanFlowConditionalFlowMatcher(TorchFlowMatcher): + ''' + Implementation of MeanFlow, a 1-step flow matching method. + [1] Geng, Zhengyang, et al. "Mean flows for one-step generative modeling." arXiv preprint arXiv:2505.13447 (2025). + Used dispersive losses: + [2] Sheng, Juyi, et al. "MP1: MeanFlow Tames Policy Learning in 1-step for Robotic Manipulation." arXiv preprint arXiv:2507.10543 (2025). + ''' + def __init__(self, num_sampling_steps=1, **kwargs): + if num_sampling_steps != 1: + print("Warning: MeanFlow is designed for 1-NFE generation.") + super().__init__(MeanFlowMatcher(**kwargs), num_sampling_steps) + + +class ImprovedMeanFlowConditionalFlowMatcher(TorchFlowMatcher): + ''' + Implementation of Improved MeanFlow, a 1-step flow matching method. + [1] Geng, Zhengyang, et al. "Improved Mean Flows: On the Challenges of Fastforward Generative Models." arXiv preprint arXiv:2512.02012 (2025). + ''' + def __init__(self, num_sampling_steps=1, **kwargs): + # Overwrite use_imf to True + kwargs['use_imf'] = True + if num_sampling_steps != 1: + print("Warning: Improved MeanFlow is designed for 1-NFE generation.") + super().__init__(MeanFlowMatcher(**kwargs), num_sampling_steps) + + +class ConsistencyFlowMatcher(TorchFlowMatcher): + def __init__(self, num_sampling_steps=1, **kwargs): + if num_sampling_steps != 1: + print("Warning: ConsistencyFlow is designed for 1-NFE generation.") + super().__init__(ConsistencyFlowMatcher(**kwargs), num_sampling_steps) diff --git a/roboverse_learn/il/utils/flow/mean_flow_matcher.py b/roboverse_learn/il/utils/flow/mean_flow_matcher.py new file mode 100644 index 000000000..5c17fb9d6 --- /dev/null +++ b/roboverse_learn/il/utils/flow/mean_flow_matcher.py @@ -0,0 +1,169 @@ +import torch +from functools import partial + +from roboverse_learn.il.utils.flow.base_flow_matcher import BaseFlowMatcher + + +def stopgrad(x): + return x.detach() + + +def adaptive_l2_loss(error, gamma=0.5, c=1e-3): + """ + Adaptive L2 loss. + """ + delta_sq = torch.mean(error ** 2, dim=tuple(range(1, error.ndim))) + p = 1.0 - gamma + w = 1.0 / (delta_sq + c).pow(p) + loss = delta_sq + return (stopgrad(w) * loss).mean() + + +def dispersive_loss(z, tau=1.0): + """ + Dispersive Loss. + """ + if z.shape[0] <= 1: + return 0.0 + dist_matrix = torch.cdist(z, z, p=2) ** 2 + # Normalize to prevent overflow/underflow + dist_matrix = dist_matrix / (torch.max(dist_matrix).detach() + 1e-8) + exp_term = torch.exp(-dist_matrix / tau) + mean_exp = torch.mean(exp_term) + loss = torch.log(mean_exp) + return loss + + +class MeanFlowMatcher(BaseFlowMatcher): + def __init__( + self, + flow_ratio=0.5, + time_dist_mu=-0.4, + time_dist_sigma=1.0, + adaptive_loss_gamma=0.5, + dispersive_loss_tau=1.0, + dispersive_loss_weight=0.0, + cfg_scale=0.5, + use_imf=False, + **kwargs, + ): + super().__init__() + self.flow_ratio = flow_ratio + self.time_dist_mu = time_dist_mu + self.time_dist_sigma = time_dist_sigma + self.adaptive_loss_gamma = adaptive_loss_gamma + self.dispersive_loss_tau = dispersive_loss_tau + self.dispersive_loss_weight = dispersive_loss_weight + self.cfg_scale = cfg_scale + self.use_imf = use_imf + + def sample_t_r(self, batch_size, device): + """ + Samples t and r from a log-normal distribution. + """ + # Log-normal distribution + normal_samples = ( + torch.randn(batch_size, 2, device=device) * self.time_dist_sigma + + self.time_dist_mu + ) + samples = torch.sigmoid(normal_samples) + + # t = max, r = min + t = torch.max(samples, dim=1)[0] + r = torch.min(samples, dim=1)[0] + + # Set r=t for a portion of the batch + num_selected = int(self.flow_ratio * batch_size) + indices = torch.randperm(batch_size, device=device)[:num_selected] + r[indices] = t[indices] + + return t, r + + def compute_loss(self, model, target, start=None, **kwargs): + """ + Compute the MeanFlow + Dispersive loss. + Assumes `model` returns (prediction, internal_features_list). + """ + if start is None: + raise ValueError("MeanFlowMatcher requires a 'start' (vision latent) tensor.") + + x1 = target + x0 = start + batch_size = x0.shape[0] + device = x0.device + + # Sample t and r + t, r = self.sample_t_r(batch_size, device) + t_ = t.view(-1, *([1] * (x0.dim() - 1))) + r_ = r.view(-1, *([1] * (x0.dim() - 1))) + + # Define path and sample z_t + z_t = (1 - t_) * x1 + t_ * x0 + + # Ground-truth instantaneous velocity v = dx/dt + v = x0 - x1 + + def pred_meanflow(z_in, t_in, r_in): + return model(x=z_in, timestep=t_in, r=r_in, **kwargs) + + if self.use_imf: + with torch.no_grad(): + v_net, _ = pred_meanflow(z_t, t, t) + dz_tangent = v_net + else: + dz_tangent = v + + # JVP inputs + primals = (z_t, t, r) + tangents = (dz_tangent, torch.ones_like(t), torch.zeros_like(r)) + pred, dudt = torch.autograd.functional.jvp(pred_meanflow, primals, tangents, create_graph=True) + predicted_mean_vel, internal_features = pred + + # dudt[0] is the JVP for the velocity output + u_tgt = v - (t_ - r_) * dudt[0] + + # MeanFlow Loss + error = predicted_mean_vel - stopgrad(u_tgt) + meanflow_loss = adaptive_l2_loss(error, gamma=self.adaptive_loss_gamma) + + loss = meanflow_loss + metrics = {'meanflow_loss': meanflow_loss.item()} + + # Dispersive Loss + if self.dispersive_loss_weight > 0 and internal_features is not None: + dis_loss_total = 0.0 + # internal_features is a list of tensors from the network's hidden layers + for features in internal_features: + dis_loss_total += dispersive_loss(features, tau=self.dispersive_loss_tau) + + metrics['dispersive_loss'] = dis_loss_total.item() + loss += self.dispersive_loss_weight * dis_loss_total + + metrics['loss'] = loss.item() + return loss, metrics + + def sample(self, model, shape, device, num_steps=None, return_traces=False, start=None, **kwargs): + """ + Generate samples in 1-NFE using MeanFlow. + """ + if start is None: + raise ValueError("MeanFlowMatcher requires a 'start' (vision latent) tensor for sampling.") + + x_source = start + batch_size = x_source.shape[0] + + t = torch.ones(batch_size, device=device) + r = torch.zeros(batch_size, device=device) + + # Model predicts u(x0, 1, 0) which equals (x0 - x1) + mean_velocity, _ = model(x_source, t, r=r, **kwargs) + + # x1 = x0 - u(x0, 1, 0) + x_target = x_source - mean_velocity + + if return_traces: + traj_history = [x_source.detach().clone().cpu(), x_target.detach().clone().cpu()] + vel_history = [torch.zeros_like(x_source).cpu(), mean_velocity.detach().clone().cpu()] + return x_target, (traj_history, vel_history) + + return x_target diff --git a/roboverse_learn/il/dp/models/diffusion/flow_net.py b/roboverse_learn/il/utils/models/flow_net.py similarity index 99% rename from roboverse_learn/il/dp/models/diffusion/flow_net.py rename to roboverse_learn/il/utils/models/flow_net.py index 1f0b446b6..d58b5cfc4 100644 --- a/roboverse_learn/il/dp/models/diffusion/flow_net.py +++ b/roboverse_learn/il/utils/models/flow_net.py @@ -3,7 +3,7 @@ import torch.nn.functional as F from roboverse_learn.il.dp.models.diffusion.positional_embedding import RotaryPosEmb, SinusoidalPosEmb -from roboverse_learn.il.dp.models.diffusion.layers import Mlp +from roboverse_learn.il.utils.models.layers import Mlp class Attention(nn.Module): diff --git a/roboverse_learn/il/dp/models/diffusion/layers.py b/roboverse_learn/il/utils/models/layers.py similarity index 100% rename from roboverse_learn/il/dp/models/diffusion/layers.py rename to roboverse_learn/il/utils/models/layers.py diff --git a/roboverse_learn/il/dp/models/diffusion/action_ae.py b/roboverse_learn/il/vita/policies/action_ae.py similarity index 98% rename from roboverse_learn/il/dp/models/diffusion/action_ae.py rename to roboverse_learn/il/vita/policies/action_ae.py index 9b2262bb7..48abab6e4 100644 --- a/roboverse_learn/il/dp/models/diffusion/action_ae.py +++ b/roboverse_learn/il/vita/policies/action_ae.py @@ -1,6 +1,6 @@ import torch.nn as nn import torch.nn.functional as F -from roboverse_learn.il.dp.models.diffusion.layers import Mlp +from roboverse_learn.il.utils.models.layers import Mlp def weights_init_encoder(m): diff --git a/roboverse_learn/il/vita/policies/vita_policy.py b/roboverse_learn/il/vita/policies/vita_policy.py index b47e3f225..5daa37f49 100644 --- a/roboverse_learn/il/vita/policies/vita_policy.py +++ b/roboverse_learn/il/vita/policies/vita_policy.py @@ -8,10 +8,10 @@ from roboverse_learn.il.utils.pytorch_util import dict_apply from roboverse_learn.il.base.base_image_policy import BaseImagePolicy -from roboverse_learn.il.dp.models.diffusion.flow_net import SimpleFlowNet -from roboverse_learn.il.dp.models.diffusion.action_ae import CNNActionEncoder, SimpleActionDecoder +from roboverse_learn.il.utils.models.flow_net import SimpleFlowNet +from roboverse_learn.il.vita.policies.action_ae import CNNActionEncoder, SimpleActionDecoder from roboverse_learn.il.dp.models.vision.multi_image_obs_encoder import MultiImageObsEncoder -from roboverse_learn.il.utils.flow_matchers import TorchFlowMatcher +from roboverse_learn.il.utils.flow.flow_matchers import TorchFlowMatcher class VITAImagePolicy(BaseImagePolicy): From c90402ade5c083a1c7eb39aa10bc778af137d7cf Mon Sep 17 00:00:00 2001 From: Dechen Gao Date: Wed, 10 Dec 2025 05:38:47 -0800 Subject: [PATCH 17/50] refactor (#720) --- .gitignore | 1 + roboverse_learn/il/README.md | 22 +++-- .../assets/bimanual_viperx_ee_insertion.xml | 59 ----------- .../bimanual_viperx_ee_transfer_cube.xml | 48 --------- .../act/assets/bimanual_viperx_insertion.xml | 53 ---------- .../assets/bimanual_viperx_transfer_cube.xml | 42 -------- roboverse_learn/il/act/assets/scene.xml | 38 ------- roboverse_learn/il/act/assets/tabletop.stl | Bin 684 -> 0 bytes .../assets/vx300s_10_custom_finger_left.stl | Bin 83384 -> 0 bytes .../assets/vx300s_10_custom_finger_right.stl | Bin 83384 -> 0 bytes .../act/assets/vx300s_10_gripper_finger.stl | Bin 42884 -> 0 bytes .../il/act/assets/vx300s_11_ar_tag.stl | Bin 3884 -> 0 bytes .../il/act/assets/vx300s_1_base.stl | Bin 99984 -> 0 bytes .../il/act/assets/vx300s_2_shoulder.stl | Bin 63884 -> 0 bytes .../il/act/assets/vx300s_3_upper_arm.stl | Bin 102984 -> 0 bytes .../il/act/assets/vx300s_4_upper_forearm.stl | Bin 49584 -> 0 bytes .../il/act/assets/vx300s_5_lower_forearm.stl | Bin 99884 -> 0 bytes .../il/act/assets/vx300s_6_wrist.stl | Bin 70784 -> 0 bytes .../il/act/assets/vx300s_7_gripper.stl | Bin 450084 -> 0 bytes .../il/act/assets/vx300s_8_gripper_prop.stl | Bin 31684 -> 0 bytes .../il/act/assets/vx300s_9_gripper_bar.stl | Bin 379484 -> 0 bytes .../il/act/assets/vx300s_dependencies.xml | 17 ---- roboverse_learn/il/act/assets/vx300s_left.xml | 59 ----------- .../il/act/assets/vx300s_right.xml | 59 ----------- roboverse_learn/il/base/base_dataset.py | 53 ---------- roboverse_learn/il/base/base_model.py | 12 --- .../base_policy.py => configs/base_config.py} | 0 .../{dp_runner.yaml => default_runner.yaml} | 24 +++-- ...ion_policy_eval.yaml => default_eval.yaml} | 0 .../ddim_unet.yaml} | 6 +- .../ddpm_dit.yaml} | 6 +- .../ddpm_unet.yaml} | 6 +- .../fm_dit.yaml} | 6 +- .../fm_unet.yaml} | 6 +- .../score.yaml} | 6 +- .../vita.yaml} | 6 +- ...n_policy_train.yaml => default_train.yaml} | 0 roboverse_learn/il/datasets/base_dataset.py | 2 +- .../il/datasets/robot_image_dataset.py | 3 +- roboverse_learn/il/eval_runner/__init__.py | 0 roboverse_learn/il/il_run.sh | 93 +++++------------- .../il/{ => policies}/act/README.md | 6 +- .../il/{ => policies}/act/act_eval_runner.py | 10 +- .../il/{ => policies}/act/act_run.sh | 8 +- .../il/{ => policies}/act/ckpt_dir_path.txt | 0 .../il/{ => policies}/act/conda_env.yaml | 0 .../il/{ => policies}/act/constants.py | 0 .../il/{ => policies}/act/detr/LICENSE | 0 .../il/{ => policies}/act/detr/README.md | 0 .../il/{ => policies}/act/detr/main.py | 0 .../act/detr/models/__init__.py | 0 .../act/detr/models/backbone.py | 2 +- .../act/detr/models/detr_vae.py | 0 .../act/detr/models/position_encoding.py | 0 .../act/detr/models/transformer.py | 0 .../il/{ => policies}/act/detr/setup.py | 0 .../{ => policies}/act/detr/util/__init__.py | 0 .../{ => policies}/act/detr/util/box_ops.py | 0 .../il/{ => policies}/act/detr/util/misc.py | 0 .../act/detr/util/plot_utils.py | 0 .../il/{ => policies}/act/imitate_episodes.py | 0 .../il/{ => policies}/act/policy.py | 0 .../il/{ => policies}/act/scripted_policy.py | 0 .../il/{ => policies}/act/train.py | 2 +- .../il/{ => policies}/act/utils.py | 0 .../{base => policies}/base_image_policy.py | 0 .../il/{ => policies}/dp/.gitignore | 0 .../il/{ => policies}/dp/README.md | 4 +- .../il/{base => policies/dp}/__init__.py | 0 .../dp}/ddim_unet_image_policy.py | 10 +- .../dp}/ddpm_dit_image_policy.py | 8 +- .../dp}/ddpm_image_policy.py | 6 +- .../dp}/ddpm_unet_image_policy.py | 6 +- .../il/{ => policies}/dp/dp_run.sh | 16 +-- .../dp/models/bet/action_ae/__init__.py | 2 +- .../bet/action_ae/discretizers/k_means.py | 0 .../bet/latent_generators/latent_generator.py | 2 +- .../dp/models/bet/latent_generators/mingpt.py | 8 +- .../bet/latent_generators/transformer.py | 6 +- .../dp/models/bet/libraries/loss_fn.py | 0 .../dp/models/bet/libraries/mingpt/LICENSE | 0 .../models/bet/libraries/mingpt}/__init__.py | 0 .../dp/models/bet/libraries/mingpt/model.py | 0 .../dp/models/bet/libraries/mingpt/trainer.py | 0 .../dp/models/bet/libraries/mingpt/utils.py | 0 .../il/{ => policies}/dp/models/bet/utils.py | 0 .../dp/models/diffusion/conditional_unet1d.py | 4 +- .../dp/models/diffusion/conv1d_components.py | 0 .../dp/models/diffusion/mask_generator.py | 0 .../models/diffusion/positional_embedding.py | 0 .../diffusion/transformer_for_diffusion.py | 2 +- .../il/{ => policies}/dp/requirements.txt | 0 .../dp}/score_unet_image_policy.py | 8 +- .../dp/shared_memory/shared_memory_queue.py | 4 +- .../shared_memory_ring_buffer.py | 4 +- .../dp/shared_memory/shared_memory_util.py | 0 .../dp/shared_memory/shared_ndarray.py | 0 .../il/{ => policies}/fm/README.md | 13 +-- .../fm}/fm_dit_image_policy.py | 6 +- .../fm}/fm_unet_image_policy.py | 8 +- .../il/{ => policies}/fm/requirements.txt | 0 .../il/{ => policies}/vita/README.md | 12 +-- .../policies => policies/vita}/action_ae.py | 0 .../il/{ => policies}/vita/requirements.txt | 0 .../policies => policies/vita}/vita_policy.py | 6 +- roboverse_learn/il/runner/__init__.py | 0 .../libraries/mingpt => runners}/__init__.py | 0 .../il/{base => runners}/base_eval_runner.py | 2 +- .../il/{base => runners}/base_runner.py | 0 .../default_eval_runner.py} | 16 +-- .../default_runner.py} | 76 +++++--------- roboverse_learn/il/train.py | 2 +- roboverse_learn/il/utils/eval_args.py | 2 - .../il/utils/eval_runner_getter.py | 9 -- roboverse_learn/il/utils/models/flow_net.py | 2 +- .../vision/crop_randomizer.py | 0 .../models => utils}/vision/model_getter.py | 0 .../vision/multi_image_obs_encoder.py | 2 +- roboverse_learn/vla/OpenVLA/vla_eval.py | 2 +- roboverse_learn/vla/SmolVLA/smolvla_eval.py | 2 +- roboverse_learn/vla/pi0/pi_eval.py | 2 +- 121 files changed, 195 insertions(+), 712 deletions(-) delete mode 100644 roboverse_learn/il/act/assets/bimanual_viperx_ee_insertion.xml delete mode 100644 roboverse_learn/il/act/assets/bimanual_viperx_ee_transfer_cube.xml delete mode 100644 roboverse_learn/il/act/assets/bimanual_viperx_insertion.xml delete mode 100644 roboverse_learn/il/act/assets/bimanual_viperx_transfer_cube.xml delete mode 100644 roboverse_learn/il/act/assets/scene.xml delete mode 100644 roboverse_learn/il/act/assets/tabletop.stl delete mode 100644 roboverse_learn/il/act/assets/vx300s_10_custom_finger_left.stl delete mode 100644 roboverse_learn/il/act/assets/vx300s_10_custom_finger_right.stl delete mode 100644 roboverse_learn/il/act/assets/vx300s_10_gripper_finger.stl delete mode 100644 roboverse_learn/il/act/assets/vx300s_11_ar_tag.stl delete mode 100644 roboverse_learn/il/act/assets/vx300s_1_base.stl delete mode 100644 roboverse_learn/il/act/assets/vx300s_2_shoulder.stl delete mode 100644 roboverse_learn/il/act/assets/vx300s_3_upper_arm.stl delete mode 100644 roboverse_learn/il/act/assets/vx300s_4_upper_forearm.stl delete mode 100644 roboverse_learn/il/act/assets/vx300s_5_lower_forearm.stl delete mode 100644 roboverse_learn/il/act/assets/vx300s_6_wrist.stl delete mode 100644 roboverse_learn/il/act/assets/vx300s_7_gripper.stl delete mode 100644 roboverse_learn/il/act/assets/vx300s_8_gripper_prop.stl delete mode 100644 roboverse_learn/il/act/assets/vx300s_9_gripper_bar.stl delete mode 100644 roboverse_learn/il/act/assets/vx300s_dependencies.xml delete mode 100644 roboverse_learn/il/act/assets/vx300s_left.xml delete mode 100644 roboverse_learn/il/act/assets/vx300s_right.xml delete mode 100644 roboverse_learn/il/base/base_dataset.py delete mode 100644 roboverse_learn/il/base/base_model.py rename roboverse_learn/il/{runner/base_policy.py => configs/base_config.py} (100%) rename roboverse_learn/il/configs/{dp_runner.yaml => default_runner.yaml} (65%) rename roboverse_learn/il/configs/eval_config/{diffusion_policy_eval.yaml => default_eval.yaml} (100%) rename roboverse_learn/il/configs/{model_config/ddim_unet_model.yaml => policy_config/ddim_unet.yaml} (81%) rename roboverse_learn/il/configs/{model_config/ddpm_dit_model.yaml => policy_config/ddpm_dit.yaml} (80%) rename roboverse_learn/il/configs/{model_config/ddpm_unet_model.yaml => policy_config/ddpm_unet.yaml} (81%) rename roboverse_learn/il/configs/{model_config/fm_dit_model.yaml => policy_config/fm_dit.yaml} (71%) rename roboverse_learn/il/configs/{model_config/fm_unet_model.yaml => policy_config/fm_unet.yaml} (71%) rename roboverse_learn/il/configs/{model_config/score_model.yaml => policy_config/score.yaml} (81%) rename roboverse_learn/il/configs/{model_config/vita_model.yaml => policy_config/vita.yaml} (85%) rename roboverse_learn/il/configs/train_config/{diffusion_policy_train.yaml => default_train.yaml} (100%) delete mode 100644 roboverse_learn/il/eval_runner/__init__.py rename roboverse_learn/il/{ => policies}/act/README.md (83%) rename roboverse_learn/il/{ => policies}/act/act_eval_runner.py (98%) rename roboverse_learn/il/{ => policies}/act/act_run.sh (90%) rename roboverse_learn/il/{ => policies}/act/ckpt_dir_path.txt (100%) rename roboverse_learn/il/{ => policies}/act/conda_env.yaml (100%) rename roboverse_learn/il/{ => policies}/act/constants.py (100%) rename roboverse_learn/il/{ => policies}/act/detr/LICENSE (100%) rename roboverse_learn/il/{ => policies}/act/detr/README.md (100%) rename roboverse_learn/il/{ => policies}/act/detr/main.py (100%) rename roboverse_learn/il/{ => policies}/act/detr/models/__init__.py (100%) rename roboverse_learn/il/{ => policies}/act/detr/models/backbone.py (98%) rename roboverse_learn/il/{ => policies}/act/detr/models/detr_vae.py (100%) rename roboverse_learn/il/{ => policies}/act/detr/models/position_encoding.py (100%) rename roboverse_learn/il/{ => policies}/act/detr/models/transformer.py (100%) rename roboverse_learn/il/{ => policies}/act/detr/setup.py (100%) rename roboverse_learn/il/{ => policies}/act/detr/util/__init__.py (100%) rename roboverse_learn/il/{ => policies}/act/detr/util/box_ops.py (100%) rename roboverse_learn/il/{ => policies}/act/detr/util/misc.py (100%) rename roboverse_learn/il/{ => policies}/act/detr/util/plot_utils.py (100%) rename roboverse_learn/il/{ => policies}/act/imitate_episodes.py (100%) rename roboverse_learn/il/{ => policies}/act/policy.py (100%) rename roboverse_learn/il/{ => policies}/act/scripted_policy.py (100%) rename roboverse_learn/il/{ => policies}/act/train.py (99%) rename roboverse_learn/il/{ => policies}/act/utils.py (100%) rename roboverse_learn/il/{base => policies}/base_image_policy.py (100%) rename roboverse_learn/il/{ => policies}/dp/.gitignore (100%) rename roboverse_learn/il/{ => policies}/dp/README.md (83%) rename roboverse_learn/il/{base => policies/dp}/__init__.py (100%) rename roboverse_learn/il/{dp/policies => policies/dp}/ddim_unet_image_policy.py (96%) rename roboverse_learn/il/{dp/policies => policies/dp}/ddpm_dit_image_policy.py (93%) rename roboverse_learn/il/{dp/policies => policies/dp}/ddpm_image_policy.py (97%) rename roboverse_learn/il/{dp/policies => policies/dp}/ddpm_unet_image_policy.py (89%) rename roboverse_learn/il/{ => policies}/dp/dp_run.sh (81%) rename roboverse_learn/il/{ => policies}/dp/models/bet/action_ae/__init__.py (96%) rename roboverse_learn/il/{ => policies}/dp/models/bet/action_ae/discretizers/k_means.py (100%) rename roboverse_learn/il/{ => policies}/dp/models/bet/latent_generators/latent_generator.py (97%) rename roboverse_learn/il/{ => policies}/dp/models/bet/latent_generators/mingpt.py (94%) rename roboverse_learn/il/{ => policies}/dp/models/bet/latent_generators/transformer.py (92%) rename roboverse_learn/il/{ => policies}/dp/models/bet/libraries/loss_fn.py (100%) rename roboverse_learn/il/{ => policies}/dp/models/bet/libraries/mingpt/LICENSE (100%) rename roboverse_learn/il/{dp => policies/dp/models/bet/libraries/mingpt}/__init__.py (100%) rename roboverse_learn/il/{ => policies}/dp/models/bet/libraries/mingpt/model.py (100%) rename roboverse_learn/il/{ => policies}/dp/models/bet/libraries/mingpt/trainer.py (100%) rename roboverse_learn/il/{ => policies}/dp/models/bet/libraries/mingpt/utils.py (100%) rename roboverse_learn/il/{ => policies}/dp/models/bet/utils.py (100%) rename roboverse_learn/il/{ => policies}/dp/models/diffusion/conditional_unet1d.py (98%) rename roboverse_learn/il/{ => policies}/dp/models/diffusion/conv1d_components.py (100%) rename roboverse_learn/il/{ => policies}/dp/models/diffusion/mask_generator.py (100%) rename roboverse_learn/il/{ => policies}/dp/models/diffusion/positional_embedding.py (100%) rename roboverse_learn/il/{ => policies}/dp/models/diffusion/transformer_for_diffusion.py (99%) rename roboverse_learn/il/{ => policies}/dp/requirements.txt (100%) rename roboverse_learn/il/{dp/policies => policies/dp}/score_unet_image_policy.py (95%) rename roboverse_learn/il/{ => policies}/dp/shared_memory/shared_memory_queue.py (97%) rename roboverse_learn/il/{ => policies}/dp/shared_memory/shared_memory_ring_buffer.py (97%) rename roboverse_learn/il/{ => policies}/dp/shared_memory/shared_memory_util.py (100%) rename roboverse_learn/il/{ => policies}/dp/shared_memory/shared_ndarray.py (100%) rename roboverse_learn/il/{ => policies}/fm/README.md (64%) rename roboverse_learn/il/{fm/policies => policies/fm}/fm_dit_image_policy.py (97%) rename roboverse_learn/il/{fm/policies => policies/fm}/fm_unet_image_policy.py (96%) rename roboverse_learn/il/{ => policies}/fm/requirements.txt (100%) rename roboverse_learn/il/{ => policies}/vita/README.md (70%) rename roboverse_learn/il/{vita/policies => policies/vita}/action_ae.py (100%) rename roboverse_learn/il/{ => policies}/vita/requirements.txt (100%) rename roboverse_learn/il/{vita/policies => policies/vita}/vita_policy.py (97%) delete mode 100644 roboverse_learn/il/runner/__init__.py rename roboverse_learn/il/{dp/models/bet/libraries/mingpt => runners}/__init__.py (100%) rename roboverse_learn/il/{base => runners}/base_eval_runner.py (99%) rename roboverse_learn/il/{base => runners}/base_runner.py (100%) rename roboverse_learn/il/{eval_runner/dp_eval_runner.py => runners/default_eval_runner.py} (88%) rename roboverse_learn/il/{runner/dp_runner.py => runners/default_runner.py} (93%) delete mode 100644 roboverse_learn/il/utils/eval_runner_getter.py rename roboverse_learn/il/{dp/models => utils}/vision/crop_randomizer.py (100%) rename roboverse_learn/il/{dp/models => utils}/vision/model_getter.py (100%) rename roboverse_learn/il/{dp/models => utils}/vision/multi_image_obs_encoder.py (98%) diff --git a/.gitignore b/.gitignore index 8ccc3c121..1a246f924 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ checkpoints slurm.sh data_policy/ outputs/ +il_outputs/ eval_outputs/ tmp/ results/ diff --git a/roboverse_learn/il/README.md b/roboverse_learn/il/README.md index ee9dec7fc..7a2cbbd9b 100644 --- a/roboverse_learn/il/README.md +++ b/roboverse_learn/il/README.md @@ -8,12 +8,14 @@ Example: ```bash # From the repo root -cd roboverse_learn/il/dp # or fm/, vita/ depending on the policy +cd roboverse_learn/il/policies/dp # or fm/, vita/ depending on the policy pip install -r requirements.txt cd ../../.. -# Run policy training and evaluation (example: diffusion policy, DiT backbone) -bash roboverse_learn/il/il_run.sh --task_name_set close_box --algo_choose ddpm_dit +# Run policy training and evaluation +bash roboverse_learn/il/il_run.sh --task_name_set close_box --policy_name ddpm_dit # Example: DDPM + DiT +bash roboverse_learn/il/il_run.sh --task_name_set close_box --policy_name vita # Example: VITA +bash roboverse_learn/il/il_run.sh --task_name_set close_box --policy_name fm_dit # Example: FM + DiT ``` We keep each policy as self-contained as possible (code, dependencies, docs) and only share the minimum common abstractions. @@ -29,13 +31,13 @@ bash roboverse_learn/il/il_setup.sh | Name | Policy | Backbone | Model Config | Ref | | --- | --- | --- | --- | --- | -| `ddpm_dit` | Diffusion Policy (DDPM) | DiT | `model_config/ddpm_dit_model.yaml` | [1], [5] | -| `fm_dit` | Flow Matching | DiT | `model_config/fm_dit_model.yaml` | [6], [5] | -| `vita` | VITA Policy | MLP | `model_config/vita_model.yaml` | [7] | -| `ddpm_unet` | Diffusion Policy (DDPM) | UNet | `model_config/ddpm_model.yaml` | [1], [4] | -| `ddim_unet` | Diffusion Policy (DDIM) | UNet | `model_config/ddim_model.yaml` | [2], [4] | -| `fm_unet` | Flow Matching | UNet | `model_config/fm_unet_model.yaml` | [6] | -| `score_unet` | Score-Based Model | UNet | `model_config/score_model.yaml` | [3], [4] | +| `ddpm_dit` | Diffusion Policy (DDPM) | DiT | `model_config/ddpm_dit.yaml` | [1], [5] | +| `fm_dit` | Flow Matching | DiT | `model_config/fm_dit.yaml` | [6], [5] | +| `vita` | VITA Policy | MLP | `model_config/vita.yaml` | [7] | +| `ddpm_unet` | Diffusion Policy (DDPM) | UNet | `model_config/ddpm.yaml` | [1], [4] | +| `ddim_unet` | Diffusion Policy (DDIM) | UNet | `model_config/ddim.yaml` | [2], [4] | +| `fm_unet` | Flow Matching | UNet | `model_config/fm_unet.yaml` | [6] | +| `score_unet` | Score-Based Model | UNet | `model_config/score.yaml` | [3], [4] | **References** diff --git a/roboverse_learn/il/act/assets/bimanual_viperx_ee_insertion.xml b/roboverse_learn/il/act/assets/bimanual_viperx_ee_insertion.xml deleted file mode 100644 index 8002838c5..000000000 --- a/roboverse_learn/il/act/assets/bimanual_viperx_ee_insertion.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/roboverse_learn/il/act/assets/bimanual_viperx_ee_transfer_cube.xml b/roboverse_learn/il/act/assets/bimanual_viperx_ee_transfer_cube.xml deleted file mode 100644 index 05249ad2d..000000000 --- a/roboverse_learn/il/act/assets/bimanual_viperx_ee_transfer_cube.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/roboverse_learn/il/act/assets/bimanual_viperx_insertion.xml b/roboverse_learn/il/act/assets/bimanual_viperx_insertion.xml deleted file mode 100644 index 511f79471..000000000 --- a/roboverse_learn/il/act/assets/bimanual_viperx_insertion.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/roboverse_learn/il/act/assets/bimanual_viperx_transfer_cube.xml b/roboverse_learn/il/act/assets/bimanual_viperx_transfer_cube.xml deleted file mode 100644 index 2d85a47cf..000000000 --- a/roboverse_learn/il/act/assets/bimanual_viperx_transfer_cube.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/roboverse_learn/il/act/assets/scene.xml b/roboverse_learn/il/act/assets/scene.xml deleted file mode 100644 index 0f61b8a5f..000000000 --- a/roboverse_learn/il/act/assets/scene.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/roboverse_learn/il/act/assets/tabletop.stl b/roboverse_learn/il/act/assets/tabletop.stl deleted file mode 100644 index ab35cdf76426b2dddc433afa11af96ebe0e07c17..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 684 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 L5+uyeJY(`Nju%Ka diff --git a/roboverse_learn/il/act/assets/vx300s_10_custom_finger_right.stl b/roboverse_learn/il/act/assets/vx300s_10_custom_finger_right.stl deleted file mode 100644 index d6a492c2046d3850ce7c7d650e951701b93bb87d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 83384 zcmb511#}fjw6%i=2@oW>C%8L-R3(KD5+qn~_WB^EWd&o^lM=j-XM?M|KSzb}|S z!*GNmfr>jpv_^Qz7<-YRZRJw2AOAezM+}QV#hoD9Cc^g)p!4vmnqWi>66&bP^?3V5 z>-;`093f*Mfr>jpF>Io0wXRl-op;W}AVJ&8Rp;gfE5@&F`^p$dpyEytZ4+fOHMC-k zY?jK21Z^wVxIO2s7`}Hp$rwZ|shci<2vpn&qHQAEnesea<#GWJa+lBuqLu4w*f}f4 z`rOTB44c@oj0jZR38HP{R)viG`HOSQ_MNGt5kxCjnm^B2F%tEyFJllvcaR8F+zFy> zBFl(-jEJLiJ7@&a%60n2-&TxHIcmxnNTA|Q5N#9xJegy~XnMG>6A9W@u7yueS}{&E zt14q4fr>jpv`xHC9p8#k{OM39611&cM;;uvVl@4!vW$TQD((c)HgWDkk-c=SUL6?j zM1r=JtMtvIR*aC36axuV+zFy>B6Lra7>bc9WUvzn+E%X1w~tsc`gEx-V<3TwJ3+Kf zB)=bH#mF-;!ifZJD_5IuhpZSY2iK7?kU+(qAlfG4RY>fl^Z5HqxDyH5R<2Ez4_Yw_ zC2TBXAc2ZIL9|VL%2mLMk*aX86A9W@t|@!=TQRa8Y9nJHfr>jpv`sWCQr(J?;>yt0R*ZaGKExnF+sc(?fN903ziNn#fdne<1kpCp zy-lPQqt1*qF-Xw1a*h4Vuwv92F-FEf0u^_HXqyPD|FaciO3ETJNYJ)&4UDv7j2JgX z#y|oUcYa6BNTH;A!Vb zs3(zJ>b(j0d$Wvz1S;+X#jpu@YdI3?jUtzN|Ff6aDPtgkiaS9uYywIjj)W>1$fe4$ z8~ygk7)YSvPEZV+fKrqrp-MP%sq*;aFsF=x1S;+X#jpve4LB02q$ZcDSJIX>Weg%} z^&Jr)YaEV>J3%pQf@-yup`!z2J*W|?#vzxgAID9l7*G##RNM)QVH4kL=O|g9YlNyf z$))P~<6{gN0|`{z35sD8-&+|`vR%TFP%Qv*srE`#o){Se2~^w(ieVGqTT4;0eZ`Sb zEg5pDmRgBH(J}@RqlWcZP6R6M1jVokyPeo@xoq`mgldtIOSL$!E#4+$Ac2ZIK{0IN zdwX}eY`t9{$wVSp%AirS(l zHceG<5fv%YmCta}}p)X}C{P(^S0`tjMI4Ve} zcg1eEA4)uey&g3o`pTXG8iB5p9sf4NYXykp8*9k(xFmZMew!x*)|I^pRFF{birxRn z`REMe$0kI_ui8~3&^15!jF~1lK*Z}%SH?&y-$CyT69f0jUJ)utsCUKgM_p~1iubNC zG5VfgLybUJtrus_J;eh=iIGiZjBB#5R&w#gz-ZZ5Lj?)-uGsy-v3X1Il*cDV|8lat zMxbjzm-FV)YyqO*q4qL{pX{Y}t1~I^mh5w)f`ock?0#*<3w8MR;gh1Xm&vFR=&Jqu z1@lmn0CDzuZyBSA>^m>ZFgb9j>^q}^gnC!({`t=N9e6;!$75Q{kO~Z@Q>mAcBProQ(0n;1iGrH zxM1cE^A~;nH^>>htQGw^3wf1rYddXDWns$~9Po-FsQ=(>3eXau^FZarttX8z(^ z@Kzav-obOuS%EiY&4~&U>V359+ES--a_=XzqT3g*t`X>3mi?^Rrij02*lL%IaaEQj z-6CfP?vgD4RFF{Rirr@DzwQG|HhXsTy5X%g0$ux?oHi5X^cRO4@0BrpWtkZ9d3K;7 z+a;(Vp~^?Q?GrwE4ddzNMDwRr1{EzcvSf0TV5NT4gM&0lb>s>*gdU8_X0 z-Hr+p*oJq6Hl4gCCUq72ejtIaWc?1qJx?G7-9o*lhj zK37PfEB}Ur@a%pnpV&#y?%^-t92F$6Pvr+g` z?_iR`!5kGNuy5!HEtWsnnRn8x=mzpVM*>}AjD1j+G>{`jRF-(1&B;+g0{gg*(8eFb zo!d9hjGigWDXrl>b;%PZjL}g;OEgu*vNMK*x5!z>5g!Aye8PTO>xr+q4Qa&}H zY;P+^il}U_He(G#1qmE;aD>Wt)tPGl^yp@?{y+j@gsjhFBgoZk ztpRNYIuB|y$n%In1^27Y$sQx?@K;~wk}XrB17*7e33NTJY(m?I?gzDf_Q#qqdSwLv&eQL z66k8&VL!A*!{qxxZP9pgBo!4T)VpGjy^haY!pU+^ik>UmyGWpGKVH2b8%62;v=qfe&5cCZ`%kql)21n$`HY!M{vdbO|&;963 z%>FNS{{snhed}}t`Zx(?xl4VV%yNVt6(m#{Y>(+5PCO!J#}d0Ag#@~=pXdm!F6$5K zt0k7122_wxb%~vuz^vuGnMR1pbJDiznRe=A|6R*kTA5K)+zFyJ;(MO+$G(VGE*0A| z&xwjVL9}O{6A5)xL{Qce3EEb!Qv(lJF@EGZ6@iL7LG+J2ClQpjM1r=J zD>};|D~4yD6BT!YXwN(+611&cpPwAIV*JQ+jvDr48D&OMaVLoWk>~8lzAU3WCla)+ zTqSP&WySE!bE4u-5bc@gM1r=JE76@}Rt(QPCo1j)(VlruBxqZ?=07=M#jr~T7~gfG z;!Y546EIG#5kxE3>xZYT7@m1fRNM)oJ@cGM(6(|#oH%X8@XT|f;!Y6lndd};wv{W} zgtJzRA9>Cl<8Jv=W)u~7g6JQ4&bFry`BR<~3EEb!;MC`>7@m1fRNM)oJ@cGM(6(~T zX>!4e;hE<|#hoD9GtY?xZ7bL2Mi;FZo_S7G+zFyR^PEV~wsKWZaLJ0{ndd~sogmsX z&xr(WD_5iDb_~xvCo1j)(VlruBxqZ?)ZabxoT#`HM0@5rVJE}iJ#U_QPE_0pis6~(L_$4@jE>(_s<~dPuCn$zzo)ZaG!jVgr$DVmkRNM)Q z;hE<|LY36yQZRsit!`Qsj`+xsFn=5RJ-d(o>MmZtjs7X z?gYj7k>@0W@|;Mh78$uz`|(GfQxT}R6BOe|o|6d5b0VQymgG|H;2(KTMWEtNP>dgW zP9i94iG*q)lS{SdJ@cHXxDyn^GtY^HYUz_p^-Js#ddzd8;!aQuo4`CL5~{~SF4e#C z%yXjRPEZWbJg3YkhNwKJdZVZ{DRYw{appN)3KHsFi8asZBG4s!$gbFWo)Z-$)Vt!D z=R^WsvfmyOd!7>&B-Fd&ndd|TU2^;(B=$TfDoChz#WT-|1iIunN=WQ^PE?Rk?}}%h z6A5(5@t}~{^PH$4q23kGJSP(9lH*z-vFABaK|;MNo_S6r&?U#`LSoNzqJo5aS3L8a zNT5rOGls;T=R^ew9M{nEoJgQcj+chSp65gb2^^o%^PEVaOOE@7#GdCw1ql_;GtY?x zy5#tENbGq|RFF{D%`?x51iIund`RqhPE?Rk_un(mi3GZ2J|QIbJSQqhsOQ)-&xr)O zWL_jB_BM-ClctA^MIhP#XKh}NT_YkJZEf# z<;ryl+6X+r?6F1Qa zqLr)Ze8buFCI$E1(e&A+IntiEO)J@RIi3$?G72I#d`1tx? z8H0!>voq#(C(uRP6vHM~4qp}X_Q9@z@V${vRFLR!{h$>i)x&c#1`*L8wk>xj&_&x6 z!zS)-XdW|T-0tNWk_~pEg2cZokJvFXACobNkay6_oj@0DTQMwR@W?~7wlAvNv0+YB zka$1*m=$CC+Cwr15&s>m7T`{xi?%6-O?WNeTFGm9>1C;340ob}ME&L`tr)A3KD((J8Q+Ld+V}{LBxZWZ~ff~bkR1&u!*lL_Og;kpZb?P8sS6* zi5K%PSTRoi{Yb_jB156>0qz94Xq#f##G7-k*{Jhv0_1;ARFLSB^|BS?YGW_?o)ht< z$h_t51iEOOV%S9Ks_A%4q7*A`Z>r}+1qo;Gt5%Hg**-D`5rsb`4{#^YMcWj^CN3r_ z%y(Bfu=MWSASWtFjF@`eigE5mRvCkc0S6l{cPG$A+Z4kliZ>47KRwyB{KoNoPE?Ti z7IDLhF}ZY68H0$#MYb$;C(uRP6vHO&JqY8uYW=kQ_Tp4dRFLTDeanh*C1;3?K}4aM z7X#c0bkR1&u!)(2+w*?@n*yp|eH4QV5}%vgwqp23Hj*)j$WZ992Z1iyrWiJ{Or83wkd{93_UQAHysof5WF!m1{EZxCA?$B zC^{iZ#vmfui{@_WVOWj$Upr_Yaa@@wkjZ#K58xJP35D62m5_^qElexW6oYI4Ve}vg@ztb20`I@>&JB6X;SU zpG{CH+Hm)x21l7)`CmYLT z?aWa@Le=dzqMyhZL{O>VPM}M*3~Yj08AQld21f-6)kaCO?zxQdyo{Z;*?j6p=jqSL8;r4i^-Ej63?-cF=eA4dfV)n=?ao?;NuW>#qr z0$r-bX%p0X9Xztya@l(2s34)*zMFcykTHm;{=T)pJAp3Ma<&O-g>MZDqINq+1qs!L zfBycdj6p=)c@e9AffsqZ^}H9F^G_D2I_xk1iDmD#wMs& zG_GPK-@kWhW9P9^Tj7(~dnPi}VtU8+ZB6V$t_TkF#@*}LPYAfft(gTMSEV-Vr^ zm@}_CfiBe-wF&B_ri@9OSN2jlDoCjQ?v_m#WsL7VVJ{+3aVMyDwh4OwWDQOIXD?a} zMm0Unr&aYX)%vs&js9-W_$9j{P(ea%s}=z5-HH(v8$m7=+s^!83{-HxSZh-&gZ}B~ zLHj}%*1;ALr3hN1MrTMlI_08vn~H7EL$*S=#UKR<70+&w(IKfAxG!|6b5iZA9|Wyx zqobniq^ftRRg6cgpE)W>sFs1<-o^7k0$q4cdJNnz9-*qW?Y;rVKm`d}QAg)zXL#cx z(4}f^yN`neDoCh0*zS2DfeI3|29J))&TYpRFI%Gcof6V(Z@xgOVz=4 zPZ$YQkf1eq6vLit5Ep?iRR`NWdL&RmLe;4DI0F)>AVI74=&0;j7I6{iQZ?%L5f%%v z%Mw(OpjCSm!=5dIF_1u)s!_j>`#=m-kf4 z(M2n`h_EvzNT5P%$;d^y8{AXeM}-8fb|YHNT&5`Y8-@Eqm-@SBn*kLh)V7-I?1~{b z>@%l*p-Ua1Jr9}u>ZnjrSApgTVJpP<`W*L#E_FBU8NYN$sy;^o zUFzwv+i(~I6(rPCY0vn@7)YQ?y#;n#6l0)*gnGN|8NV0<33REq*=|!KfeI3;#IRfX zcvPq$p-LiqjxfeR0$r-)vwJKU0~I7xiE7Vhz!*rNOO@c?`)XDYyJd+A5~^nSF)u?s zB}kx4)kNQWkPrhEBvcJ&&o99}{4z_#3NT7m*s;TXHGI0^;QY{0!M~?(5NT~LT zJi+B3G|BG9E; zwf6WG5~v`d+S>N08WN}=p<3kjT(!6ebg3SIJvxX4DoChahCSylE&^SuFJg~!B7q7L zs>foFy&{1M5~@dL_c>+X*&5mY-gge68j|V{StOUAfn_{@ekS&77kWoQG))H0>*En^o1iEOO zVz|b_ErQ0vQ9(l15>^b?czdh_x@enXxW@FIbU$cJ9~C5IEn(jeE7Kse0I?G2qHXJb zsN94_P;LShBxEgN#c<_6VkOW;+Z4l;V0tr(QmM+FIWZ(Q>LtQhVDy40O@%{8!M(Ch?MkWkN2o)q$UE-RzWW{hN z(4}f3*W4(o)uL!_6e>ulI?*-%O4WlJfi6{}y5@LUF=&n#DoCih-8GNQis4S6OSKGK zbJeUEG*=B3Bvc#4H6PB3;ZC4SwQyW>?yMLz=MEJlR9nk6ug{9%PM}M*)Le51snr)n za|cmDLbVxP^AlA&Q6tc$TAZ#qkgD}cGa9WDnG_^c+t(hamRaOj33REJvrWjHFq7wD z&E-S|3Dt&o%@>W8K$q$PxaOo<1kFiB1qsy`am{;;l|Yy3$++g$S_I9lMFk1fr*h5T zjg>%`>XEtT2y?ntG)EW}BvjwfH4iyf0$r+S>6+_o5#Q%JTlbt4B&f$qwX;344LxC5 zruv2`ZyvyN25?dLfv!EQD-F3 zrJgs>F=TbE=nkr@K$&qB&vnn$)s_GM@suE;u86(9R@&r0)sFi?pi5m5&zvwS>YOM? zi?Kbj0M=Ec<3&PUg;;Y%7C{OU>RQE`!LkTakf7CvbO${%ytprPVHu;J2P#O=>O+d* znc+nOU07D?F;GE*Rv%If&wMQs=)y8zkAVsjwEB=@c;;)7Ko{0sdJI&Mz#3I2P(gy$ zn9xyqWO$>HKo_QO(0$tcI z(PN;31g&tyuTLYqP(gy$`O#6?>j$l({d--D1iG;Is>O&x z1qtj^YlIgnNYDyLIx5fHHWKKmbc6e_V1YAoYNt)>W4kWkyMY*P&KvZ%mLgNx>U;`)~vo2gA^oGi^DZ@z;(}YU+7XT zoLFZkSTRUJLbak|oikw(q#&VMXP$Ena9`+Bt-)C5epoR`K|-}MJ?9$WzR;ywwXPW{ zUaAyD1qsz!_MAC@1iDn~+_P7N3KFW-Z}-)%Ou4j*jw)T6Y%68Y#H2nl^-N`-!`i0) znfkkzS58x_apd{f1Sv?!*cR=1R7jvpo}Wc{T&pNlA^aUT^D@>#HG&*uvF zg)aH*T7>J_jrx8Eqfn7gzU88K1@{#9Q6VAUO^fz?&v9Sq^749Z5wYEm9|{t(jIjvM z@(TBbE?M%~F|74C@wO(*f zaUT^DvQ1>sp6x5#7rJCi%_3Z_KG#v9qP8i!gnNqnsE|poY{~2LYI1*J?H5ofvz8=QdF?}QK{M>Di{e>u6WMV$92pmCAh~Ndii|)I1hBGnj!WX@pM$As2Ybx{&7!n9~Ba+w)2>+t{oM+ zR4p3oSf5n_kfLg58Y$F|s_yWcRtsQ5g2j1JiH(SDB-K??ham0zI@5|R4_klFqfkLY zZimn{lqHDUbLB?3ToaP6`o8TJ!wVJMuloC>;)(R5auK8;A>V@UF>?No#1B`#3M)CC zN8fpagnrLa!Tri#e+stGe9`|%{BTLa_(8}!n4iks?|0BTUb<8A4qDsa@A>!haKE~E zwn$L>kz#m0S4f~sK9v^X`Rt-1pLWaj{r&hq92FAOo~NVodrfi77aSTU&1 zz^76tP(ec0J{Ik{UJVI!(P#qQ!C31-mm+IUYn$p<+*8~~g@mlBE!v*RAY9SbvYT&EQgGE5q8Rw#fhOsF0AY8jJR5Ek)tJ&;@@t<2-W{3E4WcV%T$v z=)7Y&a}!-^+tp4~gf*T>cZw7hgW8|}e@8`xx(cq5I~7Cz-}Q`|=u-E?)uvWN6e{Wt z(%2jBDeh~9gnD8;$Kh~a=u*$FtN-CTDpb_dPW>9(Q`|>|gnGL?$H;JB=u&U9$B0)H zDym$eJ|ONX?xR9Nm0hv*Qh(e*bp0rmVi`5Co>A(*BB4rD&(SE{7rIml?$JN@LPgaE z)bGbVxu3aef8;cgP_>WeND=M}U8=Tojb%iscMug-bJ7?I?kVo0LPFKlcK%$B;#e(! z@1r=btBWob!*g~5wkEv12AcHFdwCrR!S;%NRHz{F{`v*0UD7V~QoFU}I&)Nzz_y(p z0~I9Xv-|xB?G~9T*S+LF`wmjuM{Ub1A57bnV^OVFoluH9;pLTI{<3wL)fSDLP%fgU zcgZc(n2b)?SJXZdFR!N3rMBaWf&0~orsDeG`2WjMsUuNmuI{<(sG?B8W2bHVXzjPw zBBY!C*;kSFg)X)2iowb2KAtFD>aLlzO;fKLFXa=MY$8oMSoX!) zgK*t*>+0Hw9)phL`;%{7UHi-r7aGr=M)l#@-j`*WoYB1W2OqKh(EvejaIteff_f?E zGsf^&7kxwy$3WTtIP-ZPdzP;k_nu#qp@PKYWh43hrat12*+V7q>cklKPu)Je(UuT~ z3KGB0O2;!qq!J^3?I($CMJKX9PDgSU%rydCyMD>dCZ|p<7N71biK1J_vs%%S+&8!? zLj{Q@$?nG#DV0{FJl{nUM`|r#!zT9P`;J%C2y~6Q)Xus2MOtzGVP{E<{^u9AYeX;p zbA}Lx3KDlpo6dGcGKfdVn@b{d$ZEEyLTLOPxoQT3t>OJNF-D;z=hy@LVyCK-Z|tON_CF{Y8-)WhF7V z*k3F%X=fh&M`?x%5-(yl83RY<6`@6oN+KrWH2bMvM}DJnF^xc1sUe23bW=VtdS_ut z?7wu1h0N>7`@Am2P(fnd>1)Qf;e|y|^Q@9cUi${S-K!nHHMM|7plfQTr^cd_Ma1LX znI(~U>vgvI*LM7E_M!|GBo{Avxvlai^xiCWoi5WwSn9n;@l277&N%&QG&5opM$4EycJg?09$Y9NLj{S5ZB@;J5tYT7Uh?-ELa*%l%Hq{;#_ui6 ztr6(TR=lRUu38o040B52=ht7@t;l9P@oImD3KBaKH89&Gt1eQc*&vByr{nQdADi-n zC39*7x~|S>Yz9B6E?VAND~UtB;_*pY!}*)?xfv=*JQ&!@Y_PMYxI1xyBz%W?@nj3a zdDR_xGy+|R-?T6X?5ZJ3wO=oZCveO2cLmxDdFszF z9Mq66nwXBEf<(gVJdEJ-*Z)AE%eQEMb8OZy@i}E3N%U@%lvh03kmu=>o}q%o z=&@bRpBvQ`x90biM3R6M{6Lv7ez<2UjX+oS7LjK0Obx_fzXp={v@I!ry0{@PIUp@V z1&M@*I+&xHg^G&<21%mim=wH7@-Tj_ua8EctKh^Q=H>MD#rtecByrv^8BZy%RfR#m z3>73sCu(C(Y+Xm3Xf#X`EAFP?iEcLFUk4=D2y~U*+1VVGrk>~@&_WVl1|;JP>o(+N z2Bu=DAd#qQb8}{g+VWbBkVJ=SKD@@(2E5?Fq#A*)%J_MJS0bGNS94kSM|sx~&ntG9M9trl^Zlp7 z_|A4o87jEn#FcB98wS>p*J_d^iXTtOyML?CA2*Aq5$NjsdtLKvyjtRM&3=+-)Gh^| zFd&Rysgj7Hf<%&i%-lP&n%Gxrx+L~bPsN)quFnJeehfkaT~j_*HS>O|A?}9{m&Ek^ zKKydlFy0|^JcbGqNkYn)$0k=1JALO!BC>I6zCXA=PoDX|AdLXmipG`9L9eTc$gQI# z@$tD2U+QeYyBzuugbME0+bCpSo-4$L=kp~|I-4*5<9a=wbmc>hKv&G>lIHQJRmAh% zR5DE6nUY^_-GJ9+FN07)!kgzdA1n$M%TD|*3GchU{Mvwe{9Wc-IsvYWDGQi6?h6sI zpUSK1BUAD2U+VL6zdi^;1^1gYCZqXw4HNU%Qs4RE`ZPQuRXskp-zAMe*NbjB%sw}R z#p9#1B$201YW~wN_4&0MH-b<>B3V!>Gtst6V%mgNk~lLUEe~E+m#^M_N+Zyf^l*Cf z&}AmNp7})*Su*+Z4#n&9&)ye)Vk$6HGzQEzJ+p1f&2zBJjvAXJdZ(D$xU{A^j#JI@YDjJuki|5-nj*Z60v zMxblO_Lqk9V0rQHgO!qq8kLq0xL=pAyuCXJ6(lnDIb*QvrA4QBdnD0xwI6?dr4GNg zeXT~I>tfG;j5N-&qV0=yk_g+Io^Kr*%6B<71fhaNx)g_v%RZ&Wsl#+0orYxKOIy|9 z+XpYv2z1d{&9Dh&g*}RM=*$<~ z%fe7WVnsj-DUFEzT*}kTQ}m7hGmfQaE?J0ZyUu(X9o3BQo>wMTQ6q~4c|M~kD-FZ()EXobl(b#F-?6XakbljEX&wdd}@rZMxe`o#6_cUucBh@v{aHv zcHjlueyA1i>F{HyAh9p!U8DWB;$r&5B$6o9`yDHsrzOv`&sQVRl{WCLQMPPJ@!z^c zl1P5y4cpwQCHHON$527SNEP2KFr~Bz$oq-P#FC#`+M~^R3DZ|2&{fZw%-oT^tY{PR zP7+;bePoqVx8T)6{1_@oJU{1ab{tz?WV&-t62Z;A_~yaQc)dNo8iB6PD>IuL@&}3& z=Wk0QI?Fc}xW5^nm&cExg2dAsxy+1U+ zK!%f&$g)lLqlSm`!3q2rDoE@uUeXM?9xTpu+A9hFi3xf4_f7c8RlXX5t^_|-G~+E6 z^8MH)iPmNU9=~c+9)2r5Lj{TO!@|57SXGt`)B-p@I5E$0urc>u=&KRvnh?LX*&;yuzf;ZydeXQ&`?cxjmVZr4xZr{88u;&PZbcP?ne(@gT!2y~ro(A1o{ zuBJ#+ncAW;FO%?B`x^0jv(hqDkib^I-Zwx3UDy|qy`p_5*n@GMd9lrD7?!nn2VHiq zER{{{{9U$SphR_qj#z$`c}?oT&m{6;s30*i@``hC$E>2^a~dOiwCEgLm828@uePs7 zpbN`Kz1&3wi34l=jX`~L2=A9PMwX-2RaT>K2fo(f%aA}9mXCV5iwY74s`NK1tn!!7 zZeAH>g6sfNaVh<&G<64kZ7GNt&9s36hW_|I7Qsib(A?3;W)KA!u+LPj*_ABy`h zB+!NBqh9W!g2aIMNzJTx%8E|^J(4ka!FYUHv1WWjRUd`~y0CoI%Ux8EnDQv2nf6(r zI6L-&jB%rO0>0*cQ~s)v4?_Z7SU&3IE-Fa$%~-%3k%^09M`%>7S;IuU`RXRTb~_)2 z1iG+%)XQB|kSI90g4wZCWiezswZcb>B)n_i#=KZ}ABF_Fuzb|ZT~v_RShc2UoU1Oj z*Pkb2B+ZnR&#cjiCy4Z6NT3VLN4?xd1qmG2&__RzKo^#%j?h2&Gq$XC8-Bn1m!L(> ziWnms<`y*v1c;!3GRE6vx#jpp0fFVCUW%fEL~zMOMrifyvK}li-zY41kw6!gkB-o+ zUf0?0yaS(KJOx7q2`ud#p+$ClX6<&g;Jg2N81(#Egt4l3KJm0cY7v;Rqw)L60CA*B zW`XDD2%Wp=CEMDkHDB2Jt9Ir{?8#ooIQk@ysCgonJdd~tbm3Ky{ivs}SdyEq`2CKb zf>1#sM_^AQU+n-&z+itA%U)V+Zq}3!t=;yH=iUXm->%2FVLFb z`8z&C1qpM_IAhe>ydrzY>=yB(Hb4ShSk5~_XAFJ8CiiO1^IT2BP(cDqZCU!yt}=)%G$>TO8#k#mjAeG7@;wFv~?Cr4n*(MWAb!?@*(U%oSDLLs^;lZxZm!)53Y%n>T|{L1NFHg~qu&g=FiM zYJ*(ozp;2LoAdgEUuguoiY%FCR4q|Jto(=aI43eC;O!oT^B2b+1)+k(^~&pwcH0We zwkWmRtLKz?gAdJlbc-(K2E&UfgKeOl6TkzkeBw$FO>(seFjmISmi*9**BvG?VJf3)NGd>~kYY-|(WRL&1 zG5k_7QK4G`N#rs=vYWG7@B+=f84~Eq`D~vNv!#eQUX|`(!kJ#Y-u-5r-AKStL4vov zW%zU}A^U^zB!RUv66nHu*AaSl@He)(Omp6Ox;H}w39LosNKu81%?!?iPpaF!IiBX3F~b9rx` z`bJ~E<WGhyjWJ} ztR&9wPQ)|GEZM{T-!uYUSJHhp5}hw28WjCk5?v;FbFZ+*yh@*UL8u_{>nR^I; z6b~mJl|+&(3HjxDO?lFX-Wq|fB5zWeDJxbGt75K5BGuExd}4woeDj)w3>72_%ejPR zqaZQ4z#&Os%MuB6VVhBoE#*ke%_dFwN}r?*6(q0)=m=euD?P9MHI%1cygCR+PjT!O z-+xEw+E!`!vVHaVtZ=<#K%!~CLq<2*((j(;m^_cT2y|g7A@jBW`tp@&>+@93T|uZI zF}}oQWAV+>;%iH~R*q3A`5$@1izE2HXjGyDD^ zRFK&5^s#aAU|BJJHMPPMZt~$tribxP|DDwcbT#dG!?5^U4ZiyJhk~rTp84t+Vh>xlEUL(-e{f~6!pmdc)>f14rc%Mk- zQpbn!2Mu2Yp@KyH6aMCX9xQs~StE)4i<9zvjz&EBL@$N}x)xl?X1;z9BnmZ&mc-D& z6g<~`nYB#uAqW*Dx)v&GW(yGFThHZ^h=0MG4?Wh1U%8!-A%U)=>GPSr_Ht46_%=zf z-pP4`$_@FGb6yM;B%&siGsBWq5i_C}Nn&1dZ(crEV}7YfQjI{@$9l!hM>9fXtM5;W zu`(Hd{A)w*=a-0~g2XTPgUyN0s|x?bze)mokVv2l`-ZYde+scAoI(ATS1_Gbs>=+IBLs77dgdqXzzn=#+d6z--z7n)Ac5I4M`&CG zx-b{!2<_7%HQ)WH9)CPH9((t6l$`fSb0BkuU}hYTR?qOFf&^x;<0jCB`DjOI)x^np z{>@=LVXgGq+z*_ifk&$os33u}FXAT9g>xz5o>_x)W=^aLHH+n}D$bXqG311+Ke586 zoAJ}T3o=xYNWD7Le7(4`s5+X;#B{UYvTdbX@Xx0UX#~2A{ngBD4TY#PmD&tQpn}Ba zYs^eBo{NKbZb+h2&X;V|h?ac)r(z5VbUhj!XjXMHk?-OyNz}ail0EF$k`I4egrR~& zy^ST!8J{YOs~KNOB1hC?womq*tM4qS5$O8drKp*&Vns3NEwx_fR(-ajfh?Jg@?r41iGzKlko>us;x=E1||M6S0)tZ9Fb?JV4eJG+%) zs31{gZ)Wp&x-z2N$P|)D+5ZNsy0$G})1$mbpzG3nKQnvB(n7SLp4X9zH(7?cawcGn zvJ4d@7Q9bsj*6E3lI!Uuu`1zZ)-`>5p0r~{jX>AxbBWE{-HVGFLo-MseEJpE_IW$L zCuuo`3KAbTJ~#4|FC_9c$uEforH`?5&pYx|vqCfiT{~XgFy1FCC|({fAc+cljm62DD7z&3_;=7YxzjX+nIL#EN8V_uPaY$-{+O?Qa- z26g65J_IpTkZ93pi}6X0;!OCfq9pE2*voqE>B5^0udEU1+U38@sD3NAIF_C=qfdRD z?Ae+weC|cYP(k8)nyE&O+&M(#xt}Dl^wTDmXBW-9YDwZs z|IMtyf8BV?+k&Bj#MZDdW80+6V&(5mB(c5oQg%YlT<-FsmPVkf!}795&aIh5`87=? zQT4(ynG=rSRZ~}Is35VlbP6M+fS)LmzoR4$ZlBEtw&=x^H?OA==vv^s>D<#jz37;> zlO+7o&1DO__2S=J)?%n2;WJ~3^T|hF5kInzB*y(cn*G~5l0U!MP$ST_u5>ZyfF{1; zRz-&-3S=0|ZcdEkQxDZ;s339eV&%O%yi$t1gN93DX8F!+dwU1}JUCn<(3NcVPS#+6 zkGS9)C5gwcyRw1P9XwUzFop^eCwHXek9VdJDZ@udV#U$EEI5mUZ|h%IBhZC&>-BOM z%Rwx6m%S-#e%xJ2RI2z=mT>9IJY>xaw&tVmm1n3Ru{c#p^I4w?a%Lut`^@flgAI;o z%U@*-(g<|TOIgf(_`HI+QiDcVkU#~A3HkDw7xR=8ey?eaY-hpiZ2A4RJjXu9kU&?q zgn7&YeanhmCzDBH+|%nU={Z>@uCByTL1I+VEM~t=rNpP6l=;~keSuA^*`5cU4ABU5 zP0E_V^iNt!EMw^;;XU*sYf_;-58uccDoF6zNz8#Wi-=j>10*r576bEAUaiB+BP>t)PW)`18VnUAKHPa{JgQMpOx;ZL+ZW2|LJLQA=GPX~ z(Fk-I1zs4NqYDV%_QfPIZJNp64erbvMbu)bAW`N1O(R3ZkP>DoBKFoNYMLWD{?OG?WAn{Db-A zh~S;Qn`s2P3N;;T{1cc}j2+iV5(nllVh{30@YmfNF;tKU*wWhgyKP1>x>Gwz4B9)L zJ;>jSFELta1iI?}Q`xAsEQ8ovzr7?TCZEa1RqVwNC2z)1LE@L_3`U*1X+`_gy(N(; z%Lw)@b0j}IyS+xBYw)lS&Wj;wg}6$@oCYJ=wepd?^Wjzu6(kZoTH<{0Ybr5)5Ut?J zFt`o-|WxYJm!~?3>74*e~gZqmN~ij zulHC)#r=yCCK-ZtghdS$aP9ZKH8ZC*|P0F(( zIr{Qk*#|IGkT_W6qBAsaYLWER&yx7HPAk^f=);rLjL-;l?cbTm*t#*b7~xMuqk=71 zl{tNQhA)u}6(nA7D{1T-pH7Ub+Dj6j{vOT}e(ufh{M1Dw&{g-(I!3v|>BZ8)Jtfhk z-%$4LYHwcQeh-EU5}(EmF&gE_Bx(<7OGnjr8he+n7oXEgK8dg|bamMv=U-jRB+^Z9 zBZ*|ir?Mm8dh){eIx|#|7?OCY;r%F^=-4|<5({tq#(D?F2Pgc>UM7p+h2ynns36fd_g}`92f4*@|EiK0%-6CDvh=BSD_kSc)p_*a#=5q7 z#HFcKBvF6L8a77~=Y3l+RFIe}zm)ZIPCi+GP${~+%}zGyQ5Sx`NSH>T>wbn8#<48< z#i(CsCP&n&9jxi|E<8)qMhq1sN-g?k*@CW79yl)?1@5guI<4@Gp2y~U~ zpWb{>xtQ#w(ox+TbdD7p(w>)0Q-h&`1hzQzb_ps-VEe%lTI}prHsEU)?t8W+8?)=B zaXVdrXuQ0#m{{YRF+5)ZA!?Tq7+XINRFDXpl*F9zs*p&$m{yfKPgR!HETb%gd@_6vJ-y9Y0Qy%+P#dERLKC5QN@Oqh6`{G@THcTTaTPCbF= z=Lj9%eg)ed+MO@H)?PbvB>0+pMxNvT;^T24&m%4ZU3e87p(lr~Vsm7DUVUskh6)n3 zYuqsEh53u;?JCO{Ltk!Z(XYDj9YdRF1iC70dS@(OoL{v2k7k$-Exeljm#{mp+Mx|Y z1&L<&E*itGr7x77xm<$iVb3@AhB}nPUG6$%%bxlYL_&fK8mgE z-HXrk)d_U9I`ymJ6P-c$*X=HeSEc)~BSm}jVvR;HRFEkDbfFP`GQD`-n0j8-uD4-j z10wn6uR4LQv?WFxn_8t6*H6*P;?ybYv3X-6`IuZ|87fH3Tsh3RusyZd^NRXWYeadr zWqcn#bM!ckK-Y|bCPr9-RH9aXYBRJ9D9qX}=)=R_PGqPcv9)hiBkYgl!fVfXNxTa4 zVQ(fn_#dgJXau@GJqR$~txP6XKA0ehjl&bO?(G~r!SQJf6(lZ%q%cm8ODxN)>5{0v z_k2*_7Y@GX^bCza*NupOoR7~Y5mOUPmBjfA@!7h|U9^NeB3e8ZgcvnLbBKa3ZV<####S0xo)$4wM?4P~1_ z&I}TfeR+>BW3{V`L=C@3&Te0lieU}t%;O@^g;!d(`u<7E{1f%%V}F^*P(k8-`4UEh z7AeKEX>_e>H7d^b=XLOLJH}`Px(ZznGQP}8DcVI4QEo^vb|jgDFZ*jOLj{Ry!99(d z=hKJ|gXw!@Ti=ASDz_tfi>D(r0$pY64>7)EN-J^&5V7-p16KK1B=7xoBtr#>ajEAT z4Z36yv$s-NvT$fW_E(4Ae01}lH3D5&#yCRDwC~H_H0aH}h78k6A0)8klPf1v7h}5$ z_T_=;hH5Q+{{eqGmtROBj1i*+J`0Y}M7PSaQ?DGn%9X*|Q-Z|HBzK&js``jkm8hl9 z)1_xq6McvGkVNub1K90xz4;$025JPl{;WUE znCF*4B+N`i*#-Ss&;Gr+v+Q7o3KDG8O5;wGEMnO%T9317+a&h&bWh%WYj2G}SKomf zjFsE72=6LH#Lqm5-9FfppS#kBp@Kx4n}>|g33G}eXX{Dg-x&*7z4tx%q%mDJ0$o^Z z%lq+UJ{$b92Vc3eyH=kgfvp<(?Dpu$o>X!0x5Ha%Jz?zQ#+&#sCfmf6;`fu(+kRBE z73(m=!5jYGnxTROjuh%6kVv2lMRNSjTbLesA87fHN+%HFH#{LO;l$@<@E_f4!1iA`G9y8uHD=y|2qSa0~^9&Uv zaPF6!y|X9*KM@qpeUp3(LIPb`{jM9qUrLBN*=T(f&OAc}37p9#bHXR$^Aa<{`S1b> z84~F7+5OCz@JA`(m5OqyIP(k@ByjGRBlN<@c-*)pS35bq84~FFTFlEl6H-QO@uGQs zIP(k@Byc8|tes(n0ut!LwFmmlGgOekxnHs#?B-zJRUQ2A1fg111~W1EH6poYG}%Np z>Xhu`oZ;GPSzHI(CvfbZRsGV4YDQQ2v>z?|8@oNH7f*k;I70;qT;b{n9Tl^Xh0N&1 zFLf%b5$LM?E~~S`u=FDN9qK{GzqX!L&l1769LvX0K>}B}IzoS`vySBqh~TM35sg4s z_&*DrN5^IqeIBQ5`dF3i@%oyS2UQT*OU&GuPDhFOhe zjEY|uvtmUec>i7Xv^);FFzXa|J_?Eb7k)LKrp+nFuc{$qgpb+CE`RIBb2qH5<)hGr z*|WIwYDjz?v(MQ0-e1oB2$V5qC)>kb9P7ew$+1`57rHPD7P^ zmod(z*w1Qp>CBrYtght;(S_N}xbvJyRO<1~sME2q{IU^cUt9nA7b~8$6Zd;xRm*dt z3$wCu=WCIenJle&u5C$CCne3SX&iisEzQ@S*Pa}tg1m7ZjYxR-=^JnDoEg%gCjIso*^v3 z?%updL?pu_NpNn2k=x-X?%e4ikk&^gP(cF69P~3s0$p(*6=p3lABjh+XDv}d;_NT; zjn)e?i)+hT%d4C7%0QNTes7-kdOz*V(S_$E>+=_rS+9pZ`O79f7%E64ow~@#{UNgm zE=DnMgarw7;n;~>h23B>yL7!LKQ+E5Lj?&O3DM63V<3%JS|3luks=(?!I46J+y@mT zaNJKH#lcZPbm8cZJ_3mf5*SaeyZcg~1#FGv$5V{fMj&yd8Am_k9v4Ld$E@|SaNHNV za1=N0@m(Zv>|M?_ESi)}z2o3NubQNd@1hGwqxDf|98bi%iQ~!o=pZUc;GNY=AAAR~ z?812tanFE20!MCTZem1Pw)BCVrFVRYHUk1(IA24b`+*7)IEzG%k!>x_*8b_>^-DxC zbm389St(b=mkDGQ8ujH(8}(s03kB;doH?S;#X$uLth@C7Abz0a+l6oGScoBkF8qeA z{%u@Tkof({C}ZWTT%vq3nh&?8#c{UzcqiWGbwP#%y6_ve`nPdWL1IJv#YSl30FhxC z&Fia}5>%g2cnmkBs&Ai;4w>X-^N7+i3I!8nHzVO6&ccb$ryz`ykKUoR($TE zJPZkR;Wun$=>y-!MFoj?C;iQ~X9Go=S+p*7%F#D$)Y%q1e)~KO33TB%Z1r#BqJqS! z>}AY}skpeehQ7&hpw&nAW=eD3UHA=K{oA;x zAW{EnxS8(cPonAHv}$YUl!ScH=q7yQr7R2ybm13i9ii6>CFBX@*GPh{W@D%zf!~*P z8;64uSu3fvu|6F4!TIet2846#USHAwVs`^5;! zR!l6&nn}jUmGc@abhsT4kSze*7rJmQP~5A8koeXhrMWLrDbX}_DjB25(OWEYx3;|3 zeOZO>wVwLc*_Nc5_#%a$@S0gfd3+g7?{ubZxlzPbIX~PUyn5S#hu0LSj|! zf@aL%K#}3xTUj!^O#YO`UoUHeu|>62Tj;_yW^u1RLn3#PvgWY2L2@M$t?o~{_CL1& zr3%NP}szh`Y9H|J+77SLAFp$k{=$##3S zcdS#=7W`t3LfYCsByc67{)-4$8er+8k5l8?B^(RIQ6pTHq>t~Sf`mS{gsTT|CNYiy z;kp2Qb~GwT;CPTe27qG=IExv_1N6Dhs31{(!cL=eksKn$jr#Iw$1wmT(1qgxa{krL zg)C*F2;QP(M}`U#$G19-67zG2D&Z6Z=Vc&)E`6L~effp#;@2L$f2+<66(n%}gq*WI zd?FjWt0&L;xvw_M0>=Zer?2-|u+M?{ujfzCJ9`eyB<^3OxgVI1LInxTY{~kgU^Gj% zt{XobU4S8hu9NZ77^xP@Zvx(^B4gmUSW!Vj&v~tzbc9_<(}m~EpNk=ZF8t!E{`((T z_u<=xzYC(*=cpi|zk|3&5pxCjKH};_J^z6U68LWFtAuc-J-&~){zqRufC>`$Zt71v zzC}1^7oU86EdVM=;Ot*HhaR4GB+!LVzCNoM6(n#Lvp#PL33Tai6wZ!D1qprLQe01B zy7tA5{VTG`UP@(wZ-L&UM+FJ&tH=FDp$p$;M`*7{hgj{BU3lXLc^SGe2KLqUF#uGM zX#Av$QU0R*R&jUAc{Tlbf~{H8i6{FOz>q)}_SN+<0926ZQEi5ir+~k>CBF(K=aDTx z#{vp=d^Ab~j}^ez06lh6K8>uda^) zpn^pCURR9ztg_@Adxr!2cvo265`z#%6aV? zA-}oITl45uIT#Y?!oIpb27n3@&B~@Quazq!w!NfTNE2uL#~$2m$&ItfiCQ;>tg_@AmL+FG6SMR#PF2|Wei6iFFwhq8Lu!PJ3|6p*jLxb z08l}q*V7v2qH0w|R|kFN|M}ebyxo?jyj9k$3<-2$UtJ#qKn00iL5<9d|JIOe_5YAD z`ae&|Tefb(@14oSkU$sq)%7s|RFHVIsjYcrMs2ZdD*c*ARM8~-ci+am!Ga7733TaW z0Ig3X=9$Vj<~iqQVyGa2zZj#JiCC&(`yuk%PsWkx9HR1Z`mO@x5*Etyy=)$kG>pgmGmtczne@jgNl`$mHg*|V*N3SXH8)f)gV*0O)A%QOJdFwrT zRFJ^m64QTW3<-2$&s*=&qk;tfmYDu4V@RM2d)|7F9u*|;x5VU13;2~WB+!LDZ@ov4 z3KIBRV*0O)A%QOJdFwrTRFJ^m64QTW3<-2$&s*=&qk;tfmYDu4V@RM2d)|7F9u*|; zx5VUV6#U8<66nI7x89>i1qu8uG5uG@kU*E-qrX$^3oALZ8Q-!wul5ZgB=EPy^m$9z zo4|eoelJD;4hs_K!udt|yd_kS!0*23-;+TCT{yo;pSOex68QZZ{ku3wpbO_0>GPIQ zK?1+SqkrEA33TE7B7NQxDoEh>g!JzeA%QNOU!>1lLInx@E|UJeB_z;=^NaL(OQ;}$ z-*?i#8-)bA^m$8Dc0XdLKeXb#j+NHFk%9z%pG9APj{REfvEsKg^lx$?fi7GpuCG5w z1qu8Hi0nJVw?&XZ7p@bR?L=6AjtUa^Eff74DM+9T*NN-v&rv}FzsaJ1>jepP;i_-> zOMf5EGuFI4|L~wPLj?(aZTNzU+u7E4axL=j;S5(^<4R^+KP>0VEq~LyLN3{+PFUm)n{=fu&)Pc+LjqmckJkI=s30-!)?dc!{DsA#*%@SvpH4hv z@4B_&<*WX`zRo;6iYn{F?ZOfkK@f~6vLz6KATca6wCPG$q7je|ghf6?9Cp+Xo;WST zrqUwLz@VVw4DdV(C`u4#!k}y>B%J~zhzJgd5JgcO5EX=h=qP@W@7%6b)q9&}_(S{2 zId7f2RNcCDPu2b193Vk2K1b`%=d_S`W5~&hoTIm!|Jfeh z;&ZhAd`=6Ax8JKh4qaSYbSc3MbK+f7$}BS9~Y!TQ)v z3khnw$x0Da-$>AlW3WE9(?WvUZo29l33_o1*2i{QNKo5NSA8QvFOI?b*iH)xYP;#G zZzSl&F<2klX(2(SHeL0N1id&0>tj1DBy`m`>Qs}U7e{T$#MRU_^7r3oj9heB`+AvQ z8B_m7S8$<)1l3&hFQn%@)2n;5s2 z6c~>sTbfP-6&R@I5~s%k{TLF|X3@1E=tTtvouD2IEhMPTBCQK82zpV0K_{rkLJJ9M zv*=n7^r8ZTPEe19782BE(X}AxMFj?(pdJe?B&f}zYeCS9dI~y0Jr-I>P@6^9f}j`m z6m)`mEVPiIHjAzWK`$yW=mhmxXdyvu7F`R1Uer_232FnjkHUCVHkW4{Xp_@Ig0Xe26zZce9u?Yk zjdfZ`Ft)ChLVXm*qtd*t6;F#sKxtmrN})aq<0X|RTR*K0n9JT;B~Or=<~=MV7+cp$ zp*{-Z)i>H(Cs$^huYb5)VsseL)abi(ka6Vb(JWd>=%u1o3iVMKuYN@rtLm?&S$t-h z#K@b|#CYW5AfxxoC$nhL2(47qN})aq6>D78sLu{*pzDMrfs?Rtoh|7_V;5Jy!Ab zdqlBP<>;SJYiv~AGT0c`>Z$-OBp6%Q=cMim?`ZBP=n>rlrJipMSGc}&nhj`9<8r8R(w9t zNGkd^>+HjAtlxi`ZGOLdaWJW}g|)LPU_P{eX^^TbIzbBwD#qxjFsLb->mLBI!G-TouGvTb!@IoP#K1?!=Yn~8U(hcK4ug>Ghf?-j^B22 z4?=xMIa)~YJI^cc9LYT)xie-sl=X4FK;f#e@p1GyZ3j#e`aY1AIj7SuE`8Fd)BKaN z7bouxW*4NI8(zw-m@r{a@WmmiW@ctih0nJ&=<~^Xd*8dC?wMT?T)8~O9RKzb zDb*)y?$pi_hoIMWrz?V=J)L6iEm|SOfK{vAwxVU|I?=X!IK|x1vnW;%=Sbm@PyRVI zXQ@a$Q?V`BUUoNrETvdtgx5#3K6j2quN}!df-eP9%$n>CLUcZ0x%HqWt@yp*@6uDu zA*0@nE7c>%_a}@&qQTV4V4b!pX63Lli81Pz58W872fYUW?)~72mMLbd#uY-mKDWM0 z(DLQz4}y0$OEHH|w&F^4rM4n*>fsMvTHXr#yhFlz&}-)Z?n9~Wo7eI-r$1=fe&0Xc z+OmGKE;{Q4_llxLC#v1^O}-7EvBa&p&*xc4yngEew{OCsZ5hv^9v(rjqzxaV&%17H z=Ek5UuhA#&xy0C#+n)Fi>fyy8F}<~DnAV=hlDlh^v)XAZ9*bVlqla*PG<#~jdwtOI z%z(r0b;l!#rgugG#uBeoBwibP*c~Ng2Jm$~HwKSIFZFltZje21fEL5CkO;RsjPWY_ ziKSY9I0U`EoOcLgVru`0J6_Rp@A6MD?y^+rCl+X}>%<^2ZPG!E?W3yy;PyFdNU#29 z58(dsOYcwJdeE{gdq3_`EY&sNCa18^ol@xp@3j{?bVciW77}~Me}p?{!-emo2R(vb zywAr*(TL9%F(SD~dZ=qqX#;*f^XhEpx7g&js)zmea44r?$vcYZ(6}m-mPCY78MH@g zMfhU>>|-eliTO!uVll+!zYz3telO@=CysC|>R5~sz4W4GYahnPpoN6|t|cPbdOhL@ z*GpQY_P46Jw3W1x+g7fH$8`wR>hb+yAFH-M5W_B&PSm!p8-sISmR74r#Az$liW57n z3Od@5PS3hM7*TUXIe(*0*}YQPbHmH(;q!?2dXR8tmj8OUF#5y!F8&Fb#pS2IE;Ewf zY-HX)b-H;lx!gE^sIl25Ec@Fp`O%o$uc=x6H>EPRPKbto>+FBNM{zkVB)E&DyeTWh zrSw$))ZRKlFYewe3dCCqqXRp(^)K#IT+W?0Db@QK>HZs}9wam`t>)E2T%Fds~Yfhqi$?k0PmDVzc%aOJ^F2uQT zx_?9*L9evzGGoClN#>}>m6E{x(!!{(wcmez=i+i&NG!~~COCFXvUz(zX{C^$g+%gK zQ-c0AoupK|mQR=TuuuKo|WNHYDrxSSRe`cCOhg%~)arGI#iPS8u= zPd{fqH}SXku|fS`T8_jD z%a<1O3yr%@&FQt)SQofhvm$%%9(4~{X{Pqw<(^Cb7yJ9qvA@eqUkG2DnVE})poN4! z!u@r6QOD)^va5W?iom6s6%~8;*vF!3i_bURYWZMp?YTSYtEHVBf)*0gn+b<*DHvar zh@cmBbL^4YAy^Og8cQ1vooxH*YKNeO1T{2bgj*^S^rEtdsCk^~p6bM4EAr}MX|G%l z68bfmoP3X44<3tN`q{4Te~C)PQG)R#mzuoG?QH+vjW(s;jUK!=d7( z9o9IdqJ;$YUlg$_EfGO4s?vl*|JgRtjX?{~M8=jc_1=5Ntq0FIs#NlvmVKRfKjjj% zkf7qRtnW5Cmyn>>Z1WN8!bbzl_U1yl8xq*E5@VD-PmFjmytx{^ICGMBAd_bq-@Nx->5*Z1<^3C$7(;s}ne8u3Ge`C*F~(16 zVBYl8G?Vv7nPuEG%b0xYccq>B>`lZXGSv|nakIV~jC-CJsOUEa_f)KZPqFAFj8 z$77`}-$7eBJ&R@1S^s}0;$x7opS(h&`^28?)<1cBlnVChw~8^Q>8iMF zh9X|r-ti4ZFbhC}Eh68s2=V*1|44}^=%rt^EEO#z^wxcN>MX-p@OA1}b+XG@l1ROb z=x?r1Husn0o1K1JWT-klwq%B!SBe-rY}FfzpoPT6Au9qS&NegeC{uf2Drmnl7Z6{FLJCYRNmUgtY5srYGbSF>?M?Y*Ojb~Te@HK*mw50fj_6pF4$ zK~wYcQcXLbx;nm8Bvy_{ub4BWz1eS?()v{~ULKVdi@{^j>-dbSXi;WsbIWqIgMcDV z|9Vp_1}*Fl9?2%0)@8e^-n{+e#-{3L_H1lssjjHVi^jTg=gM)e3FnH^uXCA;Mhm-t zld1?>oa<51m&X{@sz=9of?gbB+*0`zK?@1>+o_XHWc_(jQ6j?e;@!t4P7Ij3MiI2| zxcbQ0Ja>vGn|e$vKW>B+6NW35`{ z+FCmX$9~tYgw(G4MATU#-5w+CG4V3Oa{ewW^uc59>Y(JgEacTJxv}lG^42W9 zQt5=6MLG4DSGNlF@GK-`z0~qDadqFF;m5}$3jBZ_bo3I*Zkx*ErvtTOY#Ypmx*itR%bVxL5t)jEGL7( z*s_1(bARM|yIU#}k_)lCOx)}5-K_PwQ!08%?#1#larNsx>-GmNlE<;UOq|yuv0PEY z7$hWbWOKTNj?&7wXtBoTMt?!&x!utp)qmI>=YFUfP`8tlIG z>9x*yMT_LmF*2}J=N7NTc;#8*32)pz@Je$xhN_2SAtCPvxTBJ+)|Mn=Eb$0>$$JMF z6VG0%bjK1}E-Tq*yNXWYs7~4O)<29|%oqEtq-qFDQqxX*u+kp??^x}Q~@>zy6X2{4H^nN?D5A~LXJ;U&3 z8EQ5us~pmkB&uuYD>mWe)O?Y6FQiI*Cul~7&cQ99uTHE)j*j)eqQqirJe{rze#suA>(b?a>VNGs)uCDy31|HNjN zwe!zdpX0Z8{a;Ra=TbEY-Usbjs}sqyNGoWB)Bmv^j6LJnj5jZdC!7{>iP|6u33bL; z&bgow5n7zzc6QM_lB_yk+f$XwC;yi-(mR&=?UaP8hVg{Ab{|jJw8}@x6J#vLu{zn( zgCwTc#@H3qS5g&Gbyu|KQ6Xqiv~yhh@9Hd7eXfX2Y4g<@KrLZgNQh@_?ZhTjt-q~# sa+7^!c@~~mdJF*~K3`2QMbJV*U)feM5)t%b{p>R_QK@)_GhR6K|13{N#{d8T diff --git a/roboverse_learn/il/act/assets/vx300s_10_gripper_finger.stl b/roboverse_learn/il/act/assets/vx300s_10_gripper_finger.stl deleted file mode 100644 index d6df86bef815342723e35f4aac53a0f276877bfb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42884 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 diff --git a/roboverse_learn/il/act/assets/vx300s_11_ar_tag.stl b/roboverse_learn/il/act/assets/vx300s_11_ar_tag.stl deleted file mode 100644 index 193014b60c1cc7547a828d8549304acb1729ac8d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3884 zcmb7{O==Wj5Qh5%uH0u35!?i$B4lBPQP52wqG1l<4a803(i`Yf#8uoF20?}yTM+aL zdIOIjh}K*6*7v>r6Qd22sqU(${<}N#==ku?Y;k;Vw!eF0_uB5>^_zRMyGMsd4{x8I zp8lTw_i@~0O{|MmSmS&g z1Aami--C!mXHLB_gQ?b$GS3s(zpByj$G*@?nBd4YP8qqX+L7_>AjNjS}z= zZ)1X4b&+OoHg*l?XY^`BSd7kMoI|LUW!vF1j-eX(1Qi*2*ZV} zXp~^DoE~ms-B-0@r>6)w^5vPY)RbOhwPLufgY;)QAw7vTL(iPqZ z-OCqWKj!=?BU}5_nYUY#N;A}ib%@NY9Og!o;ETxLEd;DPa!upL0JnAL$~k(bdhxhpgb5|>s*(*G?Idg1! z5|(?>SUmTuMAU)%4|9iKB~pf!%?9s}I*&<0tx|?p-;yn+h%?osj2Kn}-fDHPCJD7l zIbvAVTg)A2s!16!th3ey%$;{sAyhL(M6d8%kLNo)gfH>iiOd*N1X$8aSZ0zHo>y&J zL6b6KFvAH#HB&^?Ve>|Jx8}4}B4t={HlAUbAc*X)M^~$qVdL))Y>CHEP0EPD9b*h` zqQ^)p_)@EsBL=tKW2h!&#Ng($S4r4Yw7i{%F_MU0K^sTA?fdolbAo<%v5vII?A4df z!{h}vaU5en6e|)FF*K{}TU1^%?ENQWSyhb^I{)5(`XDqD>*h&TZr9h-brg-pqg;tL zyNiCLFSTMvD!~Sq?z%<^)^QG@R_wwAp&E9Y@#+(`^vY`SE%DzOn1Rv$3nJ!G>l@Ca zYLu`H{ny0gmXuzp75axh7NT^QR-I(!G6|Q5S-GzfVO>Cd=MZYe$yTCdr5euJt@>I0 E0ev|q5C8xG diff --git a/roboverse_learn/il/act/assets/vx300s_1_base.stl b/roboverse_learn/il/act/assets/vx300s_1_base.stl deleted file mode 100644 index 5a7efda2fe330aeadad63ee4ce29e996ebdf2195..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 99984 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 z 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 diff --git a/roboverse_learn/il/act/assets/vx300s_2_shoulder.stl b/roboverse_learn/il/act/assets/vx300s_2_shoulder.stl deleted file mode 100644 index dc22aa7e51355a0a8d0439f85be356c6aa9e1c3c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 63884 zcmb823Ak0$|NoB$GBrq1q#U~En9?mt(mnTH;z-F%5kiGbsnDE|h!BO$L-*WD>Xrr-h03IIXBPq_dGp5&whVipZ6ZuUVERt);;}GYAKN)X3l*Oeai>;759>G-w1J0|zKW#mi{swjcD_qtoF9kKA= z&dJ9HG%Tx)U>3Y@YPYJ&5kLNWZ1VHXUt2;It-I{@{`HB;hj%+60}oY{ z7_)eA>93Xdc;b&k(#d7(?y!U^O1$>r#ii%oU+IZ<8`dZ8zxJ|nc&MVpTPxd@PG~gQ z6J45iNxr>pj`dJNvqr7zQ+nES-+SVucMnR=&%R~}Rg{?gYOm5u&fdAk?W_BKYnB}R z^~aV_MTzArQ>6o@?(2zq3$`UHC$6`IDoWhFbNkXx{n~k=?m?>)y?6i05~?UM?TP(L zd)-;&i6c7CN-VneQ%k6##I=>Xmj3-$Pfr{-`liG$&#tzFDoRXi`Af;NiG4gV^Wg4@ z=>y%qQbmbZM=vXx)#wsW?EBkpiDy>7WIa?-qW8`Zl#Cg3g(vRW_vM0-Z@VL?iW1+R zaYo6$ORn|AS1+AiaNPPwtcNN}oYVQI;`^6e=ZU|UXWGuc=x$4>qQsQWm8tWG-{6Tx zb5>?nJpO~dYE@Cf#5LiW`0?eZwXLk*Ac=D~hX}HcxpPX@>Fcle9 zt^4k*w$*Fu4fjMU+bHqPBXLWpqQou5H&)ktb+soZoH-zI+OYPPP(_Ij*FRf**5pe) z(O~w>#JdGWmQY2BPj_2eeZcM)c;bhZUnX8`aFQidQR11EJJftU?sQKax4da`M)R{Q zp^6f7&TCe)=eF*iXm@1S@(*`PwbQ!m~2&kp(RvNqI3MHntRII zdg7G!wtMWqC|PU6KeKr)6^4>ANFYS`O@CbLlh+j?RrMdexv{Ot&Pc9p^6fweXLbjJ!;#Rvy;P)IM?0>RZ#*l@1p7WLH}2t{Ak2icVRnn31(q= zdTujw&&9p=#hK41$IaW_whXFhU99c74eOOSV#2U&a_mpjETM`L*durK>+gx~_f{v5 zZuqSwR8azZee*GodZK=_CCOcrEy{U3s3?JR;qBwz^u*h9`X#^lV74VxQ37Yo*k8wc z;@a3V$@+I3kw<72&d`?qHhW^|vKh(gZ}zZ+DoWt`c(`%HQn#;ST_+|ldiZQhsGY=Uhvuq6Dty2Olc*#LI_vPQJ5mALk*85@<1|{!;9TTc$TozInr` zmQY0rv`S}RbFwFf{=6>nbLMzUsG&2cs_b#BCQ} zm?$Yb)Do&Fffl^@{mVV^^32^5NA26%5~?VHyTqAWuJ**ZId>Lx{-}v1R8azVqE)M| z^+eAvd$s+%d3{T$q6F@C>jn(<#G4J?%2buCvOP;xlrV8=cqSISGp}vCp7-09A%_UE zaMx}&cZm0RcK3@44j(ep5~^rj^bBS^b0vu;%L?i@c+L{4D1qL`-&YRu#MlK*6T7@r zV+mE1Ko2JoyTB8F&p0`8@}_q!p^6gdMU`HEh9_pfc3Yy|d!JfD6(!J98@2HSPi#A- zBJucc-&sNxCD0pOF{Xy{DX#Jmi(BcCD1qEIi3n8tfdzPvwq0fotetyTeRIjpYm&VM0i$6H9v~}+a zbB8Q04r3?<{t2?!+|`s&rFJ3~3y5y0q(m4y?51Q@lHi> zP$G)nVY<)SHEwrT0vRN~lsh0grR9JTJBP;dRXOf3#Le$6{ALyK`ym zGMw$$S4yZ-I{}YrqfSbdoV}0rP$GoWMDoM;;?LCxGrFH@yQ=aXdI^@(2)VFDe^D~{n_s+#5~|crz~h=;`=`3UdA#*dB8+3P2kxjYnSVA~AM7h7RH>bSM_GA; z)L%E8V?C4z<5+CuiI10zT7$k1Jd{wSb^;y~3fC3Yv>IeRlnCQkZ0;Q+N zs8TxtkIVjis_3tquC*RYgmEl3qyFI~10QVVJ(N(Tb^;!aHl9}W?1YilLy0hs#rA2s ztayCc{@z0gRca^T@z~l$-CF-M)_N!r#%N|Kh8*qU4P(qd333xnEy=lo&UyZXK zN`!GNcI%qKRsG+Mdk-a4shxnwtoD0VE-f5mJ(LLJSZt418&v=OU2E^5getWY@c3r) z#LDwFU1vR%2xHS*FF&LD{g+yL4<%Hooq)%PE2=6db-mJhC=tf7Sj*R^R8Ky$srOJq zmD&k-+}{3=%7fcqXg!n&<5=v4qbsTxd}!K5dwnROO6>$Z+7>m-9=NTy^-v;=O}pFv zz3O)=|Mu;b5~|crz+?W3L$gn>KgN0}5yr9Dg-8BeT~oN;dnlnw?F2kZHP^ESP9`{sT zobA2hdwZ`{B8+3PoqpJ>re-;wYtbeup-SxpJeK@ zJ(N(Tb^;z_KfEEk^_FX_hZ12Ni+$SvfSMiVd|2$ZLkU%CC*V=%h_Trtx@@w2I3>b3 z7CV1Y+nT#y_}&~d>?tBP!gC9v*PYA57)RKi1vFm}iM-(7oFM~;Uja15x_PQb&TC3!?hyK{H> zhTFY|5;&Kr)K0*|pY7H|i7w&_+?Ioq&gLMXiStVeH!W@2{#`29??gc=)@T^-v;=-Mx1A0egE7C2;SfQab?;e|NSXN`$fN zGo1BCYww{1?%`BwC*a|G8P-FIFm`>PFRli=w8E<-a`rW4XM;lz{B@m;bHr=BtqKtsdsq3z@B&;Ms{v?F2mh(_J1B((akF!yx>M3=bvn3`nJR0v`S;)p{rq z#_rkq>iU2BHc<&Y%TlSGfQNtDwjN4^v3mz_@XAlUhZ1-ur&2os5C7J{dMFXb?p;RR zN%#%``$`GCTcA=q0T2K7!+IzY#_m1J*|Q$@9!lUH36QpmytATGI{^>>R?T`S5ysK?j8Ovb-uMQx zb^;#QNAADz*^tm4AXL7y^u%})s%Rfh`;1YKT*CEn^*v**hrUD9y6DfjUU+TW;ofp; zUtQl>=6YcNxVM>6Li_O1?NCCqa5UXJ%Ulmtlt6#ZJr(6{hZ35lZwl>N1;;}bCD5O9 zPj|T}4Eq6GSL?(IjehZ35lZwjNwToonIpL1_yN@#Cj`-Eoc^JkP$MG5WIY@g69eg2FRswknov+Wa_rO%&HLKP*nm$`jHv-J5h zN~ofQ_LjF#XqG;IMhR7v&|d%c3C+^y&nTga68iMAeL}PJ`7=tWqJ%z$ZJ*FAeg2FR zswkmPlj-;k3&*93zwX}cpoh1$xZ2d=hil2=RpEn&Mef@EXK}b={~(vdyigDVD*nOo6W4j&x`zz-`0{eR=epjb1_`FF%6~?H`FVdU`3G6Fop_XPbb{^AioLJPn&h>U* zslpg_`JJ5eAaQN=fSJrfEzM%=Jl-5|Xwl?zMyKj`II}<%#;D7$_nZd_oL9_3EzM%= zJQkdDU$>7>AD3!1^wxwbj8T{06gm$Q*jLO#EzM%=Ja+r-)g_;9bl(A}!Web=ZKU%c z@yw{Q46{&6vlu&%Uq1PL=^^p4sW(5qFQW=$)a4hO&V$4*2h1#B7HVl0W9KpH_wy=u z-8v$*WACO3RT!f#zaw=XB(SfTg<6`$*m;cX{AlHn8;08RN)^Va%dcCV2Z`f$exra{ zsHItqoyW~1R#t9UaCxfksSOgUFh*T|bL%`v9Jlj{1yVmWv%X9f#;D70xt#|I+|QYXTAIb! zc`SY4gzPiD+N9n+Xa9^UjO}*#Z_k|v2^tftGc(C{I?1({AZ68h*#;D7&4$gzbwbdJ* zW)^B`7GvkpwCkwsXG@MN`snM^3RGc?x*UPwJV; zjJg~b;yg&4`S$7pW}%j5F?JsK0;1We&y+ng`{0BsjOl@qFV4dg1%}8liyq9v@^Gw- zU#q209{6FIDvVJVYm4zXk?pY0QOtseW-)f#;rA|nHyE!9W7Nf7$0#A^LBh1VWz0e? z&0_34{8>`DcG!0Zs=^p`arR*BlJg+JW6m*3n#I_8__N(^hboLw7gqtsU^x#G>n?AQ zU>0g=7Gvk(uiDD^o4pdMFh*Tm%@`TxJV==SRROb5OS2d|58wJ^Up;rHger_t7p)S; zt2qx6*jLO#EzM%=JbWvf#W7cfG3ugq#V9)GLE_)dtrE;aEzM%=JpA1t`_lcFCRAZ; zx5JF=!Pq|MK?2*sEV~_&h5Jf48qnX>vh`aZP@oE9)WzKnBMzMh33xCI9-772ZHK=* z+kK@9W7NgH9pfOKhY&Glp_XPbb{@W$VLen~jJoK3V05PQAc4LQvrtR37&{N&TgvYE z!$-F7!;zlMLS6KtFlN+wkTBOrhFPekS&W^B@AYM$+Ptbv6~?HG-XKP@Iu8;!9?U{5 z&0_34eDBrnD^(byE_$^XU+X+b{B(HJ46{&6vlu%M-wV$^KK1D`RT!f#dgmDR>pV!j zd&~~D7tYbWn#I_8_@{~Nu=C>yRT!f#o-#0c*?Ew_zG4cdEH=RpGZ4`!j3W-)di{wXzk*z=dS zRfRF?;^`IR&z%PeY&o+~OS2d|5C61n_mwJ)Q5R3)7!~h4NSL<0EwfNdvlu%M|DGZH zV#9R>sxU@fyiLGZedj@9;JxSDw+0-OuUU+phkyH#y}IeG2~`-QF5Ze@1_9?m0{e4a2_Pi9dNdN8^u`_G>ftG@Nddx0>xP6Xj8PYFaWU_Q^B{qJ#Vpj)EXK~mzja0*E~W}&)Wth%%tqoownu0dW9Q-D zo?8!97^5zK1#liDep}u=!z|R&EXK}*zcd`S>BTZt7^5zKb#NZj<0}h?zF&Pfo)N-t zSi;{}kQIIngWp(Iu3T9-ZQP@lP(^>I;BO($!xQu1p@e2d*J@GM6@{tGu51rN>*^X} zoA}Fy`D*g}*9*s9eYYi4Q6l^r2HP>~uGb2`&o0X&Gz)tjqch!h>~QESg~@?GTS65j z!mnZAanpUi(^JL-p&#$+HDoTXk&%opO_Z}?l(tT1Mp;>4vF&5c*tiNn(VZi}YETM`L z_zgN7Ke)pmlM5dkH$9KgEdHM0JbKL>U)X-~qn1!biSYXwY{%NZBMQeK^i&?9S-8Vt z+_v+$;)=e7YyN%45~?T>em?_`r9I9md~u&=^9aqtJs6|OoyW^>cPiZPvlrZ2iJ}Dh z4;ZoTh;vsRUf5>xir(!K@L^qf`4K^Rt`Zu-l=E65&@* z@VN8hiuu`RUe6;m3(r@W%ffj~Z@XsxzH63QLKP*#ueadwK>dH`w;B3M9-&!yPQ(lw z&SUEM78MtzmRdp;CBm=J;4$_84yd^D$R&A%X5o1k^L{vwK2LS8=$l$-300H`zovu7 znD_}5Qyb|v4kp0gx__-e(?>D ze@|Fh@xt}{r<5A+jNQ6l_e6CNO@eYEp~wGkZQ$oKHh!>`qR z5`6Qob@^63vgMXgMTzjMP}#eAgl6fxcQfM99YIT|qD1&rC_MaGl1FG3->SRq@Mogg z!{f86C=q@Y3J-s_=MkF4x9ZMg2X{qTLKP)&b#Z*8`S!zIwRwbQ@vXY^@GXNSR8fNO z$Q|KZA4{mBMED&kw%oUHd4y*19l7)Ht*9kbQ6l_~6dt~%&LcF7@5r5pzZ_KSAaZnx${K&DcKolxhi8l)w`!M{TC#{z*8G&@8^ccH7~fwk@HG z68g?M9rsW4mQY2B@XK{2+e^X=$Rg?%vM8Lzpjmjf5i*KXdcKA15mQY0r zy#3;+&2-$qRm&qZi*KWyhktWt300KPH_++0e>-RiRg?(Fv|!8q8^}CDv-ob=t(AYv zY5P~IC=rg$fro#Snn!4szJWIP5BIj#5~?T>jxmCVeN>8oAUd+88d&LoddV5tWF7Iykj@B{rUNqVw$S>;Q`vsVHvZ2oL@F1M(ufIKyXb+gtU50*l|iga63@aF|)jtIU*Gh@c zf9+j;{?A8ykH;ShG;KA)s0&6(>>Qg+CZ1YIj$(J|}Y#|9|>0WRiegAA#en!d0 zj!@1%`hpU!m7Ax3oLwuftt0-o=IM&byH_s+K?SiA)2=_Ws^zC+{95(9`P)pt<-HQQ z1hcSw?U1Ea``ySR_}Zp=<@g7M#~JZQ3%ZtYUvX{ST6OsTn)1H4d}#L-m0V(MDUaJc4hZ^=RhA6ZX&Ba%Sl^IgggJ_AYOC`4x7{;el%qS)3^wG1t}+^|N~# zVq-vXJCulCA0X;oP+2QM+8zJB{Jwg*?6~sm;M*)gg=<9u$N!u7-kw<0qHg(Jr@d+k zD!ByClJ#Tv@x;?(H)K|{e9sb8a*09x>zS*zjVJo-oG!ogwI?k>C71Z8;=tL!Ugy)AH7 z9~#x6{MSM5EK!14@N;X$vwi+szm@fCw$OS|L6){9tmMv;xl0SnFB$xqCG-f67+0?t zM-6oJyqa_4Lz!E4yU7w%kfp@RE%z0NNARg$$ZD+2=C#IuC0k|U1J9#-C_^5nesN(t<@uoZ28!Xf30 zPdPu2U>5vvY{HiM^)*iy-s;~Xj=KWs}#6!o8Ox136C zgu4&69yb{?8)sXO+qFrUW&1n9-$x0Tqt9^2a7WA--DjpMCRW17xZ|u{=ZLkvr*DTC zY5OJauV8>MYh~vOfBpLYZQV( z_4=F#`&ZkxZ7ae*95o&((0_k{dHQb*t`E0&aXgg3ISNII&>LG0qHySzGSeSTL8;gJ zI@8CwF+#vQY)e4wfBEMb5UQw$u5CKrV5b8!|6Kan6m#6I2iHBvuawIj@xtP_?(#&S zX4i^{P^+z!?GqY`yOr3(x+b}MAv~}A@dyYi zN+8B{!x2xaXk9E1dwu`x_jrV$q6A_tPsjiFaNmsiVIsR`VL!VaR5S}7 zyo;ve2VV0>grK4XVlGd|7oJ#^Y1{LV?9!ql>p?}c;KBQDI=<_i#u0*w5{S9nyp8I) zN16G;EsHU>T1&IAoOkVX{FtIK86~JFftbtF@#PaPDQorFW!X1U@7k7uie|xsy@7Q6 z;LZ!$DnUgF#9W?^cR%;fg7t$&X5aee4C_Hfv*5vJk#xM_&^i%Ru4nnI`O7CtP*DOgm#5=@y}nH zYqx`nX2HX?$9RtVZtTGkf{GG|x!n9VF@1j!xEojxDw+ik_Q*{uy5_Tl5>%8x%;o8L zy6#5_v#*NG_*d&eMYG_+KD>F$`Q*OXR{=pq3B+8Uju-8_JC0z=wC&b|ie|xsPXK1t z(_QhGRYHP_5{S9n{1VspT%3t1)3#d=Dw+ikJ|~z~bmE2xK}89~T%L}9T|F096y{H` z9#k|79(=M$#|z90I3=hkftbtF@pkV#T436elxf>-TS7&%;KApTbo}v62cazq2r5b- z=JIs>%6EThYudz=Y1^#_70rSNpLf#nuXg*PObIGVAm(!0QfJVA8@+h=U0O@CxZJ%j zF?Rz?n6|w@6(txuVt(SGj9;sw+6ZJ}Ikpe)nbPs9&&NdwDq0sYmz&?!-YU!Zy=y(F zXcj!&al-TYD~*4P5LA>v%;o8LpDp#u{aKRITAGFBIN#h`gV|rN%P2ub3B+7(evkU@ zmyEwYtOpg%f(Nb}Jh7(Z*TwcOSAvQXh`HQ+Q#8D-c`CA3t@WUyS@6L1@1AS-d3g5- zK}89~TyEyxd8>Z8Z+)x>70rSN+A;SeT#%_7A*d*Un9I%GdCkv3FI;PB7M8pAc%0c+ z<8C=3`^6jgFE#%+Yjt!@&$myjdTq}!r66!;z?xt=h>jh{)JGFSn<#@B5OHk3ehX!0* zt%nlKg2&R#Q6+s=_4OV~P|>=G*^asGc;t@4RGlr&t%nlKg2%m6N=lmc#cvZ>DxRx43!<##$_Nm*$yeSH6X%?6NG!s325~Ch}J|Lhd!PxcmWvwc;mS$l&wl8y7 z*XVXwf{GG|u@~I40X+QP%_Eov4;-g~1JTpZC8%g!#O`R4@MnqjP=Zt^@+_V*r2P*DOgt}gd%01toFS`Q_d1rJ>Re|Fi| zdniFg>mo)g;hqiP;oB?gp#-zwfp)CRkN9mOm!P6`5u>$pzh;0iZ97^~>!Ad*;DI*y zt(EBM=Mq%3E@Ibe3xWHG^-zLY@W6c~+zx-ou>=*ZihQc$f{NBf%;!Y69p+4|G;O=}P=ZQG$xrMU1o3JsZHopY18FrCC^x z>n7}9DM3XE#JIZLvjIH(Rck$zU=}=Z{fGTTC8%g!#AqelvjIGO>tj8XU=}>s*LHhX z2`X9_F?;0hm$>QwHqZ3lxXhl3Bv2RaF8b<>V`c=JBbL8@SwOhf`GO#e1bfbo=yCY; zvavDzYU%2t7Gh>0K6&n#)bt=2Nh%~!Je}to<6Eq!6l1h<~PW&7Gh>0X3yCX&&C_h zTs&q?o(Ho)JbvlL#TWj1reCYw*WHpCFrm~ERJ1N)_Jf^A@~B_SdbPtB5MeFNLd;&R zBMR@So5@rx&m))x;^B1@i@*A=w_mGSdu=UiwE|ykg^Jci%zm)*c(3lJvbU%1XZICr zX%^z9M@}y8(C0+&apV_;nJ>=7@8_YCOTfdm)Hs4SrMhQ6=+eM?uoekqA!e`EtyTB2 zGs>pT!tb?F4`yL`i@Qe`pY}(oU#o+c6_-u=yQ?LrXkEnYqdJerKL5F_`Kwd&2xfs8 zx~a7I)M4GcNA-K>XTJQ_Z3h*tiUo*~T7e0Y~@BOlbLB9>FXSled-@ebBwL z_t;QzYvzhO@U20pXkEnYaXOFvc5acGc;w4@1hYV}|L6$wPGsi?2xfs`Z_p7p_gtP?-L-oj!7LE7-zcsM zk4N*}=Vo49agHUZXkEnY4LXlD113E^`1fsj1hYV#u;sR@@Ju}Nsa*=D_Ni~r5-M63 zF?)l~W75U9WR6>1Vm*{#76|t395L|G7Ma)o_%V-Q7KoP1%BsR^@V-owO!Lp&-ld{- z5wmaSJkGxE)w2653hZ_$!7LE_FIZR=wmzLp&&ce5$XdHrRJ1N)_6D8D(%F53_DTt6 zk#M~}v{(0RJ3e#es(bT1Kp+dm>~rQ-h3#(Nkv%f=H|=D%gNoKg%$}NCtHsxRTsG?8 zd#r~N%mQ(K`x#Z?9p|Dy7M2~}{~$|H(YlD)w{sp#{=7Bw{T6p#DZwld?5R28&;^fX zhMl{M-3}#~1>(F0S5}30=fie8Dsyh-3wEujXkEnYcR7#KmJKfJ)8mdjf>|Kgi*m#b zCwI=AbLAs>1hYU~eR1QeuwU}foI5kuCEZ$4(YlD)i*gxGQ!jzi{4fMTKqq+wGvDS@2-LD;+;{@$`%mRFpu>%th!8fGt zxNuaZ*3v92XAj5xD${3UgrK4XVlKx>y_IE?PdO{KqTX)iO;A`%v#^}K5A&C^j%;6` z1QjI^bGi8|W6S<2*tJ`$RIBQ35fSo8O8a=$xpNDl6J^ z|7)!W70rT2*pr!e`_FprTpu2>T_u1QjI^bGaF#c=v5s zt4eMM70rSN`x53C-ktA=5LA>v%;n}gnQc=N=6GazJg8_EJlHcx$4j2NB|=b90x_4T zXIl^f{GG|xjY@edghL}8&vZCK}ECR!8>X? z-Z!0z5LA>v%;o9$&Z(ob4gURY>Hqz4sQKFP*qTS~Y*O{wF8$2cpJ81r$33xQ$Mek- z%d@X`%%=bOWvThUC8#KYn0L{D=vEuSED*dunsHt&4$Iy$X<_ACpSMkE-P|?g9x+>f zTPmAeJF7CYcT-DHQ3CPLo35-nc2OU*9S=WTkZrrIVYc|Gx_Jb%;LRhOj{i_~Y_@;( z-<8KUf8X8*sc2oqla76`YQ}?S7>`C{+hi}Bd1Q7_*T?e+X2FAJrTNOD`3c!>NAHzA z{gCr4K}G8#E}Af>>gF>~HXi@{)G|A*#Yx#QPfoOVP9>NH4_+(jc$@hrX1C31m2LQH zy>eBwF5}U&PcR-0S~kxf*1%9toMi8utfg6~#1_Nc2Qw#SPpfH??bPWEOHfe)G4{g1 zfhES{+k^MWp1kSs?2+GY$Rn5q54L>fZx-*-E8Bhf50xvnEwcm_t&12(b7_xbj7Rs< zM%lejsgvF9%7ne+D8Vdvumw-YhyL9wd+iPnR5p0Jt|h2wUBtL)y>QK}G8##=G>l|`pDzY6#+zus}1rPQNf_+uQeMLpc@Cf?4oj z4<|V0MLgzIv@YV!Q~H}RQ*vIVcwQ;NEOn(-c%g9K2p3slwcM- z!oH}#229J$U@yv|umvrsAQQ~S0g#kPcs5{R)EBJEX*?UfSDf=Bpl z;M>F^wuw}7V;K3)EplvT=+fGI6BF0&XdslG(DB}G?31-0~yyN)$ zU^mHeGzM_&#?A!2!s`p;y`}V)L^eedPmbrNZvq19>=RB$h zPbv6r?q`;uf-EH(jy|U<7CXsrdA&XlHR^Ov3%lh?FpD1UI>#|L9%bqQ1zAeK@9?9K z_a1+5`Lyh+TMi36NH7aD?{IEA&aA&*`Lk`dwp9hO64=xI2J#L4cW*W>FaKv#u&>be zfnXM5-r=0bF(ui|+$Y-FwW5+s@P6e8b6zF%yuy7Vm%v#$@m=jptfXL6#DmZrZ6hygoLL zx-rA+LkVWl!`-9Y_0cf%-Q4r-c?ALrvXp?IUgzQUp#-x)^X}ueL$41i+z!-5j6EH; zaC&_x!7LEG`#2B1KB#D2#Jra{Laz@hxdhw!bbQq5lQUo3HzIZGx{HfqvEKF=aNW7( zSkC=szC)fqDMC&k^@6JteVk zK{~b5BdtxJ)NKa|(pb(jKON5{sAyfpT%L|sUjI&_PQhnI8)go)9#k|79=w9hn7D&R zM+hoPAm;LP{IIfx!P_XUrCC_cwkw~Yq6A_tH{XSCc?D}#$?c$`S@2-1osPdVWS688 zRFpu><>~lC*KNS@$ntnl(JXlIUXqR%ul+tkP*DOgmz(F>##0jJyvmw$*Pd5YGz%WQ zC#K^^-F9z;prQm~E>Fi>etuqo_psLo70u#yxVB_ik!Rf zuxC3JtxG@GilV;v$RpTtayxL{M7G=#*bZJ%B;di8@4pFV@p#;^Kb}&tM*awzx6bZJ zLD9O1+4B82!7Nn5*;94^p0?rP&t2<5Me8DF%lF>|v*3ZNAoTFpp!J}lbrG}W3$_DS zZ63iac;Gq@_myu;+!6GO)E1T@Sh0PfulbxOu60 zx^uN~9U$hJk2q|1@ifu3;@-^Pr`9n~6QPnz@SJuY+as6-I@}I;47sOoX2S5ZtOpfj zDZz8vd4QO``jv#%;#%R%w{&nk;DHwIura$^4<4yp0&R>tf+SAfJT{MD7HFP5Zmr-! zf(o*f;5i*0kJE}Du-l=>fOj9#u1)l76+3Lqop!C*CL&9T@Qy=*Yo!FUK(hr8wj)M@ z3bK^I_PM)(pYtQz@3qMZGa|1PGimr4vux~VKM7`vNykY%K6Oe0bA7luIqV!;wGfyi z1%%nVIEv;wcKf++*+-Dbb}?} zPDMR*ZPW1qwjt7oOWMAd(1$yTbRFt5HJA#8RUmDDjV!jwJ7u@fG9imiw{#_PkG;-F{d8n;plB96 z!m*aqm!1?Ms3?J$%gvV@+0%7Qb)aY#JUEKd{03Qid4!;%1Y#~X^WtnATrOk714Xmo z!BLcE?_O|dgrK4XVlFphk-N4omoe^vqFL|=$66k~=FkX1MG3@QZhjNqwS_r?<5Nfc zT7I|I(kw1_V}5EQpeVuE5$oFQ!nLZ@TAGFB97So~Up)|u5LA>v%;n~oA3cjB@2v+F z&4LFu!P*DOgmz!B`{#}OoeK-e1plB96!m*a8zt&@>5>%8x%;o9$rUUoa znJEHAv*3X%%8j+W?%r$4WcG_dQ37#DOnq~40a{Q$Cr6-Y7CbnL(#)Q;h(n^$E=TIjErFt0@Zc!QbbOCf3L^v+B@lCYI{stJ;-t)36DXPm500Wt$6pxP zJwi}X0x_4HuO>V6PMRZ#IkvJ|OS7Z4 zY1=W!mTgO@Xcjy;iZUJFcI9~yf{GG|xjY^3Q2+R(Y1=W!mi3^bS?~zQS`Pf{*a$&I z3B+7(+jesV$7fC3-dJmC7MHuRmZojbBcLe3*bz-Wc$+h36=^Na!g6e%8*4d#!#2l49@c}3X2HW9CycfHsrCC_c zp0gbRn(|}y&6Xcs%JG#T*pJ3?5Wzew5rT>mi1R!!R^NIk!7O-id_%BS0YOFUA`Z74 zwPhr0P)oDm!EqYSLkTKMAP$cpJY+;}P)oDm!OmY zTAGFB94nPiP*DOg?gqg)dUMP>WuLsh+`KOdYiSmj2lD|~;-u2o0s@K>jDz_AQhqzk zUCntYfh;V?9tq|(h!9k?E@JGfU>=r~%!Cls(kyu3$OiK>Lp}{;85rT>mh;i-)^RT33W{RMeX2AnjS}w zMG3^=lb3JXF~^qmP=ZlFYl;5Y^IutW$dS{Jc9ntmP@ zo>$gG31-0q=UXrjON5}JbrIvN4CZ0s^^wwAnuX=KZi0DOA_NsB5aa3!=3zlg&AIe~ zTABq9T>rs5ED?f=5{S`C1oN<Sydtg``XT9@#kX`t&d$~uMZ`d1;2276yAw` zT%1U5T=T9as31!T_O+eIb9W3+y#3(`c?7dSbA*y3zMcO_;^t=OT7n9)lwe=m5l`QL zWpcx=ZSB5Nf?1&1b9Th$6{jROzW#rC9?Sy4F-?x>c=Ddf<4f?nTBvAU#Oygc;^7Ic zl0&|mn@2DU1pC2`_+;iG$+NHTXt#sznuVBSnjFz=eVt@_t6M87xdi*cj%fH%o#f{J z6YW|l!7R`m)8vT5_u4ag>G(!=t*9VN3B<04jI*S9wpnu6lRMN(Ad740`nw?Z{IOed z?+c6VTB!&7z92Ym%B@xE)=tUUPd{M^Dq0sYd$o?3`1G%dMdJ^#Yo!FUKyVb4BbJ@i zHo5-uq1J0IFu1icAyd{rd76|rH9ntN#Wb)_lp35Vc1%e~39I?yt1Cwo9 zFR=s_t&5mFPDh-!`qxB@v7hA;%mTrYR*rahL;d8mI&WEmiq=KUKB^-|4mmNoB>hYt z!7LDHV_aVp$NZ=aQ76^{CavsO6sGpqiihJ^+qID6o|LBMfhZQ9I4_jkB zlwcMJ_68ks@`Q&JG@8=S`@o1hYW1Z|8{dUo=kM)zn>sxrCpGrIh2p95MQ=#>s-wF}od9v@X}s z_4;tOPwmh)xp?E$e>somc4?a&_WM*zP|>=G*;8}G%mb;K6=ZI{ro3&Jlu&)3IEhGYe!KeV}L-JlKoMC#Wcan9I$3>SOOs$msq+(JXkdhm(#cF4$P01QjI^b9p-c zabi(I<~0Zu&4LGeIOf~)JvT=PDoP;ca`W57;Fd|5pCM2*3m)vRq~l9g?-3!WD1n&E z&3Mjd<4KukB2Y979_+7}5hF)+i4atjK+NUoc)cU~C1t*hK+!CCu!m#5F!!q^-=06DXPm5B4R@%tk{}IxkM3D1kU6_MBXtk=c3zMYG_+ z`@H!}%BRnY5LA>v%;o9$Nqv9I$UH@XqFM0ZT{|5=v$|fn5>%8x%;o9$ny>dQm-&_g zMYG_+yLLW7MG3@Qo{q2E>A-TCH!4sx3m&}Bn_pk+913B(~`#-FFmJS=wnxf!og z%JG9*7t3)^4CY};nR!?^t4g3KftYtuI}Zy8&fF3qm<1xdbDDWrQf3|&j-wCOH+K!g zeP!lhNttJSU5jJgkTmt!fVjX0g*EE zuwV`d^WAHxXkEs^JQFE14-4m+2x@5-Dur!{nTI81=3(Jn7J;G!V(f)rzKoQahlTTH zLpeKvOQ2|7#E;^ z2EI+?+*E<0brIue2J^6>-R1mO5rSFp2%m9$+s-+*0!8a0###9q=V8J9gY$Sr2xh?} zyyN)$pq-CQ6|IXHS646(OX|W;c2Cw@@R%9L61pP}$9d%v7ae+PvgH#)EWuj21V?bW zXXoRGU6=gnoGEz(vq1B%?TA};dn|EYylqky#7b}kmm|!a0|_%HM;^f}(7Y!)V%Hmc zB-dUr*{&58WGTTroFh8DSCSlbcT4L*C70j`E=M$dc1ZH&pXON)C71=8cQ{Afbj%gW zk#oXLkgt0wP$C@l ztM@2uhh87NFTq0z-b>tC>GeS+m*AbjJQeNpCugR>JQMa9aNW7(SkC=sJZ`O%l$j|4 zC3nk1;pq6IAgIBQmzWvp%5rT>mh`HSSmE$ElCSPo~&c2P(TAGFBY>Dy- zDoP;c@^pOA-19IG3+Iyw6wQJM+h8+)Vv{IAMG3@QZr+y+Xvmo<%viy&mS$l&?>_1H zjCvy@1QjI^bGh-jZyRR>vgZ{Q&4LH-iTMN-B@lDDIfBa)<>qYXJVk+`S=F0~|mDeazV&A*d*Un9J>%sI$Waie|xs?PEIL z{KoSm1QjI^bGdnTuCq%*X1NIz&4LHp$8`MEZbKpj6(taJxt&2ECG$*_a)dqVvQ0Fj zzDqd@KSEThq6F8zplw@1>?suCz#7jf7o!lUPqmbDVhf=4*=-mevArpT(IbrFYc z;(rm$LM4v$5B4tS#R(LxiLrqI1@P=NuX$5 z#9^BVkL?l6f(K`;aC_IEyO{ALP_!=MuuX)AKX>y8W}#9z`-NYtNmp{0>UzlE%xI9TGqbUI znh4uI#JnaDhwU!zW)~%XPh6UvZJrH6C6@@#_WvT71v=ag5V>pRXHp5S75cfeoy1FD ze4No*nuVBq-EH~h1G^{J4#v}VsN@pi*$xj7caN;dBbWs`Jg-2!(5+wc`R6uRf(o*f z2+#K1?a*Vu`x5D39u~V++z!-*f-EJ%J5KI)D8VeyVcVX&9aNB|M0m#mvCpKx7M?kF zrTHQyoa+N|`0cq{?q=}-F{f37g+qsbTS$U4wdoIL3D4Zh&arh;9xVDZM{O*Gb*Djy1fF76;B$vP%x-a6f9j(lkcRgu$^KDdE z3o)}8yYKWtRP~D|U%2v+*-$v23}U>8;oKrzTens>zr3+SR{MNJm|e$ObocX@1)#SsIDdG!}Nm}&KcG%?<`Rw9MgccD(=yH zA&($yp)Ru!a}0&^7_@cbLY^g5kfqlkD>>rfey1*|+iGvS<-DHhfsu2FF*{J$mV9}7 z(*>_Rc0k_o2uBI&d>Pn|su6EiT+`?9JR%&sq_bau*sss<3LX#aU5tIhns97a{#wo3 z{LK6T{+M%pa|s@Qx8+}rc(age#bXPC~X}V)0=j9-ETRSTp}ECi0yc;->DtA zca>lkXm>qg93+UgGal-|c9#mWI3hEDt)6MspyPGEZC4K^cs)9g&eIR>_>4aj*#dAo zFxmw%TMS3sSk$EB|M|0>N-lwY=*F>NJ6`;7YX`Q8N-zs_I0_rY$d@-RV7p5NS=v&w IQab+s0B?#a?f?J) diff --git a/roboverse_learn/il/act/assets/vx300s_3_upper_arm.stl b/roboverse_learn/il/act/assets/vx300s_3_upper_arm.stl deleted file mode 100644 index 111c586e18fb0f44cee7e4593d24c71382a6e9b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 102984 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 Qg}}QoCNx_X0=Dn|ANIr_wEzGB diff --git a/roboverse_learn/il/act/assets/vx300s_6_wrist.stl b/roboverse_learn/il/act/assets/vx300s_6_wrist.stl deleted file mode 100644 index ab8423e99da86cb9e13c197f8137b670f789ea2a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 70784 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 diff --git a/roboverse_learn/il/act/assets/vx300s_7_gripper.stl b/roboverse_learn/il/act/assets/vx300s_7_gripper.stl deleted file mode 100644 index 043db9caeaa59dcb0a6bf570956b0904eb60ba63..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 450084 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

+Hgr>qo{?*1^}%qZ;2qh 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

mev-9LM@Y2(F?th1J! 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 z zJtuOnSQ3>fn+@z%G1R?wBQ0u6{`Z{7ZE#=XKFH5Wl=Yvs-GsGdMXyf{r|b=hz!D}( zZ8!09cpvn8kEkm|;oj5iwxKwc6W&UoYC;VTij4VEw=dl^dOJW_s}eCdtq zf`ns_Z`PYNeouyP*$g>)Op0f7&u_;UMB)UZSKPlbtrpdaiC^BoyxKVP#yx5*VWRyD zvoUP;1hJ73NkLe?rqz-{D<-xcGPeWecDaU(;$yp%oJ=&%Io2e6ZV?8cUei=^nFj z?B}qN5lKNy*C`TL%z<+x&Vtkp6iDG1Bgv|3VV#l*L5 z_u>v@SoensHI^{(yE$g#{oP?BBa(u!d`+t*g;q>VT5qzx^E3JDi8YomG2@z(v>c1+ z-Ka8Q`I=Ts3ayxUqIsU)NnNbp*eDa0uW7ZU(29w(j@e#q9P+DmYb;^n(Vxu5-91hg8)d@s zHLaEuS}}3j%X_PhH~TMHV+j*$A2?p?>iIXuij6X1`I=Ts3aywJ_2Qvwqu(AMH?xF^ z{cbiJi(ffLY?KMh*R)zvXvIXYCi_0aQ)6c~vxJHB-ZC5A`y3}W%7o=>S}iHGVq)qW zW3?P_op4q&OPE-E$q8Cl_s={=Y?KMh*R)zvXvM^Dr;Sq^vmafjnI%jduz}e~wm4dB zlnKk%v|3VV#l(+O?5^6Fy@m{62@|rHW#y2)tPskCCy*-UoQF z0y2ZrO_u^5pn!lU-nW43Eg;Jm5m{0F%dQ^%fAi=N&t$wASy@?`kx^AqSb_w`gIL%4 z>@0D7Ph)!du|AGK+F0L!b=QdpFd!I_;b$1ubiOg*=Opxb2A1(z3S-VAV$`ea77rp~ z#j_)`*H^X=p8kHHfO4j0Cu~jzEzYbPxI`ao!)F_)#WLHA6Mcy|)S}&s{N3(~*g#15sD-^yBVjc1&!bOU$g=*O$Fa0ZZ5Y&2ncv}# z5!^88VJty{W^FkRyBe^%9tUdC`XO4p;?zhO@+?G)eG&2iL}s4>Ya$C^1GQ*97lJ*2 z8VQ41v^opH>v@fYk6N_W3c;&$jNpbzUttLn*b8zT5rLzG_Jd-BcaS(1gz&X@QHyqW zLhybtMnpES1PR)=32pa-VkJBdETNs3Xz^YRYjRtKTC|T6f_GjqqF}3#pdFIXcIUzQXg48Ryi1AMh+2hOv^Nlf zcM~-eSb_wOH)>O^uYA;^y@A-^-9(IFC^mMHz-v=%gui?bpIlTAfM*|FYgu*)@;?Hd zk@3mHnu#l~`+51U*UuxWWp>6M|6Ts-PAjVq=h*Q1SZKbAY*@KWXgYHG*UE0I)~w#J zoZj9SEwWc!yXB=KlfB}iJ*^xOVuRXS!oEm&v?ctsDa-mcm(wajeUFd!l6Bogi`J#+ z-8Jk5u|f6Q5sz9d7wR`5!e7>gY9Uducgc<~Z_z1%{Z^3RhIv?m1dfHMRbs={%AJFP zgxJ9Iyy~UzmOs??eCfB?Ij8z4i{P{^4uz z_7WY{)YFk18;R5++bYzuvh`kLL}*B^bE?;la3t_52$kg@xdw@__0mX;X(~6M3TqiJot6 zT>jQ3ONYu~?{4_;H=6V8k+ZCOHJ7(OlvaNB{fYlK<_U(s>a3 z=aN->rF5HHbC2 ztwOCPBMwdJH5enJR-yG9o~f78c{fJTQQIR&t@>vioYHwWMnpES1PMI8P+9)*y$(;}Ti>8}(^^@TaUOFC)S_!J+oGGLYbLM+32LnqnaGVR)S_!UtDa1Zz$1_9LW1U) zY|Qr|%J=9=x$N@pN64t=di8lY8ZlWNWA2f^wa4~iopGq&iXXVg&OZFioz~xq{W2JM7a705&jb2C(=R!=M|SgEt(S}x@H1L_{B#YmeMr? z$D7#j={NoFd#KfXbEvfYtHP$Xx+8GZ;Xg+US4+L3Vnk@jJq{#z-PLP8RJNwXlhHn5udh(QSyjvV z9(%~zDr^b+!FqfV;pg`i`@7m#F~XXl^MN*a7gpnuQ~mtod#FX%{Bk{7Y@yY)zumq< z0%zBV@Yx2=GIXA^x(?#3Bm`;O(T-X?tEuM{BLrg`Sb_x4kB)!EgwF(CgDmTu$md*V zzOZ^AA}yE)YbLM+XHHsIQaYoG4Qi2X6>8zU8zaJ|i4PauT_Eo)r5-EJcCHQ7Vp-?i z7~$AZEhK85MNtdKQq2UGAVG5Eox8_@T66}ZH8Dm+$AKmJgzjEKWoycr6WK?3`uX_$ z7`2K7-$O0-gYIi&XEkiCYXeJ=!1>W7aHZ#KMduGZrl?guw+glRdepfjMudjs`iOcI z32I?hk11-E*q~WItDYsTC0X#{B9Fr*@am*%C9BsRUd=*K{o(ZiTI`+aRTLvaL+;Fg z1kYDWhsyGg@8Jr`Yx`-ZTohXqt*Yg$2cTN)Ka^J2K!k?eSriFeMMGt4%EMKLqZEHV z5>bmIA$rzuyty{81PNSyTmnY}jr^>(DmJcs;^=?5J|Zn1CAxCPh|rKb5|QBXrfYDh zEdTf(wYOOPS{xa4OvTQwh*}iQyOD>CBO^wHh9tHW)rAD^6K!+U25RxjsiQqcL^iMl z35>9Y%HeU~k&~ZL{MTzOYVppy#Oz8gA=;OSF4fOJzQ_A;^=ql8i*P(;(*Q>ryj1Y_oEJ1=>sJ>RHEdTf( zM+$Ui;Am8hGXu4Vf5>|UEslPvPmR~6)wRFfxdaK0wiUiU@XDbXC#yDM*Pzc$;#Q#+ z%~x4Hju>GZMFf^0K{IMrdn8nre`FSoN@xzw>es znsndh+Q5~JXE=yuxT_kq$o3U#@%*SW4!27E$H)dl(pOr71bWU9;p>rWd$~tpmm00w zE`eHnJ|Kz{C!$uVSCmg^YsCh&$R0Ur@rZ%g(*NPekw9NUY=pmPW`I2zd&PR4*L;1T z7GCo;6Ig--Ud_@VpMKN-?p%U1QH&6b2zgFMpN+|TW+tV750$Nr{3mNt`YaC7E6Xx<=vzr~oU zt5?dCHG?V34ZdT7@0GBe%cS%0!|T&a*Z$FVQ#Z+^6O~ZLn-a~QgEi5&o!3>%vp8X1 z97juY;kd-ses@T#_P1GXBs#?JrLmS0?$bN18$OUe^84S~wmwBN=|ozT@uozx=ePvr z#St5v7Y9r734PXxo@ktLi1h9WHhYq^irr*T9?&+r8)1OI( zrbvr2DV1neCYPXmOJalbEn!JMksBp-em^bq>Fs;8`>^^f&SvF|MBcG-zwuisxvX5P zwwxqKK8iAC;TXktS6t$x>eg~T=5#)jb8JO*AwgLyOEhaG<$5V?mpSG2@oj&i%wMR5 z@5;CYWxy00VFt{oE+kmid~5W)=p+9S;+0*Ji7hHYg7U4EXjV4LDY~k2X3ys1+qSmZ z-sBAOdp`Jvic3)LHn9=rZj0(df@RIOM)`Ly>Mq2sUrHvns00bhu~wp4*^Ie&bC1l* zqsO&vXS2OYtN49Ke5b`FD0iFK;M{Fkf&|OC+-)z-=p)4HX_DbDDnWws_?2k3KbrM! z>X-S)jxlZLQWinf!Z&JMf^q_ijW8!*R2LE~YaTyij!2aY@y=_K;V&vdf^zqjXtqCN z?jJuSQ?g}L+s-zNp!5~L+k@}?xCG?{6dRlq5KE9?IhPagf@1lKlG%ITQJh6EDnWv> zFP3P&Mq@5sH!^eIQ`ficYx6)#tHRumBJ=xEE-l$ zXbBQ5YkpzLZY(x9A2Mp?zf)f`p(RMLteKVB-?P3h3wVzXpD)4ZdRZ?=pq9$~tZR&5 z$YT^skg(6$+UJv5FR~G4*s#xn+b35k2OYiB5F;288(4ybefpDXl6N!2N=RUdeO|O& z-v7XwMBfi>_^4%{Pc4_XQes5GRv}@Zhb@=)W^9}EIPBAZtfhAW3KFDk zTZUR}gX32<5)APS6j?_CTT43j$7h0~QF4!->!lcp+?TJ>h7hPl5f-^eUyx8UPOFf> z9toAhBiB1sTrb71+`_qa6v2u!XeLZwbEp!gsB>gzs$dUCV+td^W%~P>b(u@SV|u1RJ0q zw^c~+oek{~t17>VptyQ5$JM!Bx+_=Acd!cD@Yw*{KrOl}SIl?G3KG^tUaOG69uZ}# zpMT^|LovtIxn8=5P|Okff;N0sX9Bh8zCkhH1t>@mz&~zZA%Q(&b?tAtD^sjBRA~fW-3JGqZ_DHBK|H!lF#e8>x>*W!{_sZ50x{`e={1t>QIY&n)4}FCM_K4dm^jO); z{L|S-T7_Edx9QyVe`^&I*dz2!i_hhoFQ!+o-f4XE0iVFH*mq0uPp@_hGQSsPjd^0n zNUwFQlYB zvM$l}h*92_N$uPIJnzahY90LA+r^!?Ule@(Mc+IkQ+t$m=1mVIS|8UljddZ>>(x!g z*W7{__)W#-mtGXCdw4*ejn{t}>5ZM)FHv+%uQY1S zTfVV)QR9n(Z^sYJBgStV>FxUaw8Wba^>SK;#F`g37T^9!mmo8FAhqgTvN8F+X^DO( z_e!HyQOU;QZ(ivdod3X}tV>+<+eq*GH4_r^YWGTG2@==-v7xxB-!*u%J?EJiRBM!X zRkM=Bn9g+Mn>G|rT6tk`=N*HysHNV(=qH)*ejMqo{@ts(^q;-bsCE528;XZ{U4w?V z4W_)N)_<_XlXXUUbuKASoU^ruW25eG8;Z}JbYZajAFe%W)$WnriUtD`ua);qJ6dfw z7H@2SVesy=gPaypl=J72-mT9*k~q??fK7td}vQ+ zv?FoNluwFJ-*8DV?b)u>yQAo;z3}#4&riO*cN(wG8(;mdI71%tGm{fewhj6=;MXI) zeOs1P4X)oijask&@2ld=Zt4~cJh&u}*hM4p+vmnrpE{?PGczEe&okL99)F|W-FoyM z)q5p-rBQ3ZS)Ug-IKErZbozPGdtR2PMQ8Az$MmV{IihD8>q4UcQ=5zbQ`I>*Y}gfa z!m)G@7qZam$OPsyeC~rsIbkV#zJ=0i%#I-v=C?3A4b8uOm9%N(xeMWh6 z`kbCPdP9#iYT@~CiLZ99N)CTypx5G)&S&BIz*$uHANGx^`hRRnuDYzB*LdIK+h_@| zeYj)6^JL7eXVmt(zSYCqe(aG;umlP0bz|1)Z@J?&;52OmLP#E z5#=Linll8ne&*|G1wQ%p`5>K3dwKrh>_sK<9cYgu3aOdR`TZi=Un!i=s>-PEI z+F=P2xZ2ToU)K%x7EV}}Y*Y8irMMr&^$Kaq(6hVT+iSs7$u$>uZiia94!XoyoBMl@ zby%CM)Az!qSb~Jxs=FQ-?bVy#Bw2seT}yEnhr2smC5#z=#29b+aogKh&%M7Lmf#wN zH5v2Ke{b~edZ1PE@*blTsD*2yOLUz-+^hP1a&pXbtE;gD3G7448&zwhw_)+%t+Zy%rRHSbfL;r*)i6?-fyv)`r>9qY*e;TO{4Im#tMpFwP3iONi9WXjqA z!Y`y1C+^-++q2{6boBBJ|&0KzY%6(?F>zqx(YSb65h zU|lLRq0ujE0|>v6R-E85OA#D74y;RMCNw^0ZLoSs_=U9M#6H(=URG_7!^gT*Wjq%1mgaux0|aP__ikJ{j8M$Z=p@Dl?(6%$f<* zLfH~e9nd_}kybVCE38XpCN!Q{Gl5ztTjH0aI%JAyRbv}im�N{IX^OwNSRi8J}H} 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


<34N!sL?C*sEm+4vn_wLn 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 diff --git a/roboverse_learn/il/act/assets/vx300s_dependencies.xml b/roboverse_learn/il/act/assets/vx300s_dependencies.xml deleted file mode 100644 index 93037ab71..000000000 --- a/roboverse_learn/il/act/assets/vx300s_dependencies.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/roboverse_learn/il/act/assets/vx300s_left.xml b/roboverse_learn/il/act/assets/vx300s_left.xml deleted file mode 100644 index 3af6c2350..000000000 --- a/roboverse_learn/il/act/assets/vx300s_left.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/roboverse_learn/il/act/assets/vx300s_right.xml b/roboverse_learn/il/act/assets/vx300s_right.xml deleted file mode 100644 index 495df4780..000000000 --- a/roboverse_learn/il/act/assets/vx300s_right.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/roboverse_learn/il/base/base_dataset.py b/roboverse_learn/il/base/base_dataset.py deleted file mode 100644 index 68830efde..000000000 --- a/roboverse_learn/il/base/base_dataset.py +++ /dev/null @@ -1,53 +0,0 @@ -from typing import Dict - -import torch -import torch.nn - -from roboverse_learn.il.utils.normalizer import LinearNormalizer - - -class BaseLowdimDataset(torch.utils.data.Dataset): - def get_validation_dataset(self) -> "BaseLowdimDataset": - # return an empty dataset by default - return BaseLowdimDataset() - - def get_normalizer(self, **kwargs) -> LinearNormalizer: - raise NotImplementedError() - - def get_all_actions(self) -> torch.Tensor: - raise NotImplementedError() - - def __len__(self) -> int: - return 0 - - def __getitem__(self, idx: int) -> Dict[str, torch.Tensor]: - """ - output: - obs: T, Do - action: T, Da - """ - raise NotImplementedError() - - -class BaseImageDataset(torch.utils.data.Dataset): - def get_validation_dataset(self) -> "BaseLowdimDataset": - # return an empty dataset by default - return BaseImageDataset() - - def get_normalizer(self, **kwargs) -> LinearNormalizer: - raise NotImplementedError() - - def get_all_actions(self) -> torch.Tensor: - raise NotImplementedError() - - def __len__(self) -> int: - return 0 - - def __getitem__(self, idx: int) -> Dict[str, torch.Tensor]: - """ - output: - obs: - key: T, * - action: T, Da - """ - raise NotImplementedError() diff --git a/roboverse_learn/il/base/base_model.py b/roboverse_learn/il/base/base_model.py deleted file mode 100644 index eab1c5d5b..000000000 --- a/roboverse_learn/il/base/base_model.py +++ /dev/null @@ -1,12 +0,0 @@ -import torch - - -class BaseModel: - def __init__(self): - pass - - def forward(self, obs): - raise NotImplementedError("Subclasses should implement this method.") - - def compute_loss(self, batch): - raise NotImplementedError("Subclasses should implement this method.") diff --git a/roboverse_learn/il/runner/base_policy.py b/roboverse_learn/il/configs/base_config.py similarity index 100% rename from roboverse_learn/il/runner/base_policy.py rename to roboverse_learn/il/configs/base_config.py diff --git a/roboverse_learn/il/configs/dp_runner.yaml b/roboverse_learn/il/configs/default_runner.yaml similarity index 65% rename from roboverse_learn/il/configs/dp_runner.yaml rename to roboverse_learn/il/configs/default_runner.yaml index d18623539..a1af490f1 100644 --- a/roboverse_learn/il/configs/dp_runner.yaml +++ b/roboverse_learn/il/configs/default_runner.yaml @@ -1,13 +1,14 @@ defaults: - _self_ - dataset_config: robot_image_dataset - - model_config: ${oc.env:algo_model,ddpm_dit_model} - - eval_config: diffusion_policy_eval - - train_config: diffusion_policy_train + - policy_config: ${oc.env:policy_name,ddpm_dit} + - eval_config: default_eval + - train_config: default_train +policy_name: ${oc.env:policy_name,ddpm_dit} task_name: placeholder name: robot_${task_name} -_target_: roboverse_learn.il.runner.dp_runner.DPRunner +_target_: roboverse_learn.il.runners.default_runner.DefaultRunner image_shape: &image_shape [3, 256, 256] @@ -39,24 +40,27 @@ eval_enable: null eval_path: null multi_run: - run_dir: ./info/outputs/DP/${task_name} + run_dir: ./il_outputs/${policy_name}/${task_name} wandb_name_base: ${task_name} hydra: job: override_dirname: ${name} run: - dir: ./info/outputs/DP/${task_name} + dir: ./il_outputs/${policy_name}/${task_name} sweep: - dir: ./info/outputs/DP/${task_name} + dir: ./il_outputs/${policy_name}/${task_name} subdir: ${hydra.job.num} logging: - project: RoboVerse_DP + project: RoboVerse_IL_${task_name} resume: False mode: online name: ${task_name} - tags: ${task_name} + tags: + - ${task_name} + - ${policy_name} + - ${exp_name} id: null group: null @@ -68,4 +72,4 @@ checkpoint: format_str: 'epoch={epoch:04d}-test_mean_score={test_mean_score:.3f}.ckpt' save_last_ckpt: True save_last_snapshot: False - save_root_dir: ./info/outputs/DP/${task_name} + save_root_dir: ./il_outputs/${policy_name}/${task_name} diff --git a/roboverse_learn/il/configs/eval_config/diffusion_policy_eval.yaml b/roboverse_learn/il/configs/eval_config/default_eval.yaml similarity index 100% rename from roboverse_learn/il/configs/eval_config/diffusion_policy_eval.yaml rename to roboverse_learn/il/configs/eval_config/default_eval.yaml diff --git a/roboverse_learn/il/configs/model_config/ddim_unet_model.yaml b/roboverse_learn/il/configs/policy_config/ddim_unet.yaml similarity index 81% rename from roboverse_learn/il/configs/model_config/ddim_unet_model.yaml rename to roboverse_learn/il/configs/policy_config/ddim_unet.yaml index 05286fd9b..e31d56a4c 100644 --- a/roboverse_learn/il/configs/model_config/ddim_unet_model.yaml +++ b/roboverse_learn/il/configs/policy_config/ddim_unet.yaml @@ -1,4 +1,4 @@ -_target_: roboverse_learn.il.dp.policies.ddim_unet_image_policy.DiffusionUnetImagePolicy +_target_: roboverse_learn.il.policies.dp.ddim_unet_image_policy.DiffusionUnetImagePolicy shape_meta: ${shape_meta} @@ -13,10 +13,10 @@ noise_scheduler: prediction_type: epsilon # or sample obs_encoder: - _target_: roboverse_learn.il.dp.models.vision.multi_image_obs_encoder.MultiImageObsEncoder + _target_: roboverse_learn.il.utils.vision.multi_image_obs_encoder.MultiImageObsEncoder shape_meta: ${shape_meta} rgb_model: - _target_: roboverse_learn.il.dp.models.vision.model_getter.get_resnet + _target_: roboverse_learn.il.utils.vision.model_getter.get_resnet name: resnet18 weights: null resize_shape: null diff --git a/roboverse_learn/il/configs/model_config/ddpm_dit_model.yaml b/roboverse_learn/il/configs/policy_config/ddpm_dit.yaml similarity index 80% rename from roboverse_learn/il/configs/model_config/ddpm_dit_model.yaml rename to roboverse_learn/il/configs/policy_config/ddpm_dit.yaml index bb453cb7c..e118720c8 100644 --- a/roboverse_learn/il/configs/model_config/ddpm_dit_model.yaml +++ b/roboverse_learn/il/configs/policy_config/ddpm_dit.yaml @@ -1,4 +1,4 @@ -_target_: roboverse_learn.il.dp.policies.ddpm_dit_image_policy.DiffusionDiTImagePolicy +_target_: roboverse_learn.il.policies.dp.ddpm_dit_image_policy.DiffusionDiTImagePolicy shape_meta: ${shape_meta} @@ -13,10 +13,10 @@ noise_scheduler: prediction_type: epsilon # or sample obs_encoder: - _target_: roboverse_learn.il.dp.models.vision.multi_image_obs_encoder.MultiImageObsEncoder + _target_: roboverse_learn.il.utils.vision.multi_image_obs_encoder.MultiImageObsEncoder shape_meta: ${shape_meta} rgb_model: - _target_: roboverse_learn.il.dp.models.vision.model_getter.get_resnet + _target_: roboverse_learn.il.utils.vision.model_getter.get_resnet name: resnet18 weights: null resize_shape: null diff --git a/roboverse_learn/il/configs/model_config/ddpm_unet_model.yaml b/roboverse_learn/il/configs/policy_config/ddpm_unet.yaml similarity index 81% rename from roboverse_learn/il/configs/model_config/ddpm_unet_model.yaml rename to roboverse_learn/il/configs/policy_config/ddpm_unet.yaml index 575fd912b..ffad076fb 100644 --- a/roboverse_learn/il/configs/model_config/ddpm_unet_model.yaml +++ b/roboverse_learn/il/configs/policy_config/ddpm_unet.yaml @@ -1,4 +1,4 @@ -_target_: roboverse_learn.il.dp.policies.ddpm_unet_image_policy.DiffusionUnetImagePolicy +_target_: roboverse_learn.il.policies.dp.ddpm_unet_image_policy.DiffusionUnetImagePolicy shape_meta: ${shape_meta} @@ -13,10 +13,10 @@ noise_scheduler: prediction_type: epsilon # or sample obs_encoder: - _target_: roboverse_learn.il.dp.models.vision.multi_image_obs_encoder.MultiImageObsEncoder + _target_: roboverse_learn.il.utils.vision.multi_image_obs_encoder.MultiImageObsEncoder shape_meta: ${shape_meta} rgb_model: - _target_: roboverse_learn.il.dp.models.vision.model_getter.get_resnet + _target_: roboverse_learn.il.utils.vision.model_getter.get_resnet name: resnet18 weights: null resize_shape: null diff --git a/roboverse_learn/il/configs/model_config/fm_dit_model.yaml b/roboverse_learn/il/configs/policy_config/fm_dit.yaml similarity index 71% rename from roboverse_learn/il/configs/model_config/fm_dit_model.yaml rename to roboverse_learn/il/configs/policy_config/fm_dit.yaml index 4779783c2..f3adee0ec 100644 --- a/roboverse_learn/il/configs/model_config/fm_dit_model.yaml +++ b/roboverse_learn/il/configs/policy_config/fm_dit.yaml @@ -1,13 +1,13 @@ -_target_: roboverse_learn.il.fm.policies.fm_dit_image_policy.FlowMatchingDiTImagePolicy +_target_: roboverse_learn.il.policies.fm.fm_dit_image_policy.FlowMatchingDiTImagePolicy shape_meta: ${shape_meta} obs_encoder: - _target_: roboverse_learn.il.dp.models.vision.multi_image_obs_encoder.MultiImageObsEncoder + _target_: roboverse_learn.il.utils.vision.multi_image_obs_encoder.MultiImageObsEncoder shape_meta: ${shape_meta} rgb_model: - _target_: roboverse_learn.il.dp.models.vision.model_getter.get_resnet + _target_: roboverse_learn.il.utils.vision.model_getter.get_resnet name: resnet18 weights: null resize_shape: null diff --git a/roboverse_learn/il/configs/model_config/fm_unet_model.yaml b/roboverse_learn/il/configs/policy_config/fm_unet.yaml similarity index 71% rename from roboverse_learn/il/configs/model_config/fm_unet_model.yaml rename to roboverse_learn/il/configs/policy_config/fm_unet.yaml index 41bb00e90..8101446cb 100644 --- a/roboverse_learn/il/configs/model_config/fm_unet_model.yaml +++ b/roboverse_learn/il/configs/policy_config/fm_unet.yaml @@ -1,13 +1,13 @@ -_target_: roboverse_learn.il.fm.policies.fm_unet_image_policy.FlowMatchingUnetImagePolicy +_target_: roboverse_learn.il.policies.fm.fm_unet_image_policy.FlowMatchingUnetImagePolicy shape_meta: ${shape_meta} obs_encoder: - _target_: roboverse_learn.il.dp.models.vision.multi_image_obs_encoder.MultiImageObsEncoder + _target_: roboverse_learn.il.utils.vision.multi_image_obs_encoder.MultiImageObsEncoder shape_meta: ${shape_meta} rgb_model: - _target_: roboverse_learn.il.dp.models.vision.model_getter.get_resnet + _target_: roboverse_learn.il.utils.vision.model_getter.get_resnet name: resnet18 weights: null resize_shape: null diff --git a/roboverse_learn/il/configs/model_config/score_model.yaml b/roboverse_learn/il/configs/policy_config/score.yaml similarity index 81% rename from roboverse_learn/il/configs/model_config/score_model.yaml rename to roboverse_learn/il/configs/policy_config/score.yaml index 60e21dcaa..d4658d220 100644 --- a/roboverse_learn/il/configs/model_config/score_model.yaml +++ b/roboverse_learn/il/configs/policy_config/score.yaml @@ -1,4 +1,4 @@ -_target_: roboverse_learn.il.dp.policies.score_unet_image_policy.ScoreMatchingUnetImagePolicy +_target_: roboverse_learn.il.policies.dp.score_unet_image_policy.ScoreMatchingUnetImagePolicy shape_meta: ${shape_meta} @@ -13,10 +13,10 @@ noise_scheduler: prediction_type: epsilon # or sample obs_encoder: - _target_: roboverse_learn.il.dp.models.vision.multi_image_obs_encoder.MultiImageObsEncoder + _target_: roboverse_learn.il.utils.vision.multi_image_obs_encoder.MultiImageObsEncoder shape_meta: ${shape_meta} rgb_model: - _target_: roboverse_learn.il.dp.models.vision.model_getter.get_resnet + _target_: roboverse_learn.il.utils.vision.model_getter.get_resnet name: resnet18 weights: null resize_shape: null diff --git a/roboverse_learn/il/configs/model_config/vita_model.yaml b/roboverse_learn/il/configs/policy_config/vita.yaml similarity index 85% rename from roboverse_learn/il/configs/model_config/vita_model.yaml rename to roboverse_learn/il/configs/policy_config/vita.yaml index afee69d6f..d0098e380 100644 --- a/roboverse_learn/il/configs/model_config/vita_model.yaml +++ b/roboverse_learn/il/configs/policy_config/vita.yaml @@ -1,13 +1,13 @@ -_target_: roboverse_learn.il.vita.policies.vita_policy.VITAImagePolicy +_target_: roboverse_learn.il.policies.vita.vita_policy.VITAImagePolicy shape_meta: ${shape_meta} obs_encoder: - _target_: roboverse_learn.il.dp.models.vision.multi_image_obs_encoder.MultiImageObsEncoder + _target_: roboverse_learn.il.utils.vision.multi_image_obs_encoder.MultiImageObsEncoder shape_meta: ${shape_meta} rgb_model: - _target_: roboverse_learn.il.dp.models.vision.model_getter.get_resnet + _target_: roboverse_learn.il.utils.vision.model_getter.get_resnet name: resnet18 weights: null resize_shape: null diff --git a/roboverse_learn/il/configs/train_config/diffusion_policy_train.yaml b/roboverse_learn/il/configs/train_config/default_train.yaml similarity index 100% rename from roboverse_learn/il/configs/train_config/diffusion_policy_train.yaml rename to roboverse_learn/il/configs/train_config/default_train.yaml diff --git a/roboverse_learn/il/datasets/base_dataset.py b/roboverse_learn/il/datasets/base_dataset.py index e2d04a803..ac560f0e9 100644 --- a/roboverse_learn/il/datasets/base_dataset.py +++ b/roboverse_learn/il/datasets/base_dataset.py @@ -1,7 +1,7 @@ from typing import Dict import torch -import torch.nn + from roboverse_learn.il.utils.normalizer import LinearNormalizer diff --git a/roboverse_learn/il/datasets/robot_image_dataset.py b/roboverse_learn/il/datasets/robot_image_dataset.py index f8be9376c..6698d41e5 100644 --- a/roboverse_learn/il/datasets/robot_image_dataset.py +++ b/roboverse_learn/il/datasets/robot_image_dataset.py @@ -12,9 +12,8 @@ downsample_mask, get_val_mask, ) -from roboverse_learn.il.base.base_dataset import BaseImageDataset +from roboverse_learn.il.datasets.base_dataset import BaseImageDataset from roboverse_learn.il.utils.normalizer import LinearNormalizer -from termcolor import cprint class RobotImageDataset(BaseImageDataset): diff --git a/roboverse_learn/il/eval_runner/__init__.py b/roboverse_learn/il/eval_runner/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/roboverse_learn/il/il_run.sh b/roboverse_learn/il/il_run.sh index 76e4eb8d8..d8722f6d9 100644 --- a/roboverse_learn/il/il_run.sh +++ b/roboverse_learn/il/il_run.sh @@ -1,14 +1,14 @@ #!/bin/bash -# Usage: bash roboverse_learn/il/il_run.sh --task_name_set close_box --algo_choose ddpm_dit --demo_num 100 --sim_set mujoco +# Usage: bash roboverse_learn/il/il_run.sh --task_name_set close_box --policy_name ddpm_dit --demo_num 100 --sim_set mujoco task_name_set="close_box" # Tasks, e.g., close_box, stack_cube, pick_cube -algo_choose="ddpm_dit" # IL algorithm, opts: ddpm_unet, ddpm_dit, ddim_unet, fm_unet, fm_dit, vita, act, score +policy_name="ddpm_dit" # IL policy, opts: ddpm_unet, ddpm_dit, ddim_unet, fm_unet, fm_dit, vita, act, score sim_set="mujoco" # Simulator, e.g., mujoco, isaacsim demo_num=90 # Number of demonstrations to collect, train, and eval # Training/eval control train_enable=True -eval_enable=False +eval_enable=True # Training parameters level=0 @@ -28,8 +28,8 @@ while [[ $# -gt 0 ]]; do task_name_set="$2" shift 2 ;; - --algo_choose) - algo_choose="$2" + --policy_name) + policy_name="$2" shift 2 ;; --sim_set) @@ -58,7 +58,7 @@ while [[ $# -gt 0 ]]; do ;; *) echo "Unknown parameter: $1" - echo "Optional parameters: --task_name_set --algo_choose --sim_set --demo_num --train_enable --eval_enable --num_epochs --gpu" + echo "Optional parameters: --task_name_set --policy_name --sim_set --demo_num --train_enable --eval_enable --num_epochs --gpu" exit 1 ;; esac @@ -72,72 +72,27 @@ sed -i "s/^num_demo_success=.*/num_demo_success=$demo_num/" ./roboverse_learn/il sed -i "s/^expert_data_num=.*/expert_data_num=$demo_num/" ./roboverse_learn/il/collect_demo.sh bash ./roboverse_learn/il/collect_demo.sh -# Map algo_choose to model config -case "$algo_choose" in - "ddpm_unet") - algo_model="ddpm_unet_model" - config_name="dp_runner" - main_script="./roboverse_learn/il/train.py" - output_dir="DP" - ;; - "ddpm_dit") - algo_model="ddpm_dit_model" - config_name="dp_runner" - main_script="./roboverse_learn/il/train.py" - output_dir="DP" - ;; - "ddim_unet") - algo_model="ddim_unet_model" - config_name="dp_runner" - main_script="./roboverse_learn/il/train.py" - output_dir="DP" - ;; - "fm_unet") - algo_model="fm_unet_model" - config_name="dp_runner" - main_script="./roboverse_learn/il/train.py" - output_dir="FM" - ;; - "fm_dit") - algo_model="fm_dit_model" - config_name="dp_runner" - main_script="./roboverse_learn/il/train.py" - output_dir="FM" - ;; - "score") - algo_model="score_model" - config_name="dp_runner" - main_script="./roboverse_learn/il/train.py" - output_dir="DP" - ;; - "vita") - algo_model="vita_model" - config_name="dp_runner" - main_script="./roboverse_learn/il/train.py" - output_dir="VITA" - ;; - "act") - echo "=== Running ACT training ===" - sed -i "s/^task_name_set=.*/task_name_set=$task_name_set/" ./roboverse_learn/il/act/act_run.sh - sed -i "s/^sim_set=.*/sim_set=$sim_set/" ./roboverse_learn/il/act/act_run.sh - sed -i "s/^expert_data_num=.*/expert_data_num=$demo_num/" ./roboverse_learn/il/act/act_run.sh - bash ./roboverse_learn/il/act/act_run.sh - echo "=== Completed all data collection, training, and evaluation ===" - exit 0 - ;; - *) - echo "Unsupported algorithm: $algo_choose" - echo "Available options: act, ddpm_unet, ddpm_dit, ddim_unet, fm_unet, fm_dit, score, vita" - exit 1 - ;; -esac +# Map policy_name to model config +config_name="default_runner" +main_script="./roboverse_learn/il/train.py" + +# if policy_name is ACT +if [ "${policy_name}" = "act" ]; then + echo "=== Running ACT training ===" + sed -i "s/^task_name_set=.*/task_name_set=$task_name_set/" ./roboverse_learn/il/policies/act/act_run.sh + sed -i "s/^sim_set=.*/sim_set=$sim_set/" ./roboverse_learn/il/policies/act/act_run.sh + sed -i "s/^expert_data_num=.*/expert_data_num=$demo_num/" ./roboverse_learn/il/policies/act/act_run.sh + bash ./roboverse_learn/il/policies/act/act_run.sh + echo "=== Completed all data collection, training, and evaluation ===" + exit 0 +fi # Run training/evaluation for DP/FM/VITA policies -echo "=== Running ${algo_choose} (${algo_model}) ===" -echo "Selected model: $algo_model" +echo "=== Running ${policy_name} ===" eval_ckpt_name=$demo_num -eval_path="./info/outputs/${output_dir}/${task_name_set}/checkpoints/${eval_ckpt_name}.ckpt" +output_dir="./il_outputs/${policy_name}" +eval_path="${output_dir}/${task_name_set}/checkpoints/${eval_ckpt_name}.ckpt" echo "Checkpoint path: $eval_path" @@ -146,7 +101,7 @@ if [ "${delta_ee}" = 1 ]; then extra="${extra}_delta" fi -export algo_model +export policy_name="${policy_name}" python ${main_script} --config-name=${config_name}.yaml \ task_name=${task_name_set} \ "dataset_config.zarr_path=./data_policy/${task_name_set}FrankaL${level}_${extra}_${demo_num}.zarr" \ diff --git a/roboverse_learn/il/act/README.md b/roboverse_learn/il/policies/act/README.md similarity index 83% rename from roboverse_learn/il/act/README.md rename to roboverse_learn/il/policies/act/README.md index 2086b38f4..645a3a8f6 100644 --- a/roboverse_learn/il/act/README.md +++ b/roboverse_learn/il/policies/act/README.md @@ -2,7 +2,7 @@ ## 1. Install ```bash -cd roboverse_learn/il/act/detr && pip install -e . +cd roboverse_learn/il/policies/act/detr && pip install -e . cd ../../../../../ ``` @@ -18,14 +18,16 @@ cd ../../../../../ Move back to RoboVerse/ and run the following training script. Similar to diffusion policy, it first stores the expert data in zarr format, and then trains a policy. You can configure joint or end effector control. ```bash -./roboverse_learn/il/act/act_run.sh +./roboverse_learn/il/policies/act/act_run.sh ``` ### 3.1 Train only + ```bash train_enable=True eval_enable=False ``` + ### 3.2 Eval only ```bash train_enable=False diff --git a/roboverse_learn/il/act/act_eval_runner.py b/roboverse_learn/il/policies/act/act_eval_runner.py similarity index 98% rename from roboverse_learn/il/act/act_eval_runner.py rename to roboverse_learn/il/policies/act/act_eval_runner.py index e22c84e64..63a6a9ebb 100755 --- a/roboverse_learn/il/act/act_eval_runner.py +++ b/roboverse_learn/il/policies/act/act_eval_runner.py @@ -145,7 +145,7 @@ def parse_args(): "--algo", type=str, default="openvla", - choices=["diffusion_policy", "openvla", "rdt", "act"], + choices=["act"], ) parser.add_argument( "--ckpt_path", @@ -358,7 +358,7 @@ class SimpleRenderCfg: import pickle - from roboverse_learn.il.act.policy import ACTPolicy + from roboverse_learn.il.policies.act import ACTPolicy ckpt_path = os.path.join(args.ckpt_path, act_ckpt_name) policy = ACTPolicy(policy_config) @@ -387,7 +387,7 @@ def post_process(a): max_timesteps = int(max_timesteps * 1) ckpt_name = args.ckpt_path.split("/")[-1] - os.makedirs(f"tmp/{args.algo}/{args.task}/{ckpt_name}", exist_ok=True) + os.makedirs(f"il_outputs/{args.algo}/{args.task}/{ckpt_name}", exist_ok=True) ## cuRobo controller (commented out - not needed for ACT joint control) # *_, robot_ik = get_curobo_models(scenario.robots[0]) @@ -508,12 +508,12 @@ def post_process(a): step += 1 - images_to_video(image_list, f"tmp/{args.algo}/{args.task}/{ckpt_name}/{i}.mp4") + images_to_video(image_list, f"il_outputs/{args.algo}/{args.task}/{ckpt_name}/{i}.mp4") success_rate = TotalSuccess / num_eval print("Success Rate: ", success_rate) - result_dir = f"tmp/{args.algo}/{args.task}/{ckpt_name}" + result_dir = f"il_outputs/{args.algo}/{args.task}/{ckpt_name}" result_file = os.path.join(result_dir, "success_rate.txt") with open(result_file, "w") as f: f.write(f"Success Rate: {success_rate:.4f}\n") diff --git a/roboverse_learn/il/act/act_run.sh b/roboverse_learn/il/policies/act/act_run.sh similarity index 90% rename from roboverse_learn/il/act/act_run.sh rename to roboverse_learn/il/policies/act/act_run.sh index 6f1571616..8c54e7d89 100755 --- a/roboverse_learn/il/act/act_run.sh +++ b/roboverse_learn/il/policies/act/act_run.sh @@ -5,7 +5,7 @@ eval_enable=true ## Parameters task_name_set=close_box -expert_data_num=100 +expert_data_num=90 gpu_id=0 sim_set=mujoco num_epochs=100 @@ -35,7 +35,7 @@ fi if [ "${train_enable}" = "true" ]; then echo "=== Training ===" export CUDA_VISIBLE_DEVICES=${gpu_id} - python -m roboverse_learn.il.act.train \ + python -m roboverse_learn.il.policies.act.train \ --task_name ${task_name_set}_${extra}_chunk${chunk_size} \ --num_episodes ${expert_data_num} \ --dataset_dir data_policy/${task_name_set}FrankaL${level}_${extra}_${expert_data_num}.zarr \ @@ -49,14 +49,14 @@ fi if [ "${eval_enable}" = "true" ]; then echo "=== Evaluation ===" # # export TORCH_CUDA_ARCH_LIST="8.9" - ckpt_path=$(cat ./roboverse_learn/il/act/ckpt_dir_path.txt) + ckpt_path=$(cat ./roboverse_learn/il/policies/act/ckpt_dir_path.txt) # Domain Randomization parameters for evaluation eval_level=3 # 0=None, 1=Scene+Material, 2=+Light, 3=+Camera eval_scene_mode=2 # 0=Manual, 1=USD Table, 2=USD Scene, 3=Full USD eval_seed=42 # Randomization seed (optional) - python -m roboverse_learn.il.act.act_eval_runner \ + python -m roboverse_learn.il.policies.act.act_eval_runner \ --task ${task_name_set} \ --robot franka \ --num_envs 1 \ diff --git a/roboverse_learn/il/act/ckpt_dir_path.txt b/roboverse_learn/il/policies/act/ckpt_dir_path.txt similarity index 100% rename from roboverse_learn/il/act/ckpt_dir_path.txt rename to roboverse_learn/il/policies/act/ckpt_dir_path.txt diff --git a/roboverse_learn/il/act/conda_env.yaml b/roboverse_learn/il/policies/act/conda_env.yaml similarity index 100% rename from roboverse_learn/il/act/conda_env.yaml rename to roboverse_learn/il/policies/act/conda_env.yaml diff --git a/roboverse_learn/il/act/constants.py b/roboverse_learn/il/policies/act/constants.py similarity index 100% rename from roboverse_learn/il/act/constants.py rename to roboverse_learn/il/policies/act/constants.py diff --git a/roboverse_learn/il/act/detr/LICENSE b/roboverse_learn/il/policies/act/detr/LICENSE similarity index 100% rename from roboverse_learn/il/act/detr/LICENSE rename to roboverse_learn/il/policies/act/detr/LICENSE diff --git a/roboverse_learn/il/act/detr/README.md b/roboverse_learn/il/policies/act/detr/README.md similarity index 100% rename from roboverse_learn/il/act/detr/README.md rename to roboverse_learn/il/policies/act/detr/README.md diff --git a/roboverse_learn/il/act/detr/main.py b/roboverse_learn/il/policies/act/detr/main.py similarity index 100% rename from roboverse_learn/il/act/detr/main.py rename to roboverse_learn/il/policies/act/detr/main.py diff --git a/roboverse_learn/il/act/detr/models/__init__.py b/roboverse_learn/il/policies/act/detr/models/__init__.py similarity index 100% rename from roboverse_learn/il/act/detr/models/__init__.py rename to roboverse_learn/il/policies/act/detr/models/__init__.py diff --git a/roboverse_learn/il/act/detr/models/backbone.py b/roboverse_learn/il/policies/act/detr/models/backbone.py similarity index 98% rename from roboverse_learn/il/act/detr/models/backbone.py rename to roboverse_learn/il/policies/act/detr/models/backbone.py index 749e41df8..ff54678f9 100644 --- a/roboverse_learn/il/act/detr/models/backbone.py +++ b/roboverse_learn/il/policies/act/detr/models/backbone.py @@ -11,7 +11,7 @@ from torchvision.models._utils import IntermediateLayerGetter from typing import Dict, List -from util.misc import NestedTensor, is_main_process +from roboverse_learn.il.policies.act.detr.util import NestedTensor, is_main_process from .position_encoding import build_position_encoding diff --git a/roboverse_learn/il/act/detr/models/detr_vae.py b/roboverse_learn/il/policies/act/detr/models/detr_vae.py similarity index 100% rename from roboverse_learn/il/act/detr/models/detr_vae.py rename to roboverse_learn/il/policies/act/detr/models/detr_vae.py diff --git a/roboverse_learn/il/act/detr/models/position_encoding.py b/roboverse_learn/il/policies/act/detr/models/position_encoding.py similarity index 100% rename from roboverse_learn/il/act/detr/models/position_encoding.py rename to roboverse_learn/il/policies/act/detr/models/position_encoding.py diff --git a/roboverse_learn/il/act/detr/models/transformer.py b/roboverse_learn/il/policies/act/detr/models/transformer.py similarity index 100% rename from roboverse_learn/il/act/detr/models/transformer.py rename to roboverse_learn/il/policies/act/detr/models/transformer.py diff --git a/roboverse_learn/il/act/detr/setup.py b/roboverse_learn/il/policies/act/detr/setup.py similarity index 100% rename from roboverse_learn/il/act/detr/setup.py rename to roboverse_learn/il/policies/act/detr/setup.py diff --git a/roboverse_learn/il/act/detr/util/__init__.py b/roboverse_learn/il/policies/act/detr/util/__init__.py similarity index 100% rename from roboverse_learn/il/act/detr/util/__init__.py rename to roboverse_learn/il/policies/act/detr/util/__init__.py diff --git a/roboverse_learn/il/act/detr/util/box_ops.py b/roboverse_learn/il/policies/act/detr/util/box_ops.py similarity index 100% rename from roboverse_learn/il/act/detr/util/box_ops.py rename to roboverse_learn/il/policies/act/detr/util/box_ops.py diff --git a/roboverse_learn/il/act/detr/util/misc.py b/roboverse_learn/il/policies/act/detr/util/misc.py similarity index 100% rename from roboverse_learn/il/act/detr/util/misc.py rename to roboverse_learn/il/policies/act/detr/util/misc.py diff --git a/roboverse_learn/il/act/detr/util/plot_utils.py b/roboverse_learn/il/policies/act/detr/util/plot_utils.py similarity index 100% rename from roboverse_learn/il/act/detr/util/plot_utils.py rename to roboverse_learn/il/policies/act/detr/util/plot_utils.py diff --git a/roboverse_learn/il/act/imitate_episodes.py b/roboverse_learn/il/policies/act/imitate_episodes.py similarity index 100% rename from roboverse_learn/il/act/imitate_episodes.py rename to roboverse_learn/il/policies/act/imitate_episodes.py diff --git a/roboverse_learn/il/act/policy.py b/roboverse_learn/il/policies/act/policy.py similarity index 100% rename from roboverse_learn/il/act/policy.py rename to roboverse_learn/il/policies/act/policy.py diff --git a/roboverse_learn/il/act/scripted_policy.py b/roboverse_learn/il/policies/act/scripted_policy.py similarity index 100% rename from roboverse_learn/il/act/scripted_policy.py rename to roboverse_learn/il/policies/act/scripted_policy.py diff --git a/roboverse_learn/il/act/train.py b/roboverse_learn/il/policies/act/train.py similarity index 99% rename from roboverse_learn/il/act/train.py rename to roboverse_learn/il/policies/act/train.py index 75b3722a8..1896f8580 100644 --- a/roboverse_learn/il/act/train.py +++ b/roboverse_learn/il/policies/act/train.py @@ -215,7 +215,7 @@ def train_bc(train_dataloader, val_dataloader, config): # save training curves plot_history(train_history, validation_history, num_epochs, ckpt_dir, seed) - file_path = os.path.join("./roboverse_learn/il/act", "ckpt_dir_path.txt") + file_path = os.path.join("./roboverse_learn/il/policies/act", "ckpt_dir_path.txt") with open(file_path, 'w') as f: f.write(ckpt_dir) diff --git a/roboverse_learn/il/act/utils.py b/roboverse_learn/il/policies/act/utils.py similarity index 100% rename from roboverse_learn/il/act/utils.py rename to roboverse_learn/il/policies/act/utils.py diff --git a/roboverse_learn/il/base/base_image_policy.py b/roboverse_learn/il/policies/base_image_policy.py similarity index 100% rename from roboverse_learn/il/base/base_image_policy.py rename to roboverse_learn/il/policies/base_image_policy.py diff --git a/roboverse_learn/il/dp/.gitignore b/roboverse_learn/il/policies/dp/.gitignore similarity index 100% rename from roboverse_learn/il/dp/.gitignore rename to roboverse_learn/il/policies/dp/.gitignore diff --git a/roboverse_learn/il/dp/README.md b/roboverse_learn/il/policies/dp/README.md similarity index 83% rename from roboverse_learn/il/dp/README.md rename to roboverse_learn/il/policies/dp/README.md index 62ddb4734..ac27fb4d9 100644 --- a/roboverse_learn/il/dp/README.md +++ b/roboverse_learn/il/policies/dp/README.md @@ -3,7 +3,7 @@ ## 1. Install ```bash -cd roboverse_learn/il/dp +cd roboverse_learn/il/policies/dp pip install -r requirements.txt ``` @@ -18,7 +18,7 @@ Register for a Weights & Biases (wandb) account to obtain an API key. ## 3. Train and eval ```bash -./roboverse_learn/il/dp/dp_run.sh +./roboverse_learn/il/policies/dp/dp_run.sh ``` ### 3.1 Train only diff --git a/roboverse_learn/il/base/__init__.py b/roboverse_learn/il/policies/dp/__init__.py similarity index 100% rename from roboverse_learn/il/base/__init__.py rename to roboverse_learn/il/policies/dp/__init__.py diff --git a/roboverse_learn/il/dp/policies/ddim_unet_image_policy.py b/roboverse_learn/il/policies/dp/ddim_unet_image_policy.py similarity index 96% rename from roboverse_learn/il/dp/policies/ddim_unet_image_policy.py rename to roboverse_learn/il/policies/dp/ddim_unet_image_policy.py index 69efd3822..268030972 100644 --- a/roboverse_learn/il/dp/policies/ddim_unet_image_policy.py +++ b/roboverse_learn/il/policies/dp/ddim_unet_image_policy.py @@ -1,19 +1,17 @@ from typing import Dict import torch -import torch.nn as nn import torch.nn.functional as F from diffusers.schedulers.scheduling_ddim import DDIMScheduler -from roboverse_learn.il.dp.models.diffusion.conditional_unet1d import ConditionalUnet1D -from roboverse_learn.il.dp.models.diffusion.mask_generator import LowdimMaskGenerator -from roboverse_learn.il.dp.models.vision.multi_image_obs_encoder import MultiImageObsEncoder +from roboverse_learn.il.policies.dp.models.diffusion.conditional_unet1d import ConditionalUnet1D +from roboverse_learn.il.policies.dp.models.diffusion.mask_generator import LowdimMaskGenerator +from roboverse_learn.il.utils.vision.multi_image_obs_encoder import MultiImageObsEncoder from einops import rearrange, reduce -from loguru import logger as log from roboverse_learn.il.utils.module_attr_mixin import ModuleAttrMixin from roboverse_learn.il.utils.normalizer import LinearNormalizer from roboverse_learn.il.utils.pytorch_util import dict_apply -from roboverse_learn.il.base.base_image_policy import BaseImagePolicy +from roboverse_learn.il.policies.base_image_policy import BaseImagePolicy class BaseImagePolicy(ModuleAttrMixin): diff --git a/roboverse_learn/il/dp/policies/ddpm_dit_image_policy.py b/roboverse_learn/il/policies/dp/ddpm_dit_image_policy.py similarity index 93% rename from roboverse_learn/il/dp/policies/ddpm_dit_image_policy.py rename to roboverse_learn/il/policies/dp/ddpm_dit_image_policy.py index 6b6d3986c..741483f9c 100644 --- a/roboverse_learn/il/dp/policies/ddpm_dit_image_policy.py +++ b/roboverse_learn/il/policies/dp/ddpm_dit_image_policy.py @@ -2,12 +2,12 @@ from typing import Any, Dict, Mapping, Optional -from diffusers.schedulers.scheduling_ddpm import DDPMScheduler -from roboverse_learn.il.utils.models.flow_net import FlowTransformer -from roboverse_learn.il.dp.models.vision.multi_image_obs_encoder import MultiImageObsEncoder import torch +from diffusers.schedulers.scheduling_ddpm import DDPMScheduler -from roboverse_learn.il.dp.policies.ddpm_image_policy import DiffusionDenoisingImagePolicy +from roboverse_learn.il.utils.models.flow_net import FlowTransformer +from roboverse_learn.il.utils.vision.multi_image_obs_encoder import MultiImageObsEncoder +from roboverse_learn.il.policies.dp.ddpm_image_policy import DiffusionDenoisingImagePolicy class DiffusionDiTImagePolicy(DiffusionDenoisingImagePolicy): diff --git a/roboverse_learn/il/dp/policies/ddpm_image_policy.py b/roboverse_learn/il/policies/dp/ddpm_image_policy.py similarity index 97% rename from roboverse_learn/il/dp/policies/ddpm_image_policy.py rename to roboverse_learn/il/policies/dp/ddpm_image_policy.py index ccfdf5868..397f65cf3 100644 --- a/roboverse_learn/il/dp/policies/ddpm_image_policy.py +++ b/roboverse_learn/il/policies/dp/ddpm_image_policy.py @@ -5,13 +5,13 @@ import torch import torch.nn.functional as F from diffusers.schedulers.scheduling_ddpm import DDPMScheduler -from roboverse_learn.il.dp.models.diffusion.mask_generator import LowdimMaskGenerator -from roboverse_learn.il.dp.models.vision.multi_image_obs_encoder import MultiImageObsEncoder +from roboverse_learn.il.policies.dp.models.diffusion.mask_generator import LowdimMaskGenerator +from roboverse_learn.il.utils.vision.multi_image_obs_encoder import MultiImageObsEncoder from einops import reduce from roboverse_learn.il.utils.normalizer import LinearNormalizer from roboverse_learn.il.utils.pytorch_util import dict_apply -from roboverse_learn.il.base.base_image_policy import BaseImagePolicy +from roboverse_learn.il.policies.base_image_policy import BaseImagePolicy class DiffusionDenoisingImagePolicy(BaseImagePolicy): diff --git a/roboverse_learn/il/dp/policies/ddpm_unet_image_policy.py b/roboverse_learn/il/policies/dp/ddpm_unet_image_policy.py similarity index 89% rename from roboverse_learn/il/dp/policies/ddpm_unet_image_policy.py rename to roboverse_learn/il/policies/dp/ddpm_unet_image_policy.py index e8e5ddb91..53e49dada 100644 --- a/roboverse_learn/il/dp/policies/ddpm_unet_image_policy.py +++ b/roboverse_learn/il/policies/dp/ddpm_unet_image_policy.py @@ -3,11 +3,11 @@ from typing import Any, Dict, Mapping, Optional, Sequence from diffusers.schedulers.scheduling_ddpm import DDPMScheduler -from roboverse_learn.il.dp.models.diffusion.conditional_unet1d import ConditionalUnet1D -from roboverse_learn.il.dp.models.vision.multi_image_obs_encoder import MultiImageObsEncoder +from roboverse_learn.il.policies.dp.models.diffusion.conditional_unet1d import ConditionalUnet1D +from roboverse_learn.il.utils.vision.multi_image_obs_encoder import MultiImageObsEncoder import torch -from roboverse_learn.il.dp.policies.ddpm_image_policy import DiffusionDenoisingImagePolicy +from roboverse_learn.il.policies.dp.ddpm_image_policy import DiffusionDenoisingImagePolicy class DiffusionUnetImagePolicy(DiffusionDenoisingImagePolicy): diff --git a/roboverse_learn/il/dp/dp_run.sh b/roboverse_learn/il/policies/dp/dp_run.sh similarity index 81% rename from roboverse_learn/il/dp/dp_run.sh rename to roboverse_learn/il/policies/dp/dp_run.sh index 415d6cf7b..0c940d71b 100644 --- a/roboverse_learn/il/dp/dp_run.sh +++ b/roboverse_learn/il/policies/dp/dp_run.sh @@ -4,7 +4,7 @@ train_enable=True # True for training, False for evaluation eval_enable=True task_name_set=close_box -config_name=dp_runner +config_name=default_runner num_epochs=100 port=50010 seed=42 @@ -26,11 +26,15 @@ dr_seed=42 # Random seed for reproducible DR (null for random) ## Choose training or inference algorithm # Supported models: -# "ddpm_unet_model", "ddpm_dit_model", "ddim_unet_model", "fm_unet_model", "fm_dit_model", "score_model", "vita_model" -export algo_model="ddpm_dit_model" -eval_path="./info/outputs/DP/${task_name_set}/checkpoints/${eval_ckpt_name}.ckpt" +# "ddpm_unet", "ddpm_dit", "ddim_unet", "vita", "fm_dit", "fm_unet", "score" +policy_name="ddpm_dit" +if [[ "${policy_name}" != *_model ]]; then + policy_name="${policy_name}_model" +fi +export policy_name +eval_path="./il_outputs/${policy_name}/${task_name_set}/checkpoints/${eval_ckpt_name}.ckpt" -echo "Selected model: $algo_model" +echo "Selected model: $policy_name" echo "Checkpoint path: $eval_path" extra="obs:${obs_space}_act:${act_space}" @@ -43,7 +47,7 @@ fi data_level=0 # Level used when collecting data zarr_path="./data_policy/${task_name_set}FrankaL${data_level}_${extra}_${expert_data_num}.zarr" -python ./roboverse_learn/il/dp/main.py --config-name=${config_name}.yaml \ +python ./roboverse_learn/il/policies/dp/main.py --config-name=${config_name}.yaml \ task_name=${task_name_set} \ dataset_config.zarr_path="${zarr_path}" \ train_config.training_params.seed=${seed} \ diff --git a/roboverse_learn/il/dp/models/bet/action_ae/__init__.py b/roboverse_learn/il/policies/dp/models/bet/action_ae/__init__.py similarity index 96% rename from roboverse_learn/il/dp/models/bet/action_ae/__init__.py rename to roboverse_learn/il/policies/dp/models/bet/action_ae/__init__.py index 6808f9035..94bfad283 100644 --- a/roboverse_learn/il/dp/models/bet/action_ae/__init__.py +++ b/roboverse_learn/il/policies/dp/models/bet/action_ae/__init__.py @@ -1,7 +1,7 @@ import abc from typing import Optional, Union -import roboverse_learn.il.dp.models.bet.utils as utils +import roboverse_learn.il.policies.dp.models.bet.utils as utils import torch import torch.nn as nn from torch.utils.data import DataLoader diff --git a/roboverse_learn/il/dp/models/bet/action_ae/discretizers/k_means.py b/roboverse_learn/il/policies/dp/models/bet/action_ae/discretizers/k_means.py similarity index 100% rename from roboverse_learn/il/dp/models/bet/action_ae/discretizers/k_means.py rename to roboverse_learn/il/policies/dp/models/bet/action_ae/discretizers/k_means.py diff --git a/roboverse_learn/il/dp/models/bet/latent_generators/latent_generator.py b/roboverse_learn/il/policies/dp/models/bet/latent_generators/latent_generator.py similarity index 97% rename from roboverse_learn/il/dp/models/bet/latent_generators/latent_generator.py rename to roboverse_learn/il/policies/dp/models/bet/latent_generators/latent_generator.py index 36fbf4399..53485f15b 100644 --- a/roboverse_learn/il/dp/models/bet/latent_generators/latent_generator.py +++ b/roboverse_learn/il/policies/dp/models/bet/latent_generators/latent_generator.py @@ -1,7 +1,7 @@ import abc from typing import Optional, Tuple -import roboverse_learn.il.dp.models.bet.utils as utils +import roboverse_learn.il.policies.dp.models.bet.utils as utils import torch diff --git a/roboverse_learn/il/dp/models/bet/latent_generators/mingpt.py b/roboverse_learn/il/policies/dp/models/bet/latent_generators/mingpt.py similarity index 94% rename from roboverse_learn/il/dp/models/bet/latent_generators/mingpt.py rename to roboverse_learn/il/policies/dp/models/bet/latent_generators/mingpt.py index 55b6d7748..5f4c7510a 100644 --- a/roboverse_learn/il/dp/models/bet/latent_generators/mingpt.py +++ b/roboverse_learn/il/policies/dp/models/bet/latent_generators/mingpt.py @@ -1,13 +1,13 @@ from typing import Optional, Tuple -import roboverse_learn.il.dp.models.bet.latent_generators.latent_generator as latent_generator -import roboverse_learn.il.dp.models.bet.libraries.mingpt.model as mingpt_model -import roboverse_learn.il.dp.models.bet.libraries.mingpt.trainer as mingpt_trainer +import roboverse_learn.il.policies.dp.models.bet.latent_generators.latent_generator as latent_generator +import roboverse_learn.il.policies.dp.models.bet.libraries.mingpt.model as mingpt_model +import roboverse_learn.il.policies.dp.models.bet.libraries.mingpt.trainer as mingpt_trainer import einops import torch import torch.nn as nn import torch.nn.functional as F -from roboverse_learn.il.dp.models.bet.libraries.loss_fn import FocalLoss, soft_cross_entropy +from roboverse_learn.il.policies.dp.models.bet.libraries.loss_fn import FocalLoss, soft_cross_entropy class MinGPT(latent_generator.AbstractLatentGenerator): diff --git a/roboverse_learn/il/dp/models/bet/latent_generators/transformer.py b/roboverse_learn/il/policies/dp/models/bet/latent_generators/transformer.py similarity index 92% rename from roboverse_learn/il/dp/models/bet/latent_generators/transformer.py rename to roboverse_learn/il/policies/dp/models/bet/latent_generators/transformer.py index b9baf4f48..222996213 100644 --- a/roboverse_learn/il/dp/models/bet/latent_generators/transformer.py +++ b/roboverse_learn/il/policies/dp/models/bet/latent_generators/transformer.py @@ -1,12 +1,12 @@ from typing import Optional, Tuple -import roboverse_learn.il.dp.models.bet.latent_generators.latent_generator as latent_generator +import roboverse_learn.il.policies.dp.models.bet.latent_generators.latent_generator as latent_generator import einops import torch import torch.nn as nn import torch.nn.functional as F -from roboverse_learn.il.dp.models.bet.libraries.loss_fn import FocalLoss, soft_cross_entropy -from roboverse_learn.il.dp.models.diffusion.transformer_for_diffusion import ( +from roboverse_learn.il.policies.dp.models.bet.libraries.loss_fn import FocalLoss, soft_cross_entropy +from roboverse_learn.il.policies.dp.models.diffusion.transformer_for_diffusion import ( TransformerForDiffusion, ) diff --git a/roboverse_learn/il/dp/models/bet/libraries/loss_fn.py b/roboverse_learn/il/policies/dp/models/bet/libraries/loss_fn.py similarity index 100% rename from roboverse_learn/il/dp/models/bet/libraries/loss_fn.py rename to roboverse_learn/il/policies/dp/models/bet/libraries/loss_fn.py diff --git a/roboverse_learn/il/dp/models/bet/libraries/mingpt/LICENSE b/roboverse_learn/il/policies/dp/models/bet/libraries/mingpt/LICENSE similarity index 100% rename from roboverse_learn/il/dp/models/bet/libraries/mingpt/LICENSE rename to roboverse_learn/il/policies/dp/models/bet/libraries/mingpt/LICENSE diff --git a/roboverse_learn/il/dp/__init__.py b/roboverse_learn/il/policies/dp/models/bet/libraries/mingpt/__init__.py similarity index 100% rename from roboverse_learn/il/dp/__init__.py rename to roboverse_learn/il/policies/dp/models/bet/libraries/mingpt/__init__.py diff --git a/roboverse_learn/il/dp/models/bet/libraries/mingpt/model.py b/roboverse_learn/il/policies/dp/models/bet/libraries/mingpt/model.py similarity index 100% rename from roboverse_learn/il/dp/models/bet/libraries/mingpt/model.py rename to roboverse_learn/il/policies/dp/models/bet/libraries/mingpt/model.py diff --git a/roboverse_learn/il/dp/models/bet/libraries/mingpt/trainer.py b/roboverse_learn/il/policies/dp/models/bet/libraries/mingpt/trainer.py similarity index 100% rename from roboverse_learn/il/dp/models/bet/libraries/mingpt/trainer.py rename to roboverse_learn/il/policies/dp/models/bet/libraries/mingpt/trainer.py diff --git a/roboverse_learn/il/dp/models/bet/libraries/mingpt/utils.py b/roboverse_learn/il/policies/dp/models/bet/libraries/mingpt/utils.py similarity index 100% rename from roboverse_learn/il/dp/models/bet/libraries/mingpt/utils.py rename to roboverse_learn/il/policies/dp/models/bet/libraries/mingpt/utils.py diff --git a/roboverse_learn/il/dp/models/bet/utils.py b/roboverse_learn/il/policies/dp/models/bet/utils.py similarity index 100% rename from roboverse_learn/il/dp/models/bet/utils.py rename to roboverse_learn/il/policies/dp/models/bet/utils.py diff --git a/roboverse_learn/il/dp/models/diffusion/conditional_unet1d.py b/roboverse_learn/il/policies/dp/models/diffusion/conditional_unet1d.py similarity index 98% rename from roboverse_learn/il/dp/models/diffusion/conditional_unet1d.py rename to roboverse_learn/il/policies/dp/models/diffusion/conditional_unet1d.py index 8c5228110..519f7e388 100644 --- a/roboverse_learn/il/dp/models/diffusion/conditional_unet1d.py +++ b/roboverse_learn/il/policies/dp/models/diffusion/conditional_unet1d.py @@ -4,12 +4,12 @@ import einops import torch import torch.nn as nn -from roboverse_learn.il.dp.models.diffusion.conv1d_components import ( +from roboverse_learn.il.policies.dp.models.diffusion.conv1d_components import ( Conv1dBlock, Downsample1d, Upsample1d, ) -from roboverse_learn.il.dp.models.diffusion.positional_embedding import SinusoidalPosEmb +from roboverse_learn.il.policies.dp.models.diffusion.positional_embedding import SinusoidalPosEmb from einops.layers.torch import Rearrange logger = logging.getLogger(__name__) diff --git a/roboverse_learn/il/dp/models/diffusion/conv1d_components.py b/roboverse_learn/il/policies/dp/models/diffusion/conv1d_components.py similarity index 100% rename from roboverse_learn/il/dp/models/diffusion/conv1d_components.py rename to roboverse_learn/il/policies/dp/models/diffusion/conv1d_components.py diff --git a/roboverse_learn/il/dp/models/diffusion/mask_generator.py b/roboverse_learn/il/policies/dp/models/diffusion/mask_generator.py similarity index 100% rename from roboverse_learn/il/dp/models/diffusion/mask_generator.py rename to roboverse_learn/il/policies/dp/models/diffusion/mask_generator.py diff --git a/roboverse_learn/il/dp/models/diffusion/positional_embedding.py b/roboverse_learn/il/policies/dp/models/diffusion/positional_embedding.py similarity index 100% rename from roboverse_learn/il/dp/models/diffusion/positional_embedding.py rename to roboverse_learn/il/policies/dp/models/diffusion/positional_embedding.py diff --git a/roboverse_learn/il/dp/models/diffusion/transformer_for_diffusion.py b/roboverse_learn/il/policies/dp/models/diffusion/transformer_for_diffusion.py similarity index 99% rename from roboverse_learn/il/dp/models/diffusion/transformer_for_diffusion.py rename to roboverse_learn/il/policies/dp/models/diffusion/transformer_for_diffusion.py index 29c164a79..105c260a3 100644 --- a/roboverse_learn/il/dp/models/diffusion/transformer_for_diffusion.py +++ b/roboverse_learn/il/policies/dp/models/diffusion/transformer_for_diffusion.py @@ -4,7 +4,7 @@ import torch import torch.nn as nn from roboverse_learn.il.utils.module_attr_mixin import ModuleAttrMixin -from roboverse_learn.il.dp.models.diffusion.positional_embedding import SinusoidalPosEmb +from roboverse_learn.il.policies.dp.models.diffusion.positional_embedding import SinusoidalPosEmb logger = logging.getLogger(__name__) diff --git a/roboverse_learn/il/dp/requirements.txt b/roboverse_learn/il/policies/dp/requirements.txt similarity index 100% rename from roboverse_learn/il/dp/requirements.txt rename to roboverse_learn/il/policies/dp/requirements.txt diff --git a/roboverse_learn/il/dp/policies/score_unet_image_policy.py b/roboverse_learn/il/policies/dp/score_unet_image_policy.py similarity index 95% rename from roboverse_learn/il/dp/policies/score_unet_image_policy.py rename to roboverse_learn/il/policies/dp/score_unet_image_policy.py index e6e787553..32cf49592 100644 --- a/roboverse_learn/il/dp/policies/score_unet_image_policy.py +++ b/roboverse_learn/il/policies/dp/score_unet_image_policy.py @@ -4,15 +4,15 @@ import torch.nn as nn import torch.nn.functional as F from diffusers.schedulers.scheduling_ddpm import DDPMScheduler -from roboverse_learn.il.dp.models.diffusion.conditional_unet1d import ConditionalUnet1D -from roboverse_learn.il.dp.models.diffusion.mask_generator import LowdimMaskGenerator -from roboverse_learn.il.dp.models.vision.multi_image_obs_encoder import MultiImageObsEncoder +from roboverse_learn.il.policies.dp.models.diffusion.conditional_unet1d import ConditionalUnet1D +from roboverse_learn.il.policies.dp.models.diffusion.mask_generator import LowdimMaskGenerator +from roboverse_learn.il.utils.vision.multi_image_obs_encoder import MultiImageObsEncoder from einops import rearrange, reduce from loguru import logger as log from roboverse_learn.il.utils.normalizer import LinearNormalizer from roboverse_learn.il.utils.pytorch_util import dict_apply -from roboverse_learn.il.base.base_image_policy import BaseImagePolicy +from roboverse_learn.il.policies.base_image_policy import BaseImagePolicy class ScoreMatchingUnetImagePolicy(BaseImagePolicy): diff --git a/roboverse_learn/il/dp/shared_memory/shared_memory_queue.py b/roboverse_learn/il/policies/dp/shared_memory/shared_memory_queue.py similarity index 97% rename from roboverse_learn/il/dp/shared_memory/shared_memory_queue.py rename to roboverse_learn/il/policies/dp/shared_memory/shared_memory_queue.py index b9196c205..a8c89dd2f 100644 --- a/roboverse_learn/il/dp/shared_memory/shared_memory_queue.py +++ b/roboverse_learn/il/policies/dp/shared_memory/shared_memory_queue.py @@ -4,11 +4,11 @@ from typing import Dict, List, Union import numpy as np -from roboverse_learn.il.dp.shared_memory.shared_memory_util import ( +from roboverse_learn.il.policies.dp.shared_memory.shared_memory_util import ( ArraySpec, SharedAtomicCounter, ) -from roboverse_learn.il.dp.shared_memory.shared_ndarray import SharedNDArray +from roboverse_learn.il.policies.dp.shared_memory.shared_ndarray import SharedNDArray class SharedMemoryQueue: diff --git a/roboverse_learn/il/dp/shared_memory/shared_memory_ring_buffer.py b/roboverse_learn/il/policies/dp/shared_memory/shared_memory_ring_buffer.py similarity index 97% rename from roboverse_learn/il/dp/shared_memory/shared_memory_ring_buffer.py rename to roboverse_learn/il/policies/dp/shared_memory/shared_memory_ring_buffer.py index 5fceea8e9..0c14eab8d 100644 --- a/roboverse_learn/il/dp/shared_memory/shared_memory_ring_buffer.py +++ b/roboverse_learn/il/policies/dp/shared_memory/shared_memory_ring_buffer.py @@ -5,11 +5,11 @@ from typing import Dict, List, Union import numpy as np -from roboverse_learn.il.dp.shared_memory.shared_memory_util import ( +from roboverse_learn.il.policies.dp.shared_memory.shared_memory_util import ( ArraySpec, SharedAtomicCounter, ) -from roboverse_learn.il.dp.shared_memory.shared_ndarray import SharedNDArray +from roboverse_learn.il.policies.dp.shared_memory.shared_ndarray import SharedNDArray class SharedMemoryRingBuffer: diff --git a/roboverse_learn/il/dp/shared_memory/shared_memory_util.py b/roboverse_learn/il/policies/dp/shared_memory/shared_memory_util.py similarity index 100% rename from roboverse_learn/il/dp/shared_memory/shared_memory_util.py rename to roboverse_learn/il/policies/dp/shared_memory/shared_memory_util.py diff --git a/roboverse_learn/il/dp/shared_memory/shared_ndarray.py b/roboverse_learn/il/policies/dp/shared_memory/shared_ndarray.py similarity index 100% rename from roboverse_learn/il/dp/shared_memory/shared_ndarray.py rename to roboverse_learn/il/policies/dp/shared_memory/shared_ndarray.py diff --git a/roboverse_learn/il/fm/README.md b/roboverse_learn/il/policies/fm/README.md similarity index 64% rename from roboverse_learn/il/fm/README.md rename to roboverse_learn/il/policies/fm/README.md index 264b4bbe3..3e4e9e135 100644 --- a/roboverse_learn/il/fm/README.md +++ b/roboverse_learn/il/policies/fm/README.md @@ -1,11 +1,11 @@ # Flow Matching Policies (IL) -Flow Matching variants (UNet and DiT) live here and use the shared IL runners under `il/dp/`. +Flow Matching variants (UNet and DiT) live here and use the shared IL runners under `il/policies/fm/`. ## Install ```bash -cd roboverse_learn/il/dp +cd roboverse_learn/il/policies/fm pip install -r requirements.txt ``` @@ -19,16 +19,11 @@ Create a Weights & Biases account to obtain an API key for logging. ## Train and eval -Use the shared driver and point it at a Flow Matching model: - ```bash -# Choose one: fm_dit_model (DiT backbone) or fm_unet_model (UNet backbone) -export algo_model="fm_dit_model" - -./roboverse_learn/il/dp/dp_run.sh +bash roboverse_learn/il/il_run.sh --task_name_set close_box --policy_name fm_unet # or fm_dit ``` -Inside `dp_run.sh` you can toggle `train_enable` / `eval_enable`, set task names, seeds, GPU id, and checkpoint paths for evaluation. +Inside `il_run.sh` you can toggle `train_enable` / `eval_enable`, set task names, seeds, GPU id, and checkpoint paths for evaluation. ## References diff --git a/roboverse_learn/il/fm/policies/fm_dit_image_policy.py b/roboverse_learn/il/policies/fm/fm_dit_image_policy.py similarity index 97% rename from roboverse_learn/il/fm/policies/fm_dit_image_policy.py rename to roboverse_learn/il/policies/fm/fm_dit_image_policy.py index cca803b22..e3f21010e 100644 --- a/roboverse_learn/il/fm/policies/fm_dit_image_policy.py +++ b/roboverse_learn/il/policies/fm/fm_dit_image_policy.py @@ -5,11 +5,11 @@ from einops import reduce from roboverse_learn.il.utils.models.flow_net import FlowTransformer -from roboverse_learn.il.dp.models.diffusion.mask_generator import LowdimMaskGenerator -from roboverse_learn.il.dp.models.vision.multi_image_obs_encoder import MultiImageObsEncoder +from roboverse_learn.il.policies.dp.models.diffusion.mask_generator import LowdimMaskGenerator +from roboverse_learn.il.utils.vision.multi_image_obs_encoder import MultiImageObsEncoder from roboverse_learn.il.utils.normalizer import LinearNormalizer from roboverse_learn.il.utils.pytorch_util import dict_apply -from roboverse_learn.il.base.base_image_policy import BaseImagePolicy +from roboverse_learn.il.policies.base_image_policy import BaseImagePolicy class FlowMatchingDiTImagePolicy(BaseImagePolicy): diff --git a/roboverse_learn/il/fm/policies/fm_unet_image_policy.py b/roboverse_learn/il/policies/fm/fm_unet_image_policy.py similarity index 96% rename from roboverse_learn/il/fm/policies/fm_unet_image_policy.py rename to roboverse_learn/il/policies/fm/fm_unet_image_policy.py index 5134064dc..58d04272d 100644 --- a/roboverse_learn/il/fm/policies/fm_unet_image_policy.py +++ b/roboverse_learn/il/policies/fm/fm_unet_image_policy.py @@ -4,12 +4,12 @@ import torch.nn.functional as F from einops import reduce -from roboverse_learn.il.dp.models.diffusion.conditional_unet1d import ConditionalUnet1D -from roboverse_learn.il.dp.models.diffusion.mask_generator import LowdimMaskGenerator -from roboverse_learn.il.dp.models.vision.multi_image_obs_encoder import MultiImageObsEncoder +from roboverse_learn.il.policies.dp.models.diffusion.conditional_unet1d import ConditionalUnet1D +from roboverse_learn.il.policies.dp.models.diffusion.mask_generator import LowdimMaskGenerator +from roboverse_learn.il.utils.vision.multi_image_obs_encoder import MultiImageObsEncoder from roboverse_learn.il.utils.normalizer import LinearNormalizer from roboverse_learn.il.utils.pytorch_util import dict_apply -from roboverse_learn.il.base.base_image_policy import BaseImagePolicy +from roboverse_learn.il.policies.base_image_policy import BaseImagePolicy class FlowMatchingUnetImagePolicy(BaseImagePolicy): diff --git a/roboverse_learn/il/fm/requirements.txt b/roboverse_learn/il/policies/fm/requirements.txt similarity index 100% rename from roboverse_learn/il/fm/requirements.txt rename to roboverse_learn/il/policies/fm/requirements.txt diff --git a/roboverse_learn/il/vita/README.md b/roboverse_learn/il/policies/vita/README.md similarity index 70% rename from roboverse_learn/il/vita/README.md rename to roboverse_learn/il/policies/vita/README.md index ebdefd8fa..01f1be1a7 100644 --- a/roboverse_learn/il/vita/README.md +++ b/roboverse_learn/il/policies/vita/README.md @@ -1,11 +1,11 @@ # VITA Policy (IL) -VITA is a vision-to-action Flow Matching policy built on the shared IL runners under `il/dp/`. +VITA is a vision-to-action Flow Matching policy built on the shared IL runners under `il/policies/vita/`. ## Install ```bash -cd roboverse_learn/il/dp +cd roboverse_learn/il/policies/vita pip install -r requirements.txt ``` @@ -19,15 +19,11 @@ Create a Weights & Biases account to obtain an API key for logging. ## Train and eval -Use the shared driver and select the VITA model: - ```bash -export algo_model="vita_model" -./roboverse_learn/il/dp/dp_run.sh +bash roboverse_learn/il/il_run.sh --task_name_set close_box --policy_name vita ``` -Inside `dp_run.sh` you can toggle `train_enable` / `eval_enable`, set task names, seeds, GPU id, and checkpoint paths for evaluation. - +Inside `il_run.sh` you can toggle `train_enable` / `eval_enable`, set task names, seeds, GPU id, and checkpoint paths for evaluation. ## References - Dechen Gao et al., "VITA: Vision-to-Action Flow Matching Policy." (2025). diff --git a/roboverse_learn/il/vita/policies/action_ae.py b/roboverse_learn/il/policies/vita/action_ae.py similarity index 100% rename from roboverse_learn/il/vita/policies/action_ae.py rename to roboverse_learn/il/policies/vita/action_ae.py diff --git a/roboverse_learn/il/vita/requirements.txt b/roboverse_learn/il/policies/vita/requirements.txt similarity index 100% rename from roboverse_learn/il/vita/requirements.txt rename to roboverse_learn/il/policies/vita/requirements.txt diff --git a/roboverse_learn/il/vita/policies/vita_policy.py b/roboverse_learn/il/policies/vita/vita_policy.py similarity index 97% rename from roboverse_learn/il/vita/policies/vita_policy.py rename to roboverse_learn/il/policies/vita/vita_policy.py index 5daa37f49..12d7f2709 100644 --- a/roboverse_learn/il/vita/policies/vita_policy.py +++ b/roboverse_learn/il/policies/vita/vita_policy.py @@ -6,11 +6,11 @@ from roboverse_learn.il.utils.normalizer import LinearNormalizer from roboverse_learn.il.utils.pytorch_util import dict_apply -from roboverse_learn.il.base.base_image_policy import BaseImagePolicy +from roboverse_learn.il.policies.base_image_policy import BaseImagePolicy from roboverse_learn.il.utils.models.flow_net import SimpleFlowNet -from roboverse_learn.il.vita.policies.action_ae import CNNActionEncoder, SimpleActionDecoder -from roboverse_learn.il.dp.models.vision.multi_image_obs_encoder import MultiImageObsEncoder +from roboverse_learn.il.policies.vita.action_ae import CNNActionEncoder, SimpleActionDecoder +from roboverse_learn.il.utils.vision.multi_image_obs_encoder import MultiImageObsEncoder from roboverse_learn.il.utils.flow.flow_matchers import TorchFlowMatcher diff --git a/roboverse_learn/il/runner/__init__.py b/roboverse_learn/il/runner/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/roboverse_learn/il/dp/models/bet/libraries/mingpt/__init__.py b/roboverse_learn/il/runners/__init__.py similarity index 100% rename from roboverse_learn/il/dp/models/bet/libraries/mingpt/__init__.py rename to roboverse_learn/il/runners/__init__.py diff --git a/roboverse_learn/il/base/base_eval_runner.py b/roboverse_learn/il/runners/base_eval_runner.py similarity index 99% rename from roboverse_learn/il/base/base_eval_runner.py rename to roboverse_learn/il/runners/base_eval_runner.py index 9f2b24407..7304dc035 100644 --- a/roboverse_learn/il/base/base_eval_runner.py +++ b/roboverse_learn/il/runners/base_eval_runner.py @@ -1,4 +1,4 @@ -from roboverse_learn.il.runner.base_policy import BasePolicyCfg +from roboverse_learn.il.configs.base_config import BasePolicyCfg try: from curobo.types.math import Pose diff --git a/roboverse_learn/il/base/base_runner.py b/roboverse_learn/il/runners/base_runner.py similarity index 100% rename from roboverse_learn/il/base/base_runner.py rename to roboverse_learn/il/runners/base_runner.py diff --git a/roboverse_learn/il/eval_runner/dp_eval_runner.py b/roboverse_learn/il/runners/default_eval_runner.py similarity index 88% rename from roboverse_learn/il/eval_runner/dp_eval_runner.py rename to roboverse_learn/il/runners/default_eval_runner.py index 8b71e6288..04444a736 100644 --- a/roboverse_learn/il/eval_runner/dp_eval_runner.py +++ b/roboverse_learn/il/runners/default_eval_runner.py @@ -4,26 +4,26 @@ import hydra import numpy as np import torch -from roboverse_learn.il.runner.base_policy import DiffusionPolicyCfg -from roboverse_learn.il.base.base_eval_runner import BaseEvalRunner -from roboverse_learn.il.runner.dp_runner import DPRunner +from roboverse_learn.il.configs.base_config import DiffusionPolicyCfg +from roboverse_learn.il.runners.base_eval_runner import BaseEvalRunner +from roboverse_learn.il.runners.default_runner import DefaultRunner -class DPEvalRunner(BaseEvalRunner): +class DefaultEvalRunner(BaseEvalRunner): """Runner for a diffusion policy, loads in a workspace and policy from checkpoint, and overrides some of the PolicyCFG attributes to match how the policy was trained """ - def _init_policy(self, dprunner: DPRunner, **kwargs): + def _init_policy(self, default_runner: DefaultRunner, **kwargs): self.task_name = kwargs.get("task_name") payload = torch.load(open(kwargs["checkpoint_path"], "rb"), pickle_module=dill) cfg = payload["cfg"] - dprunner.load_payload(payload, exclude_keys=None, include_keys=None) + default_runner.load_payload(payload, exclude_keys=None, include_keys=None) # get policy from workspace - policy = dprunner.model + policy = default_runner.model if cfg.train_config.training_params.use_ema: - policy = dprunner.ema_model + policy = default_runner.ema_model device = torch.device(self.device) policy.to(device) diff --git a/roboverse_learn/il/runner/dp_runner.py b/roboverse_learn/il/runners/default_runner.py similarity index 93% rename from roboverse_learn/il/runner/dp_runner.py rename to roboverse_learn/il/runners/default_runner.py index 0bacf5d27..f8dd183e5 100644 --- a/roboverse_learn/il/runner/dp_runner.py +++ b/roboverse_learn/il/runners/default_runner.py @@ -4,9 +4,8 @@ import pathlib import random import time -from dataclasses import dataclass -from typing import Literal from omegaconf import OmegaConf +from torch.utils.data import DataLoader import hydra import imageio.v2 as iio @@ -14,25 +13,18 @@ import torch import tqdm import wandb -from roboverse_learn.il.utils.ema_model import EMAModel from loguru import logger as log -from metasim.scenario.scenario import ScenarioCfg + from metasim.scenario.cameras import PinholeCameraCfg -from metasim.constants import SimType from metasim.utils.demo_util import get_traj from metasim.utils.setup_util import get_robot -from roboverse_learn.il.base.base_eval_runner import BaseEvalRunner -from roboverse_learn.il.base.base_runner import BaseRunner -from roboverse_learn.il.utils.eval_args import Args -from roboverse_learn.il.utils.eval_runner_getter import get_runner -from roboverse_learn.il.utils.json_logger import JsonLogger -from roboverse_learn.il.utils.lr_scheduler import get_scheduler -from roboverse_learn.il.utils.pytorch_util import dict_apply, optimizer_to -from torch.utils.data import DataLoader - from metasim.task.registry import get_task_class - from metasim.randomization import DomainRandomizationManager, DRConfig +from roboverse_learn.il.utils.ema_model import EMAModel +from roboverse_learn.il.runners.base_runner import BaseRunner +from roboverse_learn.il.utils.json_logger import JsonLogger +from roboverse_learn.il.utils.lr_scheduler import get_scheduler +from roboverse_learn.il.utils.pytorch_util import optimizer_to RANDOMIZATION_AVAILABLE = True @@ -119,7 +111,8 @@ def _validate_state_correctness(current_state, expected_state): return True -class DPRunner(BaseRunner): + +class DefaultRunner(BaseRunner): include_keys = ["global_step", "epoch"] def __init__(self, cfg: OmegaConf, output_dir=None): @@ -132,7 +125,8 @@ def __init__(self, cfg: OmegaConf, output_dir=None): random.seed(seed) # configure model - self.model = hydra.utils.instantiate(cfg.model_config) + self.model = hydra.utils.instantiate(cfg.policy_config) + self.policy_name = cfg.policy_name self.ema_model = None if cfg.train_config.training_params.use_ema: @@ -193,13 +187,6 @@ def train(self): if cfg.train_config.training_params.use_ema: ema = hydra.utils.instantiate(cfg.train_config.ema, model=self.ema_model) - # configure env - # env_runner: BaseImageRunner - # env_runner = hydra.utils.instantiate( - # cfg.task.env_runner, - # output_dir=self.output_dir) - # assert isinstance(env_runner, BaseImageRunner) - env_runner = None wandb_run = None # configure logging @@ -255,14 +242,7 @@ def train(self): batch = dataset.postprocess(batch, device) if train_sampling_batch is None: train_sampling_batch = batch - # print("obs_dict:", batch) - # print("dict_keys:", batch.keys()) - # print("dict_items:", batch.items()) - # print() - # from pprint import pprint - - # pprint(batch) - # compute loss + raw_loss = self.model.compute_loss(batch) loss = ( raw_loss @@ -359,12 +339,6 @@ def train(self): # sample trajectory from training set, and evaluate difference batch = train_sampling_batch obs_dict = batch["obs"] - # print("obs_dict:", obs_dict) - # print("dict_keys:", obs_dict.keys()) - # print("dict_items:", obs_dict.items()) - # print() - # from pprint import pprint - # pprint(obs_dict) gt_action = batch["action"] result = policy.predict_action(obs_dict) @@ -488,6 +462,7 @@ def evaluate(self, ckpt_path=None): randomization_manager = None else: from dataclasses import dataclass as dc + @dc class SimpleRenderCfg: mode: str = render_mode @@ -520,9 +495,11 @@ class SimpleRenderCfg: ) args.checkpoint_path = checkpoint ckpt_name = args.checkpoint_path.name + "_" + time_str - ckpt_name = f"{args.task}/{args.algo}/{args.robot}/{ckpt_name}" - runnerCls = get_runner(args.algo) - policyRunner: BaseEvalRunner = runnerCls( + ckpt_name = f"{args.task}/{self.policy_name}/{args.robot}/{ckpt_name}" + + from roboverse_learn.il.runners.default_eval_runner import DefaultEvalRunner + + policyRunner = DefaultEvalRunner( self, scenario=scenario, num_envs=num_envs, @@ -535,7 +512,7 @@ class SimpleRenderCfg: action_set_steps = ( 2 if policyRunner.policy_cfg.action_config.action_type == "ee" else 1 ) - ## Data + # Data tic = time.time() assert os.path.exists(env.traj_filepath), ( f"Trajectory file: {env.traj_filepath} does not exist." @@ -577,7 +554,6 @@ class SimpleRenderCfg: max_demos = args.max_demo max_demos = min(max_demos, num_demos) - for demo_start_idx in range( args.task_id_range_low, args.task_id_range_low + max_demos, num_envs ): @@ -588,7 +564,8 @@ class SimpleRenderCfg: if randomization_manager is not None and dr_level > 0: for env_id, demo_idx in enumerate(current_demo_idxs): log.info(f"[DP Eval] Episode {demo_idx}: Applying DR") - randomization_manager.apply_randomization(demo_idx=demo_idx, is_initial=(demo_start_idx == args.task_id_range_low)) + randomization_manager.apply_randomization( + demo_idx=demo_idx, is_initial=(demo_start_idx == args.task_id_range_low)) randomization_manager.update_positions_to_table(demo_idx=demo_idx, env_id=env_id) randomization_manager.update_camera_look_at(env_id=env_id) randomization_manager.apply_camera_randomization() @@ -639,15 +616,16 @@ class SimpleRenderCfg: SuccessEnd = success.tolist() total_success += SuccessOnce.count(True) total_completed += len(SuccessOnce) - os.makedirs(f"tmp/{ckpt_name}", exist_ok=True) + base_eval_dir = pathlib.Path(self.output_dir).joinpath("eval", ckpt_name) + base_eval_dir.mkdir(parents=True, exist_ok=True) for i, demo_idx in enumerate(range(demo_start_idx, demo_end_idx)): demo_idx_str = str(demo_idx).zfill(4) if i % args.save_video_freq == 0: iio.mimwrite( - f"tmp/{ckpt_name}/{demo_idx}.mp4", + str(base_eval_dir.joinpath(f"{demo_idx}.mp4")), [images[i] for images in images_list], ) - with open(f"tmp/{ckpt_name}/{demo_idx_str}.txt", "w") as f: + with open(base_eval_dir.joinpath(f"{demo_idx_str}.txt"), "w") as f: f.write(f"Demo Index: {demo_idx}\n") f.write(f"Num Envs: {num_envs}\n") f.write(f"SuccessOnce: {SuccessOnce[i]}\n") @@ -665,7 +643,7 @@ class SimpleRenderCfg: log.info(f"SuccessEnd: {SuccessEnd}") log.info(f"TimeOut: {TimeOut}") log.info(f"FINAL RESULTS: Average Success Rate = {total_success / total_completed:.4f}") - with open(f"tmp/{ckpt_name}/final_stats.txt", "w") as f: + with open(base_eval_dir.joinpath("final_stats.txt"), "w") as f: f.write(f"Total Success: {total_success}\n") f.write(f"Total Completed: {total_completed}\n") f.write(f"Average Success Rate: {total_success / total_completed:.4f}\n") @@ -758,7 +736,7 @@ def collate(x): config_name=pathlib.Path(__file__).stem, ) def main(cfg): - workspace = DPRunner(cfg) + workspace = DefaultRunner(cfg) workspace.run() diff --git a/roboverse_learn/il/train.py b/roboverse_learn/il/train.py index 6306cc9e5..66e2c9fe5 100644 --- a/roboverse_learn/il/train.py +++ b/roboverse_learn/il/train.py @@ -8,7 +8,7 @@ here = os.path.dirname(os.path.abspath(__file__)) project_root = os.path.dirname(here) sys.path.insert(0, project_root) -from roboverse_learn.il.base.base_runner import BaseRunner +from roboverse_learn.il.runners.base_runner import BaseRunner abs_config_path = str(pathlib.Path(__file__).resolve().parent.joinpath("configs").absolute()) OmegaConf.register_new_resolver("eval", eval, replace=True) diff --git a/roboverse_learn/il/utils/eval_args.py b/roboverse_learn/il/utils/eval_args.py index 946d471bc..222959ae4 100644 --- a/roboverse_learn/il/utils/eval_args.py +++ b/roboverse_learn/il/utils/eval_args.py @@ -26,8 +26,6 @@ class Args: """Low end of the task id range""" task_id_range_high: int = 1000 """High end of the task id range""" - algo: str = "diffusion_policy" - """Algorithm to use""" subset: str = "pickcube_l0" """Subset your ckpt trained on""" action_set_steps: int = 1 diff --git a/roboverse_learn/il/utils/eval_runner_getter.py b/roboverse_learn/il/utils/eval_runner_getter.py deleted file mode 100644 index 17ffacd8a..000000000 --- a/roboverse_learn/il/utils/eval_runner_getter.py +++ /dev/null @@ -1,9 +0,0 @@ -def get_runner(algo): - if algo == "diffusion_policy": - from roboverse_learn.il.eval_runner.dp_eval_runner import DPEvalRunner - - return DPEvalRunner - else: - raise NotImplementedError( - f"algorithm {algo} currently not supported by evaluation! Check roboverse_learn/eval_runner and roboverse_learn/eval_runner/utils/common/eval_runner_getter.py" - ) diff --git a/roboverse_learn/il/utils/models/flow_net.py b/roboverse_learn/il/utils/models/flow_net.py index d58b5cfc4..badaa7ac0 100644 --- a/roboverse_learn/il/utils/models/flow_net.py +++ b/roboverse_learn/il/utils/models/flow_net.py @@ -2,7 +2,7 @@ import torch.nn as nn import torch.nn.functional as F -from roboverse_learn.il.dp.models.diffusion.positional_embedding import RotaryPosEmb, SinusoidalPosEmb +from roboverse_learn.il.policies.dp.models.diffusion.positional_embedding import RotaryPosEmb, SinusoidalPosEmb from roboverse_learn.il.utils.models.layers import Mlp diff --git a/roboverse_learn/il/dp/models/vision/crop_randomizer.py b/roboverse_learn/il/utils/vision/crop_randomizer.py similarity index 100% rename from roboverse_learn/il/dp/models/vision/crop_randomizer.py rename to roboverse_learn/il/utils/vision/crop_randomizer.py diff --git a/roboverse_learn/il/dp/models/vision/model_getter.py b/roboverse_learn/il/utils/vision/model_getter.py similarity index 100% rename from roboverse_learn/il/dp/models/vision/model_getter.py rename to roboverse_learn/il/utils/vision/model_getter.py diff --git a/roboverse_learn/il/dp/models/vision/multi_image_obs_encoder.py b/roboverse_learn/il/utils/vision/multi_image_obs_encoder.py similarity index 98% rename from roboverse_learn/il/dp/models/vision/multi_image_obs_encoder.py rename to roboverse_learn/il/utils/vision/multi_image_obs_encoder.py index 912518709..a5aa59bf7 100644 --- a/roboverse_learn/il/dp/models/vision/multi_image_obs_encoder.py +++ b/roboverse_learn/il/utils/vision/multi_image_obs_encoder.py @@ -6,7 +6,7 @@ import torchvision from roboverse_learn.il.utils.pytorch_util import dict_apply, replace_submodules from roboverse_learn.il.utils.module_attr_mixin import ModuleAttrMixin -from roboverse_learn.il.dp.models.vision.crop_randomizer import CropRandomizer +from roboverse_learn.il.utils.vision.crop_randomizer import CropRandomizer class MultiImageObsEncoder(ModuleAttrMixin): diff --git a/roboverse_learn/vla/OpenVLA/vla_eval.py b/roboverse_learn/vla/OpenVLA/vla_eval.py index b400b84c0..f8371e0e9 100644 --- a/roboverse_learn/vla/OpenVLA/vla_eval.py +++ b/roboverse_learn/vla/OpenVLA/vla_eval.py @@ -24,7 +24,7 @@ from metasim.utils.demo_util import get_traj from metasim.utils.setup_util import get_robot from metasim.randomization import DomainRandomizationManager, DRConfig -from roboverse_learn.il.runner.base_policy import BasePolicyCfg, ActionCfg, ObsCfg, EndEffectorCfg +from roboverse_learn.il.configs.base_config import BasePolicyCfg, ActionCfg, ObsCfg, EndEffectorCfg @configclass diff --git a/roboverse_learn/vla/SmolVLA/smolvla_eval.py b/roboverse_learn/vla/SmolVLA/smolvla_eval.py index 97cd78209..56c9fe922 100755 --- a/roboverse_learn/vla/SmolVLA/smolvla_eval.py +++ b/roboverse_learn/vla/SmolVLA/smolvla_eval.py @@ -31,7 +31,7 @@ from metasim.utils.demo_util import get_traj from metasim.utils.setup_util import get_robot from metasim.randomization import DomainRandomizationManager, DRConfig -from roboverse_learn.il.runner.base_policy import BasePolicyCfg, ActionCfg, ObsCfg, EndEffectorCfg +from roboverse_learn.il.configs.base_config import BasePolicyCfg, ActionCfg, ObsCfg, EndEffectorCfg @configclass diff --git a/roboverse_learn/vla/pi0/pi_eval.py b/roboverse_learn/vla/pi0/pi_eval.py index 9d8687c23..eb434f65a 100644 --- a/roboverse_learn/vla/pi0/pi_eval.py +++ b/roboverse_learn/vla/pi0/pi_eval.py @@ -26,7 +26,7 @@ from openpi_client import image_tools, websocket_client_policy -from roboverse_learn.il.runner.base_policy import ActionCfg, BasePolicyCfg, ObsCfg +from roboverse_learn.il.configs.base_config import ActionCfg, BasePolicyCfg, ObsCfg @configclass From 28f33a544fdbf72444c9542b5277fe5959027771 Mon Sep 17 00:00:00 2001 From: JIAjindou Date: Sun, 14 Dec 2025 09:51:42 +0800 Subject: [PATCH 18/50] [fix] IL new issues --- roboverse_learn/il/il_run.sh | 18 +- roboverse_learn/il/runners/default_runner.py | 2 +- scripts/advanced/collect_demo.py | 495 ++++++++++++++----- 3 files changed, 385 insertions(+), 130 deletions(-) diff --git a/roboverse_learn/il/il_run.sh b/roboverse_learn/il/il_run.sh index d8722f6d9..8154478ab 100644 --- a/roboverse_learn/il/il_run.sh +++ b/roboverse_learn/il/il_run.sh @@ -3,8 +3,8 @@ task_name_set="close_box" # Tasks, e.g., close_box, stack_cube, pick_cube policy_name="ddpm_dit" # IL policy, opts: ddpm_unet, ddpm_dit, ddim_unet, fm_unet, fm_dit, vita, act, score -sim_set="mujoco" # Simulator, e.g., mujoco, isaacsim -demo_num=90 # Number of demonstrations to collect, train, and eval +sim_set="isaacsim" # Simulator, e.g., mujoco, isaacsim +demo_num=100 # Number of demonstrations to collect, train, and eval # Training/eval control train_enable=True @@ -64,13 +64,13 @@ while [[ $# -gt 0 ]]; do esac done -# Collect demo -echo "=== Running collect_demo.sh ===" -sed -i "s/^task_name_set=.*/task_name_set=$task_name_set/" ./roboverse_learn/il/collect_demo.sh -sed -i "s/^sim_set=.*/sim_set=$sim_set/" ./roboverse_learn/il/collect_demo.sh -sed -i "s/^num_demo_success=.*/num_demo_success=$demo_num/" ./roboverse_learn/il/collect_demo.sh -sed -i "s/^expert_data_num=.*/expert_data_num=$demo_num/" ./roboverse_learn/il/collect_demo.sh -bash ./roboverse_learn/il/collect_demo.sh +# # Collect demo +# echo "=== Running collect_demo.sh ===" +# sed -i "s/^task_name_set=.*/task_name_set=$task_name_set/" ./roboverse_learn/il/collect_demo.sh +# sed -i "s/^sim_set=.*/sim_set=$sim_set/" ./roboverse_learn/il/collect_demo.sh +# sed -i "s/^num_demo_success=.*/num_demo_success=$demo_num/" ./roboverse_learn/il/collect_demo.sh +# sed -i "s/^expert_data_num=.*/expert_data_num=$demo_num/" ./roboverse_learn/il/collect_demo.sh +# bash ./roboverse_learn/il/collect_demo.sh # Map policy_name to model config config_name="default_runner" diff --git a/roboverse_learn/il/runners/default_runner.py b/roboverse_learn/il/runners/default_runner.py index f8dd183e5..5d0b854b8 100644 --- a/roboverse_learn/il/runners/default_runner.py +++ b/roboverse_learn/il/runners/default_runner.py @@ -493,7 +493,7 @@ class SimpleRenderCfg: raise ValueError( "No checkpoint found, please provide a valid checkpoint path." ) - args.checkpoint_path = checkpoint + args.checkpoint_path = pathlib.Path(checkpoint) ckpt_name = args.checkpoint_path.name + "_" + time_str ckpt_name = f"{args.task}/{self.policy_name}/{args.robot}/{ckpt_name}" diff --git a/scripts/advanced/collect_demo.py b/scripts/advanced/collect_demo.py index 617d1a02a..d500df2ea 100644 --- a/scripts/advanced/collect_demo.py +++ b/scripts/advanced/collect_demo.py @@ -1,20 +1,3 @@ -"""Demo collection script with domain randomization support. - -Collects demonstration data by replaying trajectories with optional domain randomization. - -Randomization Levels: -- Level 0: No randomization -- Level 1: Scene + Material randomization -- Level 2: Level 1 + Lighting randomization -- Level 3: Level 2 + Camera randomization - -Scene Modes: -- Mode 0: Manual geometry -- Mode 1: USD Table + Manual environment -- Mode 2: USD Scene (Kujiale) + USD Table -- Mode 3: Full USD (Scene + Table + Desktop objects) -""" - from __future__ import annotations from copy import deepcopy @@ -44,6 +27,8 @@ class Args: """Simulator backend""" demo_start_idx: int | None = None """The index of the first demo to collect, None for all demos""" + # max_demo_idx: int | None = None + # """Maximum number of demos to collect, None for all demos""" num_demo_success: int | None = None """Target number of successful demos to collect""" retry_num: int = 0 @@ -70,11 +55,19 @@ class Args: """Rollout unfinished and failed trajectories""" renderer: Literal["isaaclab", "mujoco", "isaacgym", "genesis", "pybullet", "sapien2", "sapien3"] = "mujoco" - # Domain randomization options - level: Literal[0, 1, 2, 3] = 0 - """Randomization level: 0=None, 1=Scene+Material, 2=+Light, 3=+Camera""" - scene_mode: Literal[0, 1, 2, 3] = 0 - """Scene mode: 0=Manual, 1=USD Table, 2=USD Scene, 3=Full USD""" + ## Domain randomization options + enable_randomization: bool = False + """Enable domain randomization during demo collection""" + randomize_materials: bool = True + """Enable material randomization (when randomization is enabled)""" + randomize_lights: bool = False + """Enable light randomization (when randomization is enabled)""" + randomize_cameras: bool = True + """Enable camera randomization (when randomization is enabled)""" + randomize_physics: bool = True + """Enable physics (mass/friction/pose) randomization using ObjectRandomizer""" + randomization_frequency: Literal["per_demo", "per_episode"] = "per_demo" + """When to apply randomization: per_demo (once at start) or per_episode (every episode)""" randomization_seed: int | None = None """Seed for reproducible randomization. If None, uses random seed""" @@ -82,24 +75,32 @@ def __post_init__(self): assert self.run_all or self.run_unfinished or self.run_failed, ( "At least one of run_all, run_unfinished, or run_failed must be True" ) + # if self.max_demo_idx is None: + # self.max_demo_idx = math.inf if self.num_demo_success is None: self.num_demo_success = 100 if self.demo_start_idx is None: self.demo_start_idx = 0 + # Validate randomization settings + if self.enable_randomization: + if not ( + self.randomize_materials or self.randomize_lights or self.randomize_cameras or self.randomize_physics + ): + log.warning("Randomization enabled but no randomization types selected, disabling randomization") + self.enable_randomization = False + log.info(f"Args: {self}") # Log randomization settings - if self.level > 0: - mode_names = {0: "Manual", 1: "USD Table", 2: "USD Scene", 3: "Full USD"} + if self.enable_randomization: log.info("=" * 60) log.info("DOMAIN RANDOMIZATION CONFIGURATION") - log.info(f" Level: {self.level}") - log.info(f" Scene Mode: {self.scene_mode} ({mode_names[self.scene_mode]})") - log.info(" Randomization:") - log.info(" Level 1+: Scene + Material") - log.info(" Level 2+: + Lighting") - log.info(" Level 3+: + Camera") + log.info(f" Materials: {'✓' if self.randomize_materials else '✗'}") + log.info(f" Lights: {'✓' if self.randomize_lights else '✗'}") + log.info(f" Cameras: {'✓' if self.randomize_cameras else '✗'}") + log.info(f" Physics: {'✓' if self.randomize_physics else '✗'} (ObjectRandomizer)") + log.info(f" Frequency: {self.randomization_frequency}") log.info(f" Seed: {self.randomization_seed if self.randomization_seed else 'Random'}") log.info("=" * 60) @@ -119,7 +120,6 @@ def __post_init__(self): from tqdm.rich import tqdm_rich as tqdm from metasim.scenario.cameras import PinholeCameraCfg -from metasim.scenario.lights import DiskLightCfg, SphereLightCfg from metasim.scenario.robot import RobotCfg from metasim.sim import BaseSimHandler from metasim.task.registry import get_task_class @@ -130,9 +130,18 @@ def __post_init__(self): rootutils.setup_root(__file__, pythonpath=True) -# Import randomization components +# Import randomization components (after rootutils setup) try: - from metasim.randomization import DomainRandomizationManager, DRConfig + from roboverse_pack.randomization import ( + CameraPresets, + CameraRandomizer, + LightPresets, + LightRandomizer, + MaterialPresets, + MaterialRandomizer, + ObjectPresets, + ObjectRandomizer, + ) RANDOMIZATION_AVAILABLE = True except ImportError as e: @@ -140,6 +149,270 @@ def __post_init__(self): RANDOMIZATION_AVAILABLE = False +def log_randomization_result( + randomizer_type: str, obj_name: str, property_name: str, before_value, after_value, unit: str = "" +): + """Log randomization results in a consistent format.""" + if hasattr(before_value, "cpu"): + before_str = str(before_value.cpu().numpy().round(3) if hasattr(before_value, "numpy") else before_value) + else: + before_str = str(before_value) + + if hasattr(after_value, "cpu"): + after_str = str(after_value.cpu().numpy().round(3) if hasattr(after_value, "numpy") else after_value) + else: + after_str = str(after_value) + + log.info(f" [{randomizer_type}] {obj_name}.{property_name}: {before_str} -> {after_str} {unit}") + + +def log_randomization_header(randomizer_name: str, description: str = ""): + """Log a consistent header for randomization sections.""" + log.info("=" * 50) + if description: + log.info(f"{randomizer_name}: {description}") + else: + log.info(randomizer_name) + + +class DomainRandomizationManager: + """Manages domain randomization for demo collection with unified interface.""" + + def __init__(self, args: Args, scenario, handler): + self.args = args + self.scenario = scenario + self.handler = handler + self.randomizers = [] + self._demo_count = 0 + + # Early validation + if not self._validate_setup(): + return + + log_randomization_header("DOMAIN RANDOMIZATION SETUP", "Initializing randomizers") + self._setup_randomizers() + log.info(f"Setup complete: {len(self.randomizers)} randomizers ready") + + def _validate_setup(self) -> bool: + """Validate if randomization can be set up.""" + if not self.args.enable_randomization: + log.info("Domain randomization disabled") + return False + + if not RANDOMIZATION_AVAILABLE: + log.warning("Domain randomization requested but components not available") + return False + + return True + + def _setup_randomizers(self): + """Initialize all randomizers based on configuration.""" + seed = self.args.randomization_seed + self._setup_reproducibility(seed) + + # Setup each randomization type symmetrically + if self.args.randomize_materials: + self._setup_material_randomizers(seed) + + if self.args.randomize_lights: + self._setup_light_randomizers(seed) + + if self.args.randomize_cameras: + self._setup_camera_randomizers(seed) + + if self.args.randomize_physics: + self._setup_physics_randomizers(seed) + + def _setup_reproducibility(self, seed: int | None): + """Setup global reproducibility if seed is provided.""" + if seed is not None: + log.info(f"Setting up reproducible randomization with seed: {seed}") + torch.manual_seed(seed) + import numpy as np + + np.random.seed(seed) + if torch.cuda.is_available(): + torch.cuda.manual_seed(seed) + + def _setup_material_randomizers(self, seed: int | None): + """Setup material randomizers for all objects.""" + objects = getattr(self.scenario, "objects", []) + if not objects: + log.info(" No objects found for material randomization") + return + + log.info(f" Setting up material randomizers for {len(objects)} objects") + for obj in objects: + obj_name = obj.name + config = self._get_material_config(obj_name) + + randomizer = MaterialRandomizer(config, seed=seed) + randomizer.bind_handler(self.handler) + self.randomizers.append(randomizer) + log.info(f" Added MaterialRandomizer for {obj_name}") + + def _setup_light_randomizers(self, seed: int | None): + """Setup light randomizers for all lights.""" + from metasim.scenario.lights import DiskLightCfg, DomeLightCfg, SphereLightCfg + + lights = getattr(self.scenario, "lights", []) + if not lights: + log.info(" No lights found for light randomization") + return + + log.info(f" Setting up light randomizers for {len(lights)} lights") + for light in lights: + light_name = getattr(light, "name", f"light_{len(self.randomizers)}") + + if isinstance(light, DomeLightCfg): + config = LightPresets.dome_ambient(light_name) + elif isinstance(light, (SphereLightCfg, DiskLightCfg)): + config = LightPresets.sphere_ceiling_light(light_name) + else: + log.warning(f"Unknown light type for {light_name}, using sphere_ceiling_light preset") + config = LightPresets.sphere_ceiling_light(light_name) + + randomizer = LightRandomizer(config, seed=seed) + randomizer.bind_handler(self.handler) + self.randomizers.append(randomizer) + log.info(f" Added LightRandomizer for {light_name}") + + def _setup_camera_randomizers(self, seed: int | None): + """Setup camera randomizers for all cameras.""" + cameras = getattr(self.scenario, "cameras", []) + if not cameras: + log.info(" No cameras found for camera randomization") + return + + log.info(f" Setting up camera randomizers for {len(cameras)} cameras") + for camera in cameras: + camera_name = getattr(camera, "name", f"camera_{len(self.randomizers)}") + config = CameraPresets.surveillance_camera(camera_name) + + randomizer = CameraRandomizer(config, seed=seed) + randomizer.bind_handler(self.handler) + self.randomizers.append(randomizer) + log.info(f" Added CameraRandomizer for {camera_name}") + + def _get_material_config(self, obj_name: str): + """Get appropriate material configuration based on object type.""" + obj_lower = obj_name.lower() + if "cube" in obj_lower: + return MaterialPresets.mdl_family_object(obj_name, family="metal") + elif "sphere" in obj_lower: + return MaterialPresets.rubber_object(obj_name) + else: + return MaterialPresets.mdl_family_object(obj_name, family="wood") + + def _setup_physics_randomizers(self, seed: int | None): + """Setup unified ObjectRandomizers for robots and objects.""" + robots = getattr(self.scenario, "robots", []) + objects = getattr(self.scenario, "objects", []) + + self._setup_object_randomizers(robots, objects, seed) + + def _setup_object_randomizers(self, robots: list, objects: list, seed: int | None): + """Setup unified ObjectRandomizers for all physical entities.""" + log.info(" Setting up ObjectRandomizers for physics randomization") + + # Robot randomization + if robots: + robot_name = robots[0] if isinstance(robots[0], str) else robots[0].name + robot_randomizer = ObjectRandomizer(ObjectPresets.robot_base(robot_name), seed=seed) + robot_randomizer.bind_handler(self.handler) + self.randomizers.append(robot_randomizer) + log.info(f" Added ObjectRandomizer for robot {robot_name}") + + # Object randomization + if objects: + for obj in objects: + obj_name = obj.name + config = self._get_object_physics_config(obj_name) + + obj_randomizer = ObjectRandomizer(config, seed=seed) + obj_randomizer.bind_handler(self.handler) + self.randomizers.append(obj_randomizer) + log.info(f" Added ObjectRandomizer for {obj_name}") + + if not robots and not objects: + log.info(" No robots or objects found for physics randomization") + + def _get_object_physics_config(self, obj_name: str): + """Get appropriate physics configuration based on object type.""" + obj_lower = obj_name.lower() + if "cube" in obj_lower: + return ObjectPresets.grasping_target(obj_name) + elif "sphere" in obj_lower: + return ObjectPresets.bouncy_object(obj_name) + else: + return ObjectPresets.physics_only(obj_name) + + def randomize_for_demo(self, demo_idx: int): + """Apply randomization for a new demo.""" + if not self._should_randomize(demo_idx): + return + + log_randomization_header("DOMAIN RANDOMIZATION", f"Demo {demo_idx}") + + # Apply all randomizers and collect statistics + stats = self._apply_all_randomizers() + + # Log summary + self._log_randomization_summary(stats) + self._demo_count += 1 + + def _should_randomize(self, demo_idx: int) -> bool: + """Check if randomization should be applied for this demo.""" + if not self.args.enable_randomization or not self.randomizers: + return False + + return self.args.randomization_frequency == "per_demo" or ( + self.args.randomization_frequency == "per_episode" and demo_idx == 0 + ) + + def _apply_all_randomizers(self) -> dict[str, int]: + """Apply all randomizers and return statistics.""" + stats = {"ObjectRandomizer": 0, "MaterialRandomizer": 0, "LightRandomizer": 0, "CameraRandomizer": 0} + + for randomizer in self.randomizers: + try: + obj_name = self._get_randomizer_target_name(randomizer) + randomizer_type = type(randomizer).__name__ + + # Apply randomization + randomizer() + stats[randomizer_type] = stats.get(randomizer_type, 0) + 1 + log.info(f" Applied {randomizer_type} for {obj_name}") + + except Exception as e: + log.warning(f" {type(randomizer).__name__} failed for {obj_name}: {e}") + + return stats + + def _get_randomizer_target_name(self, randomizer) -> str: + """Extract target object name from randomizer configuration.""" + if not hasattr(randomizer, "cfg"): + return "unknown" + + cfg = randomizer.cfg + if hasattr(cfg, "obj_name"): + return cfg.obj_name + elif hasattr(cfg, "light_name"): + return cfg.light_name + elif hasattr(cfg, "camera_name"): + return cfg.camera_name + else: + return "unknown" + + def _log_randomization_summary(self, stats: dict[str, int]): + """Log a summary of applied randomizers.""" + applied_types = [f"{name}: {count}" for name, count in stats.items() if count > 0] + if applied_types: + log.info(f"Applied randomizers: {', '.join(applied_types)}") + else: + log.info("No randomizers were applied") + + def get_actions(all_actions, env, demo_idxs: list[int], robot: RobotCfg): action_idxs = env._episode_steps @@ -182,12 +455,15 @@ def ensure_clean_state(handler, expected_state=None): handler.simulate() current_state = handler.get_states() + # Only start checking after minimum steps if step >= min_steps: if prev_state is not None: + # Check if key states are stable (focus on articulated objects) is_stable = True if hasattr(current_state, "objects") and hasattr(prev_state, "objects"): for obj_name, obj_state in current_state.objects.items(): if obj_name in prev_state.objects: + # Check DOF positions for articulated objects curr_dof = getattr(obj_state, "dof_pos", None) prev_dof = getattr(prev_state.objects[obj_name], "dof_pos", None) if curr_dof is not None and prev_dof is not None: @@ -195,45 +471,51 @@ def ensure_clean_state(handler, expected_state=None): is_stable = False break + # Additional validation: check if we're stable at the RIGHT state if is_stable and expected_state is not None: is_correct_state = _validate_state_correctness(current_state, expected_state) if not is_correct_state: + # We're stable but at wrong state - force more simulation log.debug(f"State stable but incorrect at step {step}, continuing simulation...") stable_count = 0 is_stable = False + # Continue simulating to let physics settle properly if is_stable: stable_count += 1 - if stable_count >= 2: + if stable_count >= 2: # Stable for 2 consecutive steps at correct state break else: stable_count = 0 prev_state = current_state + # Final validation if we ran out of steps if expected_state is not None: final_state = handler.get_states() is_final_correct = _validate_state_correctness(final_state, expected_state) if not is_final_correct: log.warning(f"State validation failed after {max_steps} steps - reset may not have taken full effect") + # Final state refresh handler.get_states() def _validate_state_correctness(current_state, expected_state): """Validate that current state matches expected initial state for critical objects.""" if not hasattr(current_state, "objects") or not hasattr(expected_state, "objects"): - return True + return True # Can't validate, assume correct + # Focus on articulated objects which are most prone to reset issues critical_objects = [] for obj_name, expected_obj in expected_state.objects.items(): if hasattr(expected_obj, "dof_pos") and getattr(expected_obj, "dof_pos", None) is not None: critical_objects.append(obj_name) if not critical_objects: - return True + return True # No critical objects to validate - tolerance = 5e-3 + tolerance = 5e-3 # Reasonable tolerance for DOF positions for obj_name in critical_objects: if obj_name not in current_state.objects: @@ -242,11 +524,13 @@ def _validate_state_correctness(current_state, expected_state): expected_obj = expected_state.objects[obj_name] current_obj = current_state.objects[obj_name] + # Check DOF positions for articulated objects (most critical for demo consistency) expected_dof = getattr(expected_obj, "dof_pos", None) current_dof = getattr(current_obj, "dof_pos", None) if expected_dof is not None and current_dof is not None: if not torch.allclose(current_dof, expected_dof, atol=tolerance): + # Log the specific difference for debugging diff = torch.abs(current_dof - expected_dof).max().item() log.debug(f"DOF mismatch for {obj_name}: max diff = {diff:.6f} (tolerance = {tolerance})") return False @@ -257,7 +541,9 @@ def _validate_state_correctness(current_state, expected_state): def force_reset_to_state(env, state, env_id): """Force reset environment to specific state with validation.""" env.reset(states=[state], env_ids=[env_id]) + # Pass expected state for validation ensure_clean_state(env.handler, expected_state=state) + # Reset episode counter AFTER stabilization to ensure demo starts from action 0 if hasattr(env, "_episode_steps"): env._episode_steps[env_id] = 0 @@ -317,6 +603,7 @@ def save(self, demo_idx: int, status: str): assert demo_idx in self.cache assert status in ["success", "failed"], f"Invalid status: {status}" + # Use demo_idx directly as continuous_idx to maintain consistency continuous_idx = demo_idx save_dir = os.path.join(self.base_save_dir, status, f"demo_{continuous_idx:04d}") @@ -324,7 +611,9 @@ def save(self, demo_idx: int, status: str): os.remove(os.path.join(save_dir, "status.txt")) os.makedirs(save_dir, exist_ok=True) - log.info(f"Saving demo {demo_idx} as {continuous_idx:04d} to {save_dir}") + log.info(f"Saving demo {demo_idx} (original) as {continuous_idx:04d} (continuous) to {save_dir}") + + ## Option 1: Save immediately, blocking and slower from metasim.utils.save_util import save_demo @@ -334,6 +623,9 @@ def save(self, demo_idx: int, status: str): with open(os.path.join(save_dir, "status.txt"), "w") as f: f.write(status) + ## Option 2: Save in a separate process, non-blocking, not friendly to KeyboardInterrupt + # self.save_request_queue.put({"demo": self.cache[demo_idx], "save_dir": save_dir}) + def delete(self, demo_idx: int): assert demo_idx in self.cache del self.cache[demo_idx] @@ -408,57 +700,32 @@ def main(): task_cls = get_task_class(args.task) if args.task in {"stack_cube", "pick_cube", "pick_butter"}: - dp_camera = True - else: - dp_camera = args.task != "close_box" - - is_libero_dataset = "libero_90" in args.task - - if is_libero_dataset: - dp_pos = (2.0, 0.0, 2) - elif dp_camera: dp_pos = (1.0, 0.0, 0.75) + elif args.task in {"close_box"}: + dp_pos = (0, 0, 0) else: - dp_pos = (1.5, 0.0, 1.5) + dp_pos = (1.0, 0.0, 0.75) - camera = PinholeCameraCfg(data_types=["rgb", "depth"], pos=dp_pos, look_at=(0.0, 0.0, 0.0)) + # is_libero_dataset = "libero_90" in args.task - # Lighting setup - if args.render.mode == "pathtracing": - ceiling_main = 18000.0 - ceiling_corners = 8000.0 - else: - ceiling_main = 12000.0 - ceiling_corners = 5000.0 - - lights = [ - DiskLightCfg( - name="ceiling_main", - intensity=ceiling_main, - color=(1.0, 1.0, 1.0), - radius=1.2, - pos=(0.0, 0.0, 2.8), - rot=(0.7071, 0.0, 0.0, 0.7071), - ), - SphereLightCfg( - name="ceiling_ne", intensity=ceiling_corners, color=(1.0, 1.0, 1.0), radius=0.6, pos=(1.0, 1.0, 2.5) - ), - SphereLightCfg( - name="ceiling_nw", intensity=ceiling_corners, color=(1.0, 1.0, 1.0), radius=0.6, pos=(-1.0, 1.0, 2.5) - ), - SphereLightCfg( - name="ceiling_sw", intensity=ceiling_corners, color=(1.0, 1.0, 1.0), radius=0.6, pos=(-1.0, -1.0, 2.5) - ), - SphereLightCfg( - name="ceiling_se", intensity=ceiling_corners, color=(1.0, 1.0, 1.0), radius=0.6, pos=(1.0, -1.0, 2.5) - ), - ] + # if is_libero_dataset: + # dp_pos = (2.0, 0.0, 2) + # elif dp_camera: + # # import warnings + # # warnings.warn("Using dp camera position!") + # dp_pos = (1.0, 0.0, 0.75) + # else: + # dp_pos = (1.5, 0.0, 1.5) + + # libero specific camera position + # dp_pos = (0.8, -0, 1.6) + # look_at = (-2.5, 0.0, 0.0) + camera = PinholeCameraCfg(data_types=["rgb", "depth"], pos=dp_pos, look_at=(0.0, 0.0, 0.0)) scenario = task_cls.scenario.update( robots=[args.robot], scene=args.scene, cameras=[camera], - lights=lights, render=args.render, simulator=args.sim, renderer=args.renderer, @@ -469,23 +736,12 @@ def main(): device = torch.device("cuda" if torch.cuda.is_available() else "cpu") env = task_cls(scenario, device=device) + # Initialize domain randomization manager + randomization_manager = DomainRandomizationManager(args, scenario, env.handler) ## Data assert os.path.exists(env.traj_filepath), f"Trajectory file does not exist: {env.traj_filepath}" init_states, all_actions, all_states = get_traj(env.traj_filepath, robot, env.handler) - # Initialize domain randomization manager - randomization_manager = DomainRandomizationManager( - config=DRConfig( - level=args.level, - scene_mode=args.scene_mode, - randomization_seed=args.randomization_seed, - ), - scenario=scenario, - handler=env.handler, - init_states=init_states, - render_cfg=args.render, - ) - tot_demo = len(all_actions) if args.split == "train": init_states = init_states[: int(tot_demo * 0.9)] @@ -502,6 +758,11 @@ def main(): ######################################################## ## Main ######################################################## + # if args.max_demo_idx > n_demo: + # log.warning( + # f"Max demo {args.max_demo_idx} is greater than the number of demos in the dataset {n_demo}, using {n_demo}" + # ) + # max_demo = min(args.max_demo_idx, n_demo) max_demo = n_demo try_num = args.retry_num + 1 @@ -510,8 +771,10 @@ def main(): ## CollectingDemo -> Timeout -> Retry/GiveUp -> NextDemo ## Setup + # Get task description from environment task_desc = getattr(env, "task_desc", "") - collector = DemoCollector(env.handler, robot, task_desc) + collector = DemoCollector(env.handler, robot, task_desc, demo_start_idx=args.demo_start_idx) + # pbar = tqdm(total=max_demo - args.demo_start_idx, desc="Collecting demos") pbar = tqdm(total=args.num_demo_success, desc="Collecting successful demos") ## State variables @@ -542,28 +805,25 @@ def main(): demo_indexer.move_on() log.info(f"Initialize with demo idxs: {demo_idxs}") - ## Apply initial randomization (create scene and update positions) + ## Apply initial randomization for env_id, demo_idx in enumerate(demo_idxs): - randomization_manager.apply_randomization(demo_idx, is_initial=True) - randomization_manager.update_positions_to_table(demo_idx, env_id) - randomization_manager.update_camera_look_at(env_id) - randomization_manager.apply_camera_randomization() # Apply camera randomization after baseline adjustment + randomization_manager.randomize_for_demo(demo_idx) - ## Reset to initial states (after position adjustment) + ## Reset to initial states obs, extras = env.reset(states=[init_states[demo_idx] for demo_idx in demo_idxs]) - ## Wait for environment to stabilize after reset + ## Wait for environment to stabilize after reset (before counting demo steps) + # For initial setup, we can't validate individual states easily, so just ensure stability ensure_clean_state(env.handler) - ## Reset episode step counters after stabilization + ## Reset episode step counters AFTER stabilization if hasattr(env, "_episode_steps"): for env_id in range(env.handler.num_envs): env._episode_steps[env_id] = 0 - ## Record the clean, stabilized initial state + ## Now record the clean, stabilized initial state obs = env.handler.get_states() obs = state_tensor_to_nested(env.handler, obs) - for env_id, demo_idx in enumerate(demo_idxs): log.info(f"Starting Demo {demo_idx} in Env {env_id}") collector.create(demo_idx, obs[env_id]) @@ -572,13 +832,17 @@ def main(): stop_flag = False while not all(finished): + if stop_flag: + pass + if tot_success >= args.num_demo_success: - log.info(f"Reached target number of successful demos ({args.num_demo_success}). Stopping collection.") - break + log.info(f"Reached target number of successful demos ({args.num_demo_success}).") + stop_flag = True if demo_indexer.next_idx >= max_demo: - log.warning(f"Reached maximum demo index ({max_demo}). Stopping collection.") - break + if not stop_flag: + log.warning(f"Reached maximum demo index ({max_demo}), finishing in-flight demos.") + stop_flag = True pbar.set_description(f"Frame {global_step} Success {tot_success} Giveup {tot_give_up}") actions = get_actions(all_actions, env, demo_idxs, robot) @@ -616,10 +880,7 @@ def main(): demo_idxs[env_id] = new_demo_idx log.info(f"Transitioning Env {env_id}: Demo {demo_idx} to Demo {new_demo_idx}") - randomization_manager.apply_randomization(new_demo_idx, is_initial=False) - randomization_manager.update_positions_to_table(new_demo_idx, env_id) - randomization_manager.update_camera_look_at(env_id) - randomization_manager.apply_camera_randomization() # Apply camera randomization + randomization_manager.randomize_for_demo(new_demo_idx) force_reset_to_state(env, init_states[new_demo_idx], env_id) obs = env.handler.get_states() @@ -642,10 +903,7 @@ def main(): if failure_count[env_id] < try_num: log.info(f"Demo {demo_idx} failed {failure_count[env_id]} times, retrying...") - randomization_manager.apply_randomization(demo_idx, is_initial=False) - randomization_manager.update_positions_to_table(demo_idx, env_id) - randomization_manager.update_camera_look_at(env_id) - randomization_manager.apply_camera_randomization() # Apply camera randomization + randomization_manager.randomize_for_demo(demo_idx) force_reset_to_state(env, init_states[demo_idx], env_id) obs = env.handler.get_states() @@ -661,10 +919,7 @@ def main(): if demo_indexer.next_idx < max_demo: new_demo_idx = demo_indexer.next_idx demo_idxs[env_id] = new_demo_idx - randomization_manager.apply_randomization(new_demo_idx, is_initial=False) - randomization_manager.update_positions_to_table(new_demo_idx, env_id) - randomization_manager.update_camera_look_at(env_id) - randomization_manager.apply_camera_randomization() # Apply camera randomization + randomization_manager.randomize_for_demo(new_demo_idx) force_reset_to_state(env, init_states[new_demo_idx], env_id) obs = env.handler.get_states() From e234c8cdd3017f00b5dfd53f2672f1390948e439 Mon Sep 17 00:00:00 2001 From: JIAjindou Date: Sun, 14 Dec 2025 10:00:54 +0800 Subject: [PATCH 19/50] [fix] IL setup path --- roboverse_learn/il/README.md | 2 +- roboverse_learn/il/il_run.sh | 14 +- scripts/advanced/collect_demo.py | 497 ++++++++----------------------- 3 files changed, 129 insertions(+), 384 deletions(-) diff --git a/roboverse_learn/il/README.md b/roboverse_learn/il/README.md index 7a2cbbd9b..af74507d7 100644 --- a/roboverse_learn/il/README.md +++ b/roboverse_learn/il/README.md @@ -10,7 +10,7 @@ Example: # From the repo root cd roboverse_learn/il/policies/dp # or fm/, vita/ depending on the policy pip install -r requirements.txt -cd ../../.. +cd ../../../.. # Run policy training and evaluation bash roboverse_learn/il/il_run.sh --task_name_set close_box --policy_name ddpm_dit # Example: DDPM + DiT diff --git a/roboverse_learn/il/il_run.sh b/roboverse_learn/il/il_run.sh index 8154478ab..4e0056eb2 100644 --- a/roboverse_learn/il/il_run.sh +++ b/roboverse_learn/il/il_run.sh @@ -64,13 +64,13 @@ while [[ $# -gt 0 ]]; do esac done -# # Collect demo -# echo "=== Running collect_demo.sh ===" -# sed -i "s/^task_name_set=.*/task_name_set=$task_name_set/" ./roboverse_learn/il/collect_demo.sh -# sed -i "s/^sim_set=.*/sim_set=$sim_set/" ./roboverse_learn/il/collect_demo.sh -# sed -i "s/^num_demo_success=.*/num_demo_success=$demo_num/" ./roboverse_learn/il/collect_demo.sh -# sed -i "s/^expert_data_num=.*/expert_data_num=$demo_num/" ./roboverse_learn/il/collect_demo.sh -# bash ./roboverse_learn/il/collect_demo.sh +# Collect demo +echo "=== Running collect_demo.sh ===" +sed -i "s/^task_name_set=.*/task_name_set=$task_name_set/" ./roboverse_learn/il/collect_demo.sh +sed -i "s/^sim_set=.*/sim_set=$sim_set/" ./roboverse_learn/il/collect_demo.sh +sed -i "s/^num_demo_success=.*/num_demo_success=$demo_num/" ./roboverse_learn/il/collect_demo.sh +sed -i "s/^expert_data_num=.*/expert_data_num=$demo_num/" ./roboverse_learn/il/collect_demo.sh +bash ./roboverse_learn/il/collect_demo.sh # Map policy_name to model config config_name="default_runner" diff --git a/scripts/advanced/collect_demo.py b/scripts/advanced/collect_demo.py index d500df2ea..5826278f8 100644 --- a/scripts/advanced/collect_demo.py +++ b/scripts/advanced/collect_demo.py @@ -1,3 +1,20 @@ +"""Demo collection script with domain randomization support. + +Collects demonstration data by replaying trajectories with optional domain randomization. + +Randomization Levels: +- Level 0: No randomization +- Level 1: Scene + Material randomization +- Level 2: Level 1 + Lighting randomization +- Level 3: Level 2 + Camera randomization + +Scene Modes: +- Mode 0: Manual geometry +- Mode 1: USD Table + Manual environment +- Mode 2: USD Scene (Kujiale) + USD Table +- Mode 3: Full USD (Scene + Table + Desktop objects) +""" + from __future__ import annotations from copy import deepcopy @@ -27,8 +44,6 @@ class Args: """Simulator backend""" demo_start_idx: int | None = None """The index of the first demo to collect, None for all demos""" - # max_demo_idx: int | None = None - # """Maximum number of demos to collect, None for all demos""" num_demo_success: int | None = None """Target number of successful demos to collect""" retry_num: int = 0 @@ -55,19 +70,11 @@ class Args: """Rollout unfinished and failed trajectories""" renderer: Literal["isaaclab", "mujoco", "isaacgym", "genesis", "pybullet", "sapien2", "sapien3"] = "mujoco" - ## Domain randomization options - enable_randomization: bool = False - """Enable domain randomization during demo collection""" - randomize_materials: bool = True - """Enable material randomization (when randomization is enabled)""" - randomize_lights: bool = False - """Enable light randomization (when randomization is enabled)""" - randomize_cameras: bool = True - """Enable camera randomization (when randomization is enabled)""" - randomize_physics: bool = True - """Enable physics (mass/friction/pose) randomization using ObjectRandomizer""" - randomization_frequency: Literal["per_demo", "per_episode"] = "per_demo" - """When to apply randomization: per_demo (once at start) or per_episode (every episode)""" + # Domain randomization options + level: Literal[0, 1, 2, 3] = 0 + """Randomization level: 0=None, 1=Scene+Material, 2=+Light, 3=+Camera""" + scene_mode: Literal[0, 1, 2, 3] = 0 + """Scene mode: 0=Manual, 1=USD Table, 2=USD Scene, 3=Full USD""" randomization_seed: int | None = None """Seed for reproducible randomization. If None, uses random seed""" @@ -75,32 +82,24 @@ def __post_init__(self): assert self.run_all or self.run_unfinished or self.run_failed, ( "At least one of run_all, run_unfinished, or run_failed must be True" ) - # if self.max_demo_idx is None: - # self.max_demo_idx = math.inf if self.num_demo_success is None: self.num_demo_success = 100 if self.demo_start_idx is None: self.demo_start_idx = 0 - # Validate randomization settings - if self.enable_randomization: - if not ( - self.randomize_materials or self.randomize_lights or self.randomize_cameras or self.randomize_physics - ): - log.warning("Randomization enabled but no randomization types selected, disabling randomization") - self.enable_randomization = False - log.info(f"Args: {self}") # Log randomization settings - if self.enable_randomization: + if self.level > 0: + mode_names = {0: "Manual", 1: "USD Table", 2: "USD Scene", 3: "Full USD"} log.info("=" * 60) log.info("DOMAIN RANDOMIZATION CONFIGURATION") - log.info(f" Materials: {'✓' if self.randomize_materials else '✗'}") - log.info(f" Lights: {'✓' if self.randomize_lights else '✗'}") - log.info(f" Cameras: {'✓' if self.randomize_cameras else '✗'}") - log.info(f" Physics: {'✓' if self.randomize_physics else '✗'} (ObjectRandomizer)") - log.info(f" Frequency: {self.randomization_frequency}") + log.info(f" Level: {self.level}") + log.info(f" Scene Mode: {self.scene_mode} ({mode_names[self.scene_mode]})") + log.info(" Randomization:") + log.info(" Level 1+: Scene + Material") + log.info(" Level 2+: + Lighting") + log.info(" Level 3+: + Camera") log.info(f" Seed: {self.randomization_seed if self.randomization_seed else 'Random'}") log.info("=" * 60) @@ -120,6 +119,7 @@ def __post_init__(self): from tqdm.rich import tqdm_rich as tqdm from metasim.scenario.cameras import PinholeCameraCfg +from metasim.scenario.lights import DiskLightCfg, SphereLightCfg from metasim.scenario.robot import RobotCfg from metasim.sim import BaseSimHandler from metasim.task.registry import get_task_class @@ -130,18 +130,9 @@ def __post_init__(self): rootutils.setup_root(__file__, pythonpath=True) -# Import randomization components (after rootutils setup) +# Import randomization components try: - from roboverse_pack.randomization import ( - CameraPresets, - CameraRandomizer, - LightPresets, - LightRandomizer, - MaterialPresets, - MaterialRandomizer, - ObjectPresets, - ObjectRandomizer, - ) + from metasim.randomization import DomainRandomizationManager, DRConfig RANDOMIZATION_AVAILABLE = True except ImportError as e: @@ -149,270 +140,6 @@ def __post_init__(self): RANDOMIZATION_AVAILABLE = False -def log_randomization_result( - randomizer_type: str, obj_name: str, property_name: str, before_value, after_value, unit: str = "" -): - """Log randomization results in a consistent format.""" - if hasattr(before_value, "cpu"): - before_str = str(before_value.cpu().numpy().round(3) if hasattr(before_value, "numpy") else before_value) - else: - before_str = str(before_value) - - if hasattr(after_value, "cpu"): - after_str = str(after_value.cpu().numpy().round(3) if hasattr(after_value, "numpy") else after_value) - else: - after_str = str(after_value) - - log.info(f" [{randomizer_type}] {obj_name}.{property_name}: {before_str} -> {after_str} {unit}") - - -def log_randomization_header(randomizer_name: str, description: str = ""): - """Log a consistent header for randomization sections.""" - log.info("=" * 50) - if description: - log.info(f"{randomizer_name}: {description}") - else: - log.info(randomizer_name) - - -class DomainRandomizationManager: - """Manages domain randomization for demo collection with unified interface.""" - - def __init__(self, args: Args, scenario, handler): - self.args = args - self.scenario = scenario - self.handler = handler - self.randomizers = [] - self._demo_count = 0 - - # Early validation - if not self._validate_setup(): - return - - log_randomization_header("DOMAIN RANDOMIZATION SETUP", "Initializing randomizers") - self._setup_randomizers() - log.info(f"Setup complete: {len(self.randomizers)} randomizers ready") - - def _validate_setup(self) -> bool: - """Validate if randomization can be set up.""" - if not self.args.enable_randomization: - log.info("Domain randomization disabled") - return False - - if not RANDOMIZATION_AVAILABLE: - log.warning("Domain randomization requested but components not available") - return False - - return True - - def _setup_randomizers(self): - """Initialize all randomizers based on configuration.""" - seed = self.args.randomization_seed - self._setup_reproducibility(seed) - - # Setup each randomization type symmetrically - if self.args.randomize_materials: - self._setup_material_randomizers(seed) - - if self.args.randomize_lights: - self._setup_light_randomizers(seed) - - if self.args.randomize_cameras: - self._setup_camera_randomizers(seed) - - if self.args.randomize_physics: - self._setup_physics_randomizers(seed) - - def _setup_reproducibility(self, seed: int | None): - """Setup global reproducibility if seed is provided.""" - if seed is not None: - log.info(f"Setting up reproducible randomization with seed: {seed}") - torch.manual_seed(seed) - import numpy as np - - np.random.seed(seed) - if torch.cuda.is_available(): - torch.cuda.manual_seed(seed) - - def _setup_material_randomizers(self, seed: int | None): - """Setup material randomizers for all objects.""" - objects = getattr(self.scenario, "objects", []) - if not objects: - log.info(" No objects found for material randomization") - return - - log.info(f" Setting up material randomizers for {len(objects)} objects") - for obj in objects: - obj_name = obj.name - config = self._get_material_config(obj_name) - - randomizer = MaterialRandomizer(config, seed=seed) - randomizer.bind_handler(self.handler) - self.randomizers.append(randomizer) - log.info(f" Added MaterialRandomizer for {obj_name}") - - def _setup_light_randomizers(self, seed: int | None): - """Setup light randomizers for all lights.""" - from metasim.scenario.lights import DiskLightCfg, DomeLightCfg, SphereLightCfg - - lights = getattr(self.scenario, "lights", []) - if not lights: - log.info(" No lights found for light randomization") - return - - log.info(f" Setting up light randomizers for {len(lights)} lights") - for light in lights: - light_name = getattr(light, "name", f"light_{len(self.randomizers)}") - - if isinstance(light, DomeLightCfg): - config = LightPresets.dome_ambient(light_name) - elif isinstance(light, (SphereLightCfg, DiskLightCfg)): - config = LightPresets.sphere_ceiling_light(light_name) - else: - log.warning(f"Unknown light type for {light_name}, using sphere_ceiling_light preset") - config = LightPresets.sphere_ceiling_light(light_name) - - randomizer = LightRandomizer(config, seed=seed) - randomizer.bind_handler(self.handler) - self.randomizers.append(randomizer) - log.info(f" Added LightRandomizer for {light_name}") - - def _setup_camera_randomizers(self, seed: int | None): - """Setup camera randomizers for all cameras.""" - cameras = getattr(self.scenario, "cameras", []) - if not cameras: - log.info(" No cameras found for camera randomization") - return - - log.info(f" Setting up camera randomizers for {len(cameras)} cameras") - for camera in cameras: - camera_name = getattr(camera, "name", f"camera_{len(self.randomizers)}") - config = CameraPresets.surveillance_camera(camera_name) - - randomizer = CameraRandomizer(config, seed=seed) - randomizer.bind_handler(self.handler) - self.randomizers.append(randomizer) - log.info(f" Added CameraRandomizer for {camera_name}") - - def _get_material_config(self, obj_name: str): - """Get appropriate material configuration based on object type.""" - obj_lower = obj_name.lower() - if "cube" in obj_lower: - return MaterialPresets.mdl_family_object(obj_name, family="metal") - elif "sphere" in obj_lower: - return MaterialPresets.rubber_object(obj_name) - else: - return MaterialPresets.mdl_family_object(obj_name, family="wood") - - def _setup_physics_randomizers(self, seed: int | None): - """Setup unified ObjectRandomizers for robots and objects.""" - robots = getattr(self.scenario, "robots", []) - objects = getattr(self.scenario, "objects", []) - - self._setup_object_randomizers(robots, objects, seed) - - def _setup_object_randomizers(self, robots: list, objects: list, seed: int | None): - """Setup unified ObjectRandomizers for all physical entities.""" - log.info(" Setting up ObjectRandomizers for physics randomization") - - # Robot randomization - if robots: - robot_name = robots[0] if isinstance(robots[0], str) else robots[0].name - robot_randomizer = ObjectRandomizer(ObjectPresets.robot_base(robot_name), seed=seed) - robot_randomizer.bind_handler(self.handler) - self.randomizers.append(robot_randomizer) - log.info(f" Added ObjectRandomizer for robot {robot_name}") - - # Object randomization - if objects: - for obj in objects: - obj_name = obj.name - config = self._get_object_physics_config(obj_name) - - obj_randomizer = ObjectRandomizer(config, seed=seed) - obj_randomizer.bind_handler(self.handler) - self.randomizers.append(obj_randomizer) - log.info(f" Added ObjectRandomizer for {obj_name}") - - if not robots and not objects: - log.info(" No robots or objects found for physics randomization") - - def _get_object_physics_config(self, obj_name: str): - """Get appropriate physics configuration based on object type.""" - obj_lower = obj_name.lower() - if "cube" in obj_lower: - return ObjectPresets.grasping_target(obj_name) - elif "sphere" in obj_lower: - return ObjectPresets.bouncy_object(obj_name) - else: - return ObjectPresets.physics_only(obj_name) - - def randomize_for_demo(self, demo_idx: int): - """Apply randomization for a new demo.""" - if not self._should_randomize(demo_idx): - return - - log_randomization_header("DOMAIN RANDOMIZATION", f"Demo {demo_idx}") - - # Apply all randomizers and collect statistics - stats = self._apply_all_randomizers() - - # Log summary - self._log_randomization_summary(stats) - self._demo_count += 1 - - def _should_randomize(self, demo_idx: int) -> bool: - """Check if randomization should be applied for this demo.""" - if not self.args.enable_randomization or not self.randomizers: - return False - - return self.args.randomization_frequency == "per_demo" or ( - self.args.randomization_frequency == "per_episode" and demo_idx == 0 - ) - - def _apply_all_randomizers(self) -> dict[str, int]: - """Apply all randomizers and return statistics.""" - stats = {"ObjectRandomizer": 0, "MaterialRandomizer": 0, "LightRandomizer": 0, "CameraRandomizer": 0} - - for randomizer in self.randomizers: - try: - obj_name = self._get_randomizer_target_name(randomizer) - randomizer_type = type(randomizer).__name__ - - # Apply randomization - randomizer() - stats[randomizer_type] = stats.get(randomizer_type, 0) + 1 - log.info(f" Applied {randomizer_type} for {obj_name}") - - except Exception as e: - log.warning(f" {type(randomizer).__name__} failed for {obj_name}: {e}") - - return stats - - def _get_randomizer_target_name(self, randomizer) -> str: - """Extract target object name from randomizer configuration.""" - if not hasattr(randomizer, "cfg"): - return "unknown" - - cfg = randomizer.cfg - if hasattr(cfg, "obj_name"): - return cfg.obj_name - elif hasattr(cfg, "light_name"): - return cfg.light_name - elif hasattr(cfg, "camera_name"): - return cfg.camera_name - else: - return "unknown" - - def _log_randomization_summary(self, stats: dict[str, int]): - """Log a summary of applied randomizers.""" - applied_types = [f"{name}: {count}" for name, count in stats.items() if count > 0] - if applied_types: - log.info(f"Applied randomizers: {', '.join(applied_types)}") - else: - log.info("No randomizers were applied") - - def get_actions(all_actions, env, demo_idxs: list[int], robot: RobotCfg): action_idxs = env._episode_steps @@ -455,15 +182,12 @@ def ensure_clean_state(handler, expected_state=None): handler.simulate() current_state = handler.get_states() - # Only start checking after minimum steps if step >= min_steps: if prev_state is not None: - # Check if key states are stable (focus on articulated objects) is_stable = True if hasattr(current_state, "objects") and hasattr(prev_state, "objects"): for obj_name, obj_state in current_state.objects.items(): if obj_name in prev_state.objects: - # Check DOF positions for articulated objects curr_dof = getattr(obj_state, "dof_pos", None) prev_dof = getattr(prev_state.objects[obj_name], "dof_pos", None) if curr_dof is not None and prev_dof is not None: @@ -471,51 +195,45 @@ def ensure_clean_state(handler, expected_state=None): is_stable = False break - # Additional validation: check if we're stable at the RIGHT state if is_stable and expected_state is not None: is_correct_state = _validate_state_correctness(current_state, expected_state) if not is_correct_state: - # We're stable but at wrong state - force more simulation log.debug(f"State stable but incorrect at step {step}, continuing simulation...") stable_count = 0 is_stable = False - # Continue simulating to let physics settle properly if is_stable: stable_count += 1 - if stable_count >= 2: # Stable for 2 consecutive steps at correct state + if stable_count >= 2: break else: stable_count = 0 prev_state = current_state - # Final validation if we ran out of steps if expected_state is not None: final_state = handler.get_states() is_final_correct = _validate_state_correctness(final_state, expected_state) if not is_final_correct: log.warning(f"State validation failed after {max_steps} steps - reset may not have taken full effect") - # Final state refresh handler.get_states() def _validate_state_correctness(current_state, expected_state): """Validate that current state matches expected initial state for critical objects.""" if not hasattr(current_state, "objects") or not hasattr(expected_state, "objects"): - return True # Can't validate, assume correct + return True - # Focus on articulated objects which are most prone to reset issues critical_objects = [] for obj_name, expected_obj in expected_state.objects.items(): if hasattr(expected_obj, "dof_pos") and getattr(expected_obj, "dof_pos", None) is not None: critical_objects.append(obj_name) if not critical_objects: - return True # No critical objects to validate + return True - tolerance = 5e-3 # Reasonable tolerance for DOF positions + tolerance = 5e-3 for obj_name in critical_objects: if obj_name not in current_state.objects: @@ -524,13 +242,11 @@ def _validate_state_correctness(current_state, expected_state): expected_obj = expected_state.objects[obj_name] current_obj = current_state.objects[obj_name] - # Check DOF positions for articulated objects (most critical for demo consistency) expected_dof = getattr(expected_obj, "dof_pos", None) current_dof = getattr(current_obj, "dof_pos", None) if expected_dof is not None and current_dof is not None: if not torch.allclose(current_dof, expected_dof, atol=tolerance): - # Log the specific difference for debugging diff = torch.abs(current_dof - expected_dof).max().item() log.debug(f"DOF mismatch for {obj_name}: max diff = {diff:.6f} (tolerance = {tolerance})") return False @@ -541,9 +257,7 @@ def _validate_state_correctness(current_state, expected_state): def force_reset_to_state(env, state, env_id): """Force reset environment to specific state with validation.""" env.reset(states=[state], env_ids=[env_id]) - # Pass expected state for validation ensure_clean_state(env.handler, expected_state=state) - # Reset episode counter AFTER stabilization to ensure demo starts from action 0 if hasattr(env, "_episode_steps"): env._episode_steps[env_id] = 0 @@ -603,7 +317,6 @@ def save(self, demo_idx: int, status: str): assert demo_idx in self.cache assert status in ["success", "failed"], f"Invalid status: {status}" - # Use demo_idx directly as continuous_idx to maintain consistency continuous_idx = demo_idx save_dir = os.path.join(self.base_save_dir, status, f"demo_{continuous_idx:04d}") @@ -611,9 +324,7 @@ def save(self, demo_idx: int, status: str): os.remove(os.path.join(save_dir, "status.txt")) os.makedirs(save_dir, exist_ok=True) - log.info(f"Saving demo {demo_idx} (original) as {continuous_idx:04d} (continuous) to {save_dir}") - - ## Option 1: Save immediately, blocking and slower + log.info(f"Saving demo {demo_idx} as {continuous_idx:04d} to {save_dir}") from metasim.utils.save_util import save_demo @@ -623,9 +334,6 @@ def save(self, demo_idx: int, status: str): with open(os.path.join(save_dir, "status.txt"), "w") as f: f.write(status) - ## Option 2: Save in a separate process, non-blocking, not friendly to KeyboardInterrupt - # self.save_request_queue.put({"demo": self.cache[demo_idx], "save_dir": save_dir}) - def delete(self, demo_idx: int): assert demo_idx in self.cache del self.cache[demo_idx] @@ -700,32 +408,57 @@ def main(): task_cls = get_task_class(args.task) if args.task in {"stack_cube", "pick_cube", "pick_butter"}: - dp_pos = (1.0, 0.0, 0.75) - elif args.task in {"close_box"}: - dp_pos = (0, 0, 0) + dp_camera = True else: - dp_pos = (1.0, 0.0, 0.75) - - # is_libero_dataset = "libero_90" in args.task + dp_camera = args.task != "close_box" - # if is_libero_dataset: - # dp_pos = (2.0, 0.0, 2) - # elif dp_camera: - # # import warnings - # # warnings.warn("Using dp camera position!") - # dp_pos = (1.0, 0.0, 0.75) - # else: - # dp_pos = (1.5, 0.0, 1.5) + is_libero_dataset = "libero_90" in args.task - # libero specific camera position - # dp_pos = (0.8, -0, 1.6) - # look_at = (-2.5, 0.0, 0.0) + if is_libero_dataset: + dp_pos = (2.0, 0.0, 2) + elif dp_camera: + dp_pos = (1.0, 0.0, 0.75) + else: + dp_pos = (1.5, 0.0, 1.5) camera = PinholeCameraCfg(data_types=["rgb", "depth"], pos=dp_pos, look_at=(0.0, 0.0, 0.0)) + + # Lighting setup + if args.render.mode == "pathtracing": + ceiling_main = 18000.0 + ceiling_corners = 8000.0 + else: + ceiling_main = 12000.0 + ceiling_corners = 5000.0 + + lights = [ + DiskLightCfg( + name="ceiling_main", + intensity=ceiling_main, + color=(1.0, 1.0, 1.0), + radius=1.2, + pos=(0.0, 0.0, 2.8), + rot=(0.7071, 0.0, 0.0, 0.7071), + ), + SphereLightCfg( + name="ceiling_ne", intensity=ceiling_corners, color=(1.0, 1.0, 1.0), radius=0.6, pos=(1.0, 1.0, 2.5) + ), + SphereLightCfg( + name="ceiling_nw", intensity=ceiling_corners, color=(1.0, 1.0, 1.0), radius=0.6, pos=(-1.0, 1.0, 2.5) + ), + SphereLightCfg( + name="ceiling_sw", intensity=ceiling_corners, color=(1.0, 1.0, 1.0), radius=0.6, pos=(-1.0, -1.0, 2.5) + ), + SphereLightCfg( + name="ceiling_se", intensity=ceiling_corners, color=(1.0, 1.0, 1.0), radius=0.6, pos=(1.0, -1.0, 2.5) + ), + ] + scenario = task_cls.scenario.update( robots=[args.robot], scene=args.scene, cameras=[camera], + lights=lights, render=args.render, simulator=args.sim, renderer=args.renderer, @@ -736,12 +469,23 @@ def main(): device = torch.device("cuda" if torch.cuda.is_available() else "cpu") env = task_cls(scenario, device=device) - # Initialize domain randomization manager - randomization_manager = DomainRandomizationManager(args, scenario, env.handler) ## Data assert os.path.exists(env.traj_filepath), f"Trajectory file does not exist: {env.traj_filepath}" init_states, all_actions, all_states = get_traj(env.traj_filepath, robot, env.handler) + # Initialize domain randomization manager + randomization_manager = DomainRandomizationManager( + config=DRConfig( + level=args.level, + scene_mode=args.scene_mode, + randomization_seed=args.randomization_seed, + ), + scenario=scenario, + handler=env.handler, + init_states=init_states, + render_cfg=args.render, + ) + tot_demo = len(all_actions) if args.split == "train": init_states = init_states[: int(tot_demo * 0.9)] @@ -758,11 +502,6 @@ def main(): ######################################################## ## Main ######################################################## - # if args.max_demo_idx > n_demo: - # log.warning( - # f"Max demo {args.max_demo_idx} is greater than the number of demos in the dataset {n_demo}, using {n_demo}" - # ) - # max_demo = min(args.max_demo_idx, n_demo) max_demo = n_demo try_num = args.retry_num + 1 @@ -771,10 +510,8 @@ def main(): ## CollectingDemo -> Timeout -> Retry/GiveUp -> NextDemo ## Setup - # Get task description from environment task_desc = getattr(env, "task_desc", "") - collector = DemoCollector(env.handler, robot, task_desc, demo_start_idx=args.demo_start_idx) - # pbar = tqdm(total=max_demo - args.demo_start_idx, desc="Collecting demos") + collector = DemoCollector(env.handler, robot, task_desc) pbar = tqdm(total=args.num_demo_success, desc="Collecting successful demos") ## State variables @@ -805,25 +542,28 @@ def main(): demo_indexer.move_on() log.info(f"Initialize with demo idxs: {demo_idxs}") - ## Apply initial randomization + ## Apply initial randomization (create scene and update positions) for env_id, demo_idx in enumerate(demo_idxs): - randomization_manager.randomize_for_demo(demo_idx) + randomization_manager.apply_randomization(demo_idx, is_initial=True) + randomization_manager.update_positions_to_table(demo_idx, env_id) + randomization_manager.update_camera_look_at(env_id) + randomization_manager.apply_camera_randomization() # Apply camera randomization after baseline adjustment - ## Reset to initial states + ## Reset to initial states (after position adjustment) obs, extras = env.reset(states=[init_states[demo_idx] for demo_idx in demo_idxs]) - ## Wait for environment to stabilize after reset (before counting demo steps) - # For initial setup, we can't validate individual states easily, so just ensure stability + ## Wait for environment to stabilize after reset ensure_clean_state(env.handler) - ## Reset episode step counters AFTER stabilization + ## Reset episode step counters after stabilization if hasattr(env, "_episode_steps"): for env_id in range(env.handler.num_envs): env._episode_steps[env_id] = 0 - ## Now record the clean, stabilized initial state + ## Record the clean, stabilized initial state obs = env.handler.get_states() obs = state_tensor_to_nested(env.handler, obs) + for env_id, demo_idx in enumerate(demo_idxs): log.info(f"Starting Demo {demo_idx} in Env {env_id}") collector.create(demo_idx, obs[env_id]) @@ -832,17 +572,13 @@ def main(): stop_flag = False while not all(finished): - if stop_flag: - pass - if tot_success >= args.num_demo_success: - log.info(f"Reached target number of successful demos ({args.num_demo_success}).") - stop_flag = True + log.info(f"Reached target number of successful demos ({args.num_demo_success}). Stopping collection.") + break if demo_indexer.next_idx >= max_demo: - if not stop_flag: - log.warning(f"Reached maximum demo index ({max_demo}), finishing in-flight demos.") - stop_flag = True + log.warning(f"Reached maximum demo index ({max_demo}). Stopping collection.") + break pbar.set_description(f"Frame {global_step} Success {tot_success} Giveup {tot_give_up}") actions = get_actions(all_actions, env, demo_idxs, robot) @@ -880,7 +616,10 @@ def main(): demo_idxs[env_id] = new_demo_idx log.info(f"Transitioning Env {env_id}: Demo {demo_idx} to Demo {new_demo_idx}") - randomization_manager.randomize_for_demo(new_demo_idx) + randomization_manager.apply_randomization(new_demo_idx, is_initial=False) + randomization_manager.update_positions_to_table(new_demo_idx, env_id) + randomization_manager.update_camera_look_at(env_id) + randomization_manager.apply_camera_randomization() # Apply camera randomization force_reset_to_state(env, init_states[new_demo_idx], env_id) obs = env.handler.get_states() @@ -903,7 +642,10 @@ def main(): if failure_count[env_id] < try_num: log.info(f"Demo {demo_idx} failed {failure_count[env_id]} times, retrying...") - randomization_manager.randomize_for_demo(demo_idx) + randomization_manager.apply_randomization(demo_idx, is_initial=False) + randomization_manager.update_positions_to_table(demo_idx, env_id) + randomization_manager.update_camera_look_at(env_id) + randomization_manager.apply_camera_randomization() # Apply camera randomization force_reset_to_state(env, init_states[demo_idx], env_id) obs = env.handler.get_states() @@ -919,7 +661,10 @@ def main(): if demo_indexer.next_idx < max_demo: new_demo_idx = demo_indexer.next_idx demo_idxs[env_id] = new_demo_idx - randomization_manager.randomize_for_demo(new_demo_idx) + randomization_manager.apply_randomization(new_demo_idx, is_initial=False) + randomization_manager.update_positions_to_table(new_demo_idx, env_id) + randomization_manager.update_camera_look_at(env_id) + randomization_manager.apply_camera_randomization() # Apply camera randomization force_reset_to_state(env, init_states[new_demo_idx], env_id) obs = env.handler.get_states() @@ -937,4 +682,4 @@ def main(): if __name__ == "__main__": - main() + main() \ No newline at end of file From 8c82ce38d60313dd0dbd45cae21cb5bbfa64cf8a Mon Sep 17 00:00:00 2001 From: JIAjindou Date: Sun, 14 Dec 2025 10:12:59 +0800 Subject: [PATCH 20/50] [fix] break issue of collect demo --- scripts/advanced/collect_demo.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/scripts/advanced/collect_demo.py b/scripts/advanced/collect_demo.py index 5826278f8..cccf56f1a 100644 --- a/scripts/advanced/collect_demo.py +++ b/scripts/advanced/collect_demo.py @@ -511,7 +511,9 @@ def main(): ## Setup task_desc = getattr(env, "task_desc", "") - collector = DemoCollector(env.handler, robot, task_desc) + # collector = DemoCollector(env.handler, robot, task_desc) + # pbar = tqdm(total=args.num_demo_success, desc="Collecting successful demos") + collector = DemoCollector(env.handler, robot, task_desc, demo_start_idx=args.demo_start_idx) pbar = tqdm(total=args.num_demo_success, desc="Collecting successful demos") ## State variables @@ -572,13 +574,17 @@ def main(): stop_flag = False while not all(finished): + if stop_flag: + pass + if tot_success >= args.num_demo_success: - log.info(f"Reached target number of successful demos ({args.num_demo_success}). Stopping collection.") - break + log.info(f"Reached target number of successful demos ({args.num_demo_success}).") + stop_flag = True if demo_indexer.next_idx >= max_demo: - log.warning(f"Reached maximum demo index ({max_demo}). Stopping collection.") - break + if not stop_flag: + log.warning(f"Reached maximum demo index ({max_demo}), finishing in-flight demos.") + stop_flag = True pbar.set_description(f"Frame {global_step} Success {tot_success} Giveup {tot_give_up}") actions = get_actions(all_actions, env, demo_idxs, robot) From dd06e9de8c54cbe456930676b5b0529b33faf58d Mon Sep 17 00:00:00 2001 From: JIAjindou Date: Sun, 14 Dec 2025 10:45:50 +0800 Subject: [PATCH 21/50] [fix] pyroki intall guide --- .../get_started/advanced_installation/pyroki.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/source/metasim/get_started/advanced_installation/pyroki.md b/docs/source/metasim/get_started/advanced_installation/pyroki.md index 18ed193d5..68ea57d7a 100644 --- a/docs/source/metasim/get_started/advanced_installation/pyroki.md +++ b/docs/source/metasim/get_started/advanced_installation/pyroki.md @@ -13,12 +13,14 @@ git clone https://github.com/chungmin99/pyroki.git cd pyroki pip install -e . ``` -For Isaacsim, also need the following commands: + +If you encounter a NumPy version mismatch between lsaacSim 5.0.0 and PyRoki, for example, an error +`TypeError: asarray() got an unexpected keyword argument 'copy'`, try running the following commands: ```bash pip install numpy==1.26.0 # For Isaacsim -pip install jax==0.6.0 # For Isaacsim +pip install jax==0.4.30 jaxlib==0.4.30 +pip install sentry-sdk==1.43.0 typing-extensions==4.12.2 websockets==12.0 +pip install --upgrade websockets ``` - - From 29b2e09d8493e1c8c76dc5ee20e169fad7450e0c Mon Sep 17 00:00:00 2001 From: JIAjindou Date: Sun, 14 Dec 2025 10:54:20 +0800 Subject: [PATCH 22/50] [fix] motion planning doc --- .../source/metasim/get_started/quick_start/4_motion_planning.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/metasim/get_started/quick_start/4_motion_planning.md b/docs/source/metasim/get_started/quick_start/4_motion_planning.md index d4f8e3a60..728dfe5bf 100644 --- a/docs/source/metasim/get_started/quick_start/4_motion_planning.md +++ b/docs/source/metasim/get_started/quick_start/4_motion_planning.md @@ -1,6 +1,6 @@ # 4. Motion Planning In this tutorial, we will show you how to use MetaSim to plan a motion for a robot. -Note here, we use the `curobo` package to plan the motion. If you haven't installed it, please refer to our [curobo installation guide](https://roboverse.wiki/metasim/get_started/advanced_installation/curobo). +Note here, we can use the `pyroki` or `curobo` package to plan the motion. If you haven't installed it, please refer to our [pyroki installation guide](https://roboverse.wiki/metasim/get_started/advanced_installation/pyroki) or [curobo installation guide](https://roboverse.wiki/metasim/get_started/advanced_installation/curobo). ## Common Usage From a4c68f91c1bfb2b67ca47f2b18869250b7d16f5a Mon Sep 17 00:00:00 2001 From: JIAjindou Date: Sun, 14 Dec 2025 10:57:54 +0800 Subject: [PATCH 23/50] [fix] get start hybrid sim --- metasim/sim/hybrid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/metasim/sim/hybrid.py b/metasim/sim/hybrid.py index cc8237259..e6ef4a9e5 100644 --- a/metasim/sim/hybrid.py +++ b/metasim/sim/hybrid.py @@ -42,9 +42,9 @@ def close(self) -> None: self.physics_handler.close() self.render_handler.close() - def set_dof_targets(self, obj_name: str, actions: list[Action]) -> None: + def set_dof_targets(self, actions: list[Action]) -> None: """Set the dof targets of the robot in the physics handler.""" - self.physics_handler.set_dof_targets(obj_name, actions) + self.physics_handler.set_dof_targets(actions) def _set_states(self, states: TensorState, env_ids: list[int] | None = None) -> None: """Set states in both physics and render handlers.""" From 58fa4f7b0bacb4459ac0c6d52b12eee4dee37675 Mon Sep 17 00:00:00 2001 From: JIAjindou Date: Sun, 14 Dec 2025 10:59:31 +0800 Subject: [PATCH 24/50] [fix] state reply demo --- scripts/advanced/replay_demo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/advanced/replay_demo.py b/scripts/advanced/replay_demo.py index 9d1f66ef2..a0f3602b2 100644 --- a/scripts/advanced/replay_demo.py +++ b/scripts/advanced/replay_demo.py @@ -225,7 +225,7 @@ def main(): obs = env.handler.get_states() ## XXX: hack - success = env.checker.check(env.handler) + success = env.checker.check(env.handler, obs) if success.any(): log.info(f"Env {success.nonzero().squeeze(-1).tolist()} succeeded!") if success.all(): From 101c6c635cc3bdcdbe82267dcee5dd62d6c40ccb Mon Sep 17 00:00:00 2001 From: JIAjindou Date: Sun, 14 Dec 2025 11:05:26 +0800 Subject: [PATCH 25/50] [fix] jax install --- .../metasim/get_started/advanced_installation/pyroki.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/source/metasim/get_started/advanced_installation/pyroki.md b/docs/source/metasim/get_started/advanced_installation/pyroki.md index 68ea57d7a..e600ad013 100644 --- a/docs/source/metasim/get_started/advanced_installation/pyroki.md +++ b/docs/source/metasim/get_started/advanced_installation/pyroki.md @@ -9,16 +9,18 @@ PyRoki requires Python 3.10 or higher. Python 3.12+ is recommended for best comp ## Installation ```bash +cd third_part git clone https://github.com/chungmin99/pyroki.git cd pyroki pip install -e . +cd ../../ +pip install jax==0.4.30 jaxlib==0.4.30 ``` - +### For Isaacsim If you encounter a NumPy version mismatch between lsaacSim 5.0.0 and PyRoki, for example, an error `TypeError: asarray() got an unexpected keyword argument 'copy'`, try running the following commands: ```bash -pip install numpy==1.26.0 # For Isaacsim -pip install jax==0.4.30 jaxlib==0.4.30 +pip install numpy==1.26.0 pip install sentry-sdk==1.43.0 typing-extensions==4.12.2 websockets==12.0 pip install --upgrade websockets ``` From e25b28d291afdd3e10fe2a0873a4e19bb5df73aa Mon Sep 17 00:00:00 2001 From: JIAjindou Date: Sun, 14 Dec 2025 11:07:13 +0800 Subject: [PATCH 26/50] [fix] replay demo libero doc --- .../get_started/quick_start/8_replay_demo.md | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/docs/source/metasim/get_started/quick_start/8_replay_demo.md b/docs/source/metasim/get_started/quick_start/8_replay_demo.md index 02ba4497d..eb2ca4064 100644 --- a/docs/source/metasim/get_started/quick_start/8_replay_demo.md +++ b/docs/source/metasim/get_started/quick_start/8_replay_demo.md @@ -9,10 +9,9 @@ python scripts/advanced/replay_demo.py --sim=isaacsim --task=close_box --num_env ``` task could also be: -- `PickCube` -- `StackCube` -- `CloseBox` -- `BasketballInHoop` +- `pick_cube` +- `stack_cube` +- `close_box` ## States replay @@ -20,8 +19,7 @@ task could also be: python scripts/advanced/replay_demo.py --sim=isaacsim --task=close_box --num_envs 4 --object-states ``` task could also be: -- `CloseBox` -- `BasketballInHoop` +- `close_box` ## Varifies commands @@ -30,7 +28,7 @@ task could also be: e.g. ```bash -python scripts/advanced/replay_demo.py --sim=isaacsim --task=LiberoPickButter +python scripts/advanced/replay_demo.py --sim=isaacsim --task=libero.pick_butter ``` Simulator: @@ -38,14 +36,9 @@ Simulator: - `mujoco` Task: -- `LiberoPickAlphabetSoup` -- `LiberoPickBbqSauce` -- `LiberoPickChocolatePudding` -- `LiberoPickCreamCheese` -- `LiberoPickMilk` -- `LiberoPickOrangeJuice` -- `LiberoPickSaladDressing` -- `LiberoPickTomatoSauce` +- `libero.kitchen_scene1_open_bottom_drawer` +- `libero.kitchen_scene1_open_top_drawer` +- `libero.kitchen_scene1_put_the_black_bowl_on_the_plate` ### Humanoid From d976a65dca1c0f06349171f3d2b72d9635d7d007 Mon Sep 17 00:00:00 2001 From: HanchuZhou Date: Sun, 14 Dec 2025 17:43:37 -0800 Subject: [PATCH 27/50] Add New Tasks for Pick_Place and Track (#726) * Update pick_place configs and tasks * Fix bug that success rate is more than 100% in evaluate_lift.py; Fixed hard-coded path in track.py * add contributor * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fixing success rate bug; Enable more flexible evaluation that user can create less number of environment in evaluation * Make training script individual; Add gripper downward reward * Update pick_place configs and tasks * Fix bug that success rate is more than 100% in evaluate_lift.py; Fixed hard-coded path in track.py * Fixing success rate bug; Enable more flexible evaluation that user can create less number of environment in evaluation * Make training script individual; Add gripper downward reward * Resolved merge conflicts by keeping local versions * "Refactor approach_grasp tasks, yaml, and track tasks yaml" * "Refactor pick_place grasp logic and add object configs" * Refactor pick_place grasp logic and add object configs * Adjust Spoon, two spoon class in one file, two spoon yaml. * Apply pre-commit fixes and add new pick_place_spoon2 config * Local devcontainer config (ignored from push) * Fix duplicated sim key in pick_place_spoon2.yaml * Trigger CI * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * update the position of banana and screw driver * add approach_grasp.py * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Roll back readme * roll back * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * roll back collect demo * roll back collect demo * Revise devcontainer.json for development setup Updated the devcontainer configuration with new settings, including service name, workspace folder, and VSCode customizations. * Remove devcontainer.json from .gitignore --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Anchor1021 --- CONTRIBUTORS.md | 3 +- get_started/obj_layout/object_layout.py | 203 +++++- .../{track.yaml => pick_place_banana.yaml} | 20 +- .../rl/fast_td3/configs/pick_place_bowl.yaml | 89 +++ .../configs/pick_place_cuttingtool.yaml | 89 +++ .../configs/pick_place_screwdriver.yaml | 89 +++ ...{pick_place.yaml => pick_place_spoon.yaml} | 10 +- .../fast_td3/configs/pick_place_spoon2.yaml | 89 +++ .../rl/fast_td3/configs/track_banana.yaml | 95 +++ .../fast_td3/configs/track_screwdriver.yaml | 95 +++ .../rl/fast_td3/configs/track_spoon.yaml | 97 +++ roboverse_learn/rl/fast_td3/evaluate.py | 83 ++- roboverse_learn/rl/fast_td3/evaluate_lift.py | 17 +- .../rl/fast_td3/replay_lift_states.py | 2 +- roboverse_learn/rl/fast_td3/train.py | 41 +- .../tasks/pick_place/approach_grasp.py | 15 + .../tasks/pick_place/approach_grasp_banana.py | 282 ++++++++ .../tasks/pick_place/approach_grasp_bowl.py | 0 .../pick_place/approach_grasp_cuttingtool.py | 0 .../pick_place/approach_grasp_screwdriver.py | 285 ++++++++ .../tasks/pick_place/approach_grasp_spoon.py | 418 ++++++++++++ roboverse_pack/tasks/pick_place/base.py | 31 +- .../pick_place/{track.py => track_banana.py} | 102 ++- .../tasks/pick_place/track_screwdriver.py | 446 ++++++++++++ .../tasks/pick_place/track_spoon.py | 644 ++++++++++++++++++ scripts/advanced/collect_demo.py | 2 +- 26 files changed, 3155 insertions(+), 92 deletions(-) rename roboverse_learn/rl/fast_td3/configs/{track.yaml => pick_place_banana.yaml} (82%) create mode 100644 roboverse_learn/rl/fast_td3/configs/pick_place_bowl.yaml create mode 100644 roboverse_learn/rl/fast_td3/configs/pick_place_cuttingtool.yaml create mode 100644 roboverse_learn/rl/fast_td3/configs/pick_place_screwdriver.yaml rename roboverse_learn/rl/fast_td3/configs/{pick_place.yaml => pick_place_spoon.yaml} (90%) create mode 100644 roboverse_learn/rl/fast_td3/configs/pick_place_spoon2.yaml create mode 100644 roboverse_learn/rl/fast_td3/configs/track_banana.yaml create mode 100644 roboverse_learn/rl/fast_td3/configs/track_screwdriver.yaml create mode 100644 roboverse_learn/rl/fast_td3/configs/track_spoon.yaml create mode 100644 roboverse_pack/tasks/pick_place/approach_grasp_banana.py create mode 100644 roboverse_pack/tasks/pick_place/approach_grasp_bowl.py create mode 100644 roboverse_pack/tasks/pick_place/approach_grasp_cuttingtool.py create mode 100644 roboverse_pack/tasks/pick_place/approach_grasp_screwdriver.py create mode 100644 roboverse_pack/tasks/pick_place/approach_grasp_spoon.py rename roboverse_pack/tasks/pick_place/{track.py => track_banana.py} (75%) create mode 100644 roboverse_pack/tasks/pick_place/track_screwdriver.py create mode 100644 roboverse_pack/tasks/pick_place/track_spoon.py diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 528f39029..5dfd0af59 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -21,11 +21,13 @@ Guidelines for modifications: ## Contributors * Bangjun Wang +* Boqi Zhao * Chaoyi Xu * Chengyang Zhao * Dechen Gao * Di Fan * Dylan Goetting +* Hanchu Zhou * Haoran Lu * Haozhe Chen * Haozhe Lou @@ -47,7 +49,6 @@ Guidelines for modifications: * Yutong Liang * Yuyang Li * Zhigen Zhao -* Hanchu Zhou ## Acknowledgements diff --git a/get_started/obj_layout/object_layout.py b/get_started/obj_layout/object_layout.py index d08f45148..b4857d944 100644 --- a/get_started/obj_layout/object_layout.py +++ b/get_started/obj_layout/object_layout.py @@ -385,16 +385,63 @@ def __post_init__(self): usd_path="roboverse_data/assets/EmbodiedGenData/demo_assets/table/usd/table.usd", urdf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/table/result/table.urdf", mjcf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/table/mjcf/table.xml", - fix_base_link=True, ), + # RigidObjCfg( + # name="lamp", + # scale=(1, 1, 1), + # physics=PhysicStateType.RIGIDBODY, + # usd_path="roboverse_data/assets/EmbodiedGenData/new_assets/lighting_fixtures/1/usd/0a4489b1a2875c82a580f8b62d346e08.usd", + # urdf_path="roboverse_data/assets/EmbodiedGenData/new_assets/lighting_fixtures/1/0a4489b1a2875c82a580f8b62d346e08.urdf", + # mjcf_path="roboverse_data/assets/EmbodiedGenData/new_assets/lighting_fixtures/1/mjcf/0a4489b1a2875c82a580f8b62d346e08.xml", + # ), RigidObjCfg( - name="banana", + name="basket", scale=(1, 1, 1), physics=PhysicStateType.RIGIDBODY, - usd_path="roboverse_data/assets/EmbodiedGenData/demo_assets/banana/usd/banana.usd", - urdf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/banana/result/banana.urdf", - mjcf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/banana/mjcf/banana.xml", + usd_path="roboverse_data/assets/EmbodiedGenData/new_assets/basket/1/usd/663158968e3f5900af1f6e7cecef24c7.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/new_assets/basket/1/663158968e3f5900af1f6e7cecef24c7.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/new_assets/basket/1/mjcf/663158968e3f5900af1f6e7cecef24c7.xml", ), + # RigidObjCfg( + # name="bowl", + # scale=(1, 1, 1), + # physics=PhysicStateType.RIGIDBODY, + # usd_path="roboverse_data/assets/EmbodiedGenData/new_assets/bowl/1/usd/0f296af3df66565c9e1a7c2bc7b35d72.usd", + # urdf_path="roboverse_data/assets/EmbodiedGenData/new_assets/bowl/1/0f296af3df66565c9e1a7c2bc7b35d72.urdf", + # mjcf_path="roboverse_data/assets/EmbodiedGenData/new_assets/bowl/1/mjcf/0f296af3df66565c9e1a7c2bc7b35d72.xml", + # ), + RigidObjCfg( + name="cutting_tools", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/new_assets/cutting_tools/1/usd/c5810e7c2c785fe3940372b205090bad.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/new_assets/cutting_tools/1/c5810e7c2c785fe3940372b205090bad.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/new_assets/cutting_tools/1/mjcf/c5810e7c2c785fe3940372b205090bad.xml", + ), + # RigidObjCfg( + # name="screw_driver", + # scale=(1, 1, 1), + # physics=PhysicStateType.RIGIDBODY, + # usd_path="roboverse_data/assets/EmbodiedGenData/new_assets/screwdriver/1/usd/ae51f060e3455e9f84a4fec81cc9284b.usd", + # urdf_path="roboverse_data/assets/EmbodiedGenData/new_assets/screwdriver/1/ae51f060e3455e9f84a4fec81cc9284b.urdf", + # mjcf_path="roboverse_data/assets/EmbodiedGenData/new_assets/screwdriver/1/mjcf/ae51f060e3455e9f84a4fec81cc9284b.xml", + # ), + RigidObjCfg( + name="spoon", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/new_assets/spoon/1/usd/2f1c3077a8d954e58fc0bf75cf35e849.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/new_assets/spoon/1/2f1c3077a8d954e58fc0bf75cf35e849.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/new_assets/spoon/1/mjcf/2f1c3077a8d954e58fc0bf75cf35e849.xml", + ), + # RigidObjCfg( + # name="banana", + # scale=(1, 1, 1), + # physics=PhysicStateType.RIGIDBODY, + # usd_path="roboverse_data/assets/EmbodiedGenData/demo_assets/banana/usd/banana.usd", + # urdf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/banana/result/banana.urdf", + # mjcf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/banana/mjcf/banana.xml", + # ), RigidObjCfg( name="mug", scale=(1, 1, 1), @@ -411,37 +458,88 @@ def __post_init__(self): urdf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/book/result/book.urdf", mjcf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/book/mjcf/book.xml", ), + # RigidObjCfg( + # name="lamp", + # scale=(1, 1, 1), + # physics=PhysicStateType.RIGIDBODY, + # usd_path="roboverse_data/assets/EmbodiedGenData/demo_assets/lamp/usd/lamp.usd", + # urdf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/lamp/result/lamp.urdf", + # mjcf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/lamp/mjcf/lamp.xml", + # ), + # RigidObjCfg( + # name="remote_control", + # scale=(1, 1, 1), + # physics=PhysicStateType.RIGIDBODY, + # usd_path="roboverse_data/assets/EmbodiedGenData/demo_assets/remote_control/usd/remote_control.usd", + # urdf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/remote_control/result/remote_control.urdf", + # mjcf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/remote_control/mjcf/remote_control.xml", + # ), + # RigidObjCfg( + # name="rubiks_cube", + # scale=(1, 1, 1), + # physics=PhysicStateType.RIGIDBODY, + # usd_path="roboverse_data/assets/EmbodiedGenData/demo_assets/rubik's_cube/usd/rubik's_cube.usd", + # urdf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/rubik's_cube/result/rubik's_cube.urdf", + # mjcf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/rubik's_cube/mjcf/rubik's_cube.xml", + # ), RigidObjCfg( - name="lamp", + name="vase", scale=(1, 1, 1), physics=PhysicStateType.RIGIDBODY, - usd_path="roboverse_data/assets/EmbodiedGenData/demo_assets/lamp/usd/lamp.usd", - urdf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/lamp/result/lamp.urdf", - mjcf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/lamp/mjcf/lamp.xml", + usd_path="roboverse_data/assets/EmbodiedGenData/demo_assets/vase/usd/vase.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/vase/result/vase.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/vase/mjcf/vase.xml", ), + # Trajectory markers RigidObjCfg( - name="remote_control", - scale=(1, 1, 1), - physics=PhysicStateType.RIGIDBODY, - usd_path="roboverse_data/assets/EmbodiedGenData/demo_assets/remote_control/usd/remote_control.usd", - urdf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/remote_control/result/remote_control.urdf", - mjcf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/remote_control/mjcf/remote_control.xml", + name="traj_marker_0", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, ), RigidObjCfg( - name="rubiks_cube", - scale=(1, 1, 1), - physics=PhysicStateType.RIGIDBODY, - usd_path="roboverse_data/assets/EmbodiedGenData/demo_assets/rubik's_cube/usd/rubik's_cube.usd", - urdf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/rubik's_cube/result/rubik's_cube.urdf", - mjcf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/rubik's_cube/mjcf/rubik's_cube.xml", + name="traj_marker_1", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, ), RigidObjCfg( - name="vase", - scale=(1, 1, 1), - physics=PhysicStateType.RIGIDBODY, - usd_path="roboverse_data/assets/EmbodiedGenData/demo_assets/vase/usd/vase.usd", - urdf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/vase/result/vase.urdf", - mjcf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/vase/mjcf/vase.xml", + name="traj_marker_2", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + ), + RigidObjCfg( + name="traj_marker_3", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + ), + RigidObjCfg( + name="traj_marker_4", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, ), ], ) @@ -468,10 +566,6 @@ def __post_init__(self): "pos": torch.tensor([0.3, -0.28, 0.82]), # Book on table "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), }, - "lamp": { - "pos": torch.tensor([0.68, 0.10, 1.05]), # Lamp on table - "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), - }, "remote_control": { "pos": torch.tensor([0.68, -0.54, 0.811]), # Remote on table "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), @@ -484,6 +578,51 @@ def __post_init__(self): "pos": torch.tensor([0.30, 0.05, 0.95]), # Vase on table "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), }, + "lamp": { + "pos": torch.tensor([0.680000, 0.310000, 1.050000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "basket": { + "pos": torch.tensor([0.280000, 0.130000, 0.825000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "bowl": { + "pos": torch.tensor([0.620000, -0.080000, 0.863000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "cutting_tools": { + "pos": torch.tensor([0.640000, -0.320000, 0.820000]), + "rot": torch.tensor([0.930507, 0.000000, -0.000000, 0.366273]), + }, + "screw_driver": { + "pos": torch.tensor([0.320000, -0.340000, 0.811000]), + "rot": torch.tensor([-0.868588, -0.274057, -0.052298, 0.409518]), + }, + "spoon": { + "pos": torch.tensor([0.530000, -0.690000, 0.850000]), + "rot": torch.tensor([0.961352, -0.120799, 0.030845, 0.245473]), + }, + # Trajectory markers - initial positions + "traj_marker_0": { + "pos": torch.tensor([0.380000, -0.500000, 1.160000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "traj_marker_1": { + "pos": torch.tensor([0.390000, -0.420000, 0.900000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "traj_marker_2": { + "pos": torch.tensor([0.350000, -0.320000, 0.850000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "traj_marker_3": { + "pos": torch.tensor([0.330000, -0.160000, 1.100000]), + "rot": torch.tensor([0.601833, 0.798621, 0.000000, -0.000000]), + }, + "traj_marker_4": { + "pos": torch.tensor([0.310000, 0.150000, 1.130000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, }, "robots": { "franka": { @@ -657,8 +796,6 @@ def __post_init__(self): handler.set_states(states) - handler.simulate() - if step % 10 == 0: handler.refresh_render() step += 1 diff --git a/roboverse_learn/rl/fast_td3/configs/track.yaml b/roboverse_learn/rl/fast_td3/configs/pick_place_banana.yaml similarity index 82% rename from roboverse_learn/rl/fast_td3/configs/track.yaml rename to roboverse_learn/rl/fast_td3/configs/pick_place_banana.yaml index b06adffb0..c4c2b9d7e 100644 --- a/roboverse_learn/rl/fast_td3/configs/track.yaml +++ b/roboverse_learn/rl/fast_td3/configs/pick_place_banana.yaml @@ -1,20 +1,15 @@ -# Configuration for FastTD3 Training - Track Task -# Stage 3: Track task - for training trajectory tracking -# Starts from saved grasp states, only trains trajectory tracking +# Base Configuration for FastTD3 Training - Banana +# Default configuration with IsaacGym simulator and Franka robot # ------------------------------------------------------------------------------- # Environment # ------------------------------------------------------------------------------- sim: "isaacgym" robots: ["franka"] -task: "pick_place.track" +task: "pick_place.approach_grasp_simple_banana" decimation: 4 train_or_eval: "train" -headless: True - -# State file path for track task (pkl file path for grasp states) -# If null, uses default path or env var PICK_PLACE_TRACK_STATE_FILE -state_file_path: "eval_states/pick_place.approach_grasp_simple_franka_lift_states_100states_20251126_170312.pkl" +headless: False # ------------------------------------------------------------------------------- # Seeds & Device @@ -46,7 +41,6 @@ tau: 0.1 # ------------------------------------------------------------------------------- policy_frequency: 2 num_updates: 5 - # ------------------------------------------------------------------------------- # Optimizer & Network # ------------------------------------------------------------------------------- @@ -83,10 +77,12 @@ measure_burnin: 3 # ------------------------------------------------------------------------------- # Logging & Checkpointing # ------------------------------------------------------------------------------- -wandb_project: "pick_place_track" -exp_name: "pick_place_track" +wandb_project: "get_started_fttd3" +exp_name: "get_started_fttd3_banana" use_wandb: false checkpoint_path: null +run_name: "pick_place.approach_grasp_simple_banana" # Unique run name +model_dir: "models/banana" # Separate directory for banana checkpoints eval_interval: 5000 save_interval: 5000 video_width: 1024 diff --git a/roboverse_learn/rl/fast_td3/configs/pick_place_bowl.yaml b/roboverse_learn/rl/fast_td3/configs/pick_place_bowl.yaml new file mode 100644 index 000000000..5b73f22c4 --- /dev/null +++ b/roboverse_learn/rl/fast_td3/configs/pick_place_bowl.yaml @@ -0,0 +1,89 @@ +# Base Configuration for FastTD3 Training - Bowl +# Default configuration with IsaacGym simulator and Franka robot + +# ------------------------------------------------------------------------------- +# Environment +# ------------------------------------------------------------------------------- +sim: "isaacgym" +robots: ["franka"] +task: "pick_place.approach_grasp_simple_bowl" +decimation: 4 +train_or_eval: "train" +headless: False + +# ------------------------------------------------------------------------------- +# Seeds & Device +# ------------------------------------------------------------------------------- +seed: 1 +cuda: true +torch_deterministic: true +device_rank: 0 + +# ------------------------------------------------------------------------------- +# Rollout & Timesteps +# ------------------------------------------------------------------------------- +num_envs: 400 +num_eval_envs: 400 +total_timesteps: 2000000 +learning_starts: 10 +num_steps: 1 + +# ------------------------------------------------------------------------------- +# Replay, Batching, Discounting +# ------------------------------------------------------------------------------- +buffer_size: 20480 +batch_size: 32768 +gamma: 0.99 +tau: 0.1 + +# ------------------------------------------------------------------------------- +# Update Schedule +# ------------------------------------------------------------------------------- +policy_frequency: 2 +num_updates: 5 +# ------------------------------------------------------------------------------- +# Optimizer & Network +# ------------------------------------------------------------------------------- +critic_learning_rate: 0.0003 +actor_learning_rate: 0.0003 +weight_decay: 0.1 +critic_hidden_dim: 512 +actor_hidden_dim: 256 +init_scale: 0.01 +num_atoms: 101 + +# ------------------------------------------------------------------------------- +# Value Distribution & Exploration +# ------------------------------------------------------------------------------- +v_min: 0 +v_max: 600.0 +policy_noise: 0.001 +std_min: 0.001 +std_max: 0.4 +noise_clip: 0.5 + +# ------------------------------------------------------------------------------- +# Algorithm Flags +# ------------------------------------------------------------------------------- +use_cdq: true +compile: true +obs_normalization: true +max_grad_norm: 0.0 +amp: true +amp_dtype: "fp16" +disable_bootstrap: false +measure_burnin: 3 + +# ------------------------------------------------------------------------------- +# Logging & Checkpointing +# ------------------------------------------------------------------------------- +wandb_project: "get_started_fttd3" +exp_name: "get_started_fttd3_bowl" +use_wandb: false +checkpoint_path: null +run_name: "pick_place.approach_grasp_simple_bowl" # Unique run name for bowl task +model_dir: "models/bowl" # Separate directory for bowl checkpoints +eval_interval: 5000 +save_interval: 5000 +video_width: 1024 +video_height: 1024 diff --git a/roboverse_learn/rl/fast_td3/configs/pick_place_cuttingtool.yaml b/roboverse_learn/rl/fast_td3/configs/pick_place_cuttingtool.yaml new file mode 100644 index 000000000..a39ca92ba --- /dev/null +++ b/roboverse_learn/rl/fast_td3/configs/pick_place_cuttingtool.yaml @@ -0,0 +1,89 @@ +# Base Configuration for FastTD3 Training - Cutting Tool +# Default configuration with IsaacGym simulator and Franka robot + +# ------------------------------------------------------------------------------- +# Environment +# ------------------------------------------------------------------------------- +sim: "isaacgym" +robots: ["franka"] +task: "pick_place.approach_grasp_simple_cuttingtool" +decimation: 4 +train_or_eval: "train" +headless: False + +# ------------------------------------------------------------------------------- +# Seeds & Device +# ------------------------------------------------------------------------------- +seed: 1 +cuda: true +torch_deterministic: true +device_rank: 0 + +# ------------------------------------------------------------------------------- +# Rollout & Timesteps +# ------------------------------------------------------------------------------- +num_envs: 400 +num_eval_envs: 400 +total_timesteps: 2000000 +learning_starts: 10 +num_steps: 1 + +# ------------------------------------------------------------------------------- +# Replay, Batching, Discounting +# ------------------------------------------------------------------------------- +buffer_size: 20480 +batch_size: 32768 +gamma: 0.99 +tau: 0.1 + +# ------------------------------------------------------------------------------- +# Update Schedule +# ------------------------------------------------------------------------------- +policy_frequency: 2 +num_updates: 5 +# ------------------------------------------------------------------------------- +# Optimizer & Network +# ------------------------------------------------------------------------------- +critic_learning_rate: 0.0003 +actor_learning_rate: 0.0003 +weight_decay: 0.1 +critic_hidden_dim: 512 +actor_hidden_dim: 256 +init_scale: 0.01 +num_atoms: 101 + +# ------------------------------------------------------------------------------- +# Value Distribution & Exploration +# ------------------------------------------------------------------------------- +v_min: 0 +v_max: 600.0 +policy_noise: 0.001 +std_min: 0.001 +std_max: 0.4 +noise_clip: 0.5 + +# ------------------------------------------------------------------------------- +# Algorithm Flags +# ------------------------------------------------------------------------------- +use_cdq: true +compile: true +obs_normalization: true +max_grad_norm: 0.0 +amp: true +amp_dtype: "fp16" +disable_bootstrap: false +measure_burnin: 3 + +# ------------------------------------------------------------------------------- +# Logging & Checkpointing +# ------------------------------------------------------------------------------- +wandb_project: "get_started_fttd3" +exp_name: "get_started_fttd3_cuttingtool" +use_wandb: false +checkpoint_path: null +run_name: "pick_place.approach_grasp_simple_cuttingtool" # Unique run name for cutting_tools task +model_dir: "models/cuttingtool" # Separate directory for cutting_tools checkpoints +eval_interval: 5000 +save_interval: 5000 +video_width: 1024 +video_height: 1024 diff --git a/roboverse_learn/rl/fast_td3/configs/pick_place_screwdriver.yaml b/roboverse_learn/rl/fast_td3/configs/pick_place_screwdriver.yaml new file mode 100644 index 000000000..5b776f62d --- /dev/null +++ b/roboverse_learn/rl/fast_td3/configs/pick_place_screwdriver.yaml @@ -0,0 +1,89 @@ +# Base Configuration for FastTD3 Training - Screwdriver +# Default configuration with IsaacGym simulator and Franka robot + +# ------------------------------------------------------------------------------- +# Environment +# ------------------------------------------------------------------------------- +sim: "isaacgym" +robots: ["franka"] +task: "pick_place.approach_grasp_simple_screwdriver" +decimation: 4 +train_or_eval: "train" +headless: False + +# ------------------------------------------------------------------------------- +# Seeds & Device +# ------------------------------------------------------------------------------- +seed: 1 +cuda: true +torch_deterministic: true +device_rank: 0 + +# ------------------------------------------------------------------------------- +# Rollout & Timesteps +# ------------------------------------------------------------------------------- +num_envs: 400 +num_eval_envs: 400 +total_timesteps: 2000000 +learning_starts: 10 +num_steps: 1 + +# ------------------------------------------------------------------------------- +# Replay, Batching, Discounting +# ------------------------------------------------------------------------------- +buffer_size: 20480 +batch_size: 32768 +gamma: 0.99 +tau: 0.1 + +# ------------------------------------------------------------------------------- +# Update Schedule +# ------------------------------------------------------------------------------- +policy_frequency: 2 +num_updates: 5 +# ------------------------------------------------------------------------------- +# Optimizer & Network +# ------------------------------------------------------------------------------- +critic_learning_rate: 0.0003 +actor_learning_rate: 0.0003 +weight_decay: 0.1 +critic_hidden_dim: 512 +actor_hidden_dim: 256 +init_scale: 0.01 +num_atoms: 101 + +# ------------------------------------------------------------------------------- +# Value Distribution & Exploration +# ------------------------------------------------------------------------------- +v_min: 0 +v_max: 600.0 +policy_noise: 0.001 +std_min: 0.001 +std_max: 0.4 +noise_clip: 0.5 + +# ------------------------------------------------------------------------------- +# Algorithm Flags +# ------------------------------------------------------------------------------- +use_cdq: true +compile: true +obs_normalization: true +max_grad_norm: 0.0 +amp: true +amp_dtype: "fp16" +disable_bootstrap: false +measure_burnin: 3 + +# ------------------------------------------------------------------------------- +# Logging & Checkpointing +# ------------------------------------------------------------------------------- +wandb_project: "get_started_fttd3" +exp_name: "get_started_fttd3_screwdriver" +use_wandb: false +checkpoint_path: null +run_name: "pick_place.approach_grasp_simple_screwdriver" # Unique run name +model_dir: "models/screwdriver" # Separate directory for screwdriver checkpoints +eval_interval: 5000 +save_interval: 5000 +video_width: 1024 +video_height: 1024 diff --git a/roboverse_learn/rl/fast_td3/configs/pick_place.yaml b/roboverse_learn/rl/fast_td3/configs/pick_place_spoon.yaml similarity index 90% rename from roboverse_learn/rl/fast_td3/configs/pick_place.yaml rename to roboverse_learn/rl/fast_td3/configs/pick_place_spoon.yaml index 065d7cd8c..ab03278a1 100644 --- a/roboverse_learn/rl/fast_td3/configs/pick_place.yaml +++ b/roboverse_learn/rl/fast_td3/configs/pick_place_spoon.yaml @@ -1,4 +1,4 @@ -# Base Configuration for FastTD3 Training +# Base Configuration for FastTD3 Training - Spoon # Default configuration with IsaacGym simulator and H1 humanoid robot # ------------------------------------------------------------------------------- @@ -6,10 +6,10 @@ # ------------------------------------------------------------------------------- sim: "isaacgym" robots: ["franka"] -task: "pick_place.approach_grasp_simple" +task: "pick_place.approach_grasp_simple_spoon" decimation: 4 train_or_eval: "train" -headless: True +headless: False # ------------------------------------------------------------------------------- # Seeds & Device @@ -78,9 +78,11 @@ measure_burnin: 3 # Logging & Checkpointing # ------------------------------------------------------------------------------- wandb_project: "get_started_fttd3" -exp_name: "get_started_fttd3" +exp_name: "get_started_fttd3_spoon" use_wandb: false checkpoint_path: null +run_name: "pick_place.approach_grasp_simple_spoon" # Unique run name +model_dir: "models/spoon" # Separate directory for basket layout eval_interval: 5000 save_interval: 5000 video_width: 1024 diff --git a/roboverse_learn/rl/fast_td3/configs/pick_place_spoon2.yaml b/roboverse_learn/rl/fast_td3/configs/pick_place_spoon2.yaml new file mode 100644 index 000000000..59c38dd19 --- /dev/null +++ b/roboverse_learn/rl/fast_td3/configs/pick_place_spoon2.yaml @@ -0,0 +1,89 @@ +# Base Configuration for FastTD3 Training - Spoon Scene 2 +# Configuration for spoon grasping task with second scene layout + +# ------------------------------------------------------------------------------- +# Environment +# ------------------------------------------------------------------------------- +sim: "isaacgym" +robots: ["franka"] +task: "pick_place.approach_grasp_simple_spoon2" +decimation: 4 +train_or_eval: "train" +headless: False + +# ------------------------------------------------------------------------------- +# Seeds & Device +# ------------------------------------------------------------------------------- +seed: 1 +cuda: true +torch_deterministic: true +device_rank: 0 + +# ------------------------------------------------------------------------------- +# Rollout & Timesteps +# ------------------------------------------------------------------------------- +num_envs: 400 +num_eval_envs: 400 +total_timesteps: 2000000 +learning_starts: 10 +num_steps: 1 + +# ------------------------------------------------------------------------------- +# Replay, Batching, Discounting +# ------------------------------------------------------------------------------- +buffer_size: 20480 +batch_size: 32768 +gamma: 0.99 +tau: 0.1 + +# ------------------------------------------------------------------------------- +# Update Schedule +# ------------------------------------------------------------------------------- +policy_frequency: 2 +num_updates: 5 +# ------------------------------------------------------------------------------- +# Optimizer & Network +# ------------------------------------------------------------------------------- +critic_learning_rate: 0.0003 +actor_learning_rate: 0.0003 +weight_decay: 0.1 +critic_hidden_dim: 512 +actor_hidden_dim: 256 +init_scale: 0.01 +num_atoms: 101 + +# ------------------------------------------------------------------------------- +# Value Distribution & Exploration +# ------------------------------------------------------------------------------- +v_min: 0 +v_max: 600.0 +policy_noise: 0.001 +std_min: 0.001 +std_max: 0.4 +noise_clip: 0.5 + +# ------------------------------------------------------------------------------- +# Algorithm Flags +# ------------------------------------------------------------------------------- +use_cdq: true +compile: true +obs_normalization: true +max_grad_norm: 0.0 +amp: true +amp_dtype: "fp16" +disable_bootstrap: false +measure_burnin: 3 + +# ------------------------------------------------------------------------------- +# Logging & Checkpointing +# ------------------------------------------------------------------------------- +wandb_project: "get_started_fttd3" +exp_name: "get_started_fttd3_spoon2" +use_wandb: false +checkpoint_path: null +run_name: "pick_place.approach_grasp_simple_spoon2" # Unique run name for scene 2 +model_dir: "models/spoon2" # Separate directory for scene 2 checkpoints +eval_interval: 5000 +save_interval: 5000 +video_width: 1024 +video_height: 1024 diff --git a/roboverse_learn/rl/fast_td3/configs/track_banana.yaml b/roboverse_learn/rl/fast_td3/configs/track_banana.yaml new file mode 100644 index 000000000..f5b6f4d5b --- /dev/null +++ b/roboverse_learn/rl/fast_td3/configs/track_banana.yaml @@ -0,0 +1,95 @@ +# Configuration for FastTD3 Training - Track Task (Banana) +# Stage 3: Track task for banana object - for training trajectory tracking +# Starts from saved grasp states from approach_grasp_simple_banana, only trains trajectory tracking + +# ------------------------------------------------------------------------------- +# Environment +# ------------------------------------------------------------------------------- +sim: "isaacgym" +robots: ["franka"] +task: "pick_place.track_banana" +decimation: 4 +train_or_eval: "train" +headless: False + +# State file path for track task (pkl file path for grasp states) +# If null, uses default path or env var PICK_PLACE_TRACK_STATE_FILE +state_file_path: "eval_states/pick_place.approach_grasp_banana_franka_lift_states_100states_20251210_153518.pkl" + + +# ------------------------------------------------------------------------------- +# Seeds & Device +# ------------------------------------------------------------------------------- +seed: 1 +cuda: true +torch_deterministic: true +device_rank: 0 + +# ------------------------------------------------------------------------------- +# Rollout & Timesteps +# ------------------------------------------------------------------------------- +num_envs: 400 +num_eval_envs: 400 +total_timesteps: 2000000 +learning_starts: 10 +num_steps: 1 + +# ------------------------------------------------------------------------------- +# Replay, Batching, Discounting +# ------------------------------------------------------------------------------- +buffer_size: 20480 +batch_size: 32768 +gamma: 0.99 +tau: 0.1 + +# ------------------------------------------------------------------------------- +# Update Schedule +# ------------------------------------------------------------------------------- +policy_frequency: 2 +num_updates: 5 +# ------------------------------------------------------------------------------- +# Optimizer & Network +# ------------------------------------------------------------------------------- +critic_learning_rate: 0.0003 +actor_learning_rate: 0.0003 +weight_decay: 0.1 +critic_hidden_dim: 512 +actor_hidden_dim: 256 +init_scale: 0.01 +num_atoms: 101 + +# ------------------------------------------------------------------------------- +# Value Distribution & Exploration +# ------------------------------------------------------------------------------- +v_min: 0 +v_max: 600.0 +policy_noise: 0.001 +std_min: 0.001 +std_max: 0.4 +noise_clip: 0.5 + +# ------------------------------------------------------------------------------- +# Algorithm Flags +# ------------------------------------------------------------------------------- +use_cdq: true +compile: true +obs_normalization: true +max_grad_norm: 0.0 +amp: true +amp_dtype: "fp16" +disable_bootstrap: false +measure_burnin: 3 + +# ------------------------------------------------------------------------------- +# Logging & Checkpointing +# ------------------------------------------------------------------------------- +wandb_project: "pick_place_track_banana" +exp_name: "pick_place_track_banana" +use_wandb: false +checkpoint_path: null +run_name: "pick_place.track_banana" # Unique run name for banana track task +model_dir: "models/track_banana" # Separate directory for track banana checkpoints +eval_interval: 5000 +save_interval: 5000 +video_width: 1024 +video_height: 1024 diff --git a/roboverse_learn/rl/fast_td3/configs/track_screwdriver.yaml b/roboverse_learn/rl/fast_td3/configs/track_screwdriver.yaml new file mode 100644 index 000000000..0c3efbde9 --- /dev/null +++ b/roboverse_learn/rl/fast_td3/configs/track_screwdriver.yaml @@ -0,0 +1,95 @@ +# Configuration for FastTD3 Training - Track Task (Screwdriver) +# Stage 3: Track task for screwdriver object - for training trajectory tracking +# Starts from saved grasp states from approach_grasp_simple_screwdriver, only trains trajectory tracking + +# ------------------------------------------------------------------------------- +# Environment +# ------------------------------------------------------------------------------- +sim: "isaacgym" +robots: ["franka"] +task: "pick_place.track_screwdriver" # Uses screwdriver-specific track task +decimation: 4 +train_or_eval: "train" +headless: False + +# State file path for track task (pkl file path for grasp states) +# If null, uses default path or env var PICK_PLACE_TRACK_STATE_FILE +state_file_path: "eval_states/pick_place.approach_grasp_screwdriver_franka_lift_states_100states_20251210_154907.pkl" + +# ------------------------------------------------------------------------------- +# Seeds & Device +# ------------------------------------------------------------------------------- +seed: 1 +cuda: true +torch_deterministic: true +device_rank: 0 + +# ------------------------------------------------------------------------------- +# Rollout & Timesteps +# ------------------------------------------------------------------------------- +num_envs: 400 +num_eval_envs: 400 +total_timesteps: 2000000 +learning_starts: 10 +num_steps: 1 + +# ------------------------------------------------------------------------------- +# Replay, Batching, Discounting +# ------------------------------------------------------------------------------- +buffer_size: 20480 +batch_size: 32768 +gamma: 0.99 +tau: 0.1 + +# ------------------------------------------------------------------------------- +# Update Schedule +# ------------------------------------------------------------------------------- +policy_frequency: 2 +num_updates: 5 + +# ------------------------------------------------------------------------------- +# Optimizer & Network +# ------------------------------------------------------------------------------- +critic_learning_rate: 0.0003 +actor_learning_rate: 0.0003 +weight_decay: 0.1 +critic_hidden_dim: 512 +actor_hidden_dim: 256 +init_scale: 0.01 +num_atoms: 101 + +# ------------------------------------------------------------------------------- +# Value Distribution & Exploration +# ------------------------------------------------------------------------------- +v_min: 0 +v_max: 600.0 +policy_noise: 0.001 +std_min: 0.001 +std_max: 0.4 +noise_clip: 0.5 + +# ------------------------------------------------------------------------------- +# Algorithm Flags +# ------------------------------------------------------------------------------- +use_cdq: true +compile: true +obs_normalization: true +max_grad_norm: 0.0 +amp: true +amp_dtype: "fp16" +disable_bootstrap: false +measure_burnin: 3 + +# ------------------------------------------------------------------------------- +# Logging & Checkpointing +# ------------------------------------------------------------------------------- +wandb_project: "pick_place_track_screwdriver" +exp_name: "pick_place_track_screwdriver" +use_wandb: false +checkpoint_path: null +run_name: "pick_place.track_screwdriver" # Unique run name for screwdriver track task +model_dir: "models/track_screwdriver" # Separate directory for track screwdriver checkpoints +eval_interval: 5000 +save_interval: 5000 +video_width: 1024 +video_height: 1024 diff --git a/roboverse_learn/rl/fast_td3/configs/track_spoon.yaml b/roboverse_learn/rl/fast_td3/configs/track_spoon.yaml new file mode 100644 index 000000000..f92bd84e1 --- /dev/null +++ b/roboverse_learn/rl/fast_td3/configs/track_spoon.yaml @@ -0,0 +1,97 @@ +# Configuration for FastTD3 Training - Track Task (Spoon) +# Stage 3: Track task for spoon object - for training trajectory tracking +# Starts from saved grasp states from approach_grasp_simple_spoon, only trains trajectory tracking + +# ------------------------------------------------------------------------------- +# Environment +# ------------------------------------------------------------------------------- +sim: "isaacgym" +robots: ["franka"] +task: "pick_place.track_spoon" # Uses spoon-specific track task +decimation: 4 +train_or_eval: "train" +headless: False + +# State file path for track task (pkl file path for grasp states) +# Note: This is for reference/documentation. The actual path is set in track_spoon.py __init__ +# After running evaluate_lift.py for the spoon task, update the state_file_path in track_spoon.py +# to point to the generated states file (e.g., from eval_states/ directory) +state_file_path: "eval_states/pick_place.approach_grasp_simple_spoon_franka_lift_states_130states_20251208_172505.pkl" + +# ------------------------------------------------------------------------------- +# Seeds & Device +# ------------------------------------------------------------------------------- +seed: 1 +cuda: true +torch_deterministic: true +device_rank: 0 + +# ------------------------------------------------------------------------------- +# Rollout & Timesteps +# ------------------------------------------------------------------------------- +num_envs: 400 +num_eval_envs: 400 +total_timesteps: 2000000 +learning_starts: 10 +num_steps: 1 + +# ------------------------------------------------------------------------------- +# Replay, Batching, Discounting +# ------------------------------------------------------------------------------- +buffer_size: 20480 +batch_size: 32768 +gamma: 0.99 +tau: 0.1 + +# ------------------------------------------------------------------------------- +# Update Schedule +# ------------------------------------------------------------------------------- +policy_frequency: 2 +num_updates: 5 + +# ------------------------------------------------------------------------------- +# Optimizer & Network +# ------------------------------------------------------------------------------- +critic_learning_rate: 0.0003 +actor_learning_rate: 0.0003 +weight_decay: 0.1 +critic_hidden_dim: 512 +actor_hidden_dim: 256 +init_scale: 0.01 +num_atoms: 101 + +# ------------------------------------------------------------------------------- +# Value Distribution & Exploration +# ------------------------------------------------------------------------------- +v_min: 0 +v_max: 600.0 +policy_noise: 0.001 +std_min: 0.001 +std_max: 0.4 +noise_clip: 0.5 + +# ------------------------------------------------------------------------------- +# Algorithm Flags +# ------------------------------------------------------------------------------- +use_cdq: true +compile: true +obs_normalization: true +max_grad_norm: 0.0 +amp: true +amp_dtype: "fp16" +disable_bootstrap: false +measure_burnin: 3 + +# ------------------------------------------------------------------------------- +# Logging & Checkpointing +# ------------------------------------------------------------------------------- +wandb_project: "pick_place_track_spoon" +exp_name: "pick_place_track_spoon" +use_wandb: false +checkpoint_path: null +run_name: "pick_place.track_spoon" # Unique run name for spoon track task +model_dir: "models/track_spoon" # Separate directory for track spoon checkpoints +eval_interval: 5000 +save_interval: 5000 +video_width: 1024 +video_height: 1024 diff --git a/roboverse_learn/rl/fast_td3/evaluate.py b/roboverse_learn/rl/fast_td3/evaluate.py index 288595042..0a2d984a4 100644 --- a/roboverse_learn/rl/fast_td3/evaluate.py +++ b/roboverse_learn/rl/fast_td3/evaluate.py @@ -421,6 +421,66 @@ def evaluate( return stats +def _adjust_state_dict_for_model(checkpoint_state: dict, model: torch.nn.Module): + """Adjust tensors from checkpoint_state to better match model.state_dict() shapes. + + - If a tensor differs only in the leading (0-th) dimension and the remaining dims match, + we slice or repeat rows to match the model shape. + - If total numel matches, we reshape. + - Otherwise we fall back to the model's own parameter to avoid shape errors. + """ + model_state = model.state_dict() + new_state = {} + for k, v in checkpoint_state.items(): + if k not in model_state: + # keep unknown keys (they may be used elsewhere) + new_state[k] = v + continue + + mv = model_state[k] + # Only handle tensors; keep other items as-is + if not isinstance(v, torch.Tensor) or not isinstance(mv, torch.Tensor): + new_state[k] = v + continue + + if v.shape == mv.shape: + new_state[k] = v + continue + + # Case: same trailing dims, mismatched leading dim (common when num_envs differs) + if v.ndim == mv.ndim and v.shape[1:] == mv.shape[1:]: + desired = mv.shape[0] + src = v + if src.shape[0] >= desired: + new_state[k] = src[:desired].to(mv.device).clone() + else: + # Repeat rows to reach desired size then slice + reps = (desired + src.shape[0] - 1) // src.shape[0] + tiled = src.repeat(reps, *([1] * (src.ndim - 1))) + new_state[k] = tiled[:desired].to(mv.device).clone() + log.info(f"Adjusted checkpoint param '{k}' from {v.shape} -> {new_state[k].shape}") + continue + + # If total elements match, reshape + if v.numel() == mv.numel(): + try: + new_state[k] = v.reshape(mv.shape).to(mv.device).clone() + log.info(f"Reshaped checkpoint param '{k}' from {v.shape} -> {mv.shape}") + continue + except Exception: + pass + + # Last resort: try broadcasting/expanding + try: + new_state[k] = v.to(mv.device).expand_as(mv).clone() + log.info(f"Expanded checkpoint param '{k}' from {v.shape} -> {mv.shape}") + continue + except Exception: + log.warning(f"Could not match shape for param '{k}' ({v.shape} -> {mv.shape}); keeping model init") + new_state[k] = mv + return new_state + + def main(): parser = argparse.ArgumentParser(description='FastTD3 Evaluation') parser.add_argument('--checkpoint', type=str, default='roboverse_data/models/walk_1400.pt', @@ -524,9 +584,26 @@ def main(): obs_normalizer = EmpiricalNormalization(shape=n_obs, device=device) # Load weights - actor.load_state_dict(checkpoint["actor_state_dict"]) - if checkpoint.get("obs_normalizer_state"): - obs_normalizer.load_state_dict(checkpoint["obs_normalizer_state"]) + # Safely adjust checkpoint actor state to match current model shapes (handles num_envs mismatch) + ck_actor_state = checkpoint.get("actor_state_dict", {}) + if ck_actor_state: + try: + adjusted = _adjust_state_dict_for_model(ck_actor_state, actor) + # load non-strictly to allow missing/extra keys + actor.load_state_dict(adjusted, strict=False) + except Exception as e: + log.exception("Failed to load actor_state_dict with adjustment, falling back to strict load: %s", e) + actor.load_state_dict(ck_actor_state, strict=False) + else: + log.warning("No actor_state_dict present in checkpoint") + + # Load obs normalizer safely + try: + obs_state = checkpoint.get("obs_normalizer_state") + if obs_state: + obs_normalizer.load_state_dict(obs_state) + except Exception: + log.warning("Failed to load obs_normalizer_state from checkpoint; skipping") # Setup AMP amp_enabled = config.get("amp", False) and torch.cuda.is_available() diff --git a/roboverse_learn/rl/fast_td3/evaluate_lift.py b/roboverse_learn/rl/fast_td3/evaluate_lift.py index f83180311..ba613cb31 100644 --- a/roboverse_learn/rl/fast_td3/evaluate_lift.py +++ b/roboverse_learn/rl/fast_td3/evaluate_lift.py @@ -332,6 +332,15 @@ def evaluate_lift_collection( else: obs = next_obs + # Treat each active env as an attempted episode, and count it as successful if it already + active_envs = (~done_masks).sum().item() if 'done_masks' in locals() else 0 + successes_in_active = sum( + 1 for i in range(num_eval_envs) + if 'done_masks' in locals() and not done_masks[i] and success_in_episode.get(i, False) + ) + attempted_episodes = episodes_completed + active_envs + total_successful_episodes = successful_episodes_count + successes_in_active + if len(collected_trajs) > 0: os.makedirs(traj_dir, exist_ok=True) os.makedirs(state_dir, exist_ok=True) @@ -357,12 +366,14 @@ def evaluate_lift_collection( log.info(f" - State count: {len(collected_states)}") else: log.warning("No successful trajectories collected") + # Success rate: fraction of attempted episodes (completed + in-progress when we stopped) + denom = max(attempted_episodes, 1) + success_rate = min(total_successful_episodes, denom) / denom stats = { "collected_count": len(collected_trajs), "target_count": target_count, - "episodes_completed": episodes_completed, - # success_rate = successful simulations / total simulations (episodes) - "success_rate": successful_episodes_count / episodes_completed if episodes_completed > 0 else 0.0, + "episodes_completed": attempted_episodes, + "success_rate": success_rate, } return stats diff --git a/roboverse_learn/rl/fast_td3/replay_lift_states.py b/roboverse_learn/rl/fast_td3/replay_lift_states.py index aad63f46d..6a77106df 100644 --- a/roboverse_learn/rl/fast_td3/replay_lift_states.py +++ b/roboverse_learn/rl/fast_td3/replay_lift_states.py @@ -36,7 +36,7 @@ from metasim.scenario.cameras import PinholeCameraCfg from metasim.task.registry import get_task_class -from roboverse_pack.tasks.pick_place.track import convert_state_dict_to_initial_state +from roboverse_pack.tasks.pick_place.track_banana import convert_state_dict_to_initial_state def load_states_from_pkl(pkl_path: str): diff --git a/roboverse_learn/rl/fast_td3/train.py b/roboverse_learn/rl/fast_td3/train.py index dfb03aaab..5effe6e06 100644 --- a/roboverse_learn/rl/fast_td3/train.py +++ b/roboverse_learn/rl/fast_td3/train.py @@ -58,11 +58,11 @@ def get_config(): torch.set_float32_matmul_precision("high") +import inspect import numpy as np import torch import torch.nn.functional as F import tqdm -import wandb from loguru import logger as log from tensordict import TensorDict from torch import optim @@ -137,11 +137,14 @@ def main() -> None: scaler = GradScaler(enabled=amp_enabled and amp_dtype == torch.float16) - if cfg("use_wandb") and cfg("train_or_eval") == "train": - wandb.init( - project=cfg("wandb_project", "fttd3_training"), - save_code=True, - ) + # Import wandb if enabled + if cfg("use_wandb"): + import wandb + if cfg("train_or_eval") == "train": + wandb.init( + project=cfg("wandb_project", "fttd3_training"), + save_code=True, + ) random.seed(cfg("seed")) np.random.seed(cfg("seed")) @@ -166,9 +169,23 @@ def main() -> None: scenario = task_cls.scenario.update( robots=cfg("robots"), simulator=cfg("sim"), num_envs=cfg("num_envs"), headless=cfg("headless"), cameras=[] ) - envs = task_cls(scenario, device=device) - from metasim.utils.viser.viser_env_wrapper import TaskViserWrapper - envs = TaskViserWrapper(envs) + # Check if task class accepts state_file_path parameter (only track tasks do) + init_signature = inspect.signature(task_cls.__init__) + accepts_state_file_path = "state_file_path" in init_signature.parameters + + # Pass state_file_path from config if task accepts it (for track tasks) + state_file_path = cfg("state_file_path", None) + if accepts_state_file_path and state_file_path is not None: + envs = task_cls(scenario, device=device, state_file_path=state_file_path) + else: + envs = task_cls(scenario, device=device) + # Only use viser wrapper if not headless and viser is available + if not cfg("headless"): + try: + from metasim.utils.viser.viser_env_wrapper import TaskViserWrapper + envs = TaskViserWrapper(envs) + except ImportError: + log.warning("Viser not available, skipping visualization wrapper") eval_envs = envs # ---------------- derive shapes ------------------------------------ @@ -299,7 +316,11 @@ def render_with_rollout() -> list: scenario_render = scenario.update( robots=robots, simulator=simulator, num_envs=num_envs, headless=headless, cameras=cameras ) - env = task_cls(scenario_render, device=device) + # Pass state_file_path from config if task accepts it (for track tasks) + if accepts_state_file_path and state_file_path is not None: + env = task_cls(scenario_render, device=device, state_file_path=state_file_path) + else: + env = task_cls(scenario_render, device=device) obs_normalizer.eval() obs, info = env.reset() diff --git a/roboverse_pack/tasks/pick_place/approach_grasp.py b/roboverse_pack/tasks/pick_place/approach_grasp.py index c2500797d..9ab913675 100644 --- a/roboverse_pack/tasks/pick_place/approach_grasp.py +++ b/roboverse_pack/tasks/pick_place/approach_grasp.py @@ -15,6 +15,7 @@ from metasim.scenario.objects import PrimitiveCubeCfg, RigidObjCfg from metasim.scenario.scenario import ScenarioCfg, SimParamCfg from metasim.task.registry import register_task +from metasim.utils.math import matrix_from_quat from roboverse_pack.tasks.pick_place.base import DEFAULT_CONFIG, PickPlaceBase @@ -42,6 +43,7 @@ class PickPlaceApproachGraspSimple(PickPlaceBase): DEFAULT_CONFIG_SIMPLE["reward_config"]["scales"].update({ "gripper_approach": 0.5, "grasp_reward": 4.0, + "gripper_downward_alignment": 0.1, }) DEFAULT_CONFIG_SIMPLE["grasp_config"] = { "grasp_check_distance": GRASP_DISTANCE_THRESHOLD, @@ -146,10 +148,12 @@ def __init__(self, scenario, device=None): self.reward_functions = [ self._reward_gripper_approach, self._reward_grasp, + self._reward_gripper_downward_alignment, ] self.reward_weights = [ self.DEFAULT_CONFIG_SIMPLE["reward_config"]["scales"]["gripper_approach"], self.DEFAULT_CONFIG_SIMPLE["reward_config"]["scales"]["grasp_reward"], + self.DEFAULT_CONFIG_SIMPLE["reward_config"]["scales"]["gripper_downward_alignment"], ] # Get config values @@ -347,6 +351,17 @@ def _reward_grasp(self, env_states) -> torch.Tensor: # Use cached grasp state (computed in step method) return self.object_grasped.float() + def _reward_gripper_downward_alignment(self, env_states) -> torch.Tensor: + """Encourage the gripper tool frame to face downward (z-axis aligned with world -Z).""" + _, gripper_quat = self._get_ee_state(env_states) + gripper_rot = matrix_from_quat(gripper_quat) # (B, 3, 3) + gripper_z_world = gripper_rot[:, :, 2] # gripper local z-axis in world frame + + down = torch.tensor([0.0, 0.0, -1.0], device=self.device, dtype=gripper_z_world.dtype) + alignment = (gripper_z_world * down).sum(dim=-1).clamp(-1.0, 1.0) # cosine of angle to -Z + # Map cosine (1 best, -1 worst) to [0,1] reward + return (alignment + 1.0) / 2.0 + def _get_initial_states(self) -> list[dict] | None: """Get initial states for all environments.""" init = [ diff --git a/roboverse_pack/tasks/pick_place/approach_grasp_banana.py b/roboverse_pack/tasks/pick_place/approach_grasp_banana.py new file mode 100644 index 000000000..b94c152a3 --- /dev/null +++ b/roboverse_pack/tasks/pick_place/approach_grasp_banana.py @@ -0,0 +1,282 @@ +"""Stage 1: Simple Approach and Grasp task for banana object. + +This task inherits from PickPlaceApproachGraspSimple and customizes it for the banana object +with specific mesh configurations and saved poses from object_layout.py. +""" + +from __future__ import annotations + +import importlib.util +import os + +import torch +from loguru import logger as log + +from metasim.constants import PhysicStateType +from metasim.scenario.objects import RigidObjCfg +from metasim.scenario.scenario import ScenarioCfg, SimParamCfg +from metasim.task.registry import register_task +from roboverse_pack.tasks.pick_place.approach_grasp import PickPlaceApproachGraspSimple + + +@register_task("pick_place.approach_grasp_simple_banana", "pick_place_approach_grasp_simple_banana") +class PickPlaceApproachGraspSimpleBanana(PickPlaceApproachGraspSimple): + """Simple Approach and Grasp task for banana object. + + This task inherits from PickPlaceApproachGraspSimple and customizes: + - Scenario: Uses banana mesh, table mesh, and basket from EmbodiedGenData + - Initial states: Loads poses from saved_poses_20251206_banana_basket.py + """ + + scenario = ScenarioCfg( + objects=[ + RigidObjCfg( + name="table", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/demo_assets/table/usd/table.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/table/result/table.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/table/mjcf/table.xml", + ), + RigidObjCfg( + name="lamp", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/new_assets/lighting_fixtures/1/usd/0a4489b1a2875c82a580f8b62d346e08.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/new_assets/lighting_fixtures/1/0a4489b1a2875c82a580f8b62d346e08.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/new_assets/lighting_fixtures/1/mjcf/0a4489b1a2875c82a580f8b62d346e08.xml", + ), + RigidObjCfg( + name="basket", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/new_assets/basket/1/usd/663158968e3f5900af1f6e7cecef24c7.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/new_assets/basket/1/663158968e3f5900af1f6e7cecef24c7.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/new_assets/basket/1/mjcf/663158968e3f5900af1f6e7cecef24c7.xml", + ), + RigidObjCfg( + name="bowl", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/new_assets/bowl/1/usd/0f296af3df66565c9e1a7c2bc7b35d72.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/new_assets/bowl/1/0f296af3df66565c9e1a7c2bc7b35d72.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/new_assets/bowl/1/mjcf/0f296af3df66565c9e1a7c2bc7b35d72.xml", + ), + RigidObjCfg( + name="cutting_tools", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/new_assets/cutting_tools/1/usd/c5810e7c2c785fe3940372b205090bad.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/new_assets/cutting_tools/1/c5810e7c2c785fe3940372b205090bad.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/new_assets/cutting_tools/1/mjcf/c5810e7c2c785fe3940372b205090bad.xml", + ), + # Use actual banana mesh from EmbodiedGenData (matches object_layout.py) + RigidObjCfg( + name="object", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/demo_assets/banana/usd/banana.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/banana/result/banana.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/banana/mjcf/banana.xml", + ), + RigidObjCfg( + name="spoon", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/new_assets/spoon/1/usd/2f1c3077a8d954e58fc0bf75cf35e849.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/new_assets/spoon/1/2f1c3077a8d954e58fc0bf75cf35e849.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/new_assets/spoon/1/mjcf/2f1c3077a8d954e58fc0bf75cf35e849.xml", + ), + # Visualization: Trajectory waypoints (5 spheres showing trajectory path) + RigidObjCfg( + name="traj_marker_0", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_1", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_2", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_3", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_4", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + ], + robots=["franka"], + sim_params=SimParamCfg( + dt=0.005, + ), + decimation=4, + ) + max_episode_steps = 200 + + def _get_initial_states(self) -> list[dict] | None: + """Get initial states for all environments. + + Uses saved poses from object_layout.py. Loads banana, table, basket, and trajectory markers + from saved_poses_20251206_banana_basket.py. + """ + # Add path to saved poses + saved_poses_path = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))), + "get_started", + "output", + "saved_poses_20251206_banana_basket.py", + ) + if os.path.exists(saved_poses_path): + # Load saved poses dynamically + spec = importlib.util.spec_from_file_location("saved_poses", saved_poses_path) + saved_poses_module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(saved_poses_module) + saved_poses = saved_poses_module.poses + else: + # Fallback to default poses if saved file not found + log.warning(f"Saved poses file not found at {saved_poses_path}, using default poses") + saved_poses = None + + if saved_poses is not None: + # Use saved poses from object_layout.py + init = [] + for _ in range(self.num_envs): + env_state = { + "objects": { + # Banana as the object to pick + "object": saved_poses["objects"].get("banana", saved_poses["objects"].get("object")), + "table": saved_poses["objects"]["table"], + "lamp": saved_poses["objects"].get("lamp"), + "basket": saved_poses["objects"].get("basket"), + "bowl": saved_poses["objects"].get("bowl"), + "cutting_tools": saved_poses["objects"].get("cutting_tools"), + "spoon": saved_poses["objects"].get("spoon"), + # Include trajectory markers if present + "traj_marker_0": saved_poses["objects"].get("traj_marker_0"), + "traj_marker_1": saved_poses["objects"].get("traj_marker_1"), + "traj_marker_2": saved_poses["objects"].get("traj_marker_2"), + "traj_marker_3": saved_poses["objects"].get("traj_marker_3"), + "traj_marker_4": saved_poses["objects"].get("traj_marker_4"), + }, + "robots": { + "franka": saved_poses["robots"]["franka"], + }, + } + # Remove None values + env_state["objects"] = {k: v for k, v in env_state["objects"].items() if v is not None} + init.append(env_state) + else: + # Default poses (fallback) - using poses from original approach_grasp_banana.py + init = [ + { + "objects": { + "table": { + "pos": torch.tensor([0.400000, -0.200000, 0.400000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "lamp": { + "pos": torch.tensor([0.610000, 0.200000, 1.050000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "basket": { + "pos": torch.tensor([0.610000, -0.300000, 0.825000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "bowl": { + "pos": torch.tensor([0.350000, 0.250000, 0.863000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "cutting_tools": { + "pos": torch.tensor([0.180000, -0.070000, 0.820000]), + "rot": torch.tensor([0.930507, 0.000000, -0.000000, 0.366273]), + }, + "spoon": { + "pos": torch.tensor([0.530000, -0.690000, 0.850000]), + "rot": torch.tensor([0.961352, -0.120799, 0.030845, 0.245473]), + }, + "object": { + "pos": torch.tensor([0.280000, -0.580000, 0.825000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "traj_marker_0": { + "pos": torch.tensor([0.280000, -0.540000, 0.850000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "traj_marker_1": { + "pos": torch.tensor([0.320000, -0.490000, 0.910000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "traj_marker_2": { + "pos": torch.tensor([0.330000, -0.430000, 1.110000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "traj_marker_3": { + "pos": torch.tensor([0.360000, -0.350000, 1.210000]), + "rot": torch.tensor([0.601833, 0.798621, 0.000000, -0.000000]), + }, + "traj_marker_4": { + "pos": torch.tensor([0.600000, -0.310000, 1.190000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + }, + "robots": { + "franka": { + "pos": torch.tensor([0.800000, -0.800000, 0.780000]), + "rot": torch.tensor([0.581682, -0.000000, -0.000001, 0.813415]), + "dof_pos": { + "panda_finger_joint1": 0.040000, + "panda_finger_joint2": 0.040000, + "panda_joint1": 0.000000, + "panda_joint2": -0.785398, + "panda_joint3": 0.000000, + "panda_joint4": -2.356194, + "panda_joint5": 0.000000, + "panda_joint6": 1.570796, + "panda_joint7": 0.785398, + }, + }, + }, + } + for _ in range(self.num_envs) + ] + + return init diff --git a/roboverse_pack/tasks/pick_place/approach_grasp_bowl.py b/roboverse_pack/tasks/pick_place/approach_grasp_bowl.py new file mode 100644 index 000000000..e69de29bb diff --git a/roboverse_pack/tasks/pick_place/approach_grasp_cuttingtool.py b/roboverse_pack/tasks/pick_place/approach_grasp_cuttingtool.py new file mode 100644 index 000000000..e69de29bb diff --git a/roboverse_pack/tasks/pick_place/approach_grasp_screwdriver.py b/roboverse_pack/tasks/pick_place/approach_grasp_screwdriver.py new file mode 100644 index 000000000..a5b8def81 --- /dev/null +++ b/roboverse_pack/tasks/pick_place/approach_grasp_screwdriver.py @@ -0,0 +1,285 @@ +"""Stage 1: Simple Approach and Grasp task for screwdriver object. + +This task inherits from PickPlaceApproachGraspSimple and customizes it for the screwdriver object +with specific mesh configurations and saved poses from object_layout.py. +""" + +from __future__ import annotations + +import importlib.util +import os + +import torch +from loguru import logger as log + +from metasim.constants import PhysicStateType +from metasim.scenario.objects import RigidObjCfg +from metasim.scenario.scenario import ScenarioCfg, SimParamCfg +from metasim.task.registry import register_task +from roboverse_pack.tasks.pick_place.approach_grasp import PickPlaceApproachGraspSimple + + +@register_task("pick_place.approach_grasp_simple_screwdriver", "pick_place_approach_grasp_simple_screwdriver") +class PickPlaceApproachGraspSimpleScrewDriver(PickPlaceApproachGraspSimple): + """Simple Approach and Grasp task for screwdriver object. + + This task inherits from PickPlaceApproachGraspSimple and customizes: + - Scenario: Uses screwdriver mesh, table mesh, and basket from EmbodiedGenData + - Initial states: Loads poses from saved_poses_20251206_screwdriver_basket.py + """ + + scenario = ScenarioCfg( + objects=[ + RigidObjCfg( + name="table", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/demo_assets/table/usd/table.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/table/result/table.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/table/mjcf/table.xml", + ), + RigidObjCfg( + name="lamp", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/new_assets/lighting_fixtures/1/usd/0a4489b1a2875c82a580f8b62d346e08.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/new_assets/lighting_fixtures/1/0a4489b1a2875c82a580f8b62d346e08.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/new_assets/lighting_fixtures/1/mjcf/0a4489b1a2875c82a580f8b62d346e08.xml", + ), + RigidObjCfg( + name="basket", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/new_assets/basket/1/usd/663158968e3f5900af1f6e7cecef24c7.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/new_assets/basket/1/663158968e3f5900af1f6e7cecef24c7.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/new_assets/basket/1/mjcf/663158968e3f5900af1f6e7cecef24c7.xml", + ), + RigidObjCfg( + name="bowl", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/new_assets/bowl/1/usd/0f296af3df66565c9e1a7c2bc7b35d72.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/new_assets/bowl/1/0f296af3df66565c9e1a7c2bc7b35d72.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/new_assets/bowl/1/mjcf/0f296af3df66565c9e1a7c2bc7b35d72.xml", + ), + RigidObjCfg( + name="cutting_tools", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/new_assets/cutting_tools/1/usd/c5810e7c2c785fe3940372b205090bad.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/new_assets/cutting_tools/1/c5810e7c2c785fe3940372b205090bad.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/new_assets/cutting_tools/1/mjcf/c5810e7c2c785fe3940372b205090bad.xml", + ), + # Use actual screwdriver mesh from EmbodiedGenData (matches object_layout.py) + RigidObjCfg( + name="object", + scale=(1.5, 1.5, 1.5), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/new_assets/screwdriver/1/usd/ae51f060e3455e9f84a4fec81cc9284b.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/new_assets/screwdriver/1/ae51f060e3455e9f84a4fec81cc9284b.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/new_assets/screwdriver/1/mjcf/ae51f060e3455e9f84a4fec81cc9284b.xml", + ), + RigidObjCfg( + name="spoon", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/new_assets/spoon/1/usd/2f1c3077a8d954e58fc0bf75cf35e849.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/new_assets/spoon/1/2f1c3077a8d954e58fc0bf75cf35e849.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/new_assets/spoon/1/mjcf/2f1c3077a8d954e58fc0bf75cf35e849.xml", + ), + # Visualization: Trajectory waypoints (5 spheres showing trajectory path) + RigidObjCfg( + name="traj_marker_0", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_1", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_2", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_3", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_4", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + ], + robots=["franka"], + sim_params=SimParamCfg( + dt=0.005, + ), + decimation=4, + ) + max_episode_steps = 200 + + def _get_initial_states(self) -> list[dict] | None: + """Get initial states for all environments. + + Uses saved poses from object_layout.py. Loads screwdriver, table, basket, and trajectory markers + from saved_poses_20251206_screwdriver_basket.py. + """ + # Add path to saved poses + saved_poses_path = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))), + "get_started", + "output", + "saved_poses_20251206_screwdriver_basket.py", + ) + if os.path.exists(saved_poses_path): + # Load saved poses dynamically + spec = importlib.util.spec_from_file_location("saved_poses", saved_poses_path) + saved_poses_module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(saved_poses_module) + saved_poses = saved_poses_module.poses + else: + # Fallback to default poses if saved file not found + log.warning(f"Saved poses file not found at {saved_poses_path}, using default poses") + saved_poses = None + + if saved_poses is not None: + # Use saved poses from object_layout.py + init = [] + for _ in range(self.num_envs): + env_state = { + "objects": { + # Screwdriver as the object to pick + "object": saved_poses["objects"].get( + "screwdriver", + saved_poses["objects"].get("screw_driver", saved_poses["objects"].get("object")), + ), + "table": saved_poses["objects"]["table"], + "lamp": saved_poses["objects"].get("lamp"), + "basket": saved_poses["objects"].get("basket"), + "bowl": saved_poses["objects"].get("bowl"), + "cutting_tools": saved_poses["objects"].get("cutting_tools"), + "spoon": saved_poses["objects"].get("spoon"), + # Include trajectory markers if present + "traj_marker_0": saved_poses["objects"].get("traj_marker_0"), + "traj_marker_1": saved_poses["objects"].get("traj_marker_1"), + "traj_marker_2": saved_poses["objects"].get("traj_marker_2"), + "traj_marker_3": saved_poses["objects"].get("traj_marker_3"), + "traj_marker_4": saved_poses["objects"].get("traj_marker_4"), + }, + "robots": { + "franka": saved_poses["robots"]["franka"], + }, + } + # Remove None values + env_state["objects"] = {k: v for k, v in env_state["objects"].items() if v is not None} + init.append(env_state) + else: + # Default poses (fallback) - using poses from original approach_grasp_screwdriver.py + init = [ + { + "objects": { + "table": { + "pos": torch.tensor([0.400000, -0.200000, 0.400000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "lamp": { + "pos": torch.tensor([0.680000, 0.310000, 1.050000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "basket": { + "pos": torch.tensor([0.240000, -0.440000, 0.925000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "bowl": { + "pos": torch.tensor([0.620000, -0.080000, 0.863000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "cutting_tools": { + "pos": torch.tensor([0.230000, 0.090000, 0.820000]), + "rot": torch.tensor([0.930507, 0.000000, -0.000000, 0.366273]), + }, + "object": { + "pos": torch.tensor([0.590000, -0.360000, 0.811000]), + "rot": torch.tensor([-0.824810, -0.390085, -0.023230, 0.408623]), + }, + "spoon": { + "pos": torch.tensor([0.470000, -0.710000, 0.830000]), + "rot": torch.tensor([0.928125, -0.061844, -0.058501, -0.362397]), + }, + "traj_marker_0": { + "pos": torch.tensor([0.600000, -0.330000, 0.840000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "traj_marker_1": { + "pos": torch.tensor([0.560000, -0.400000, 1.010000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "traj_marker_2": { + "pos": torch.tensor([0.530000, -0.400000, 1.140000]), + "rot": torch.tensor([0.962425, -0.271547, 0.000000, 0.000000]), + }, + "traj_marker_3": { + "pos": torch.tensor([0.430000, -0.340000, 1.160000]), + "rot": torch.tensor([0.601833, 0.798621, 0.000000, -0.000000]), + }, + "traj_marker_4": { + "pos": torch.tensor([0.260000, -0.400000, 1.170000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + }, + "robots": { + "franka": { + "pos": torch.tensor([0.800000, -0.800000, 0.780000]), + "rot": torch.tensor([0.581682, -0.000000, -0.000001, 0.813414]), + "dof_pos": { + "panda_finger_joint1": 0.040000, + "panda_finger_joint2": 0.040000, + "panda_joint1": 0.000000, + "panda_joint2": -0.785398, + "panda_joint3": 0.000000, + "panda_joint4": -2.356194, + "panda_joint5": 0.000000, + "panda_joint6": 1.570796, + "panda_joint7": 0.785398, + }, + }, + }, + } + for _ in range(self.num_envs) + ] + + return init diff --git a/roboverse_pack/tasks/pick_place/approach_grasp_spoon.py b/roboverse_pack/tasks/pick_place/approach_grasp_spoon.py new file mode 100644 index 000000000..ee9f42ced --- /dev/null +++ b/roboverse_pack/tasks/pick_place/approach_grasp_spoon.py @@ -0,0 +1,418 @@ +"""Stage 1: Simple Approach and Grasp task for spoon object. + +This task inherits from PickPlaceApproachGraspSimple and customizes it for the spoon object +with specific mesh configurations and saved poses from saved_poses_20251212_spoon.py. +""" + +from __future__ import annotations + +import importlib.util +import os + +import torch +from loguru import logger as log + +from metasim.constants import PhysicStateType +from metasim.scenario.objects import RigidObjCfg +from metasim.scenario.scenario import ScenarioCfg, SimParamCfg +from metasim.task.registry import register_task +from roboverse_pack.tasks.pick_place.approach_grasp import PickPlaceApproachGraspSimple + + +@register_task("pick_place.approach_grasp_simple_spoon", "pick_place_approach_grasp_simple_spoon") +class PickPlaceApproachGraspSimpleSpoon(PickPlaceApproachGraspSimple): + """Simple Approach and Grasp task for spoon object. + + This task inherits from PickPlaceApproachGraspSimple and customizes: + - Scenario: Uses spoon mesh, table mesh, and basket from EmbodiedGenData + - Initial states: Loads poses from saved_poses_20251212_spoon.py + """ + + scenario = ScenarioCfg( + objects=[ + RigidObjCfg( + name="table", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/demo_assets/table/usd/table.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/table/result/table.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/table/mjcf/table.xml", + ), + RigidObjCfg( + name="basket", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/new_assets/basket/1/usd/663158968e3f5900af1f6e7cecef24c7.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/new_assets/basket/1/663158968e3f5900af1f6e7cecef24c7.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/new_assets/basket/1/mjcf/663158968e3f5900af1f6e7cecef24c7.xml", + ), + RigidObjCfg( + name="cutting_tools", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/new_assets/cutting_tools/1/usd/c5810e7c2c785fe3940372b205090bad.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/new_assets/cutting_tools/1/c5810e7c2c785fe3940372b205090bad.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/new_assets/cutting_tools/1/mjcf/c5810e7c2c785fe3940372b205090bad.xml", + ), + # Use actual spoon mesh from EmbodiedGenData as the object to pick + RigidObjCfg( + name="object", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/new_assets/spoon/1/usd/2f1c3077a8d954e58fc0bf75cf35e849.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/new_assets/spoon/1/2f1c3077a8d954e58fc0bf75cf35e849.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/new_assets/spoon/1/mjcf/2f1c3077a8d954e58fc0bf75cf35e849.xml", + ), + RigidObjCfg( + name="mug", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/demo_assets/mug/usd/mug.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/mug/result/mug.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/mug/mjcf/mug.xml", + ), + RigidObjCfg( + name="book", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/demo_assets/book/usd/book.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/book/result/book.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/book/mjcf/book.xml", + ), + RigidObjCfg( + name="vase", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/demo_assets/vase/usd/vase.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/vase/result/vase.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/vase/mjcf/vase.xml", + ), + # Visualization: Trajectory waypoints (5 spheres showing trajectory path) + RigidObjCfg( + name="traj_marker_0", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_1", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_2", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_3", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_4", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + ], + robots=["franka"], + sim_params=SimParamCfg( + dt=0.005, + ), + decimation=4, + ) + max_episode_steps = 200 + + def _get_initial_states(self) -> list[dict] | None: + """Get initial states for all environments. + + Uses saved poses from saved_poses_20251212_spoon.py. Loads spoon, table, basket, and trajectory markers. + """ + # Add path to saved poses + saved_poses_path = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))), + "get_started", + "output", + "saved_poses_20251212_spoon.py", + ) + if os.path.exists(saved_poses_path): + # Load saved poses dynamically + spec = importlib.util.spec_from_file_location("saved_poses", saved_poses_path) + saved_poses_module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(saved_poses_module) + saved_poses = saved_poses_module.poses + else: + # Fallback to default poses if saved file not found + log.warning(f"Saved poses file not found at {saved_poses_path}, using default poses") + saved_poses = None + + if saved_poses is not None: + # Use saved poses from saved_poses_20251212_spoon.py + init = [] + for _ in range(self.num_envs): + env_state = { + "objects": { + # Spoon as the object to pick + "object": saved_poses["objects"].get("spoon"), + "table": saved_poses["objects"].get("table"), + "basket": saved_poses["objects"].get("basket"), + "cutting_tools": saved_poses["objects"].get("cutting_tools"), + "mug": saved_poses["objects"].get("mug"), + "book": saved_poses["objects"].get("book"), + "vase": saved_poses["objects"].get("vase"), + # Include trajectory markers if present + "traj_marker_0": saved_poses["objects"].get("traj_marker_0"), + "traj_marker_1": saved_poses["objects"].get("traj_marker_1"), + "traj_marker_2": saved_poses["objects"].get("traj_marker_2"), + "traj_marker_3": saved_poses["objects"].get("traj_marker_3"), + "traj_marker_4": saved_poses["objects"].get("traj_marker_4"), + }, + "robots": { + "franka": saved_poses["robots"]["franka"], + }, + } + # Remove None values + env_state["objects"] = {k: v for k, v in env_state["objects"].items() if v is not None} + init.append(env_state) + else: + # Default poses (fallback) - using poses from saved_poses_20251212_spoon.py as hardcoded values + init = [ + { + "objects": { + "table": { + "pos": torch.tensor([0.440000, -0.200000, 0.400000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "basket": { + "pos": torch.tensor([0.210000, 0.250000, 0.825000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "cutting_tools": { + "pos": torch.tensor([0.560000, -0.730000, 0.820000]), + "rot": torch.tensor([0.930507, 0.000000, -0.000000, 0.366273]), + }, + "object": { + "pos": torch.tensor([0.200000, -0.590000, 0.820000]), + "rot": torch.tensor([-0.702982, 0.088334, -0.087982, -0.700189]), + }, + "mug": { + "pos": torch.tensor([0.690000, -0.550000, 0.863000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "book": { + "pos": torch.tensor([0.700000, 0.280000, 0.820000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "vase": { + "pos": torch.tensor([0.680000, 0.050000, 0.950000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "traj_marker_0": { + "pos": torch.tensor([0.220000, -0.550000, 0.880000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "traj_marker_1": { + "pos": torch.tensor([0.220000, -0.310000, 0.900000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "traj_marker_2": { + "pos": torch.tensor([0.210000, -0.250000, 1.080000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "traj_marker_3": { + "pos": torch.tensor([0.210000, 0.040000, 1.250000]), + "rot": torch.tensor([0.601833, 0.798621, 0.000000, -0.000000]), + }, + "traj_marker_4": { + "pos": torch.tensor([0.190000, 0.250000, 1.010000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + }, + "robots": { + "franka": { + "pos": torch.tensor([0.560000, -0.230001, 0.800000]), + "rot": torch.tensor([0.120502, -0.000001, -0.000001, 0.992712]), + "dof_pos": { + "panda_finger_joint1": 0.040000, + "panda_finger_joint2": 0.040000, + "panda_joint1": 0.000000, + "panda_joint2": -0.785398, + "panda_joint3": 0.000000, + "panda_joint4": -2.356194, + "panda_joint5": 0.000000, + "panda_joint6": 1.570796, + "panda_joint7": 0.785398, + }, + }, + }, + } + for _ in range(self.num_envs) + ] + + return init + + +@register_task("pick_place.approach_grasp_simple_spoon2", "pick_place_approach_grasp_simple_spoon2") +class PickPlaceApproachGraspSimpleSpoon2(PickPlaceApproachGraspSimpleSpoon): + """Simple Approach and Grasp task for spoon object - Scene 2. + + This task inherits from PickPlaceApproachGraspSimpleSpoon and uses poses from + saved_poses_20251212_spoon2.py for a different scene layout. + """ + + def _get_initial_states(self) -> list[dict] | None: + """Get initial states for all environments. + + Uses saved poses from saved_poses_20251212_spoon2.py. Loads spoon, table, basket, and trajectory markers. + """ + # Add path to saved poses + saved_poses_path = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))), + "get_started", + "output", + "saved_poses_20251212_spoon2.py", + ) + if os.path.exists(saved_poses_path): + # Load saved poses dynamically + spec = importlib.util.spec_from_file_location("saved_poses", saved_poses_path) + saved_poses_module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(saved_poses_module) + saved_poses = saved_poses_module.poses + else: + # Fallback to default poses if saved file not found + log.warning(f"Saved poses file not found at {saved_poses_path}, using default poses") + saved_poses = None + + if saved_poses is not None: + # Use saved poses from saved_poses_20251212_spoon2.py + init = [] + for _ in range(self.num_envs): + env_state = { + "objects": { + # Spoon as the object to pick + "object": saved_poses["objects"].get("spoon"), + "table": saved_poses["objects"].get("table"), + "basket": saved_poses["objects"].get("basket"), + "cutting_tools": saved_poses["objects"].get("cutting_tools"), + "mug": saved_poses["objects"].get("mug"), + "book": saved_poses["objects"].get("book"), + "vase": saved_poses["objects"].get("vase"), + # Include trajectory markers if present + "traj_marker_0": saved_poses["objects"].get("traj_marker_0"), + "traj_marker_1": saved_poses["objects"].get("traj_marker_1"), + "traj_marker_2": saved_poses["objects"].get("traj_marker_2"), + "traj_marker_3": saved_poses["objects"].get("traj_marker_3"), + "traj_marker_4": saved_poses["objects"].get("traj_marker_4"), + }, + "robots": { + "franka": saved_poses["robots"]["franka"], + }, + } + # Remove None values + env_state["objects"] = {k: v for k, v in env_state["objects"].items() if v is not None} + init.append(env_state) + else: + # Default poses (fallback) - using poses from saved_poses_20251212_spoon2.py as hardcoded values + init = [ + { + "objects": { + "table": { + "pos": torch.tensor([0.440000, -0.200000, 0.400000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "basket": { + "pos": torch.tensor([0.640000, -0.620000, 0.825000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "cutting_tools": { + "pos": torch.tensor([0.290000, 0.370000, 0.820000]), + "rot": torch.tensor([0.640996, -0.000000, -0.000000, 0.767544]), + }, + "object": { + "pos": torch.tensor([0.140000, -0.370000, 0.820000]), + "rot": torch.tensor([-0.702982, 0.088334, -0.087982, -0.700189]), + }, + "mug": { + "pos": torch.tensor([0.520000, 0.250000, 0.863000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "book": { + "pos": torch.tensor([0.240000, 0.160000, 0.820000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "vase": { + "pos": torch.tensor([0.680000, 0.270000, 0.950000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "traj_marker_0": { + "pos": torch.tensor([0.150000, -0.350000, 0.860000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "traj_marker_1": { + "pos": torch.tensor([0.240000, -0.460000, 0.940000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "traj_marker_2": { + "pos": torch.tensor([0.270000, -0.610000, 1.080000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + "traj_marker_3": { + "pos": torch.tensor([0.380000, -0.640000, 1.310000]), + "rot": torch.tensor([0.601833, 0.798621, 0.000000, -0.000000]), + }, + "traj_marker_4": { + "pos": torch.tensor([0.670000, -0.620000, 1.020000]), + "rot": torch.tensor([1.000000, 0.000000, 0.000000, 0.000000]), + }, + }, + "robots": { + "franka": { + "pos": torch.tensor([0.550000, -0.120000, 0.800000]), + "rot": torch.tensor([-0.393287, -0.000001, -0.000000, 0.919414]), + "dof_pos": { + "panda_finger_joint1": 0.040000, + "panda_finger_joint2": 0.040000, + "panda_joint1": 0.000000, + "panda_joint2": -0.785398, + "panda_joint3": 0.000000, + "panda_joint4": -2.356194, + "panda_joint5": 0.000000, + "panda_joint6": 1.570796, + "panda_joint7": 0.785398, + }, + }, + }, + } + for _ in range(self.num_envs) + ] + + return init diff --git a/roboverse_pack/tasks/pick_place/base.py b/roboverse_pack/tasks/pick_place/base.py index 46e41be62..ba472f47f 100644 --- a/roboverse_pack/tasks/pick_place/base.py +++ b/roboverse_pack/tasks/pick_place/base.py @@ -336,6 +336,21 @@ def _reward_trajectory_tracking(self, env_states) -> torch.Tensor: target_pos = self.waypoint_positions[self.current_waypoint_idx] distance = torch.norm(ee_pos - target_pos, dim=-1) + # Progress reward: reward for reducing distance to waypoint + # This encourages the agent to make progress toward the current waypoint + # Only reward progress if we haven't reached this waypoint yet + not_already_reached = ~self.waypoints_reached[ + torch.arange(self.num_envs, device=self.device), self.current_waypoint_idx + ] + distance_reduction = self.prev_distance_to_waypoint - distance + # Scale progress reward: 0.1 means progress reward is 10% of waypoint reached bonus + # This provides continuous guidance without overwhelming the sparse reward + progress_reward_component = ( + torch.clamp(distance_reduction * self.w_tracking_progress * 0.1, min=0.0) + * not_already_reached.float() + * grasped_mask.float() + ) + # Distance-based reward (far + near) / 2 distance_reward_far = 1 - torch.tanh(1.0 * distance) distance_reward_near = 1 - torch.tanh(10.0 * distance) @@ -358,10 +373,13 @@ def _reward_trajectory_tracking(self, env_states) -> torch.Tensor: # Both distance and rotation must be satisfied to consider as reached reached = distance_reached & rotation_reached - newly_reached = reached & ( - ~self.waypoints_reached[torch.arange(self.num_envs, device=self.device), self.current_waypoint_idx] - ) - progress_reward = newly_reached.float() * self.w_tracking_progress + newly_reached = reached & not_already_reached + # Waypoint reached bonus (one-time large reward when reaching a waypoint) + waypoint_reached_bonus = newly_reached.float() * self.w_tracking_progress + + # Update prev_distance for next step (only if not newly reached, + # because if newly reached, we'll update it when advancing to next waypoint) + self.prev_distance_to_waypoint[~newly_reached] = distance[~newly_reached] if newly_reached.any(): if newly_reached[0]: @@ -405,7 +423,10 @@ def _reward_trajectory_tracking(self, env_states) -> torch.Tensor: (1 - torch.tanh(1.0 * distance_to_last[completed_mask])) * self.w_tracking_approach, ) - tracking_reward = torch.where(all_reached, maintain_reward, approach_reward + progress_reward) + # Combine all reward components: approach reward + progress reward + waypoint reached bonus + tracking_reward = torch.where( + all_reached, maintain_reward, approach_reward + progress_reward_component + waypoint_reached_bonus + ) return tracking_reward diff --git a/roboverse_pack/tasks/pick_place/track.py b/roboverse_pack/tasks/pick_place/track_banana.py similarity index 75% rename from roboverse_pack/tasks/pick_place/track.py rename to roboverse_pack/tasks/pick_place/track_banana.py index 0fb2e33a8..ea36a8e83 100644 --- a/roboverse_pack/tasks/pick_place/track.py +++ b/roboverse_pack/tasks/pick_place/track_banana.py @@ -9,13 +9,15 @@ import os import pickle from copy import deepcopy +from pathlib import Path import numpy as np import torch +import yaml from loguru import logger as log from metasim.constants import PhysicStateType -from metasim.scenario.objects import PrimitiveCubeCfg, RigidObjCfg +from metasim.scenario.objects import RigidObjCfg from metasim.scenario.scenario import ScenarioCfg, SimParamCfg from metasim.task.registry import register_task from roboverse_pack.tasks.pick_place.base import DEFAULT_CONFIG, PickPlaceBase @@ -129,7 +131,7 @@ def convert_state_dict_to_initial_state(state_dict: dict, device: torch.device, "tracking_progress": 150.0, "rotation_tracking": 2.0, }) -# 移除不需要的奖励 +# Remove unused rewards DEFAULT_CONFIG_TRACK["reward_config"]["scales"].pop("gripper_approach", None) DEFAULT_CONFIG_TRACK["reward_config"]["scales"].pop("gripper_close", None) # Disable randomization for exact state reproduction @@ -138,8 +140,8 @@ def convert_state_dict_to_initial_state(state_dict: dict, device: torch.device, DEFAULT_CONFIG_TRACK["randomization"]["joint_noise_range"] = 0.0 -@register_task("pick_place.track", "pick_place_track") -class PickPlaceTrack(PickPlaceBase): +@register_task("pick_place.track_banana", "pick_place_track_banana") +class PickPlaceTrackBanana(PickPlaceBase): """Trajectory tracking task from grasp states. Assumes object is already grasped, only learns trajectory following. @@ -148,22 +150,63 @@ class PickPlaceTrack(PickPlaceBase): scenario = ScenarioCfg( objects=[ - PrimitiveCubeCfg( + RigidObjCfg( + name="table", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/demo_assets/table/usd/table.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/table/result/table.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/table/mjcf/table.xml", + ), + RigidObjCfg( + name="lamp", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/new_assets/lighting_fixtures/1/usd/0a4489b1a2875c82a580f8b62d346e08.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/new_assets/lighting_fixtures/1/0a4489b1a2875c82a580f8b62d346e08.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/new_assets/lighting_fixtures/1/mjcf/0a4489b1a2875c82a580f8b62d346e08.xml", + ), + RigidObjCfg( + name="basket", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/new_assets/basket/1/usd/663158968e3f5900af1f6e7cecef24c7.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/new_assets/basket/1/663158968e3f5900af1f6e7cecef24c7.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/new_assets/basket/1/mjcf/663158968e3f5900af1f6e7cecef24c7.xml", + ), + RigidObjCfg( + name="bowl", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/new_assets/bowl/1/usd/0f296af3df66565c9e1a7c2bc7b35d72.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/new_assets/bowl/1/0f296af3df66565c9e1a7c2bc7b35d72.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/new_assets/bowl/1/mjcf/0f296af3df66565c9e1a7c2bc7b35d72.xml", + ), + RigidObjCfg( name="object", - size=(0.04, 0.04, 0.04), - mass=0.02, + scale=(1, 1, 1), physics=PhysicStateType.RIGIDBODY, - color=(1.0, 0.0, 0.0), + usd_path="roboverse_data/assets/EmbodiedGenData/demo_assets/banana/usd/banana.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/banana/result/banana.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/banana/mjcf/banana.xml", ), - PrimitiveCubeCfg( - name="table", - size=(0.2, 0.3, 0.4), - mass=10.0, + RigidObjCfg( + name="screw_driver", + scale=(1.5, 1.5, 1.5), physics=PhysicStateType.RIGIDBODY, - color=(0.8, 0.6, 0.4), - fix_base_link=True, + usd_path="roboverse_data/assets/EmbodiedGenData/new_assets/screwdriver/1/usd/ae51f060e3455e9f84a4fec81cc9284b.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/new_assets/screwdriver/1/ae51f060e3455e9f84a4fec81cc9284b.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/new_assets/screwdriver/1/mjcf/ae51f060e3455e9f84a4fec81cc9284b.xml", + ), + RigidObjCfg( + name="spoon", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/new_assets/spoon/1/usd/2f1c3077a8d954e58fc0bf75cf35e849.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/new_assets/spoon/1/2f1c3077a8d954e58fc0bf75cf35e849.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/new_assets/spoon/1/mjcf/2f1c3077a8d954e58fc0bf75cf35e849.xml", ), - # Trajectory waypoint markers + # Visualization: Trajectory waypoints (5 spheres showing trajectory path) RigidObjCfg( name="traj_marker_0", urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", @@ -229,9 +272,23 @@ class PickPlaceTrack(PickPlaceBase): max_episode_steps = 200 def __init__(self, scenario, device=None): - self.state_file_path = ( - "eval_states/pick_place.approach_grasp_simple_franka_lift_states_100states_20251126_170312.pkl" - ) + # Start from current file and walk upward to find RoboVerse root + root = Path(__file__).resolve() + while root != root.parent: + if (root / "roboverse_learn").exists(): + roboverseroot = root + break + root = root.parent + else: + raise RuntimeError("Could not locate RoboVerse root directory") + + # Now construct full path to the YAML + config_path = roboverseroot / "roboverse_learn" / "rl" / "fast_td3" / "configs" / "track_banana.yaml" + + with open(config_path) as f: + cfg = yaml.safe_load(f) + + self.state_file_path = cfg["state_file_path"] self._loaded_states = None if device is None: @@ -354,10 +411,17 @@ def reset(self, env_ids=None): def step(self, actions): """Step with delta control, keeping gripper closed.""" + current_joint_pos = self.handler.get_states(mode="tensor").robots[self.robot_name].joint_pos delta_actions = actions * self._action_scale - new_actions = self._last_action + delta_actions + new_actions = current_joint_pos + delta_actions real_actions = torch.clamp(new_actions, self._action_low, self._action_high) + # delta_actions = actions * self._action_scale + # new_actions = self._last_action + delta_actions + + # delta_actions = actions * self._action_scale + # new_actions = self._last_action + delta_actions + gripper_value_closed = torch.tensor(0.0, device=self.device, dtype=real_actions.dtype) real_actions[:, 0] = gripper_value_closed real_actions[:, 1] = gripper_value_closed diff --git a/roboverse_pack/tasks/pick_place/track_screwdriver.py b/roboverse_pack/tasks/pick_place/track_screwdriver.py new file mode 100644 index 000000000..543aaafb0 --- /dev/null +++ b/roboverse_pack/tasks/pick_place/track_screwdriver.py @@ -0,0 +1,446 @@ +"""Stage 3: Track task for trajectory tracking. + +Trains trajectory tracking from saved grasp states. +Object is already grasped, only needs to learn trajectory following. +""" + +from __future__ import annotations + +import os +import pickle +from copy import deepcopy +from pathlib import Path + +import numpy as np +import torch +import yaml +from loguru import logger as log + +from metasim.constants import PhysicStateType +from metasim.scenario.objects import RigidObjCfg +from metasim.scenario.scenario import ScenarioCfg, SimParamCfg +from metasim.task.registry import register_task +from roboverse_pack.tasks.pick_place.base import DEFAULT_CONFIG, PickPlaceBase + + +def load_states_from_pkl(pkl_path: str): + """Load state list from pkl file.""" + if not os.path.exists(pkl_path): + raise FileNotFoundError(f"State file not found: {pkl_path}") + + with open(pkl_path, "rb") as f: + states_list = pickle.load(f) + + log.info(f"Loaded {len(states_list)} states from {pkl_path}") + return states_list + + +def convert_state_dict_to_initial_state(state_dict: dict, device: torch.device, robot_name: str = "franka") -> dict: + """Convert state dict to initial state format.""" + initial_state = { + "objects": {}, + "robots": {}, + } + + if "objects" in state_dict and "robots" in state_dict: + for obj_name, obj_state in state_dict["objects"].items(): + pos = obj_state.get("pos") + rot = obj_state.get("rot") + + if isinstance(pos, (list, tuple, np.ndarray)): + pos = torch.tensor(pos, device=device, dtype=torch.float32) + elif isinstance(pos, torch.Tensor): + pos = pos.to(device).float() + + if isinstance(rot, (list, tuple, np.ndarray)): + rot = torch.tensor(rot, device=device, dtype=torch.float32) + elif isinstance(rot, torch.Tensor): + rot = rot.to(device).float() + + initial_state["objects"][obj_name] = { + "pos": pos, + "rot": rot, + } + + if "dof_pos" in obj_state: + initial_state["objects"][obj_name]["dof_pos"] = obj_state["dof_pos"] + + for robot_name_key, robot_state in state_dict["robots"].items(): + pos = robot_state.get("pos") + rot = robot_state.get("rot") + + if isinstance(pos, (list, tuple, np.ndarray)): + pos = torch.tensor(pos, device=device, dtype=torch.float32) + elif isinstance(pos, torch.Tensor): + pos = pos.to(device).float() + + if isinstance(rot, (list, tuple, np.ndarray)): + rot = torch.tensor(rot, device=device, dtype=torch.float32) + elif isinstance(rot, torch.Tensor): + rot = rot.to(device).float() + + initial_state["robots"][robot_name_key] = { + "pos": pos, + "rot": rot, + } + + if "dof_pos" in robot_state: + initial_state["robots"][robot_name_key]["dof_pos"] = robot_state["dof_pos"] + else: + # Flat format: convert to nested + for name, entity_state in state_dict.items(): + if name in ["objects", "robots"]: + continue + + pos = entity_state.get("pos") + rot = entity_state.get("rot") + + if isinstance(pos, (list, tuple, np.ndarray)): + pos = torch.tensor(pos, device=device, dtype=torch.float32) + elif isinstance(pos, torch.Tensor): + pos = pos.to(device).float() + elif isinstance(pos, np.ndarray): + pos = torch.from_numpy(pos).to(device).float() + + if isinstance(rot, (list, tuple, np.ndarray)): + rot = torch.tensor(rot, device=device, dtype=torch.float32) + elif isinstance(rot, torch.Tensor): + rot = rot.to(device).float() + elif isinstance(rot, np.ndarray): + rot = torch.from_numpy(rot).to(device).float() + + entity_entry = { + "pos": pos, + "rot": rot, + } + + if "dof_pos" in entity_state: + entity_entry["dof_pos"] = entity_state["dof_pos"] + + if name == robot_name: + initial_state["robots"][name] = entity_entry + else: + initial_state["objects"][name] = entity_entry + + return initial_state + + +DEFAULT_CONFIG_TRACK = deepcopy(DEFAULT_CONFIG) +DEFAULT_CONFIG_TRACK["reward_config"]["scales"].update({ + "tracking_approach": 4.0, + "tracking_progress": 150.0, + "rotation_tracking": 2.0, +}) +# Remove unused rewards +DEFAULT_CONFIG_TRACK["reward_config"]["scales"].pop("gripper_approach", None) +DEFAULT_CONFIG_TRACK["reward_config"]["scales"].pop("gripper_close", None) +# Disable randomization for exact state reproduction +DEFAULT_CONFIG_TRACK["randomization"]["box_pos_range"] = 0.0 +DEFAULT_CONFIG_TRACK["randomization"]["robot_pos_noise"] = 0.0 +DEFAULT_CONFIG_TRACK["randomization"]["joint_noise_range"] = 0.0 + + +@register_task("pick_place.track_screwdriver", "pick_place_track_screwdriver") +class PickPlaceTrackScrewDriver(PickPlaceBase): + """Trajectory tracking task from grasp states. + + Assumes object is already grasped, only learns trajectory following. + Initial states loaded from pkl file. + """ + + scenario = ScenarioCfg( + objects=[ + RigidObjCfg( + name="table", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/demo_assets/table/usd/table.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/table/result/table.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/table/mjcf/table.xml", + ), + RigidObjCfg( + name="lamp", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/new_assets/lighting_fixtures/1/usd/0a4489b1a2875c82a580f8b62d346e08.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/new_assets/lighting_fixtures/1/0a4489b1a2875c82a580f8b62d346e08.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/new_assets/lighting_fixtures/1/mjcf/0a4489b1a2875c82a580f8b62d346e08.xml", + ), + RigidObjCfg( + name="basket", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/new_assets/basket/1/usd/663158968e3f5900af1f6e7cecef24c7.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/new_assets/basket/1/663158968e3f5900af1f6e7cecef24c7.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/new_assets/basket/1/mjcf/663158968e3f5900af1f6e7cecef24c7.xml", + ), + RigidObjCfg( + name="bowl", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/new_assets/bowl/1/usd/0f296af3df66565c9e1a7c2bc7b35d72.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/new_assets/bowl/1/0f296af3df66565c9e1a7c2bc7b35d72.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/new_assets/bowl/1/mjcf/0f296af3df66565c9e1a7c2bc7b35d72.xml", + ), + RigidObjCfg( + name="cutting_tools", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/new_assets/cutting_tools/1/usd/c5810e7c2c785fe3940372b205090bad.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/new_assets/cutting_tools/1/c5810e7c2c785fe3940372b205090bad.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/new_assets/cutting_tools/1/mjcf/c5810e7c2c785fe3940372b205090bad.xml", + ), + RigidObjCfg( + name="object", + scale=(1.5, 1.5, 1.5), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/new_assets/screwdriver/1/usd/ae51f060e3455e9f84a4fec81cc9284b.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/new_assets/screwdriver/1/ae51f060e3455e9f84a4fec81cc9284b.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/new_assets/screwdriver/1/mjcf/ae51f060e3455e9f84a4fec81cc9284b.xml", + ), + RigidObjCfg( + name="spoon", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/new_assets/spoon/1/usd/2f1c3077a8d954e58fc0bf75cf35e849.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/new_assets/spoon/1/2f1c3077a8d954e58fc0bf75cf35e849.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/new_assets/spoon/1/mjcf/2f1c3077a8d954e58fc0bf75cf35e849.xml", + ), + # Visualization: Trajectory waypoints (5 spheres showing trajectory path) + RigidObjCfg( + name="traj_marker_0", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_1", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_2", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_3", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_4", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + ], + robots=["franka"], + sim_params=SimParamCfg( + dt=0.005, + ), + decimation=4, + ) + max_episode_steps = 200 + + def __init__(self, scenario, device=None): + # Start from current file and walk upward to find RoboVerse root + root = Path(__file__).resolve() + while root != root.parent: + if (root / "roboverse_learn").exists(): + roboverseroot = root + break + root = root.parent + else: + raise RuntimeError("Could not locate RoboVerse root directory") + + # Now construct full path to the YAML + config_path = roboverseroot / "roboverse_learn" / "rl" / "fast_td3" / "configs" / "track_screwdriver.yaml" + + with open(config_path) as f: + cfg = yaml.safe_load(f) + + self.state_file_path = cfg["state_file_path"] + self._loaded_states = None + + if device is None: + device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + self._device = device + + self.object_grasped = None + + super().__init__(scenario, device) + + self.object_grasped = torch.ones(self.num_envs, dtype=torch.bool, device=self.device) + self.reward_functions = [ + self._reward_trajectory_tracking, + self._reward_rotation_tracking, + ] + self.reward_weights = [ + 1.0, + 1.0, # rotation_tracking weight is already applied inside the function + ] + self.grasp_check_distance = 0.25 + + def _prepare_states(self, states, env_ids): + """Override to disable randomization for track task.""" + return states + + def _get_initial_states(self) -> list[dict] | None: + """Load initial states from pkl file.""" + if self._loaded_states is not None: + return self._loaded_states + + states_list = load_states_from_pkl(self.state_file_path) + + device = getattr(self, "_device", None) or getattr(self, "device", None) + if device is None: + device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + + initial_states = [] + robot_name = "franka" + for state_dict in states_list: + initial_state = convert_state_dict_to_initial_state(state_dict, device, robot_name=robot_name) + initial_states.append(initial_state) + + if len(initial_states) < self.num_envs: + k = self.num_envs // len(initial_states) + remainder = self.num_envs % len(initial_states) + initial_states = initial_states * k + initial_states[:remainder] + + initial_states = initial_states[: self.num_envs] + + # Default waypoint positions + default_positions = [ + torch.tensor([0.610000, -0.280000, 0.150000], device=device), + torch.tensor([0.600000, -0.190000, 0.220000], device=device), + torch.tensor([0.560000, -0.110000, 0.360000], device=device), + torch.tensor([0.530000, 0.010000, 0.470000], device=device), + torch.tensor([0.510000, 0.130000, 0.460000], device=device), + ] + default_rotations = [ + torch.tensor([1.000000, 0.000000, 0.000000, 0.000000], device=device), + torch.tensor([1.000000, 0.000000, 0.000000, 0.000000], device=device), + torch.tensor([0.998750, 0.000000, 0.049979, -0.000000], device=device), + torch.tensor([1.000000, 0.000000, 0.000000, 0.000000], device=device), + torch.tensor([0.984726, 0.000000, 0.174108, -0.000000], device=device), + ] + + for env_idx, initial_state in enumerate(initial_states): + if "objects" not in initial_state: + initial_state["objects"] = {} + + for i in range(self.num_waypoints): + marker_name = f"traj_marker_{i}" + if marker_name not in initial_state["objects"]: + if i < len(default_positions): + initial_state["objects"][marker_name] = { + "pos": default_positions[i].clone(), + "rot": default_rotations[i].clone(), + } + + self._loaded_states = initial_states + log.info(f"Loaded {len(initial_states)} initial states from {self.state_file_path}") + return initial_states + + def reset(self, env_ids=None): + """Reset environment, keeping object grasped.""" + if env_ids is None or self._last_action is None: + self._last_action = self._initial_states.robots[self.robot_name].joint_pos[:, :] + else: + self._last_action[env_ids] = self._initial_states.robots[self.robot_name].joint_pos[env_ids, :] + + if env_ids is None: + env_ids_tensor = torch.arange(self.num_envs, device=self.device) + env_ids_list = list(range(self.num_envs)) + else: + env_ids_tensor = ( + torch.tensor(env_ids, device=self.device) if not isinstance(env_ids, torch.Tensor) else env_ids + ) + env_ids_list = env_ids if isinstance(env_ids, list) else list(env_ids) + + self.current_waypoint_idx[env_ids_tensor] = 0 + self.waypoints_reached[env_ids_tensor] = False + self.prev_distance_to_waypoint[env_ids_tensor] = 0.0 + + self.object_grasped[env_ids_tensor] = True + + obs, info = super(PickPlaceBase, self).reset(env_ids=env_ids) + + states = self.handler.get_states() + if env_ids is None: + env_ids_list = list(range(self.num_envs)) + else: + env_ids_list = env_ids if isinstance(env_ids, list) else list(env_ids) + + ee_pos, _ = self._get_ee_state(states) + target_pos = self.waypoint_positions[0].unsqueeze(0).expand(len(env_ids_list), -1) + self.prev_distance_to_waypoint[env_ids_list] = torch.norm(ee_pos[env_ids_list] - target_pos, dim=-1) + + info["grasp_success"] = self.object_grasped + info["stage"] = torch.full((self.num_envs,), 3, dtype=torch.long, device=self.device) + + return obs, info + + def step(self, actions): + """Step with delta control, keeping gripper closed.""" + current_joint_pos = self.handler.get_states(mode="tensor").robots[self.robot_name].joint_pos + delta_actions = actions * self._action_scale + new_actions = current_joint_pos + delta_actions + real_actions = torch.clamp(new_actions, self._action_low, self._action_high) + # delta_actions = actions * self._action_scale + # new_actions = self._last_action + delta_actions + + gripper_value_closed = torch.tensor(0.0, device=self.device, dtype=real_actions.dtype) + real_actions[:, 0] = gripper_value_closed + real_actions[:, 1] = gripper_value_closed + + obs, reward, terminated, time_out, info = super(PickPlaceBase, self).step(real_actions) + self._last_action = real_actions + + updated_states = self.handler.get_states(mode="tensor") + box_pos = updated_states.objects["object"].root_state[:, 0:3] + gripper_pos, _ = self._get_ee_state(updated_states) + gripper_box_dist = torch.norm(gripper_pos - box_pos, dim=-1) + is_grasping = gripper_box_dist < self.grasp_check_distance + + self.object_grasped = is_grasping + newly_released = ~is_grasping + + if newly_released.any() and newly_released[0]: + log.warning(f"[Env 0] Object released during tracking! Distance: {gripper_box_dist[0].item():.4f}m") + + terminated = terminated | newly_released + + info["grasp_success"] = self.object_grasped + info["stage"] = torch.full((self.num_envs,), 3, dtype=torch.long, device=self.device) + + return obs, reward, terminated, time_out, info diff --git a/roboverse_pack/tasks/pick_place/track_spoon.py b/roboverse_pack/tasks/pick_place/track_spoon.py new file mode 100644 index 000000000..855264147 --- /dev/null +++ b/roboverse_pack/tasks/pick_place/track_spoon.py @@ -0,0 +1,644 @@ +"""Stage 3: Track task for trajectory tracking with spoon object. + +This task inherits from PickPlaceBase and implements track functionality +with spoon-specific mesh configurations and saved poses from object_layout.py. +""" + +from __future__ import annotations + +import os +import pickle +from copy import deepcopy + +import numpy as np +import torch +from loguru import logger as log + +from metasim.constants import PhysicStateType +from metasim.scenario.objects import RigidObjCfg +from metasim.scenario.scenario import ScenarioCfg, SimParamCfg +from metasim.task.registry import register_task +from roboverse_pack.tasks.pick_place.base import DEFAULT_CONFIG, PickPlaceBase + + +def load_states_from_pkl(pkl_path: str): + """Load state list from pkl file.""" + if not os.path.exists(pkl_path): + raise FileNotFoundError(f"State file not found: {pkl_path}") + + with open(pkl_path, "rb") as f: + states_list = pickle.load(f) + + log.info(f"Loaded {len(states_list)} states from {pkl_path}") + return states_list + + +def convert_state_dict_to_initial_state(state_dict: dict, device: torch.device, robot_name: str = "franka") -> dict: + """Convert state dict to initial state format.""" + initial_state = { + "objects": {}, + "robots": {}, + } + + if "objects" in state_dict and "robots" in state_dict: + for obj_name, obj_state in state_dict["objects"].items(): + pos = obj_state.get("pos") + rot = obj_state.get("rot") + + if isinstance(pos, (list, tuple, np.ndarray)): + pos = torch.tensor(pos, device=device, dtype=torch.float32) + elif isinstance(pos, torch.Tensor): + pos = pos.to(device).float() + + if isinstance(rot, (list, tuple, np.ndarray)): + rot = torch.tensor(rot, device=device, dtype=torch.float32) + elif isinstance(rot, torch.Tensor): + rot = rot.to(device).float() + + initial_state["objects"][obj_name] = { + "pos": pos, + "rot": rot, + } + + if "dof_pos" in obj_state: + initial_state["objects"][obj_name]["dof_pos"] = obj_state["dof_pos"] + + for robot_name_key, robot_state in state_dict["robots"].items(): + pos = robot_state.get("pos") + rot = robot_state.get("rot") + + if isinstance(pos, (list, tuple, np.ndarray)): + pos = torch.tensor(pos, device=device, dtype=torch.float32) + elif isinstance(pos, torch.Tensor): + pos = pos.to(device).float() + + if isinstance(rot, (list, tuple, np.ndarray)): + rot = torch.tensor(rot, device=device, dtype=torch.float32) + elif isinstance(rot, torch.Tensor): + rot = rot.to(device).float() + + initial_state["robots"][robot_name_key] = { + "pos": pos, + "rot": rot, + } + + if "dof_pos" in robot_state: + initial_state["robots"][robot_name_key]["dof_pos"] = robot_state["dof_pos"] + else: + # Flat format: convert to nested + for name, entity_state in state_dict.items(): + if name in ["objects", "robots"]: + continue + + pos = entity_state.get("pos") + rot = entity_state.get("rot") + + if isinstance(pos, (list, tuple, np.ndarray)): + pos = torch.tensor(pos, device=device, dtype=torch.float32) + elif isinstance(pos, torch.Tensor): + pos = pos.to(device).float() + elif isinstance(pos, np.ndarray): + pos = torch.from_numpy(pos).to(device).float() + + if isinstance(rot, (list, tuple, np.ndarray)): + rot = torch.tensor(rot, device=device, dtype=torch.float32) + elif isinstance(rot, torch.Tensor): + rot = rot.to(device).float() + elif isinstance(rot, np.ndarray): + rot = torch.from_numpy(rot).to(device).float() + + entity_entry = { + "pos": pos, + "rot": rot, + } + + if "dof_pos" in entity_state: + entity_entry["dof_pos"] = entity_state["dof_pos"] + + if name == robot_name: + initial_state["robots"][name] = entity_entry + else: + initial_state["objects"][name] = entity_entry + + return initial_state + + +DEFAULT_CONFIG_TRACK = deepcopy(DEFAULT_CONFIG) +DEFAULT_CONFIG_TRACK["reward_config"]["scales"].update({ + "tracking_approach": 4.0, + "tracking_progress": 150.0, + "rotation_tracking": 2.0, +}) +# Remove unused rewards +DEFAULT_CONFIG_TRACK["reward_config"]["scales"].pop("gripper_approach", None) +DEFAULT_CONFIG_TRACK["reward_config"]["scales"].pop("gripper_close", None) +# Disable randomization for exact state reproduction +DEFAULT_CONFIG_TRACK["randomization"]["box_pos_range"] = 0.0 +DEFAULT_CONFIG_TRACK["randomization"]["robot_pos_noise"] = 0.0 +DEFAULT_CONFIG_TRACK["randomization"]["joint_noise_range"] = 0.0 +# Increase reach threshold for spoon (more lenient for higher waypoints) +DEFAULT_CONFIG_TRACK["trajectory_tracking"]["reach_threshold"] = 0.08 # Increased from 0.05 to 0.08m + + +@register_task("pick_place.track_spoon", "pick_place_track_spoon") +class PickPlaceTrackSpoon(PickPlaceBase): + """Trajectory tracking task for spoon object. + + This task inherits from PickPlaceTrack and customizes: + - Scenario: Uses spoon mesh, table mesh, and basket from EmbodiedGenData + - Initial states: Loads poses from saved_poses_20251206_spoon_basket.py and forces gripper closed + """ + + scenario = ScenarioCfg( + objects=[ + # Use actual spoon mesh from EmbodiedGenData (matches approach_grasp.py) + RigidObjCfg( + name="object", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/new_assets/spoon/1/usd/2f1c3077a8d954e58fc0bf75cf35e849.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/new_assets/spoon/1/2f1c3077a8d954e58fc0bf75cf35e849.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/new_assets/spoon/1/mjcf/2f1c3077a8d954e58fc0bf75cf35e849.xml", + ), + # Use actual table mesh from EmbodiedGenData (matches approach_grasp.py) + RigidObjCfg( + name="table", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/demo_assets/table/usd/table.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/table/result/table.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/demo_assets/table/mjcf/table.xml", + fix_base_link=True, + ), + # Basket for visualization (matches approach_grasp.py) + RigidObjCfg( + name="basket", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/new_assets/basket/1/usd/663158968e3f5900af1f6e7cecef24c7.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/new_assets/basket/1/663158968e3f5900af1f6e7cecef24c7.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/new_assets/basket/1/mjcf/663158968e3f5900af1f6e7cecef24c7.xml", + ), + # Trajectory waypoint markers + RigidObjCfg( + name="traj_marker_0", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_1", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_2", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_3", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_4", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + ], + robots=["franka"], + sim_params=SimParamCfg( + dt=0.005, + ), + decimation=4, + ) + max_episode_steps = 300 # Increased to allow more time to reach all waypoints + + def __init__(self, scenario, device=None, state_file_path=None): + # Use state_file_path from config if provided, otherwise use default + if state_file_path is not None: + self.state_file_path = state_file_path + else: + # Default state file path for spoon task + self.state_file_path = ( + "eval_states/pick_place.approach_grasp_simple_franka_lift_states_154states_20251207_171537.pkl" + ) + self._loaded_states = None + + if device is None: + device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + self._device = device + + self.object_grasped = None + + super().__init__(scenario, device) + + # Override reach_threshold with more lenient value for spoon task + self.reach_threshold = DEFAULT_CONFIG_TRACK["trajectory_tracking"]["reach_threshold"] + + self.object_grasped = torch.ones(self.num_envs, dtype=torch.bool, device=self.device) + self.reward_functions = [ + self._reward_trajectory_tracking, + self._reward_rotation_tracking, + ] + self.reward_weights = [ + 1.0, + 1.0, # rotation_tracking weight is already applied inside the function + ] + + def _prepare_states(self, states, env_ids): + """Override to disable randomization for track task.""" + return states + + def _get_initial_states(self) -> list[dict] | None: + """Load initial states from pkl file with spoon-specific enhancements.""" + if self._loaded_states is not None: + return self._loaded_states + + states_list = load_states_from_pkl(self.state_file_path) + + device = getattr(self, "_device", None) or getattr(self, "device", None) + if device is None: + device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + + initial_states = [] + robot_name = "franka" + for state_dict in states_list: + initial_state = convert_state_dict_to_initial_state(state_dict, device, robot_name=robot_name) + initial_states.append(initial_state) + + if len(initial_states) < self.num_envs: + k = self.num_envs // len(initial_states) + remainder = self.num_envs % len(initial_states) + initial_states = initial_states * k + initial_states[:remainder] + + initial_states = initial_states[: self.num_envs] + + # Load trajectory marker positions from saved poses file (spoon-specific enhancement) + saved_poses_path = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))), + "get_started", + "output", + "saved_poses_20251206_spoon_basket.py", + ) + + saved_traj_markers = None + saved_table = None + saved_basket = None + + if os.path.exists(saved_poses_path): + try: + import importlib.util + + spec = importlib.util.spec_from_file_location("saved_poses", saved_poses_path) + saved_poses_module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(saved_poses_module) + saved_poses = saved_poses_module.poses + + # Extract trajectory markers + saved_traj_markers = {} + for i in range(self.num_waypoints): + marker_name = f"traj_marker_{i}" + if marker_name in saved_poses["objects"]: + saved_traj_markers[marker_name] = saved_poses["objects"][marker_name] + + # Extract table and basket if present + if "table" in saved_poses["objects"]: + saved_table = saved_poses["objects"]["table"] + if "basket" in saved_poses["objects"]: + saved_basket = saved_poses["objects"]["basket"] + + log.info(f"Loaded trajectory markers from {saved_poses_path}") + except Exception as e: + log.warning(f"Failed to load saved poses from {saved_poses_path}: {e}") + + # Default waypoint positions (fallback if saved poses not available) + default_positions = [ + torch.tensor([0.610000, -0.280000, 0.150000], device=device), + torch.tensor([0.600000, -0.190000, 0.220000], device=device), + torch.tensor([0.560000, -0.110000, 0.360000], device=device), + torch.tensor([0.530000, 0.010000, 0.470000], device=device), + torch.tensor([0.510000, 0.130000, 0.460000], device=device), + ] + default_rotations = [ + torch.tensor([1.000000, 0.000000, 0.000000, 0.000000], device=device), + torch.tensor([1.000000, 0.000000, 0.000000, 0.000000], device=device), + torch.tensor([0.998750, 0.000000, 0.049979, -0.000000], device=device), + torch.tensor([1.000000, 0.000000, 0.000000, 0.000000], device=device), + torch.tensor([0.984726, 0.000000, 0.174108, -0.000000], device=device), + ] + + for env_idx, initial_state in enumerate(initial_states): + if "objects" not in initial_state: + initial_state["objects"] = {} + + # Force gripper to be fully closed for proper grasping + if "robots" in initial_state and robot_name in initial_state["robots"]: + robot_state = initial_state["robots"][robot_name] + if "dof_pos" in robot_state: + dof_pos = robot_state["dof_pos"] + # Set finger joints to 0.0 (fully closed) + if "panda_finger_joint1" in dof_pos: + dof_pos["panda_finger_joint1"] = 0.0 + if "panda_finger_joint2" in dof_pos: + dof_pos["panda_finger_joint2"] = 0.0 + log.debug( + f"[Env {env_idx}] Forced gripper closed: finger1={dof_pos.get('panda_finger_joint1', 'N/A')}, finger2={dof_pos.get('panda_finger_joint2', 'N/A')}" + ) + + # Update table position from saved poses if available + if saved_table is not None and "table" in initial_state["objects"]: + initial_state["objects"]["table"]["pos"] = ( + saved_table["pos"].to(device) + if isinstance(saved_table["pos"], torch.Tensor) + else torch.tensor(saved_table["pos"], device=device) + ) + initial_state["objects"]["table"]["rot"] = ( + saved_table["rot"].to(device) + if isinstance(saved_table["rot"], torch.Tensor) + else torch.tensor(saved_table["rot"], device=device) + ) + + # Add basket from saved poses if available + if saved_basket is not None: + basket_pos = ( + saved_basket["pos"].to(device) + if isinstance(saved_basket["pos"], torch.Tensor) + else torch.tensor(saved_basket["pos"], device=device) + ) + basket_rot = ( + saved_basket["rot"].to(device) + if isinstance(saved_basket["rot"], torch.Tensor) + else torch.tensor(saved_basket["rot"], device=device) + ) + initial_state["objects"]["basket"] = { + "pos": basket_pos, + "rot": basket_rot, + } + + # Use trajectory markers from saved poses if available, otherwise use defaults + for i in range(self.num_waypoints): + marker_name = f"traj_marker_{i}" + if marker_name not in initial_state["objects"]: + if saved_traj_markers is not None and marker_name in saved_traj_markers: + # Use saved marker position + marker_data = saved_traj_markers[marker_name] + marker_pos = ( + marker_data["pos"].to(device) + if isinstance(marker_data["pos"], torch.Tensor) + else torch.tensor(marker_data["pos"], device=device) + ) + marker_rot = ( + marker_data["rot"].to(device) + if isinstance(marker_data["rot"], torch.Tensor) + else torch.tensor(marker_data["rot"], device=device) + ) + initial_state["objects"][marker_name] = { + "pos": marker_pos, + "rot": marker_rot, + } + elif i < len(default_positions): + # Use default position + initial_state["objects"][marker_name] = { + "pos": default_positions[i].clone(), + "rot": default_rotations[i].clone(), + } + + self._loaded_states = initial_states + log.info(f"Loaded {len(initial_states)} initial states from {self.state_file_path}") + return initial_states + + def reset(self, env_ids=None): + """Reset environment, keeping object grasped.""" + if env_ids is None or self._last_action is None: + self._last_action = self._initial_states.robots[self.robot_name].joint_pos[:, :] + # Force gripper to be closed in _last_action (spoon-specific enhancement) + self._last_action[:, 0] = 0.0 # panda_finger_joint1 + self._last_action[:, 1] = 0.0 # panda_finger_joint2 + else: + self._last_action[env_ids] = self._initial_states.robots[self.robot_name].joint_pos[env_ids, :] + # Force gripper to be closed in _last_action for reset envs (spoon-specific enhancement) + self._last_action[env_ids, 0] = 0.0 # panda_finger_joint1 + self._last_action[env_ids, 1] = 0.0 # panda_finger_joint2 + + if env_ids is None: + env_ids_tensor = torch.arange(self.num_envs, device=self.device) + env_ids_list = list(range(self.num_envs)) + else: + env_ids_tensor = ( + torch.tensor(env_ids, device=self.device) if not isinstance(env_ids, torch.Tensor) else env_ids + ) + env_ids_list = env_ids if isinstance(env_ids, list) else list(env_ids) + + self.current_waypoint_idx[env_ids_tensor] = 0 + self.waypoints_reached[env_ids_tensor] = False + self.prev_distance_to_waypoint[env_ids_tensor] = 0.0 + + self.object_grasped[env_ids_tensor] = True + + obs, info = super(PickPlaceBase, self).reset(env_ids=env_ids) + + states = self.handler.get_states() + if env_ids is None: + env_ids_list = list(range(self.num_envs)) + else: + env_ids_list = env_ids if isinstance(env_ids, list) else list(env_ids) + + ee_pos, _ = self._get_ee_state(states) + target_pos = self.waypoint_positions[0].unsqueeze(0).expand(len(env_ids_list), -1) + self.prev_distance_to_waypoint[env_ids_list] = torch.norm(ee_pos[env_ids_list] - target_pos, dim=-1) + + info["grasp_success"] = self.object_grasped + info["stage"] = torch.full((self.num_envs,), 3, dtype=torch.long, device=self.device) + + return obs, info + + def step(self, actions): + """Step with delta control, keeping gripper closed.""" + current_joint_pos = self.handler.get_states().robots[self.robot_name].joint_pos + delta_actions = actions * self._action_scale + new_actions = current_joint_pos + delta_actions + + real_actions = torch.clamp(new_actions, self._action_low, self._action_high) + + gripper_value_closed = torch.tensor(0.0, device=self.device, dtype=real_actions.dtype) + real_actions[:, 0] = gripper_value_closed + real_actions[:, 1] = gripper_value_closed + + obs, reward, terminated, time_out, info = super(PickPlaceBase, self).step(real_actions) + self._last_action = real_actions + + # Enhanced grasp detection: check actual gripper joint positions + updated_states = self.handler.get_states(mode="tensor") + box_pos = updated_states.objects["object"].root_state[:, 0:3] + gripper_pos, _ = self._get_ee_state(updated_states) + gripper_box_dist = torch.norm(gripper_pos - box_pos, dim=-1) + + gripper_joint_pos = updated_states.robots[self.robot_name].joint_pos[:, :2] + gripper_actually_closed = gripper_joint_pos.mean(dim=-1) < 0.02 + + is_grasping = (gripper_box_dist < self.grasp_check_distance) & gripper_actually_closed + self.object_grasped = is_grasping + newly_released = ~is_grasping + + if newly_released.any() and newly_released[0]: + env_idx = 0 + log.warning( + f"[Env {env_idx}] Object released during tracking! " + f"Distance: {gripper_box_dist[env_idx].item():.4f}m, " + f"Gripper joint pos: {gripper_joint_pos[env_idx].cpu().numpy()}, " + f"Gripper closed: {gripper_actually_closed[env_idx].item()}" + ) + + terminated = terminated | newly_released + + info["grasp_success"] = self.object_grasped + info["stage"] = torch.full((self.num_envs,), 3, dtype=torch.long, device=self.device) + + return obs, reward, terminated, time_out, info + + def _reward_trajectory_tracking(self, env_states) -> torch.Tensor: + """Override reward calculation with increased progress reward for later waypoints. + + This addresses the issue where agent gets stuck after waypoint 2 by: + 1. Increasing progress reward multiplier (from 0.1 to 0.3) for better guidance + 2. Adding adaptive scaling for later waypoints to encourage progress + """ + ee_pos, _ = self._get_ee_state(env_states) + grasped_mask = self.object_grasped + tracking_reward = torch.zeros(self.num_envs, device=self.device) + + if grasped_mask.any(): + target_pos = self.waypoint_positions[self.current_waypoint_idx] + distance = torch.norm(ee_pos - target_pos, dim=-1) + + # Progress reward with higher multiplier for better guidance + not_already_reached = ~self.waypoints_reached[ + torch.arange(self.num_envs, device=self.device), self.current_waypoint_idx + ] + distance_reduction = self.prev_distance_to_waypoint - distance + + # Adaptive progress reward: higher for later waypoints (2, 3, 4) to encourage progress + # Waypoints 0-1: 0.2 (20%), Waypoints 2+: 0.4 (40%) to overcome larger distances + # Create multiplier tensor matching num_envs shape + progress_multiplier = torch.where( + self.current_waypoint_idx >= 2, + torch.full((self.num_envs,), 0.4, device=self.device), + torch.full((self.num_envs,), 0.2, device=self.device), + ) + + progress_reward_component = ( + torch.clamp(distance_reduction * self.w_tracking_progress * progress_multiplier, min=0.0) + * not_already_reached.float() + * grasped_mask.float() + ) + + # Distance-based reward (far + near) / 2 + distance_reward_far = 1 - torch.tanh(1.0 * distance) + distance_reward_near = 1 - torch.tanh(10.0 * distance) + approach_reward = (distance_reward_far + distance_reward_near) / 2.0 + approach_reward = approach_reward * self.w_tracking_approach * grasped_mask.float() + + # Check distance condition + distance_reached = (distance < self.reach_threshold) & grasped_mask + + # Check rotation condition if rotation tracking is enabled + rotation_reached = torch.ones(self.num_envs, dtype=torch.bool, device=self.device) + rot_err = None + if self.enable_rotation_tracking: + from metasim.utils.math import matrix_from_quat + + box_quat = env_states.objects["object"].root_state[:, 3:7] + box_mat = matrix_from_quat(box_quat).reshape(self.num_envs, 9) + target_quat = self.waypoint_rotations[self.current_waypoint_idx] + target_mat = matrix_from_quat(target_quat).reshape(self.num_envs, 9) + rot_err = torch.norm(target_mat[:, :6] - box_mat[:, :6], dim=-1) + rotation_reached = rot_err < self.rotation_error_threshold + + # Both distance and rotation must be satisfied to consider as reached + reached = distance_reached & rotation_reached + newly_reached = reached & not_already_reached + # Waypoint reached bonus (one-time large reward when reaching a waypoint) + waypoint_reached_bonus = newly_reached.float() * self.w_tracking_progress + + # Update prev_distance for next step + self.prev_distance_to_waypoint[~newly_reached] = distance[~newly_reached] + + if newly_reached.any(): + if newly_reached[0]: + wp_idx = self.current_waypoint_idx[0].item() + rot_info = "" + if self.enable_rotation_tracking and rot_err is not None: + rot_info = f", Rotation error: {rot_err[0].item():.4f} < {self.rotation_error_threshold}" + log.info( + f"[Env 0] Reached waypoint #{wp_idx}! Distance: {distance[0].item():.4f}m < {self.reach_threshold}m{rot_info}" + ) + + self.waypoints_reached[newly_reached, self.current_waypoint_idx[newly_reached]] = True + + # Advance to next waypoint if not at the last one + can_advance = newly_reached & (self.current_waypoint_idx < self.num_waypoints - 1) + + if can_advance.any() and can_advance[0]: + old_idx = self.current_waypoint_idx[0].item() + new_idx = old_idx + 1 + log.info(f" -> Advancing to waypoint #{new_idx}") + + self.current_waypoint_idx[can_advance] += 1 + + if can_advance.any(): + new_target_pos = self.waypoint_positions[self.current_waypoint_idx[can_advance]] + self.prev_distance_to_waypoint[can_advance] = torch.norm( + ee_pos[can_advance] - new_target_pos, dim=-1 + ) + + maintain_reward = torch.zeros(self.num_envs, device=self.device) + all_reached = self.waypoints_reached.all(dim=1) + completed_mask = all_reached & grasped_mask + + if completed_mask.any(): + last_target_pos = self.waypoint_positions[-1].unsqueeze(0).expand(self.num_envs, -1) + distance_to_last = torch.norm(ee_pos - last_target_pos, dim=-1) + + maintain_reward[completed_mask] = torch.where( + distance_to_last[completed_mask] < self.reach_threshold, + torch.full((completed_mask.sum(),), 5, device=self.device), + (1 - torch.tanh(1.0 * distance_to_last[completed_mask])) * self.w_tracking_approach, + ) + + # Combine all reward components + tracking_reward = torch.where( + all_reached, maintain_reward, approach_reward + progress_reward_component + waypoint_reached_bonus + ) + + return tracking_reward diff --git a/scripts/advanced/collect_demo.py b/scripts/advanced/collect_demo.py index cccf56f1a..8bf2e7dad 100644 --- a/scripts/advanced/collect_demo.py +++ b/scripts/advanced/collect_demo.py @@ -688,4 +688,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() From 3ccb42f9f8fab614345eafce964d00f661eb9abd Mon Sep 17 00:00:00 2001 From: MurphyZhao04 Date: Sun, 14 Dec 2025 22:04:29 -0800 Subject: [PATCH 28/50] [dev] clean unused scripts --- scripts/_private/convert_images_to_video.py | 58 --------- scripts/_private/convert_rlbench_all_traj.sh | 123 ------------------ scripts/_private/write_path.py | 7 - .../demo_joint_position_control.py | 81 ------------ scripts/sim2real_script/get_realsense.py | 70 ---------- scripts/sim2real_script/reset.py | 81 ------------ scripts/statistics/available_source_demo.py | 56 -------- scripts/statistics/demo_stat.py | 27 ---- scripts/statistics/eval_results_stat.py | 33 ----- .../statistics/get_all_avaliable_demo_stat.py | 15 --- scripts/statistics/main.py | 122 ----------------- 11 files changed, 673 deletions(-) delete mode 100644 scripts/_private/convert_images_to_video.py delete mode 100644 scripts/_private/convert_rlbench_all_traj.sh delete mode 100644 scripts/_private/write_path.py delete mode 100644 scripts/sim2real_script/demo_joint_position_control.py delete mode 100644 scripts/sim2real_script/get_realsense.py delete mode 100644 scripts/sim2real_script/reset.py delete mode 100644 scripts/statistics/available_source_demo.py delete mode 100644 scripts/statistics/demo_stat.py delete mode 100644 scripts/statistics/eval_results_stat.py delete mode 100644 scripts/statistics/get_all_avaliable_demo_stat.py delete mode 100644 scripts/statistics/main.py diff --git a/scripts/_private/convert_images_to_video.py b/scripts/_private/convert_images_to_video.py deleted file mode 100644 index 84bb79883..000000000 --- a/scripts/_private/convert_images_to_video.py +++ /dev/null @@ -1,58 +0,0 @@ -import os -from glob import glob -from multiprocessing import Pool - -import cv2 -import imageio as iio -import numpy as np -from tqdm.rich import tqdm_rich as tqdm - -from metasim.utils.io_util import write_16bit_depth_video - -folders = [os.path.dirname(s) for s in open("paths.txt").read().splitlines()] -folders = [f for f in folders if not os.path.exists(os.path.join(f, "depth_uint16.mkv"))] - - -def single(folder): - ## depths - depth_paths = glob(os.path.join(folder, "depth_*.png")) - rgb_paths = glob(os.path.join(folder, "rgb_*.png")) - n_depths = len(depth_paths) - n_rgbs = len(rgb_paths) - assert n_depths == n_rgbs - assert os.path.exists(os.path.join(folder, f"depth_{n_depths - 1:04d}.png")) - assert os.path.exists(os.path.join(folder, f"rgb_{n_rgbs - 1:04d}.png")) - - depth_paths = [os.path.join(folder, f"depth_{i:04d}.png") for i in range(n_depths)] - rgb_paths = [os.path.join(folder, f"rgb_{i:04d}.png") for i in range(n_rgbs)] - - ## convert to video - depth_imgs = [cv2.imread(d, cv2.IMREAD_UNCHANGED) for d in depth_paths] - rgb_imgs = [iio.imread(d) for d in rgb_paths] - write_16bit_depth_video( - os.path.join(folder, "depth_uint16.mkv"), - depth_imgs, - ) - iio.mimwrite( - os.path.join(folder, "depth_uint8.mp4"), - [(d / 65535 * 255).astype(np.uint8) for d in depth_imgs], - fps=30, - quality=10, - ) - iio.mimwrite( - os.path.join(folder, "rgb.mp4"), - rgb_imgs, - fps=30, - quality=10, - ) - - -def main(): - with Pool(processes=64) as p, tqdm(total=len(folders)) as pbar: - for _ in p.imap_unordered(single, folders): - pbar.update() - pbar.refresh() - - -if __name__ == "__main__": - main() diff --git a/scripts/_private/convert_rlbench_all_traj.sh b/scripts/_private/convert_rlbench_all_traj.sh deleted file mode 100644 index 2372c63ac..000000000 --- a/scripts/_private/convert_rlbench_all_traj.sh +++ /dev/null @@ -1,123 +0,0 @@ -#!/bin/bash -tasks=( -"basketball_in_hoop" -"beat_the_buzz" -"block_pyramid" -"change_channel" -"change_clock" -"close_box" -"close_door" -"close_drawer" -"close_fridge" -"close_grill" -"close_jar" -"close_laptop_lid" -"close_microwave" -"empty_container" -"empty_dishwasher" -"get_ice_from_fridge" -"hang_frame_on_hanger" -"hit_ball_with_queue" -"hockey" -"insert_onto_square_peg" -"insert_usb_in_computer" -"lamp_off" -"lamp_on" -"lift_numbered_block" -"light_bulb_in" -"light_bulb_out" -"meat_off_grill" -"meat_on_grill" -"move_hanger" -"open_box" -"open_door" -"open_drawer" -"open_fridge" -"open_grill" -"open_jar" -"open_microwave" -"open_oven" -"open_washing_machine" -"open_window" -"open_wine_bottle" -"phone_on_base" -"pick_and_lift_small" -"pick_and_lift" -"pick_up_cup" -"place_cups" -"place_hanger_on_rack" -"place_shape_in_shape_sorter" -"play_jenga" -"plug_charger_in_power_supply" -"pour_from_cup_to_cup" -"press_switch" -"push_buttons" -"push_button" -"put_all_groceries_in_cupboard" -"put_books_on_bookshelf" -"put_bottle_in_fridge" -"put_groceries_in_cupboard" -"put_item_in_drawer" -"put_knife_in_knife_block" -"put_knife_on_chopping_board" -"put_money_in_safe" -"put_plate_in_colored_dish_rack" -"put_rubbish_in_bin" -"put_shoes_in_box" -"put_toilet_roll_on_stand" -"put_tray_in_oven" -"put_umbrella_in_umbrella_stand" -"reach_and_drag" -"reach_target" -"remove_cups" -"scoop_with_spatula" -"screw_nail" -"set_the_table" -"setup_checkers" -"setup_chess" -"slide_block_to_target" -"slide_cabinet_open_and_place_cups" -"solve_puzzle" -"stack_blocks" -"stack_chairs" -"stack_cups" -"stack_wine" -# "straighten_rope" -"sweep_to_dustpan" -"take_cup_out_from_cabinet" -"take_frame_off_hanger" -"take_item_out_of_drawer" -"take_lid_off_saucepan" -"take_money_out_safe" -"take_off_weighing_scales" -"take_plate_off_colored_dish_rack" -"take_shoes_out_of_box" -"take_toilet_roll_off_stand" -"take_tray_out_of_oven" -"take_umbrella_out_of_umbrella_stand" -"take_usb_out_of_computer" -"toilet_seat_down" -"toilet_seat_up" -"turn_oven_on" -"turn_tap" -"tv_on" -"unplug_charger" -"water_plants" -"weighing_scales" -"wipe_desk" -) - -if [ $# -gt 0 ]; then - task=$1 - command="python scripts/convert_traj_v1_to_v2.py --task rlbench:$task --source_path roboverse_data/trajs/rlbench/$task/v2/franka_v1.pkl" - echo $command - eval $command -else - for task in ${tasks[@]}; do - command="python scripts/convert_traj_v1_to_v2.py --task rlbench:$task --source_path roboverse_data/trajs/rlbench/$task/v2/franka_v1.pkl" - echo "" - echo "================================================" - echo $command - eval $command - done -fi diff --git a/scripts/_private/write_path.py b/scripts/_private/write_path.py deleted file mode 100644 index a8aac9154..000000000 --- a/scripts/_private/write_path.py +++ /dev/null @@ -1,7 +0,0 @@ -import os - -paths = open("paths.txt").read().split("\n") - -for path in paths: - with open(os.path.join(os.path.dirname(path), "status.txt"), "w+") as f: - f.write("success") diff --git a/scripts/sim2real_script/demo_joint_position_control.py b/scripts/sim2real_script/demo_joint_position_control.py deleted file mode 100644 index bb81b0c6e..000000000 --- a/scripts/sim2real_script/demo_joint_position_control.py +++ /dev/null @@ -1,81 +0,0 @@ -from time import time - -import hydra -import numpy as np -from loguru import logger -from omegaconf import DictConfig, OmegaConf - -from scripts.deploy.arm_hand_deployment.consts import CONFIG_PATH -from scripts.deploy.arm_hand_deployment.franka.communication.client import FrankaClient -from scripts.deploy.arm_hand_deployment.utils.client_context import robot_client_context - - -def timing_decorator(func): - def wrapper(*args, **kwargs): - start_time = time() # Record start time - result = func(*args, **kwargs) # Call the function - end_time = time() # Record end time - execution_time = end_time - start_time # Calculate execution time - print(f"Execution time of {func.__name__}: {execution_time:.4f} seconds") - return result - - return wrapper - - -@hydra.main(config_path=CONFIG_PATH, config_name="config", version_base="1.3") -def main(cfg: DictConfig): - print(OmegaConf.to_yaml(cfg)) - - port: int = int(cfg.rpc_port) - server_ip: str = cfg.server_ip - - logger.info(f"Connecting to server {server_ip}:{port}") - - initial_arm_joint_positions = [ - -0.14377304911613464, - -0.41615936160087585, - 0.1558820903301239, - -2.799217700958252, - 0.09156578779220581, - 2.3855371475219727, - 1.5030053853988647, - ] - - actions = np.load("scripts/action/actions.npy") - - with robot_client_context(server_ip, port, FrankaClient) as client: - client: FrankaClient - - assert client.MoveToJointPositions(initial_arm_joint_positions) - - qpos = client.GetJointPositions()[:7] - - # gripper command, greater than 0 is close , 0 to -1 is open - # gripper_action=-0.5 - # client.SetGripperAction(gripper_action) - # sleep_time = 1 - # sleep(sleep_time) - - print(f"Initial qpos:\n{qpos}") - - @timing_decorator - def execute(action): - assert client.ControlJointPositions(action=action) - - # warmup - execute(qpos) - - print("-" * 80) - - for action in actions: - action = action[:7].tolist() - print(action) - execute(action) - - # end_joint_positions = actions[-1] - # end_joint_positions = end_joint_positions[:7].tolist() - # assert client.MoveToJointPositions(end_joint_positions) - - -if __name__ == "__main__": - main() diff --git a/scripts/sim2real_script/get_realsense.py b/scripts/sim2real_script/get_realsense.py deleted file mode 100644 index cd226daef..000000000 --- a/scripts/sim2real_script/get_realsense.py +++ /dev/null @@ -1,70 +0,0 @@ -import cv2 -import numpy as np -import pyrealsense2 as rs - -# Configure depth and color streams -pipeline = rs.pipeline() -config = rs.config() - -# Get device product line for setting a supporting resolution -pipeline_wrapper = rs.pipeline_wrapper(pipeline) -pipeline_profile = config.resolve(pipeline_wrapper) -device = pipeline_profile.get_device() -device_product_line = str(device.get_info(rs.camera_info.product_line)) - -found_rgb = False -for s in device.sensors: - if s.get_info(rs.camera_info.name) == "RGB Camera": - found_rgb = True - break -if not found_rgb: - print("The demo requires Depth camera with Color sensor") - exit(0) - -config.enable_stream(rs.stream.depth, 640, 480, rs.format.z16, 30) - -if device_product_line == "L500": - config.enable_stream(rs.stream.color, 960, 540, rs.format.bgr8, 30) -else: - # config.enable_stream(rs.stream.color, 640, 480, rs.format.bgr8, 30) # USB 2.0 - config.enable_stream(rs.stream.color, 640, 480, rs.format.bgr8, 30) # USB 3.0 - -# Start streaming -pipeline.start(config) - -try: - while True: - # Wait for a coherent pair of frames: depth and color - frames = pipeline.wait_for_frames() - depth_frame = frames.get_depth_frame() - color_frame = frames.get_color_frame() - if not depth_frame or not color_frame: - continue - - # Convert images to numpy arrays - depth_image = np.asanyarray(depth_frame.get_data()) - color_image = np.asanyarray(color_frame.get_data()) - - # Apply colormap on depth image (image must be converted to 8-bit per pixel first) - depth_colormap = cv2.applyColorMap(cv2.convertScaleAbs(depth_image, alpha=0.03), cv2.COLORMAP_JET) - - depth_colormap_dim = depth_colormap.shape - color_colormap_dim = color_image.shape - - # If depth and color resolutions are different, resize color image to match depth image for display - if depth_colormap_dim != color_colormap_dim: - resized_color_image = cv2.resize( - color_image, dsize=(depth_colormap_dim[1], depth_colormap_dim[0]), interpolation=cv2.INTER_AREA - ) - images = np.hstack((resized_color_image, depth_colormap)) - else: - images = np.hstack((color_image, depth_colormap)) - - # Show images - cv2.namedWindow("RealSense", cv2.WINDOW_AUTOSIZE) - cv2.imshow("RealSense", images) - cv2.waitKey(1) - -finally: - # Stop streaming - pipeline.stop() diff --git a/scripts/sim2real_script/reset.py b/scripts/sim2real_script/reset.py deleted file mode 100644 index e53af051b..000000000 --- a/scripts/sim2real_script/reset.py +++ /dev/null @@ -1,81 +0,0 @@ -import hydra -from deploy.arm_hand_deployment.consts import CONFIG_PATH -from deploy.arm_hand_deployment.franka.communication.client import FrankaClient -from deploy.arm_hand_deployment.utils.client_context import robot_client_context -from loguru import logger -from omegaconf import DictConfig, OmegaConf - - -@hydra.main(config_path=CONFIG_PATH, config_name="config", version_base="1.3") -def main(cfg: DictConfig): - print(OmegaConf.to_yaml(cfg)) - - port: int = int(cfg.rpc_port) - server_ip: str = cfg.server_ip - - logger.info(f"Connecting to server {server_ip}:{port}") - - with robot_client_context(server_ip, port, FrankaClient) as client: - client: FrankaClient - - # 7 joint positions for the arm + 1 for the gripper (width of the gripper) - positions = client.GetJointPositions() - - # xyz+rpy - ee_pose = client.GetEndEffectorPose() - print(f"Joint positions: {positions}") - print(f"End effector pose: {ee_pose}") - - target_joint_positions = [ - 0.09162008114028396, - -0.19826458111314524, - -0.01990020486871322, - -2.4732269941140346, - -0.01307073642274261, - 2.30396583422025, - 0.8480939705504309, - ] - - # 7 joint positions for the arm - assert client.MoveToJointPositions(target_joint_positions) - - positions = client.GetJointPositions() - ee_pose = client.GetEndEffectorPose() - print(f"Joint positions: {positions}") - print(f"End effector pose: {ee_pose}") - - # target_end_effector_pose = [0.5, 0.1, 0.3, np.pi, 0, 0] - - # # xyz+rpy - # assert client.MoveToEndEffectorPose(target_end_effector_pose) - - # positions = client.GetJointPositions() - # ee_pose = client.GetEndEffectorPose() - # print(f"Joint positions: {positions}") - # print(f"End effector pose: {ee_pose}") - - # # See /home/abrar/hsc/deoxys_control/deoxys/deoxys/franka_interface/franka_interface.py - # # action<0 open the gripper target width of the gripper is -action*0.08 - # # action>0 close the gripper - - # # We have to open the gripper first so that deoxys can remember? - # # Honestly I am not very familiar with deoxys's gripper control. - # client.SetGripperAction(-1) - - # sleep(1) - - # client.SetGripperAction(1) - - # sleep(3) - - # # open - # client.SetGripperAction(-1) - - # sleep(3) - - -if __name__ == "__main__": - main() - main() - main() - main() diff --git a/scripts/statistics/available_source_demo.py b/scripts/statistics/available_source_demo.py deleted file mode 100644 index c3da43aea..000000000 --- a/scripts/statistics/available_source_demo.py +++ /dev/null @@ -1,56 +0,0 @@ -import glob -import gzip -import json -import pickle - -import yaml - - -def load_traj_file(path: str): - if path.endswith(".pkl"): - with open(path, "rb") as f: - return pickle.load(f) - elif path.endswith(".pkl.gz"): - with gzip.open(path, "rb") as f: - return pickle.load(f) - elif path.endswith(".json"): - with open(path) as f: - return json.load(f) - elif path.endswith(".yaml"): - with open(path) as f: - return yaml.load(f, Loader=yaml.FullLoader) - else: - raise ValueError(f"Unsupported file extension: {path}") - - -def main(): - demos = glob.glob("roboverse_data/trajs/**/*.pkl.gz", recursive=True) + glob.glob( - "roboverse_data/trajs/**/*.pkl", recursive=True - ) - total = 0 - stat_bench = {} - for demo_path in sorted(demos): - print(demo_path) - data = load_traj_file(demo_path) - bench_name = demo_path.split("/")[2] - task_name = demo_path.split("/")[3] - if bench_name not in stat_bench: - stat_bench[bench_name] = {} - if task_name not in stat_bench[bench_name]: - stat_bench[bench_name][task_name] = {} - for key in data: - if key not in stat_bench[bench_name][task_name]: - stat_bench[bench_name][task_name][key] = 0 - stat_bench[bench_name][task_name][key] += len(data[key]) - total += len(data[key]) - print(total) - for bench_name in stat_bench: - print(bench_name, "*" * 10) - for task_name in stat_bench[bench_name]: - print(task_name, "*" * 5) - for key in stat_bench[bench_name][task_name]: - print(key, stat_bench[bench_name][task_name][key]) - - -if __name__ == "__main__": - main() diff --git a/scripts/statistics/demo_stat.py b/scripts/statistics/demo_stat.py deleted file mode 100644 index 9d1135127..000000000 --- a/scripts/statistics/demo_stat.py +++ /dev/null @@ -1,27 +0,0 @@ -import glob -import os - -import cv2 - -ROOT = "data_isaaclab/demo" -ROOT = "roboverse_demo/demo_isaaclab" - -total_traj = 0 -total_frame = 0 - -for dir_ in sorted(os.listdir(ROOT)): - if not os.path.isdir(os.path.join(ROOT, dir_)): - continue - files = glob.glob(f"{ROOT}/{dir_}/*/demo_*/metadata.json") - rgb_files = glob.glob(f"{ROOT}/{dir_}/*/demo_*/*.mp4") - frames = [] - this_task_frames = 0 - for rgb_file in rgb_files: - cap = cv2.VideoCapture(rgb_file) - length = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) - this_task_frames += length - print(dir_, len(files), this_task_frames) - total_traj += len(files) - total_frame += this_task_frames -print("total_traj", total_traj) -print("total_frame", total_frame) diff --git a/scripts/statistics/eval_results_stat.py b/scripts/statistics/eval_results_stat.py deleted file mode 100644 index 7c6bac500..000000000 --- a/scripts/statistics/eval_results_stat.py +++ /dev/null @@ -1,33 +0,0 @@ -import glob - -paths = glob.glob("tmp/*/0000.txt") - - -for path in sorted(paths): - task_name = path.split("/")[-2] - eval_paths = glob.glob(f"tmp/{task_name}/*.txt") - - success_once_count = 0 - success_end_count = 0 - total_count = 0 - for eval_path in eval_paths: - with open(eval_path) as f: - lines = f.readlines() - - for line in lines: - if "SuccessOnce" in line: - # Extract boolean value from tensor string - success_once = "True" in line - if success_once: - success_once_count += 1 - elif "SuccessEnd" in line: - # Extract boolean value - success_end = "True" in line - if success_end: - success_end_count += 1 - total_count += 1 - task_name = task_name[:40].ljust(40) - print(f"Task Name: {task_name}", end="\t") - print(f"Success Once Rate: {success_once_count / total_count:.2%}", end="\t") - print(f"Success End Rate: {success_end_count / total_count:.2%}", end=" \t") - print(f"Total Episodes: {total_count}") diff --git a/scripts/statistics/get_all_avaliable_demo_stat.py b/scripts/statistics/get_all_avaliable_demo_stat.py deleted file mode 100644 index b2cc3d6d1..000000000 --- a/scripts/statistics/get_all_avaliable_demo_stat.py +++ /dev/null @@ -1,15 +0,0 @@ -import glob -import os - -ROOT = "data_isaaclab/demo" - -tasks_available = sorted(os.listdir(ROOT)) - -for task in tasks_available: - try: - robot_available = sorted(os.listdir(os.path.join(ROOT, task))) - except: - continue - for robot in robot_available: - paths = glob.glob(f"{ROOT}/{task}/{robot}/demo_*/metadata.json") - print(f"Task: {task}, Robot: {robot}, Number of demos: {len(paths)}") diff --git a/scripts/statistics/main.py b/scripts/statistics/main.py deleted file mode 100644 index 5eae23b14..000000000 --- a/scripts/statistics/main.py +++ /dev/null @@ -1,122 +0,0 @@ -import os -import pickle as pkl - -import numpy as np -from loguru import logger as log -from matplotlib import pyplot as plt - -from metasim.constants import TaskType -from metasim.scenario.metasim.task.base_task_cfg import BaseTaskCfg -from metasim.utils.demo_util import get_traj -from metasim.utils.setup_util import get_robot, get_task - -save_dir = "statistics/plots" -os.makedirs(save_dir, exist_ok=True) - -franka_joint_names = [ - "panda_joint1", - "panda_joint2", - "panda_joint3", - "panda_joint4", - "panda_joint5", - "panda_joint6", - "panda_joint7", - "panda_finger_joint1", - "panda_finger_joint2", -] -franka_joint_limits = [ - (-2.8973, 2.8973), - (-1.7628, 1.7628), - (-2.8973, 2.8973), - (-3.0718, -0.0698), - (-2.8973, 2.8973), - (-0.0175, 3.7525), - (-2.8973, 2.8973), - (0.0, 0.04), - (0.0, 0.04), -] - - -def get_available_tasks(): - from metasim.scenario.scenario import tasks - - tasks = [task for task_name, task in vars(tasks).items() if task_name.endswith("Cfg")] - - tasks = [task for task in tasks if issubclass(task, BaseTaskCfg) and task != BaseTaskCfg] - tasks = [task for task in tasks if str(task).find("open6d") == -1] - - available_tasks = [] - - for task in tasks: - task_inst = task() - task_name = task_inst.__class__.__name__.removesuffix("Cfg") - if task_inst.task_type != TaskType.TABLETOP_MANIPULATION: - log.info(f"Skipping {task_name} because it is not tabletop manipulation") - continue - if not task_inst.traj_filepath.endswith("_v2.pkl"): - log.info(f"Skipping {task_name} because it is not v2") - continue - if not os.path.exists(task_inst.traj_filepath): - log.info(f"Skipping {task_name} because the trajectory file does not exist") - continue - - available_tasks.append(task_name) - - return available_tasks - - -def plot(actions: np.ndarray, save_path: str): - fig, axes = plt.subplots(4, 2, figsize=(15, 20)) - axes = axes.flatten() - - for i in range(7): - joint_actions = actions[:, i] - ax = axes[i] - ax.hist(joint_actions, bins=300, range=franka_joint_limits[i]) - ax.set_title(f"Joint {i + 1} Action Distribution") - ax.set_xlabel("Joint Position Target") - ax.set_ylabel("Density") - ax.grid(True) - - axes[-1].remove() # Remove the empty subplot - fig.tight_layout() - fig.savefig(save_path) - plt.close(fig) - - -def main(): - available_tasks = get_available_tasks() - print("available_tasks:", available_tasks) - - all_task_actions = [] - all_task_actions_100 = [] - for task_name in available_tasks: - ## Load actions - task_inst = get_task(task_name) - data = pkl.load(open(task_inst.traj_filepath, "rb")) - robots = list(data.keys()) - _, all_actions, _ = get_traj(task_inst, get_robot(robots[0]), None) - - ## Parse actions - actions = [] - for demo_idx in range(len(all_actions)): - for step in range(len(all_actions[demo_idx])): - action = [all_actions[demo_idx][step]["dof_pos_target"][k] for k in franka_joint_names] - actions.append(action) - actions = np.array(actions) - - ## Plot actions - plot(actions, f"{save_dir}/{task_name}.png") - - all_task_actions.append(actions) - all_task_actions_100.append(actions[:100]) - - all_task_actions = np.concatenate(all_task_actions, axis=0) - plot(all_task_actions, f"{save_dir}/all_tasks.png") - - all_task_actions_100 = np.concatenate(all_task_actions_100, axis=0) - plot(all_task_actions_100, f"{save_dir}/all_tasks_100.png") - - -if __name__ == "__main__": - main() From 67197871b52fdb56adb79ad9dc9f2398e48ad69d Mon Sep 17 00:00:00 2001 From: MurphyZhao04 Date: Mon, 15 Dec 2025 01:48:24 -0800 Subject: [PATCH 29/50] [dev] new track task --- .../tasks/pick_place/approach_grasp_bowl.py | 172 +++++++++++ roboverse_pack/tasks/pick_place/track_bowl.py | 268 ++++++++++++++++++ 2 files changed, 440 insertions(+) create mode 100644 roboverse_pack/tasks/pick_place/track_bowl.py diff --git a/roboverse_pack/tasks/pick_place/approach_grasp_bowl.py b/roboverse_pack/tasks/pick_place/approach_grasp_bowl.py index e69de29bb..d623d11eb 100644 --- a/roboverse_pack/tasks/pick_place/approach_grasp_bowl.py +++ b/roboverse_pack/tasks/pick_place/approach_grasp_bowl.py @@ -0,0 +1,172 @@ +"""Stage 1: Simple Approach and Grasp task for bowl object (ground layout). + +This task: +- Uses EmbodiedGenData/assets/* assets (not demo_assets/new_assets) +- Uses a hardcoded initial state +- Targets the bowl as the grasp object (mapped to name="object" to match PickPlaceBase) +- Uses hardcoded trajectory markers (traj_marker_0..4) required by PickPlaceBase +""" + +from __future__ import annotations + +import torch + +from metasim.constants import PhysicStateType +from metasim.scenario.objects import RigidObjCfg +from metasim.scenario.scenario import ScenarioCfg, SimParamCfg +from metasim.task.registry import register_task +from roboverse_pack.tasks.pick_place.approach_grasp import PickPlaceApproachGraspSimple + + +@register_task("pick_place.approach_grasp_simple_bowl", "pick_place_approach_grasp_simple_bowl") +class PickPlaceApproachGraspSimpleBowl(PickPlaceApproachGraspSimple): + """Approach+grasp task where the target object is a bowl (ground layout).""" + + scenario = ScenarioCfg( + objects=[ + # Target: bowl, mapped to name="object" + RigidObjCfg( + name="object", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/assets/bowl/0f296af3df66565c9e1a7c2bc7b35d72/usd/0f296af3df66565c9e1a7c2bc7b35d72.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/assets/bowl/0f296af3df66565c9e1a7c2bc7b35d72/0f296af3df66565c9e1a7c2bc7b35d72.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/assets/bowl/0f296af3df66565c9e1a7c2bc7b35d72/mjcf/0f296af3df66565c9e1a7c2bc7b35d72.xml", + ), + # Context objects (match saved_poses_20251214_224520.py names) + RigidObjCfg( + name="basket", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/assets/basket/663158968e3f5900af1f6e7cecef24c7/usd/663158968e3f5900af1f6e7cecef24c7.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/assets/basket/663158968e3f5900af1f6e7cecef24c7/663158968e3f5900af1f6e7cecef24c7.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/assets/basket/663158968e3f5900af1f6e7cecef24c7/mjcf/663158968e3f5900af1f6e7cecef24c7.xml", + ), + RigidObjCfg( + name="cutting_tools", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/assets/cutting_tools/c5810e7c2c785fe3940372b205090bad/usd/c5810e7c2c785fe3940372b205090bad.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/assets/cutting_tools/c5810e7c2c785fe3940372b205090bad/c5810e7c2c785fe3940372b205090bad.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/assets/cutting_tools/c5810e7c2c785fe3940372b205090bad/mjcf/c5810e7c2c785fe3940372b205090bad.xml", + ), + RigidObjCfg( + name="lighting_fixtures", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/assets/lighting_fixtures/03f09dca16db5598a67f0715cf3fb157/usd/03f09dca16db5598a67f0715cf3fb157.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/assets/lighting_fixtures/03f09dca16db5598a67f0715cf3fb157/03f09dca16db5598a67f0715cf3fb157.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/assets/lighting_fixtures/03f09dca16db5598a67f0715cf3fb157/mjcf/03f09dca16db5598a67f0715cf3fb157.xml", + ), + RigidObjCfg( + name="screwdriver", + scale=(1.5, 1.5, 1.5), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/assets/screwdriver/ae51f060e3455e9f84a4fec81cc9284b/usd/ae51f060e3455e9f84a4fec81cc9284b.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/assets/screwdriver/ae51f060e3455e9f84a4fec81cc9284b/ae51f060e3455e9f84a4fec81cc9284b.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/assets/screwdriver/ae51f060e3455e9f84a4fec81cc9284b/mjcf/ae51f060e3455e9f84a4fec81cc9284b.xml", + ), + RigidObjCfg( + name="spoon", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/assets/spoon/2f1c3077a8d954e58fc0bf75cf35e849/usd/2f1c3077a8d954e58fc0bf75cf35e849.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/assets/spoon/2f1c3077a8d954e58fc0bf75cf35e849/2f1c3077a8d954e58fc0bf75cf35e849.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/assets/spoon/2f1c3077a8d954e58fc0bf75cf35e849/mjcf/2f1c3077a8d954e58fc0bf75cf35e849.xml", + ), + # Required: trajectory markers (PickPlaceBase requires them in initial state) + *[ + RigidObjCfg( + name=f"traj_marker_{i}", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ) + for i in range(5) + ], + ], + robots=["franka"], + sim_params=SimParamCfg(dt=0.005), + decimation=4, + ) + + max_episode_steps = 200 + + def _get_initial_states(self) -> list[dict] | None: + # Hardcoded initial state (objects + robot + traj_marker_0..4) + init = [ + { + "objects": { + # target bowl -> name "object" + "object": { + "pos": torch.tensor([1.060000, -0.380000, 0.130000]), + "rot": torch.tensor([0.998750, 0.000000, 0.049979, -0.000000]), + }, + "basket": { + "pos": torch.tensor([0.550000, -0.470000, 0.200000]), + "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), + }, + "cutting_tools": { + "pos": torch.tensor([1.140000, -0.180000, 0.040000]), + "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), + }, + "lighting_fixtures": { + "pos": torch.tensor([0.970000, 0.070000, 0.210000]), + "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), + }, + "screwdriver": { + "pos": torch.tensor([1.220000, -0.500000, 0.100000]), + "rot": torch.tensor([0.947354, 0.023689, 0.319209, 0.007982]), + }, + "spoon": { + "pos": torch.tensor([1.390000, -0.280000, 0.020000]), + "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), + }, + "traj_marker_0": { + "pos": torch.tensor([0.990000, -0.380000, 0.230000]), + "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), + }, + "traj_marker_1": { + "pos": torch.tensor([0.930000, -0.380000, 0.322500]), + "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), + }, + "traj_marker_2": { + "pos": torch.tensor([0.790000, -0.380000, 0.385000]), + "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), + }, + "traj_marker_3": { + "pos": torch.tensor([0.690000, -0.380000, 0.377500]), + "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), + }, + "traj_marker_4": { + "pos": torch.tensor([0.580000, -0.380000, 0.362500]), + "rot": torch.tensor([0.999687, -0.024997, 0.000000, 0.000000]), + }, + }, + "robots": { + "franka": { + "pos": torch.tensor([0.910000, -0.790000, 0.030000]), + "rot": torch.tensor([-0.666275, -0.000000, 0.000000, -0.745703]), + "dof_pos": { + "panda_finger_joint1": 0.040000, + "panda_finger_joint2": 0.040000, + "panda_joint1": 0.000000, + "panda_joint2": -0.785398, + "panda_joint3": 0.000000, + "panda_joint4": -2.356194, + "panda_joint5": 0.000000, + "panda_joint6": 1.570796, + "panda_joint7": 0.785398, + }, + } + }, + } + for _ in range(self.num_envs) + ] + + return init diff --git a/roboverse_pack/tasks/pick_place/track_bowl.py b/roboverse_pack/tasks/pick_place/track_bowl.py new file mode 100644 index 000000000..97c879edd --- /dev/null +++ b/roboverse_pack/tasks/pick_place/track_bowl.py @@ -0,0 +1,268 @@ +"""Stage 3: Track task for trajectory tracking with bowl object (ground layout). + +This task: +- Targets the bowl as name="object" (required by PickPlaceBase) +- Uses a hardcoded initial state +- Uses hardcoded 5 trajectory markers (required by PickPlaceBase) +- Forces object_grasped=True so tracking rewards are active without requiring a grasp state file +""" + +from __future__ import annotations + +import torch + +from metasim.constants import PhysicStateType +from metasim.scenario.objects import RigidObjCfg +from metasim.scenario.scenario import ScenarioCfg, SimParamCfg +from metasim.task.registry import register_task +from roboverse_pack.tasks.pick_place.base import PickPlaceBase + + +def _interpolate_waypoints(waypoints: torch.Tensor, steps_per_segment: list[int]) -> torch.Tensor: + """Linear interpolation: include the first waypoint, then for each segment add N points (excluding segment start). + + If waypoints has shape (K, 3) and steps_per_segment has length K-1, + output shape will be (1 + sum(steps_per_segment), 3). + """ + assert waypoints.ndim == 2 and waypoints.shape[1] == 3 + assert len(steps_per_segment) == waypoints.shape[0] - 1 + + out = [waypoints[0]] + for i, n in enumerate(steps_per_segment): + start = waypoints[i] + end = waypoints[i + 1] + n = int(n) + if n <= 0: + continue + # t = 1..n (exclude start, include end when t==n) + for t in range(1, n + 1): + alpha = float(t) / float(n) + out.append(start + (end - start) * alpha) + return torch.stack(out, dim=0) + + +@register_task("pick_place.track_bowl", "pick_place_track_bowl") +class PickPlaceTrackBowl(PickPlaceBase): + """Trajectory tracking task for bowl (ground layout).""" + + # Trajectory interpolation (object_init -> marker_0 -> ... -> marker_4) + INTERP_SEGMENT_STEPS = [40, 20, 20, 20, 20] # total 120 steps, 121 waypoints with start + REACH_THRESHOLD = 0.10 + TERMINATION_DISTANCE = 0.3 + FORESIGHT_STEPS = 0 + + # Tracking reward (match HandTraj-style: only position + rotation tracking) + K_POS = 10.0 + K_ROT = 5.0 + W_POS = 0.5 + W_ROT = 0.3 + + scenario = ScenarioCfg( + objects=[ + # Target: bowl + RigidObjCfg( + name="object", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/assets/bowl/0f296af3df66565c9e1a7c2bc7b35d72/usd/0f296af3df66565c9e1a7c2bc7b35d72.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/assets/bowl/0f296af3df66565c9e1a7c2bc7b35d72/0f296af3df66565c9e1a7c2bc7b35d72.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/assets/bowl/0f296af3df66565c9e1a7c2bc7b35d72/mjcf/0f296af3df66565c9e1a7c2bc7b35d72.xml", + ), + # Context objects + RigidObjCfg( + name="basket", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/assets/basket/663158968e3f5900af1f6e7cecef24c7/usd/663158968e3f5900af1f6e7cecef24c7.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/assets/basket/663158968e3f5900af1f6e7cecef24c7/663158968e3f5900af1f6e7cecef24c7.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/assets/basket/663158968e3f5900af1f6e7cecef24c7/mjcf/663158968e3f5900af1f6e7cecef24c7.xml", + ), + RigidObjCfg( + name="cutting_tools", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/assets/cutting_tools/c5810e7c2c785fe3940372b205090bad/usd/c5810e7c2c785fe3940372b205090bad.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/assets/cutting_tools/c5810e7c2c785fe3940372b205090bad/c5810e7c2c785fe3940372b205090bad.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/assets/cutting_tools/c5810e7c2c785fe3940372b205090bad/mjcf/c5810e7c2c785fe3940372b205090bad.xml", + ), + RigidObjCfg( + name="lighting_fixtures", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/assets/lighting_fixtures/03f09dca16db5598a67f0715cf3fb157/usd/03f09dca16db5598a67f0715cf3fb157.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/assets/lighting_fixtures/03f09dca16db5598a67f0715cf3fb157/03f09dca16db5598a67f0715cf3fb157.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/assets/lighting_fixtures/03f09dca16db5598a67f0715cf3fb157/mjcf/03f09dca16db5598a67f0715cf3fb157.xml", + ), + RigidObjCfg( + name="screwdriver", + scale=(1.5, 1.5, 1.5), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/assets/screwdriver/ae51f060e3455e9f84a4fec81cc9284b/usd/ae51f060e3455e9f84a4fec81cc9284b.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/assets/screwdriver/ae51f060e3455e9f84a4fec81cc9284b/ae51f060e3455e9f84a4fec81cc9284b.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/assets/screwdriver/ae51f060e3455e9f84a4fec81cc9284b/mjcf/ae51f060e3455e9f84a4fec81cc9284b.xml", + ), + RigidObjCfg( + name="spoon", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/EmbodiedGenData/assets/spoon/2f1c3077a8d954e58fc0bf75cf35e849/usd/2f1c3077a8d954e58fc0bf75cf35e849.usd", + urdf_path="roboverse_data/assets/EmbodiedGenData/assets/spoon/2f1c3077a8d954e58fc0bf75cf35e849/2f1c3077a8d954e58fc0bf75cf35e849.urdf", + mjcf_path="roboverse_data/assets/EmbodiedGenData/assets/spoon/2f1c3077a8d954e58fc0bf75cf35e849/mjcf/2f1c3077a8d954e58fc0bf75cf35e849.xml", + ), + # Markers + *[ + RigidObjCfg( + name=f"traj_marker_{i}", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ) + for i in range(5) + ], + ], + robots=["franka"], + sim_params=SimParamCfg(dt=0.005), + decimation=4, + ) + + max_episode_steps = 200 + + def __init__(self, scenario, device=None): + if device is None: + device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + self._device = device + self.object_grasped = None + super().__init__(scenario, device) + + # Build dense interpolated trajectory from initial object pose and marker poses. + init0 = self._get_initial_states()[0] + obj0_pos = init0["objects"]["object"]["pos"].to(self.device).float() + obj0_rot = init0["objects"]["object"]["rot"].to(self.device).float() + base_markers = [init0["objects"][f"traj_marker_{i}"]["pos"].to(self.device).float() for i in range(5)] + all_waypoints = torch.stack([obj0_pos] + base_markers, dim=0) # (6, 3) + dense = _interpolate_waypoints(all_waypoints, self.INTERP_SEGMENT_STEPS).to(self.device).float() + + self.waypoint_positions = dense + self.num_waypoints = int(dense.shape[0]) + self.target_orientation = obj0_rot # fixed target orientation (HandTraj-style) + + # Track-only reward: position + rotation + self.reward_functions = [self._reward_pos_rot_tracking] + self.reward_weights = [1.0] + + def _prepare_states(self, states, env_ids): + """Disable randomization for track task.""" + return states + + def _get_initial_states(self) -> list[dict] | None: + # Hardcoded initial state (objects + robot + traj_marker_0..4) + initial_states = [ + { + "objects": { + "object": { + "pos": torch.tensor([1.060000, -0.380000, 0.130000]), + "rot": torch.tensor([0.998750, 0.000000, 0.049979, -0.000000]), + }, + "basket": { + "pos": torch.tensor([0.550000, -0.470000, 0.200000]), + "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), + }, + "cutting_tools": { + "pos": torch.tensor([1.140000, -0.180000, 0.040000]), + "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), + }, + "lighting_fixtures": { + "pos": torch.tensor([0.970000, 0.070000, 0.210000]), + "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), + }, + "screwdriver": { + "pos": torch.tensor([1.220000, -0.500000, 0.100000]), + "rot": torch.tensor([0.947354, 0.023689, 0.319209, 0.007982]), + }, + "spoon": { + "pos": torch.tensor([1.390000, -0.280000, 0.020000]), + "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), + }, + "traj_marker_0": { + "pos": torch.tensor([0.990000, -0.380000, 0.230000]), + "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), + }, + "traj_marker_1": { + "pos": torch.tensor([0.930000, -0.380000, 0.322500]), + "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), + }, + "traj_marker_2": { + "pos": torch.tensor([0.790000, -0.380000, 0.385000]), + "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), + }, + "traj_marker_3": { + "pos": torch.tensor([0.690000, -0.380000, 0.377500]), + "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), + }, + "traj_marker_4": { + "pos": torch.tensor([0.580000, -0.380000, 0.362500]), + "rot": torch.tensor([0.999687, -0.024997, 0.000000, 0.000000]), + }, + }, + "robots": { + "franka": { + "pos": torch.tensor([0.910000, -0.790000, 0.030000]), + "rot": torch.tensor([-0.666275, -0.000000, 0.000000, -0.745703]), + "dof_pos": { + "panda_finger_joint1": 0.040000, + "panda_finger_joint2": 0.040000, + "panda_joint1": 0.000000, + "panda_joint2": -0.785398, + "panda_joint3": 0.000000, + "panda_joint4": -2.356194, + "panda_joint5": 0.000000, + "panda_joint6": 1.570796, + "panda_joint7": 0.785398, + }, + } + }, + } + for _ in range(self.num_envs) + ] + + return initial_states + + def _get_waypoint_indices(self) -> torch.Tensor: + """Waypoint index = current episode step (optionally with foresight).""" + # RLTaskEnv.step increments _episode_steps BEFORE computing reward. + step0 = torch.clamp(self._episode_steps - 1, min=0) + idx = step0 + int(self.FORESIGHT_STEPS) + return idx.clamp(0, self.num_waypoints - 1).long() + + def _reward_pos_rot_tracking(self, env_states) -> torch.Tensor: + """Only position + rotation tracking reward (HandTraj-style).""" + obj_pos = env_states.objects["object"].root_state[:, 0:3] + obj_quat = env_states.objects["object"].root_state[:, 3:7] + + idx = self._get_waypoint_indices() + target_pos = self.waypoint_positions[idx] + + pos_error = torch.norm(obj_pos - target_pos, dim=-1) + pos_reward = torch.exp(-float(self.K_POS) * pos_error) + + # Fixed target orientation (from initial object pose) + target_quat = self.target_orientation.unsqueeze(0).expand_as(obj_quat) + # Angle between quaternions: angle = 2*acos(|dot(q1,q2)|) + dot = (target_quat * obj_quat).sum(dim=-1).abs().clamp(0.0, 1.0) + angle = 2.0 * torch.acos(dot) + rot_reward = torch.exp(-float(self.K_ROT) * angle) + + return float(self.W_POS) * pos_reward + float(self.W_ROT) * rot_reward + + def _terminated(self, env_states) -> torch.Tensor: + """Terminate if object deviates too far from current target waypoint.""" + obj_pos = env_states.objects["object"].root_state[:, 0:3] + idx = self._get_waypoint_indices() + target_pos = self.waypoint_positions[idx] + dist = torch.norm(obj_pos - target_pos, dim=-1) + return dist > float(self.TERMINATION_DISTANCE) From 0e2b749547d246038e56d6f449eeb0c6c79b8231 Mon Sep 17 00:00:00 2001 From: AllenLiu <519174419@qq.com> Date: Mon, 15 Dec 2025 20:58:24 +0800 Subject: [PATCH 30/50] [bugfix] Fix gs blend render for isaacsim and mujoco (#624) * Fix GS background blending: exclude terrain from foreground mask using instance segmentation * Update * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Remove * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix mojuco gs render * Update * Update * Lint * Update * Upate * Update * [fix] mjcf scale * Update * support rerun visualizer * update rerun readme, doc and index * update rerun code format to fit pre-commit hook * unified viz wrapper * Remove deps on pyquaternion --------- Co-authored-by: nemo.liu Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: MurphyZhao04 Co-authored-by: geng-haoran --- .gitignore | 2 + .../quick_start/17_rerun_visualization.md | 306 ++++ .../metasim/get_started/quick_start/index.md | 1 + get_started/0_static_scene.py | 2 +- get_started/15_gs_background.py | 241 ++- get_started/rerun/README.md | 267 +++ get_started/rerun/replay_task_demo.py | 275 ++++ get_started/rerun/rerun_demo.py | 405 +++++ get_started/rerun/save_trajectory.py | 315 ++++ get_started/rerun/save_trajectory_simple.py | 355 ++++ get_started/rl/0_ppo.py | 39 +- get_started/rl/0_ppo_gym_style.py | 35 +- metasim/sim/base.py | 25 +- metasim/sim/genesis/genesis.py | 15 +- metasim/sim/isaacgym/isaacgym.py | 10 +- metasim/sim/isaacsim/isaacsim.py | 139 +- metasim/sim/mujoco/mujoco.py | 183 ++- metasim/sim/pybullet/pybullet.py | 12 +- metasim/sim/sapien/sapien3.py | 10 +- metasim/utils/obs_utils.py | 4 +- metasim/utils/rerun/README.md | 107 ++ metasim/utils/rerun/__init__.py | 5 + metasim/utils/rerun/rerun_env_wrapper.py | 209 +++ metasim/utils/rerun/rerun_util.py | 1462 +++++++++++++++++ metasim/utils/viser/viser_env_wrapper.py | 37 +- metasim/utils/viz_handler_wrapper.py | 321 ++++ metasim/utils/viz_task_wrapper.py | 364 ++++ pyproject.toml | 3 +- scripts/advanced/teleop_keyboard.py | 25 +- 29 files changed, 4932 insertions(+), 242 deletions(-) create mode 100644 docs/source/metasim/get_started/quick_start/17_rerun_visualization.md create mode 100644 get_started/rerun/README.md create mode 100644 get_started/rerun/replay_task_demo.py create mode 100644 get_started/rerun/rerun_demo.py create mode 100644 get_started/rerun/save_trajectory.py create mode 100644 get_started/rerun/save_trajectory_simple.py create mode 100644 metasim/utils/rerun/README.md create mode 100644 metasim/utils/rerun/__init__.py create mode 100644 metasim/utils/rerun/rerun_env_wrapper.py create mode 100644 metasim/utils/rerun/rerun_util.py create mode 100644 metasim/utils/viz_handler_wrapper.py create mode 100644 metasim/utils/viz_task_wrapper.py diff --git a/.gitignore b/.gitignore index 1a246f924..73ef50423 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ **/.vscode/ # Don't ignore the top-level .vscode directory as it is # used to configure VS Code settings +*.rrd +# rerun recording files !.vscode/ .vscode/.history .hydra/ diff --git a/docs/source/metasim/get_started/quick_start/17_rerun_visualization.md b/docs/source/metasim/get_started/quick_start/17_rerun_visualization.md new file mode 100644 index 000000000..019508e0a --- /dev/null +++ b/docs/source/metasim/get_started/quick_start/17_rerun_visualization.md @@ -0,0 +1,306 @@ +# 17. Rerun Visualization + +This tutorial shows you how to use [Rerun](https://rerun.io/) to visualize RoboVerse simulations with timeline-based exploration, recording, and replay capabilities. + +## What is Rerun? + +Rerun is an open-source SDK for logging, storing, querying, and visualizing multimodal data. Unlike traditional simulation viewers, Rerun provides: + +- **Timeline-based exploration**: Scrub through simulation history like a video +- **Recording & Replay**: Save sessions as `.rrd` files for offline viewing +- **Multi-modal support**: Visualize robots, objects, images, point clouds together +- **Cross-platform**: Works on Linux, macOS (including Apple Silicon), and Windows + +## Installation + +Install the Rerun SDK and dependencies: + +```bash +pip install rerun-sdk trimesh yourdfpy +``` + +Verify installation: + +```bash +rerun --version +``` + +## Quick Start + +### Step 1: Replay an Existing Task Demo + +The easiest way to get started is to replay a pre-recorded task trajectory. This doesn't require GPU or IK solvers. + +```bash +# Replay the stack_cube task +python get_started/rerun/replay_task_demo.py --task stack_cube --sim mujoco --output stack_cube.rrd +``` + +This will: +1. Load the `stack_cube` task configuration +2. Download required assets (URDF files, meshes) +3. Replay the trajectory step by step +4. Save the visualization as `stack_cube.rrd` + +### Step 2: View the Recording + +Open the saved recording in the Rerun viewer: + +```bash +rerun stack_cube.rrd +``` + +You'll see: +- 🤖 **Franka robot** with moving joints +- 📦 **Colored cubes** being stacked +- ⏱️ **Timeline** at the bottom for scrubbing through the simulation + +### Step 3: Live Viewer During Recording + +To see the visualization in real-time while recording: + +```bash +python get_started/rerun/replay_task_demo.py --task stack_cube --sim mujoco --output stack_cube.rrd --spawn-viewer +``` + +## Available Demo Scripts + +### 1. Replay Task Demo (Recommended for Beginners) + +Replays pre-recorded task trajectories. **No GPU or IK solver needed!** + +```bash +# Available tasks: stack_cube, close_box, pick_cube, etc. +python get_started/rerun/replay_task_demo.py --task stack_cube --sim mujoco --output stack_cube.rrd + +# Try different tasks +python get_started/rerun/replay_task_demo.py --task close_box --sim mujoco --output close_box.rrd +python get_started/rerun/replay_task_demo.py --task pick_cube --sim mujoco --output pick_cube.rrd +``` + +**Command Line Arguments:** + +| Argument | Type | Default | Description | +|----------|------|---------|-------------| +| `--task` | str | "stack_cube" | Task name to replay | +| `--robot` | str | "franka" | Robot to use | +| `--sim` | str | "mujoco" | Simulator backend | +| `--output` | str | "task_replay.rrd" | Output recording file | +| `--spawn-viewer` | bool | False | Open viewer during recording | +| `--max-steps` | int | None | Maximum steps to record | + +### 2. Simple Trajectory Recording (CPU-Only) + +Generates sinusoidal or random joint motions directly. **Works on Mac without GPU!** + +```bash +# Sinusoidal motion (smooth, periodic) +python get_started/rerun/save_trajectory_simple.py --sim mujoco --output trajectory.rrd + +# Random motion +python get_started/rerun/save_trajectory_simple.py --sim mujoco --motion-type random --output trajectory.rrd + +# More simulation steps +python get_started/rerun/save_trajectory_simple.py --sim mujoco --num-steps 500 --output trajectory.rrd +``` + +**Command Line Arguments:** + +| Argument | Type | Default | Description | +|----------|------|---------|-------------| +| `--robot` | str | "franka" | Robot model | +| `--sim` | str | "mujoco" | Simulator backend | +| `--output` | str | "trajectory.rrd" | Output recording file | +| `--num-steps` | int | 200 | Number of simulation steps | +| `--motion-type` | str | "sinusoidal" | Motion type: "sinusoidal" or "random" | +| `--spawn-viewer` | bool | False | Open viewer during recording | + +### 3. Full Demo with IK Solver (Requires GPU) + +For users with GPU and IK solver (PyRoKi or cuRobo): + +```bash +# With PyRoKi IK solver +python get_started/rerun/rerun_demo.py --sim mujoco --dynamic --solver pyroki + +# With cuRobo IK solver +python get_started/rerun/rerun_demo.py --sim mujoco --dynamic --solver curobo + +# Save recording +python get_started/rerun/rerun_demo.py --sim mujoco --dynamic --save-recording demo.rrd +``` + +## Step-by-Step Tutorial + +### Understanding the Rerun Viewer + +When you open a `.rrd` file, the Rerun viewer shows: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ [Entity Tree] │ [3D Viewport] │ +│ │ │ +│ ▼ world │ 🤖 Robot + 📦 Objects │ +│ ▼ franka │ │ +│ panda_link0│ │ +│ panda_link1│ │ +│ ... │ │ +│ ▼ cube │ │ +│ ▼ base │ │ +│ │ │ +├─────────────────┴───────────────────────────────────────────┤ +│ [Timeline] ◀ ▶ ━━━━━●━━━━━━━━━━━━━━━━━━ Step: 42/200 │ +└─────────────────────────────────────────────────────────────┘ +``` + +**Navigation Controls:** +- **Rotate**: Left mouse drag +- **Pan**: Middle mouse drag or Shift+Left drag +- **Zoom**: Scroll wheel +- **Timeline**: Drag the playhead or use play button + +### Creating Your Own Visualization + +Here's how to add Rerun visualization to your own simulation: + +```python +from metasim.utils.rerun.rerun_util import RerunVisualizer + +# 1. Initialize visualizer +visualizer = RerunVisualizer( + app_name="My Simulation", + spawn=True, # Auto-open viewer + save_path="my_sim.rrd" # Optional: save recording +) + +# 2. Add coordinate frame (optional) +visualizer.add_frame("world/origin") + +# 3. Initial visualization of objects and robots +visualizer.visualize_scenario_items(scenario.objects, object_states) +visualizer.visualize_scenario_items(scenario.robots, robot_states) + +# 4. Simulation loop +for step in range(num_steps): + # Set timeline position + visualizer.set_time(step) + + # Run simulation + handler.simulate() + obs = handler.get_states(mode="tensor") + + # Extract and update states + for name, state in robot_states.items(): + visualizer.update_item_pose(name, state) + for name, state in object_states.items(): + visualizer.update_item_pose(name, state) + +# 5. Cleanup +visualizer.close() +``` + +### State Format + +The state dictionary format expected by `update_item_pose`: + +```python +state = { + "pos": [x, y, z], # Position in world frame + "rot": [w, x, y, z], # Quaternion (wxyz format) + "dof_pos": { # Joint positions (for articulated objects) + "joint_name": value, + ... + } +} +``` + +## Comparison with Other Visualizers + +| Feature | Rerun | Native Viewer | Viser | +|---------|-------|---------------|-------| +| Timeline scrubbing | ✅ | ❌ | ❌ | +| Recording/Replay | ✅ `.rrd` | ❌ | ❌ | +| Works on Mac | ✅ | ⚠️ Limited | ✅ | +| No GPU required | ✅ | ⚠️ | ✅ | +| Interactive controls | ❌ | ✅ | ✅ | +| Web-based | ❌ | ❌ | ✅ | + +**Use Rerun when:** +- You need to record and replay simulations +- You want timeline-based exploration +- You're debugging complex trajectories +- You're on macOS without GPU + +**Use Native Viewer when:** +- You need real-time interactive simulation +- You want built-in physics visualization + +**Use Viser when:** +- You need web-based access +- You want interactive joint/IK sliders + +## Troubleshooting + +### Viewer doesn't open automatically + +```bash +# Launch viewer manually +rerun + +# Then run your script with connect mode +python your_script.py +``` + +### URDF/mesh loading issues + +Ensure dependencies are installed: +```bash +pip install trimesh yourdfpy +``` + +### Recording file too large + +Reduce the number of steps or recording frequency: +```bash +python get_started/rerun/replay_task_demo.py --task stack_cube --max-steps 100 --output small.rrd +``` + +### Performance issues on macOS + +Use headless mode for the simulator: +```bash +python get_started/rerun/replay_task_demo.py --task stack_cube --sim mujoco --output stack_cube.rrd +# The simulator runs headless, only Rerun viewer shows +``` + +## Example Output + +After running the stack_cube demo, you'll see: + +| Rerun Viewer | +|:---:| +| ![Rerun Viewer](../../../_static/standard_output/rerun_stack_cube.png) | + +The recording shows: +- Franka robot arm with all links and joints +- Red cube being picked up +- Blue base cube as the target +- Full timeline for scrubbing through the trajectory + +## Files Reference + +| File | Description | +|------|-------------| +| `get_started/rerun/replay_task_demo.py` | Replay pre-recorded task trajectories | +| `get_started/rerun/save_trajectory_simple.py` | CPU-only trajectory recording | +| `get_started/rerun/rerun_demo.py` | Full demo with IK solver | +| `metasim/utils/rerun/rerun_util.py` | Core RerunVisualizer class | +| `metasim/utils/rerun/rerun_env_wrapper.py` | RL environment wrapper | + +## Next Steps + +- Try different tasks: `close_box`, `pick_cube`, `poke_cube` +- Add Rerun visualization to your own training loop +- Explore the [Rerun documentation](https://rerun.io/docs) for advanced features +- Check out the [Viser integration](../advanced/viser/usage.md) for web-based visualization + diff --git a/docs/source/metasim/get_started/quick_start/index.md b/docs/source/metasim/get_started/quick_start/index.md index 3bfaeb84d..84d333586 100644 --- a/docs/source/metasim/get_started/quick_start/index.md +++ b/docs/source/metasim/get_started/quick_start/index.md @@ -21,4 +21,5 @@ guide 14_real_asset 15_gs_background 16_embodiedgen_layout +17_rerun_visualization ``` diff --git a/get_started/0_static_scene.py b/get_started/0_static_scene.py index b50763063..4f05a3c57 100644 --- a/get_started/0_static_scene.py +++ b/get_started/0_static_scene.py @@ -127,7 +127,7 @@ def __post_init__(self): "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), }, "sphere": { - "pos": torch.tensor([0.4, -0.6, 0.05]), + "pos": torch.tensor([0.4, -0.6, 0.1]), "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), }, "bbq_sauce": { diff --git a/get_started/15_gs_background.py b/get_started/15_gs_background.py index f8a2e4004..2383b7714 100644 --- a/get_started/15_gs_background.py +++ b/get_started/15_gs_background.py @@ -6,6 +6,7 @@ import isaacgym # noqa: F401 except ImportError: pass + import numpy as np import rootutils import torch @@ -19,12 +20,17 @@ import cv2 from huggingface_hub import snapshot_download +from tqdm import tqdm from metasim.constants import PhysicStateType, SimType from metasim.scenario.cameras import PinholeCameraCfg +from metasim.scenario.lights import DistantLightCfg, DomeLightCfg, SphereLightCfg from metasim.scenario.objects import RigidObjCfg +from metasim.scenario.robot import RobotCfg from metasim.scenario.scenario import GSSceneCfg, ScenarioCfg +from metasim.types import CameraState from metasim.utils import configclass +from metasim.utils.obs_utils import ObsSaver from metasim.utils.setup_util import get_sim_handler_class @@ -71,6 +77,8 @@ class RealAssetCfg: headless: bool = True with_gs_background: bool = True + num_views: int = 60 + circle_around: bool = False def __post_init__(self): log.info(f"RealAssetCfg: {self}") @@ -104,24 +112,91 @@ def __post_init__(self): headless=args.headless, num_envs=args.num_envs, simulator=args.sim, + decimation=2, gs_scene=GSSceneCfg( with_gs_background=args.with_gs_background, gs_background_path=f"{data_dir}/bg_scenes/scene_{args.scene_id:03d}/gs_model.ply", gs_background_pose_tum=(0, 0, 0, 0, 1, 0, 0), # format: (x, y, z, qx, qy, qz, qw) ), ) + if args.sim == "mujoco": + scenario.decimation *= 10 + + # add lights + scenario.lights = [ + # Main overhead light (Key Light) + SphereLightCfg( + pos=(0.0, 0.0, 2.0), + radius=0.5, + intensity=4000.0, + color=(1.0, 0.99, 0.96), + ), + # Fill lights from 4 directions to soften shadows + DistantLightCfg( + intensity=600.0, + color=(1.0, 1.0, 1.0), + azimuth=0.0, + polar=45.0, + ), + DistantLightCfg( + intensity=600.0, + color=(1.0, 1.0, 1.0), + azimuth=90.0, + polar=45.0, + ), + DistantLightCfg( + intensity=600.0, + color=(1.0, 1.0, 1.0), + azimuth=180.0, + polar=45.0, + ), + DistantLightCfg( + intensity=600.0, + color=(1.0, 1.0, 1.0), + azimuth=270.0, + polar=45.0, + ), + # Ambient light + DomeLightCfg( + intensity=300.0, + color=(0.8, 0.8, 0.9), + ), + ] # add cameras - scenario.cameras = [ + total_num_views = args.num_views + num_cameras = 1 # Only create 1 physical camera to save resources + cameras = [] + radius = 1.0 + height = 1.3 + look_at = (0.0, 0.0, 0.8) # Look at table surface center + center = (0.0, 0.0, 0.0) # Camera center of rotation + + fovy_deg = 75.0 + image_hw = (512, 512) # (H, W) + horizontal_aperture = 20.955 + vertical_aperture = horizontal_aperture * image_hw[0] / image_hw[1] + focal_length = vertical_aperture / (2 * np.tan(np.deg2rad(fovy_deg) / 2)) + + # Create a single camera instance + cameras.append( PinholeCameraCfg( - name="camera", - width=1024, - height=1024, - pos=(2, -1, 1.5), - look_at=(0.0, 0.0, 0.0), - data_types=["rgb", "depth", "instance_seg"], # Enable instance segmentation for GS blending + name="camera_0", + width=image_hw[1], + height=image_hw[0], + focal_length=focal_length, + horizontal_aperture=horizontal_aperture, + pos=(center[0] + radius, center[1], height), # Initial position + look_at=look_at, + data_types=[ + "rgb", + "depth", + "instance_seg", + "instance_id_seg", + ], # Enable instance segmentation for GS blending ) - ] + ) + scenario.cameras = cameras # add objects scenario.objects = [ @@ -129,9 +204,14 @@ def __post_init__(self): name="table", scale=(1, 1, 1), physics=PhysicStateType.RIGIDBODY, + fix_base_link=True, usd_path=f"{data_dir}/demo_assets/table/usd/table.usd", urdf_path=f"{data_dir}/demo_assets/table/result/table.urdf", mjcf_path=f"{data_dir}/demo_assets/table/mjcf/table.xml", + file_type={**RobotCfg.file_type, "isaacgym": "mjcf", "genesis": "mjcf"}, + # You need set pose for fix_base_link object to update usd stage for isaac 5.0. + default_position=(0.0, 0.0, 0.4), + default_orientation=(1.0, 0.0, 0.0, 0.0), ), RigidObjCfg( name="banana", @@ -140,6 +220,7 @@ def __post_init__(self): usd_path=f"{data_dir}/demo_assets/banana/usd/banana.usd", urdf_path=f"{data_dir}/demo_assets/banana/result/banana.urdf", mjcf_path=f"{data_dir}/demo_assets/banana/mjcf/banana.xml", + file_type={**RobotCfg.file_type, "isaacgym": "mjcf", "genesis": "mjcf"}, ), RigidObjCfg( name="book", @@ -148,6 +229,7 @@ def __post_init__(self): usd_path=f"{data_dir}/demo_assets/book/usd/book.usd", urdf_path=f"{data_dir}/demo_assets/book/result/book.urdf", mjcf_path=f"{data_dir}/demo_assets/book/mjcf/book.xml", + file_type={**RobotCfg.file_type, "isaacgym": "mjcf", "genesis": "mjcf"}, ), RigidObjCfg( name="lamp", @@ -156,6 +238,7 @@ def __post_init__(self): usd_path=f"{data_dir}/demo_assets/lamp/usd/lamp.usd", urdf_path=f"{data_dir}/demo_assets/lamp/result/lamp.urdf", mjcf_path=f"{data_dir}/demo_assets/lamp/mjcf/lamp.xml", + file_type={**RobotCfg.file_type, "isaacgym": "mjcf", "genesis": "mjcf"}, ), RigidObjCfg( name="mug", @@ -164,6 +247,7 @@ def __post_init__(self): usd_path=f"{data_dir}/demo_assets/mug/usd/mug.usd", urdf_path=f"{data_dir}/demo_assets/mug/result/mug.urdf", mjcf_path=f"{data_dir}/demo_assets/mug/mjcf/mug.xml", + file_type={**RobotCfg.file_type, "isaacgym": "mjcf", "genesis": "mjcf"}, ), RigidObjCfg( name="remote_control", @@ -172,6 +256,7 @@ def __post_init__(self): usd_path=f"{data_dir}/demo_assets/remote_control/usd/remote_control.usd", urdf_path=f"{data_dir}/demo_assets/remote_control/result/remote_control.urdf", mjcf_path=f"{data_dir}/demo_assets/remote_control/mjcf/remote_control.xml", + file_type={**RobotCfg.file_type, "isaacgym": "mjcf", "genesis": "mjcf"}, ), RigidObjCfg( name="rubiks_cube", @@ -180,6 +265,7 @@ def __post_init__(self): usd_path=f"{data_dir}/demo_assets/rubik's_cube/usd/rubik's_cube.usd", urdf_path=f"{data_dir}/demo_assets/rubik's_cube/result/rubik's_cube.urdf", mjcf_path=f"{data_dir}/demo_assets/rubik's_cube/mjcf/rubik's_cube.xml", + file_type={**RobotCfg.file_type, "isaacgym": "mjcf", "genesis": "mjcf"}, ), RigidObjCfg( name="vase", @@ -188,49 +274,50 @@ def __post_init__(self): usd_path=f"{data_dir}/demo_assets/vase/usd/vase.usd", urdf_path=f"{data_dir}/demo_assets/vase/result/vase.urdf", mjcf_path=f"{data_dir}/demo_assets/vase/mjcf/vase.xml", + file_type={**RobotCfg.file_type, "isaacgym": "mjcf", "genesis": "mjcf"}, ), ] - # set initial states + z_offset = 0.4 # Increase offset to see objects drop init_states = [ { "objects": { "table": { - "pos": torch.tensor([0.4, -0.2, 0.4]), + "pos": torch.tensor([0.0, 0.0, 0.4]), "rot": torch.tensor([1, 0, 0, 0]), }, "banana": { - "pos": torch.tensor([0.28, -0.58, 0.825]), + "pos": torch.tensor([-0.12, -0.38, 0.825 + z_offset]), "rot": torch.tensor([1, 0, 0, 0]), }, "book": { - "pos": torch.tensor([0.3, -0.28, 0.82]), + "pos": torch.tensor([-0.1, -0.08, 0.82 + z_offset]), "rot": torch.tensor([1, 0, 0, 0]), }, "lamp": { - "pos": torch.tensor([0.68, 0.10, 1.05]), + "pos": torch.tensor([0.28, 0.30, 1.05 + z_offset]), "rot": torch.tensor([1, 0, 0, 0]), }, "mug": { - "pos": torch.tensor([0.68, -0.34, 0.863]), + "pos": torch.tensor([0.28, -0.14, 0.863 + z_offset]), "rot": torch.tensor([1, 0, 0, 0]), }, "remote_control": { - "pos": torch.tensor([0.68, -0.54, 0.811]), + "pos": torch.tensor([0.28, -0.34, 0.811 + z_offset]), "rot": torch.tensor([1, 0, 0, 0]), }, "rubiks_cube": { - "pos": torch.tensor([0.48, -0.54, 0.83]), + "pos": torch.tensor([0.08, -0.34, 0.83 + z_offset]), "rot": torch.tensor([1, 0, 0, 0]), }, "vase": { - "pos": torch.tensor([0.30, 0.05, 0.95]), + "pos": torch.tensor([-0.1, 0.25, 0.95 + z_offset]), "rot": torch.tensor([1, 0, 0, 0]), }, }, "robots": { "franka": { - "pos": torch.tensor([0.8, -0.8, 0.78]), + "pos": torch.tensor([0.4, -0.6, 0.78]), "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), "dof_pos": { "panda_joint1": 0.0, @@ -252,16 +339,15 @@ def __post_init__(self): env_class = get_sim_handler_class(SimType(args.sim)) env = env_class(scenario) env.launch() - env.set_states(init_states) + env.set_states(init_states * scenario.num_envs) obs = env.get_states(mode="dict")[0] # get states as a dictionary - # obs_tensor = env.get_states(mode="tensor") # get states as a tensor os.makedirs("get_started/output", exist_ok=True) # save rgb image save_path = f"get_started/output/15_gs_background_{args.sim}.jpg" log.info(f"Saving image to {save_path}") - rgb = obs["cameras"]["camera"]["rgb"] + rgb = obs["cameras"]["camera_0"]["rgb"] if isinstance(rgb, torch.Tensor): rgb = rgb.detach().cpu().numpy() rgb = rgb.squeeze() @@ -271,9 +357,120 @@ def __post_init__(self): # save depth image save_path = f"get_started/output/15_gs_background_{args.sim}_depth.jpg" log.info(f"Saving depth image to {save_path}") - depth = obs["cameras"]["camera"]["depth"] + depth = obs["cameras"]["camera_0"]["depth"] if isinstance(depth, torch.Tensor): depth = depth.detach().cpu().numpy() depth = depth.squeeze() depth_color = depth_to_colormap(depth, inv_depth=True, depth_range=(1.0, 5.0)) cv2.imwrite(save_path, depth_color[:, :, ::-1].copy(), [int(cv2.IMWRITE_JPEG_QUALITY), 95]) # RGB -> BGR + + # Video with rotating camera + obs_saver = ObsSaver(video_path=f"get_started/output/15_gs_background_360_{args.sim}.mp4") + + total_step = total_num_views + robot = scenario.robots[0] if scenario.robots else None + + for idx in tqdm(range(total_step)): + # Update camera pose dynamically + current_view_idx = idx + angle = 0 + if args.circle_around: + angle = 2 * np.pi * current_view_idx / total_num_views + + pos_x = center[0] + radius * np.cos(angle) + pos_y = center[1] + radius * np.sin(angle) + + # Update the single camera's position + if hasattr(env, "cameras") and len(env.cameras) > 0: + env.cameras[0].pos = (pos_x, pos_y, height) + # Look at remains constant + + # Force update camera pose in the simulator + if hasattr(env, "_update_camera_pose"): + env._update_camera_pose() + + # Optional: Add random robot actions if robot exists + if robot is not None: + # Retrieve default pose from init_states for smoother motion + default_dof_pos = init_states[0]["robots"]["franka"]["dof_pos"] + + actions = [ + { + robot.name: { + "dof_pos_target": { + joint_name: ( + max( + robot.joint_limits[joint_name][0], + min( + robot.joint_limits[joint_name][1], + default_dof_pos.get(joint_name, 0.0) + + (torch.rand(1).item() * 2 - 1) * 0.1, # +/- 0.1 rad range + ), + ) + ) + for joint_name in robot.joint_limits.keys() + } + } + } + for _ in range(scenario.num_envs) + ] + env.set_dof_targets(actions) + + # Simulate and get observations + env.simulate() + obs = env.get_states(mode="tensor") + + # Fix RGB tensor dimensions if needed (handle 5D -> 4D conversion) + # ObsSaver expects (num_envs, H, W, C) but some simulators return (num_envs, 1, H, W, C) + + current_cam_name = "camera_0" + if current_cam_name in obs.cameras: + cam_state = obs.cameras[current_cam_name] + if cam_state.rgb is not None and cam_state.rgb.dim() == 5: + # Shape: (num_envs, 1, H, W, C) -> squeeze to (num_envs, H, W, C) + cam_state = CameraState(rgb=cam_state.rgb.squeeze(1), depth=cam_state.depth) + + # Replace cameras dict with only the current camera to create a rotating effect + obs.cameras = {"orbit_camera": cam_state} + + # Visualize depth and append to RGB image + if cam_state.depth is not None: + depth_vis = depth_to_colormap( + cam_state.depth.cpu().numpy(), inv_depth=True, depth_range=(1.0, 5.0) + ) # (H, W, 3) RGB + + depth_tensor = torch.from_numpy(depth_vis.copy()).float() # (H, W, 3) + if cam_state.rgb is not None: + # Resize depth to match rgb if needed (should be same) + # Concatenate vertically: RGB above, Depth below + # RGB shape: (num_envs, H, W, C) or (H, W, C) + rgb_tensor = cam_state.rgb + if rgb_tensor.dim() == 4: + rgb_tensor = rgb_tensor.squeeze(0) # (H, W, C) + + # Handle RGBA case by dropping alpha channel if present + if rgb_tensor.shape[-1] == 4: + rgb_tensor = rgb_tensor[..., :3] + + # Ensure depth_tensor is on same device + depth_tensor = depth_tensor.to(rgb_tensor.device) + + combined_img = torch.cat([rgb_tensor, depth_tensor], dim=0) # Vertical concat (2H, W, C) + + # Update obs with combined image + # We need to wrap it back to (1, H, W, C) if that was the original shape + if cam_state.rgb.dim() == 4: + combined_img = combined_img.unsqueeze(0) + + # Create new camera state + new_cam_state = CameraState(rgb=combined_img, depth=cam_state.depth) + obs.cameras = {"orbit_camera": new_cam_state} + + obs_saver.add(obs) + + obs_saver.save(fps=15) # Set video FPS to 15 + log.info(f"Video saved to get_started/output/15_gs_background_{args.sim}.mp4") + + if hasattr(env, "simulation_app"): + env.close() + env.simulation_app.close() diff --git a/get_started/rerun/README.md b/get_started/rerun/README.md new file mode 100644 index 000000000..08f0534fd --- /dev/null +++ b/get_started/rerun/README.md @@ -0,0 +1,267 @@ +# Rerun Visualization Integration + +This directory contains demos and utilities for visualizing RoboVerse simulations using [Rerun](https://rerun.io/). + +Rerun is an open-source SDK for logging, storing, querying, and visualizing multimodal and multi-rate data. It provides a powerful viewer with timeline-based exploration, making it ideal for robotics simulation visualization. + +## Features + +- **URDF Robot Visualization**: Load and display robot models from URDF files +- **OBJ/Mesh Support**: Visualize mesh-based objects +- **Primitive Shapes**: Cubes, spheres, cylinders with custom colors +- **Dynamic Updates**: Real-time pose and joint state updates during simulation +- **Timeline Playback**: Scrub through simulation history +- **Recording**: Save sessions as `.rrd` files for offline viewing +- **Camera Image Logging**: Display RGB and depth images from cameras + +## Installation + +Install the Rerun SDK: + +```bash +pip install rerun-sdk +``` + +Optional dependencies for full functionality: +```bash +pip install yourdfpy trimesh +``` + +## Quick Start + +### Replay Pre-recorded Task Demo (Recommended) + +The easiest way to get started - replay existing task trajectories without needing GPU or IK solvers: + +```bash +# Replay stack_cube task +python get_started/rerun/replay_task_demo.py --task stack_cube --sim mujoco --output stack_cube.rrd + +# Replay close_box task +python get_started/rerun/replay_task_demo.py --task close_box --sim mujoco --output close_box.rrd + +# With live viewer during recording +python get_started/rerun/replay_task_demo.py --task stack_cube --sim mujoco --spawn-viewer --output stack_cube.rrd + +# View the saved recording +rerun stack_cube.rrd +``` + +### Basic Static Scene + +```bash +# Using MuJoCo simulator +python get_started/rerun/rerun_demo.py --sim mujoco + +# Using PyBullet simulator +python get_started/rerun/rerun_demo.py --sim pybullet +``` + +The Rerun viewer will open automatically and display the scene. + +### Dynamic Simulation with IK + +```bash +# Run dynamic simulation with robot motion +python get_started/rerun/rerun_demo.py --sim mujoco --dynamic + +# With PyRoKi IK solver (default) +python get_started/rerun/rerun_demo.py --sim mujoco --dynamic --solver pyroki + +# With cuRobo IK solver +python get_started/rerun/rerun_demo.py --sim mujoco --dynamic --solver curobo +``` + +### Save Recording + +```bash +# Save session to file for later replay +python get_started/rerun/rerun_demo.py --sim mujoco --dynamic --save-recording my_session.rrd + +# Replay saved recording +rerun my_session.rrd +``` + +### CPU-Only Recording (No GPU Required) + +For users on Mac or systems without GPU, use the simplified script that generates random joint motions directly (no IK solver needed): + +```bash +# Sinusoidal joint motion (smooth, periodic) +python get_started/rerun/save_trajectory_simple.py --sim mujoco --output trajectory.rrd + +# Random joint motion +python get_started/rerun/save_trajectory_simple.py --sim mujoco --motion-type random --output trajectory.rrd + +# With Rerun viewer open during recording +python get_started/rerun/save_trajectory_simple.py --sim mujoco --spawn-viewer + +# Replay saved recording +rerun trajectory.rrd +``` + +## Command Line Arguments + +| Argument | Type | Default | Description | +|----------|------|---------|-------------| +| `--robot` | str | "franka" | Robot model to use | +| `--sim` | str | "mujoco" | Simulator backend (mujoco, pybullet, genesis, etc.) | +| `--num-envs` | int | 1 | Number of parallel environments | +| `--headless` | bool | True | Run simulator headless (use Rerun for visualization) | +| `--dynamic` | bool | False | Enable dynamic simulation with IK motion | +| `--solver` | str | "pyroki" | IK solver ("curobo" or "pyroki") | +| `--save-recording` | str | None | Path to save .rrd recording file | + +## Viewer Controls + +### Navigation +- **Rotate**: Left mouse drag +- **Pan**: Middle mouse drag or Shift+Left drag +- **Zoom**: Scroll wheel + +### Timeline +- Use the timeline at the bottom to scrub through simulation history +- Play/pause button for automatic playback +- Adjust playback speed + +### Entity Tree +- Toggle visibility of individual entities in the left panel +- Expand/collapse hierarchies to explore scene structure + +## Integration with RL Training + +Use the `TaskRerunWrapper` to add real-time visualization during RL training: + +```python +from metasim.utils.rerun.rerun_env_wrapper import TaskRerunWrapper + +# Wrap your environment +env = TaskRerunWrapper( + task_env=your_env, + app_name="RL Training Visualization", + update_freq=10, # Update every 10 steps +) + +# Use normally +obs = env.reset() +for step in range(1000): + action = policy(obs) + obs, reward, done, info = env.step(action) + if done: + obs = env.reset() + +env.close() +``` + +## Programmatic Usage + +### Basic Visualization + +```python +from metasim.utils.rerun.rerun_util import RerunVisualizer + +# Initialize visualizer +visualizer = RerunVisualizer( + app_name="My Robot Visualization", + spawn=True, # Auto-open viewer +) + +# Add coordinate frame +visualizer.add_frame("world/origin") + +# Visualize scenario items +visualizer.visualize_scenario_items(objects, object_states) +visualizer.visualize_scenario_items(robots, robot_states) +``` + +### Updating Poses + +```python +# During simulation loop +for step in range(num_steps): + # ... simulation code ... + + # Set time for timeline + visualizer.set_time(step) + + # Update poses + for name, state in robot_states.items(): + visualizer.update_item_pose(name, state) +``` + +### Logging Trajectories + +```python +# Log individual points +visualizer.log_trajectory_point("ee_target", [0.5, 0.0, 0.6], color=[255, 0, 0]) + +# Log complete trajectory +positions = [[0.3, 0.0, 0.5], [0.4, 0.1, 0.5], [0.5, 0.2, 0.5]] +visualizer.log_trajectory("robot_path", positions, color=[0, 255, 0]) +``` + +### Logging Camera Images + +```python +# Log RGB and depth images +visualizer.log_camera_image("camera_1", rgb_image, depth_image) +``` + +## Comparison with Viser + +| Feature | Rerun | Viser | +|---------|-------|-------| +| Viewer | Desktop app | Web browser | +| Timeline | Built-in scrubbing | Manual implementation | +| Recording | Native .rrd format | Custom | +| IK Control GUI | Not built-in | Built-in sliders | +| Joint Control GUI | Not built-in | Built-in sliders | +| Multi-modal data | Excellent | Limited | + +**Choose Rerun when you need:** +- Timeline-based data exploration +- Recording and replay of sessions +- Multi-modal data logging (images, point clouds, etc.) +- Offline analysis + +**Choose Viser when you need:** +- Interactive control via web GUI +- Real-time joint/IK manipulation +- Browser-based access + +## Troubleshooting + +### Viewer doesn't open +```bash +# Manually launch viewer +rerun + +# Then connect from Python +from metasim.utils.rerun import RerunVisualizer +visualizer = RerunVisualizer(spawn=False, connect=True) +``` + +### URDF loading fails +- Ensure `yourdfpy` and `trimesh` are installed +- Check that mesh files are accessible from URDF paths + +### Performance issues +- Reduce `update_freq` in `TaskRerunWrapper` +- Use `headless=True` to avoid duplicate rendering + +## Files + +- **`replay_task_demo.py`**: Replay pre-recorded task trajectories (CPU-only, no IK solver needed) +- **`rerun_demo.py`**: Main demo script with static and dynamic visualization +- **`save_trajectory.py`**: Save trajectory recording with IK solver (requires GPU) +- **`save_trajectory_simple.py`**: Save trajectory recording without IK solver (CPU-only, Mac compatible) +- **`../../metasim/utils/rerun/rerun_util.py`**: Core visualization utilities +- **`../../metasim/utils/rerun/rerun_env_wrapper.py`**: RL environment wrapper + +## Additional Resources + +- [Rerun Documentation](https://rerun.io/docs) +- [Rerun Python API](https://ref.rerun.io/docs/python/) +- [Rerun GitHub](https://github.com/rerun-io/rerun) +- [RoboVerse Viser Integration](../viser/README.md) - Alternative web-based visualizer + diff --git a/get_started/rerun/replay_task_demo.py b/get_started/rerun/replay_task_demo.py new file mode 100644 index 000000000..83c3fa04d --- /dev/null +++ b/get_started/rerun/replay_task_demo.py @@ -0,0 +1,275 @@ +"""Replay an existing task demo and save as Rerun recording (.rrd). + +This example demonstrates: +- Loading and replaying a pre-recorded task trajectory (e.g., stack_cube) +- Recording all states to Rerun timeline +- Saving as .rrd file for later replay + +No GPU or IK solver needed - just replays existing trajectories! + +Usage: + # Replay stack_cube task + python get_started/rerun/replay_task_demo.py --task stack_cube --sim mujoco --output stack_cube.rrd + + # Replay close_box task + python get_started/rerun/replay_task_demo.py --task close_box --sim mujoco --output close_box.rrd + + # With live viewer + python get_started/rerun/replay_task_demo.py --task stack_cube --sim mujoco --spawn-viewer + + # Replay the saved recording: + rerun stack_cube.rrd +""" + +from __future__ import annotations + +import os +from typing import Literal + +try: + import isaacgym # noqa: F401 +except ImportError: + pass + +import rootutils +import torch +import tyro +from loguru import logger as log +from rich.logging import RichHandler + +rootutils.setup_root(__file__, pythonpath=True) +log.configure(handlers=[{"sink": RichHandler(), "format": "{message}"}]) + +from metasim.scenario.cameras import PinholeCameraCfg +from metasim.task.registry import get_task_class +from metasim.utils import configclass +from metasim.utils.demo_util import get_traj +from metasim.utils.hf_util import check_and_download_recursive +from metasim.utils.state import state_tensor_to_nested + + +@configclass +class Args: + """Arguments for replaying task demo and saving Rerun recording.""" + + task: str = "stack_cube" + """Task name to replay (e.g., stack_cube, close_box, pick_cube, etc.).""" + + robot: str = "franka" + """Robot to use.""" + + sim: Literal[ + "isaacsim", + "isaacgym", + "isaaclab", + "genesis", + "pybullet", + "sapien2", + "sapien3", + "mujoco", + ] = "mujoco" + """Simulator backend.""" + + num_envs: int = 1 + """Number of parallel environments.""" + + output: str = "task_replay.rrd" + """Output path for the .rrd recording file.""" + + spawn_viewer: bool = False + """Whether to spawn Rerun viewer during recording.""" + + max_steps: int | None = None + """Maximum number of steps to record. If None, replay entire trajectory.""" + + scene: str | None = None + """Optional scene override.""" + + +def extract_states_from_obs(obs, handler, key): + """Extract states from observation tensor.""" + env_states = state_tensor_to_nested(handler, obs) + result = {} + if env_states and len(env_states) > 0: + state = env_states[0] + if key in state: + for name, item in state[key].items(): + state_dict = {} + if "pos" in item and item["pos"] is not None: + state_dict["pos"] = ( + item["pos"].cpu().numpy().tolist() if hasattr(item["pos"], "cpu") else list(item["pos"]) + ) + if "rot" in item and item["rot"] is not None: + state_dict["rot"] = ( + item["rot"].cpu().numpy().tolist() if hasattr(item["rot"], "cpu") else list(item["rot"]) + ) + if "dof_pos" in item and item["dof_pos"] is not None: + state_dict["dof_pos"] = item["dof_pos"] + result[name] = state_dict + return result + + +def download_urdf_files(scenario): + """Download URDF files for visualization.""" + urdf_paths = [] + for obj in scenario.objects: + if hasattr(obj, "urdf_path") and obj.urdf_path: + urdf_paths.append(obj.urdf_path) + for robot in scenario.robots: + if hasattr(robot, "urdf_path") and robot.urdf_path: + urdf_paths.append(robot.urdf_path) + if urdf_paths: + log.info(f"Downloading {len(urdf_paths)} URDF files...") + check_and_download_recursive(urdf_paths, n_processes=16) + + +def get_actions(all_actions, action_idx: int, num_envs: int): + """Get actions for current step across all environments.""" + envs_actions = all_actions[:num_envs] + return [ + env_actions[action_idx] if action_idx < len(env_actions) else env_actions[-1] for env_actions in envs_actions + ] + + +def get_runout(all_actions, action_idx: int): + """Check if all environments have run out of actions.""" + return all([action_idx >= len(all_actions[i]) for i in range(len(all_actions))]) + + +def main(): + args = tyro.cli(Args) + + log.info(f"Task: {args.task}") + log.info(f"Recording to: {args.output}") + log.info("NOTE: This script replays pre-recorded trajectories (no IK solver needed)") + + # ======================================================================== + # Setup Task and Environment + # ======================================================================== + task_cls = get_task_class(args.task) + + camera = PinholeCameraCfg( + name="camera", + pos=(1.5, -1.5, 1.5), + look_at=(0.0, 0.0, 0.0), + width=640, + height=480, + ) + + scenario = task_cls.scenario.update( + robots=[args.robot], + scene=args.scene, + cameras=[camera], + simulator=args.sim, + num_envs=args.num_envs, + headless=True, + ) + + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + log.info(f"Using device: {device}") + + env = task_cls(scenario, device=device) + + # ======================================================================== + # Load Trajectory + # ======================================================================== + traj_filepath = env.traj_filepath + if not os.path.exists(traj_filepath): + log.error(f"Trajectory file not found: {traj_filepath}") + log.info("Attempting to download trajectory file...") + check_and_download_recursive([traj_filepath], n_processes=4) + + assert os.path.exists(traj_filepath), f"Trajectory file: {traj_filepath} does not exist." + log.info(f"Loading trajectory from: {traj_filepath}") + + init_states, all_actions, _ = get_traj(traj_filepath, scenario.robots[0], env.handler) + + # Get total number of steps + max_traj_steps = max(len(actions) for actions in all_actions) + num_steps = min(args.max_steps, max_traj_steps) if args.max_steps else max_traj_steps + log.info(f"Trajectory has {max_traj_steps} steps, will record {num_steps} steps") + + # ======================================================================== + # Setup Rerun Recording + # ======================================================================== + from metasim.utils.rerun.rerun_util import RerunVisualizer + + download_urdf_files(scenario) + + visualizer = RerunVisualizer( + app_name=f"Task Replay: {args.task}", + spawn=args.spawn_viewer, + save_path=args.output, + ) + visualizer.add_frame("world/origin") + + # Reset environment and get initial observation + obs, _ = env.reset() + + # Initial visualization + default_object_states = extract_states_from_obs(obs, env.handler, "objects") + default_robot_states = extract_states_from_obs(obs, env.handler, "robots") + visualizer.visualize_scenario_items(scenario.objects, default_object_states) + visualizer.visualize_scenario_items(scenario.robots, default_robot_states) + + # Log initial state + visualizer.set_time(0) + for name, state in default_object_states.items(): + visualizer.update_item_pose(name, state) + for name, state in default_robot_states.items(): + visualizer.update_item_pose(name, state) + + # ======================================================================== + # Replay Trajectory and Record + # ======================================================================== + log.info("Starting trajectory replay and recording...") + + for step in range(num_steps): + visualizer.set_time(step + 1) # +1 because we logged initial state at t=0 + + # Get actions for this step + actions = get_actions(all_actions, step, args.num_envs) + + # Step the environment + obs, reward, success, time_out, extras = env.step(actions) + + # Update visualization + object_states = extract_states_from_obs(obs, env.handler, "objects") + robot_states = extract_states_from_obs(obs, env.handler, "robots") + + for name, state in object_states.items(): + visualizer.update_item_pose(name, state) + for name, state in robot_states.items(): + visualizer.update_item_pose(name, state) + + # Log progress + if step % 20 == 0: + log.info(f"Recording step {step}/{num_steps}") + + # Check for success + if success.any(): + log.info(f"Task succeeded at step {step}!") + + # Check if all trajectories are exhausted + if get_runout(all_actions, step + 1): + log.info(f"Trajectory exhausted at step {step + 1}") + break + + visualizer.close() + env.close() + + log.info("") + log.info("=" * 60) + log.info("Recording Complete!") + log.info("=" * 60) + log.info(f"Task: {args.task}") + log.info(f"Saved to: {args.output}") + log.info(f"Total steps: {step + 1}") + log.info("") + log.info("To replay the recording:") + log.info(f" rerun {args.output}") + log.info("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/get_started/rerun/rerun_demo.py b/get_started/rerun/rerun_demo.py new file mode 100644 index 000000000..2f47a9a28 --- /dev/null +++ b/get_started/rerun/rerun_demo.py @@ -0,0 +1,405 @@ +"""Comprehensive Rerun visualization demo with multiple control modes. + +This demo supports: +- Static/Dynamic scene visualization +- URDF robot and object visualization +- Primitive shapes (cubes, spheres) +- Trajectory visualization +- Camera image logging + +Rerun is an open-source SDK for logging, storing, querying, and visualizing +multimodal data. It provides a powerful viewer with timeline-based exploration. + +Usage: + python get_started/rerun/rerun_demo.py --sim mujoco + python get_started/rerun/rerun_demo.py --sim pybullet --dynamic + python get_started/rerun/rerun_demo.py --sim mujoco --save-recording output.rrd +""" + +from __future__ import annotations + +from typing import Literal + +try: + import isaacgym # noqa: F401 +except ImportError: + pass + +import rootutils +import torch +import tyro +from loguru import logger as log +from rich.logging import RichHandler + +rootutils.setup_root(__file__, pythonpath=True) +log.configure(handlers=[{"sink": RichHandler(), "format": "{message}"}]) + +from metasim.constants import PhysicStateType +from metasim.scenario.cameras import PinholeCameraCfg +from metasim.scenario.objects import ( + ArticulationObjCfg, + PrimitiveCubeCfg, + PrimitiveSphereCfg, + RigidObjCfg, +) +from metasim.scenario.scenario import ScenarioCfg +from metasim.utils import configclass +from metasim.utils.hf_util import check_and_download_recursive +from metasim.utils.setup_util import get_handler +from metasim.utils.state import state_tensor_to_nested + + +@configclass +class Args: + """Arguments for the Rerun demo.""" + + robot: str = "franka" + """Robot to use in the demo.""" + + ## Simulator + sim: Literal[ + "isaacsim", + "isaacgym", + "isaaclab", + "genesis", + "pybullet", + "sapien2", + "sapien3", + "mujoco", + ] = "mujoco" + """Simulator backend to use.""" + + num_envs: int = 1 + """Number of parallel environments.""" + + headless: bool = True + """Run simulator headless (use Rerun for visualization).""" + + ## Control modes + dynamic: bool = False + """Enable dynamic simulation with IK motion.""" + + ## IK solver (only used if dynamic=True) + solver: Literal["curobo", "pyroki"] = "pyroki" + """IK solver to use for dynamic motion.""" + + ## Recording + save_recording: str | None = None + """Path to save Rerun recording file (.rrd). If None, no recording is saved.""" + + def __post_init__(self): + """Post-initialization configuration.""" + log.info(f"Args: {self}") + + +def extract_states_from_obs(obs, handler, key): + """Extract states from observation tensor. + + Args: + obs: TensorState observation + handler: Simulator handler + key: "objects" or "robots" + + Returns: + dict[name] = {"pos": ..., "rot": ..., "dof_pos": ...} + """ + env_states = state_tensor_to_nested(handler, obs) + result = {} + if env_states and len(env_states) > 0: + state = env_states[0] + if key in state: + for name, item in state[key].items(): + state_dict = {} + if "pos" in item and item["pos"] is not None: + state_dict["pos"] = ( + item["pos"].cpu().numpy().tolist() if hasattr(item["pos"], "cpu") else list(item["pos"]) + ) + if "rot" in item and item["rot"] is not None: + state_dict["rot"] = ( + item["rot"].cpu().numpy().tolist() if hasattr(item["rot"], "cpu") else list(item["rot"]) + ) + if "dof_pos" in item and item["dof_pos"] is not None: + state_dict["dof_pos"] = item["dof_pos"] + result[name] = state_dict + return result + + +def download_urdf_files(scenario): + """Download URDF files for all objects and robots in the scenario.""" + urdf_paths = [] + + for obj in scenario.objects: + if hasattr(obj, "urdf_path") and obj.urdf_path: + urdf_paths.append(obj.urdf_path) + + for robot in scenario.robots: + if hasattr(robot, "urdf_path") and robot.urdf_path: + urdf_paths.append(robot.urdf_path) + + if urdf_paths: + log.info(f"Downloading {len(urdf_paths)} URDF files and all related meshes...") + check_and_download_recursive(urdf_paths, n_processes=16) + log.info("URDF files and meshes download completed!") + + +def main(): + args = tyro.cli(Args) + + # ======================================================================== + # Setup Scenario + # ======================================================================== + scenario = ScenarioCfg( + robots=[args.robot], + simulator=args.sim, + headless=args.headless, + num_envs=args.num_envs, + ) + + # Add cameras + scenario.cameras = [ + PinholeCameraCfg( + name="camera", + width=640, + height=480, + pos=(1.5, -1.5, 1.5), + look_at=(0.0, 0.0, 0.0), + ) + ] + + # Add objects for demonstration + scenario.objects = [ + PrimitiveCubeCfg( + name="cube", + size=(0.1, 0.1, 0.1), + color=[1.0, 0.0, 0.0], + physics=PhysicStateType.RIGIDBODY, + ), + PrimitiveSphereCfg( + name="sphere", + radius=0.1, + color=[0.0, 0.0, 1.0], + physics=PhysicStateType.RIGIDBODY, + ), + RigidObjCfg( + name="bbq_sauce", + scale=(2.0, 2.0, 2.0), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/libero/COMMON/stable_hope_objects/bbq_sauce/usd/bbq_sauce.usd", + urdf_path="roboverse_data/assets/libero/COMMON/stable_hope_objects/bbq_sauce/urdf/bbq_sauce.urdf", + mjcf_path="roboverse_data/assets/libero/COMMON/stable_hope_objects/bbq_sauce/mjcf/bbq_sauce.xml", + ), + ArticulationObjCfg( + name="box_base", + fix_base_link=True, + usd_path="roboverse_data/assets/rlbench/close_box/box_base/usd/box_base.usd", + urdf_path="roboverse_data/assets/rlbench/close_box/box_base/urdf/box_base_unique.urdf", + mjcf_path="roboverse_data/assets/rlbench/close_box/box_base/mjcf/box_base_unique.mjcf", + ), + ] + + log.info(f"Using simulator: {args.sim}") + handler = get_handler(scenario) + + # Set initial states + init_states = [ + { + "objects": { + "cube": { + "pos": torch.tensor([0.3, -0.2, 0.05]), + "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), + }, + "sphere": { + "pos": torch.tensor([0.4, -0.6, 0.1]), + "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), + }, + "bbq_sauce": { + "pos": torch.tensor([0.7, -0.3, 0.14]), + "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), + }, + "box_base": { + "pos": torch.tensor([0.5, 0.2, 0.1]), + "rot": torch.tensor([0.0, 0.7071, 0.0, 0.7071]), + "dof_pos": {"box_joint": 0.0}, + }, + }, + "robots": { + "franka": { + "pos": torch.tensor([0.0, 0.0, 0.0]), + "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), + "dof_pos": { + "panda_joint1": 0.0, + "panda_joint2": -0.785398, + "panda_joint3": 0.0, + "panda_joint4": -2.356194, + "panda_joint5": 0.0, + "panda_joint6": 1.570796, + "panda_joint7": 0.785398, + "panda_finger_joint1": 0.04, + "panda_finger_joint2": 0.04, + }, + }, + }, + } + ] + + handler.set_states(init_states * scenario.num_envs) + obs = handler.get_states(mode="tensor") + + # ======================================================================== + # Setup Rerun Visualization + # ======================================================================== + from metasim.utils.rerun.rerun_util import RerunVisualizer + + # Download URDF files before visualization + download_urdf_files(scenario) + + # Initialize the Rerun visualizer + visualizer = RerunVisualizer( + app_name="RoboVerse Demo", + spawn=True, + save_path=args.save_recording, + ) + visualizer.add_frame("world/origin") + + # Extract states from objects and robots + default_object_states = extract_states_from_obs(obs, handler, "objects") + default_robot_states = extract_states_from_obs(obs, handler, "robots") + + # Visualize all objects and robots + visualizer.visualize_scenario_items(scenario.objects, default_object_states) + visualizer.visualize_scenario_items(scenario.robots, default_robot_states) + + log.info("Rerun visualization initialized! The viewer should have opened automatically.") + + # Scene info + scene_info = ["Scene includes:"] + for obj in scenario.objects: + scene_info.append(f" • {obj.name} ({type(obj).__name__})") + for robot in scenario.robots: + scene_info.append(f" • {robot.name} ({type(robot).__name__})") + log.info("\n".join(scene_info)) + + # ======================================================================== + # Dynamic Simulation Loop (if enabled) + # ======================================================================== + if args.dynamic: + log.info("Starting dynamic simulation with IK motion...") + + from metasim.utils.ik_solver import process_gripper_command, setup_ik_solver + + robot = scenario.robots[0] + + log.info(f"Using IK solver: {args.solver}") + ik_solver = setup_ik_solver(robot, args.solver) + + trajectory_points = [] + + for step in range(200): + states = handler.get_states(mode="tensor") + visualizer.set_time(step) + + # Generate target end-effector pose + if robot.name == "franka": + x_target = 0.3 + 0.1 * (step / 100) + y_target = 0.5 - 0.5 * (step / 100) + z_target = 0.6 - 0.2 * (step / 100) + ee_pos_target = torch.zeros((args.num_envs, 3), device="cuda:0") + for i in range(args.num_envs): + if i % 3 == 0: + ee_pos_target[i] = torch.tensor([x_target, 0.0, 0.6], device="cuda:0") + elif i % 3 == 1: + ee_pos_target[i] = torch.tensor([0.3, y_target, 0.6], device="cuda:0") + else: + ee_pos_target[i] = torch.tensor([0.3, 0.0, z_target], device="cuda:0") + ee_quat_target = torch.tensor( + [[0.0, 1.0, 0.0, 0.0]] * args.num_envs, + device="cuda:0", + ) + else: + # Default motion for other robots + ee_pos_target = torch.tensor([[0.3, 0.0, 0.6]], device="cuda:0").repeat(args.num_envs, 1) + ee_quat_target = torch.tensor([[0.0, 1.0, 0.0, 0.0]] * args.num_envs, device="cuda:0") + + # Log target position as trajectory point + target_pos = ee_pos_target[0].cpu().numpy().tolist() + trajectory_points.append(target_pos) + visualizer.log_trajectory_point("ee_target", target_pos, color=[255, 0, 0]) + + # Get current robot state for IK seeding + curr_robot_q = states.robots[robot.name].joint_pos.cuda() + + # Solve IK + q_solution, ik_succ = ik_solver.solve_ik_batch(ee_pos_target, ee_quat_target, curr_robot_q) + + # Process gripper command (fixed open position) + gripper_binary = torch.ones(scenario.num_envs, device="cuda:0") # all open + gripper_widths = process_gripper_command(gripper_binary, robot, "cuda:0") + + # Compose full joint command + actions = ik_solver.compose_joint_action(q_solution, gripper_widths, curr_robot_q, return_dict=True) + + handler.set_dof_targets(actions) + handler.simulate() + obs = handler.get_states(mode="tensor") + + # Settle for first step + if step == 0: + for _ in range(50): + handler.simulate() + obs = handler.get_states(mode="tensor") + + # Update visualization + object_states = extract_states_from_obs(obs, handler, "objects") + robot_states = extract_states_from_obs(obs, handler, "robots") + + for name, state in object_states.items(): + visualizer.update_item_pose(name, state) + for name, state in robot_states.items(): + visualizer.update_item_pose(name, state) + + if step % 10 == 0: + log.info(f"Step {step}/200 completed") + + # Log complete trajectory + if trajectory_points: + visualizer.log_trajectory("ee_trajectory", trajectory_points, color=[0, 255, 0]) + + log.info("Dynamic simulation completed!") + + # ======================================================================== + # Print Usage Instructions + # ======================================================================== + log.info("") + log.info("=" * 70) + log.info("Rerun Demo Ready!") + log.info("=" * 70) + + mode_description = "Static Scene" if not args.dynamic else "Dynamic Scene (simulation completed)" + log.info(f"Mode: {mode_description}") + + log.info("\nRerun Viewer Controls:") + log.info(" • Rotate: Left mouse drag") + log.info(" • Pan: Middle mouse drag or Shift+Left drag") + log.info(" • Zoom: Scroll wheel") + log.info(" • Timeline: Use the timeline at the bottom to scrub through simulation") + + if args.save_recording: + log.info(f"\nRecording saved to: {args.save_recording}") + log.info("You can replay it with: rerun {args.save_recording}") + + log.info("=" * 70) + + # Keep running for static mode + if not args.dynamic: + log.info("\nPress Ctrl+C to exit...") + try: + while True: + pass + except KeyboardInterrupt: + log.info("\nShutting down...") + + visualizer.close() + + +if __name__ == "__main__": + main() diff --git a/get_started/rerun/save_trajectory.py b/get_started/rerun/save_trajectory.py new file mode 100644 index 000000000..93f254ff0 --- /dev/null +++ b/get_started/rerun/save_trajectory.py @@ -0,0 +1,315 @@ +"""Save simulation trajectory as Rerun recording file (.rrd). + +This example demonstrates: +- Running a dynamic simulation with robot motion +- Recording all states to Rerun timeline +- Saving as .rrd file for later replay + +Usage: + python get_started/rerun/save_trajectory.py --sim mujoco --output trajectory.rrd + + # Replay the saved recording: + rerun trajectory.rrd +""" + +from __future__ import annotations + +from typing import Literal + +try: + import isaacgym # noqa: F401 +except ImportError: + pass + +import rootutils +import torch +import tyro +from loguru import logger as log +from rich.logging import RichHandler + +rootutils.setup_root(__file__, pythonpath=True) +log.configure(handlers=[{"sink": RichHandler(), "format": "{message}"}]) + +from metasim.constants import PhysicStateType +from metasim.scenario.cameras import PinholeCameraCfg +from metasim.scenario.objects import ( + PrimitiveCubeCfg, + PrimitiveSphereCfg, + RigidObjCfg, +) +from metasim.scenario.scenario import ScenarioCfg +from metasim.utils import configclass +from metasim.utils.hf_util import check_and_download_recursive +from metasim.utils.setup_util import get_handler +from metasim.utils.state import state_tensor_to_nested + + +@configclass +class Args: + """Arguments for saving trajectory recording.""" + + robot: str = "franka" + """Robot to use.""" + + sim: Literal[ + "isaacsim", + "isaacgym", + "isaaclab", + "genesis", + "pybullet", + "sapien2", + "sapien3", + "mujoco", + ] = "mujoco" + """Simulator backend.""" + + num_envs: int = 1 + """Number of parallel environments.""" + + output: str = "trajectory.rrd" + """Output path for the .rrd recording file.""" + + num_steps: int = 200 + """Number of simulation steps to record.""" + + solver: Literal["curobo", "pyroki"] = "pyroki" + """IK solver to use.""" + + spawn_viewer: bool = False + """Whether to spawn Rerun viewer during recording.""" + + +def extract_states_from_obs(obs, handler, key): + """Extract states from observation tensor.""" + env_states = state_tensor_to_nested(handler, obs) + result = {} + if env_states and len(env_states) > 0: + state = env_states[0] + if key in state: + for name, item in state[key].items(): + state_dict = {} + if "pos" in item and item["pos"] is not None: + state_dict["pos"] = ( + item["pos"].cpu().numpy().tolist() if hasattr(item["pos"], "cpu") else list(item["pos"]) + ) + if "rot" in item and item["rot"] is not None: + state_dict["rot"] = ( + item["rot"].cpu().numpy().tolist() if hasattr(item["rot"], "cpu") else list(item["rot"]) + ) + if "dof_pos" in item and item["dof_pos"] is not None: + state_dict["dof_pos"] = item["dof_pos"] + result[name] = state_dict + return result + + +def download_urdf_files(scenario): + """Download URDF files for visualization.""" + urdf_paths = [] + for obj in scenario.objects: + if hasattr(obj, "urdf_path") and obj.urdf_path: + urdf_paths.append(obj.urdf_path) + for robot in scenario.robots: + if hasattr(robot, "urdf_path") and robot.urdf_path: + urdf_paths.append(robot.urdf_path) + if urdf_paths: + log.info(f"Downloading {len(urdf_paths)} URDF files...") + check_and_download_recursive(urdf_paths, n_processes=16) + + +def main(): + args = tyro.cli(Args) + + log.info(f"Recording trajectory to: {args.output}") + log.info(f"Simulation steps: {args.num_steps}") + + # ======================================================================== + # Setup Scenario + # ======================================================================== + scenario = ScenarioCfg( + robots=[args.robot], + simulator=args.sim, + headless=True, + num_envs=args.num_envs, + ) + + scenario.cameras = [ + PinholeCameraCfg( + name="camera", + width=640, + height=480, + pos=(1.5, -1.5, 1.5), + look_at=(0.0, 0.0, 0.0), + ) + ] + + scenario.objects = [ + PrimitiveCubeCfg( + name="cube", + size=(0.06, 0.06, 0.06), + color=[1.0, 0.3, 0.3], + physics=PhysicStateType.RIGIDBODY, + ), + PrimitiveSphereCfg( + name="sphere", + radius=0.04, + color=[0.3, 0.3, 1.0], + physics=PhysicStateType.RIGIDBODY, + ), + RigidObjCfg( + name="bbq_sauce", + scale=(1.0, 1.0, 1.0), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/libero/COMMON/stable_hope_objects/bbq_sauce/usd/bbq_sauce.usd", + urdf_path="roboverse_data/assets/libero/COMMON/stable_hope_objects/bbq_sauce/urdf/bbq_sauce.urdf", + mjcf_path="roboverse_data/assets/libero/COMMON/stable_hope_objects/bbq_sauce/mjcf/bbq_sauce.xml", + ), + ] + + handler = get_handler(scenario) + + # Set initial states + init_states = [ + { + "objects": { + "cube": { + "pos": torch.tensor([0.4, 0.2, 0.03]), + "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), + }, + "sphere": { + "pos": torch.tensor([0.5, -0.2, 0.04]), + "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), + }, + "bbq_sauce": { + "pos": torch.tensor([0.6, 0.0, 0.14]), + "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), + }, + }, + "robots": { + "franka": { + "pos": torch.tensor([0.0, 0.0, 0.0]), + "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), + "dof_pos": { + "panda_joint1": 0.0, + "panda_joint2": -0.785398, + "panda_joint3": 0.0, + "panda_joint4": -2.356194, + "panda_joint5": 0.0, + "panda_joint6": 1.570796, + "panda_joint7": 0.785398, + "panda_finger_joint1": 0.04, + "panda_finger_joint2": 0.04, + }, + }, + }, + } + ] + + handler.set_states(init_states * scenario.num_envs) + obs = handler.get_states(mode="tensor") + + # ======================================================================== + # Setup Rerun Recording + # ======================================================================== + from metasim.utils.rerun.rerun_util import RerunVisualizer + + download_urdf_files(scenario) + + visualizer = RerunVisualizer( + app_name="Trajectory Recording", + spawn=args.spawn_viewer, + save_path=args.output, + ) + visualizer.add_frame("world/origin") + + # Initial visualization + default_object_states = extract_states_from_obs(obs, handler, "objects") + default_robot_states = extract_states_from_obs(obs, handler, "robots") + visualizer.visualize_scenario_items(scenario.objects, default_object_states) + visualizer.visualize_scenario_items(scenario.robots, default_robot_states) + + # ======================================================================== + # Setup IK Solver + # ======================================================================== + from metasim.utils.ik_solver import process_gripper_command, setup_ik_solver + + robot = scenario.robots[0] + ik_solver = setup_ik_solver(robot, args.solver) + + # ======================================================================== + # Run Simulation and Record + # ======================================================================== + log.info("Starting trajectory recording...") + + trajectory_points = [] + + for step in range(args.num_steps): + states = handler.get_states(mode="tensor") + visualizer.set_time(step) + + # Generate circular motion for end effector + t = step / args.num_steps * 2 * 3.14159 + radius = 0.15 + x_target = 0.4 + radius * torch.cos(torch.tensor(t)) + y_target = radius * torch.sin(torch.tensor(t)) + z_target = 0.4 + 0.1 * torch.sin(torch.tensor(t * 2)) + + ee_pos_target = torch.tensor([[x_target, y_target, z_target]], device="cuda:0").repeat(args.num_envs, 1) + ee_quat_target = torch.tensor([[0.0, 1.0, 0.0, 0.0]] * args.num_envs, device="cuda:0") + + # Log target position + target_pos = ee_pos_target[0].cpu().numpy().tolist() + trajectory_points.append(target_pos) + visualizer.log_trajectory_point("ee_target", target_pos, color=[255, 100, 100]) + + # Solve IK + curr_robot_q = states.robots[robot.name].joint_pos.cuda() + q_solution, ik_succ = ik_solver.solve_ik_batch(ee_pos_target, ee_quat_target, curr_robot_q) + + # Gripper control + gripper_binary = torch.ones(scenario.num_envs, device="cuda:0") + gripper_widths = process_gripper_command(gripper_binary, robot, "cuda:0") + + # Apply actions + actions = ik_solver.compose_joint_action(q_solution, gripper_widths, curr_robot_q, return_dict=True) + handler.set_dof_targets(actions) + handler.simulate() + obs = handler.get_states(mode="tensor") + + # Settle physics on first step + if step == 0: + for _ in range(30): + handler.simulate() + obs = handler.get_states(mode="tensor") + + # Update visualization + object_states = extract_states_from_obs(obs, handler, "objects") + robot_states = extract_states_from_obs(obs, handler, "robots") + + for name, state in object_states.items(): + visualizer.update_item_pose(name, state) + for name, state in robot_states.items(): + visualizer.update_item_pose(name, state) + + if step % 20 == 0: + log.info(f"Recording step {step}/{args.num_steps}") + + # Log complete trajectory + if trajectory_points: + visualizer.log_trajectory("ee_trajectory", trajectory_points, color=[100, 255, 100]) + + visualizer.close() + + log.info("") + log.info("=" * 60) + log.info("Recording Complete!") + log.info("=" * 60) + log.info(f"Saved to: {args.output}") + log.info(f"Total steps: {args.num_steps}") + log.info("") + log.info("To replay the recording:") + log.info(f" rerun {args.output}") + log.info("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/get_started/rerun/save_trajectory_simple.py b/get_started/rerun/save_trajectory_simple.py new file mode 100644 index 000000000..ef9067ad9 --- /dev/null +++ b/get_started/rerun/save_trajectory_simple.py @@ -0,0 +1,355 @@ +"""Save simulation trajectory as Rerun recording file (.rrd) - CPU-only version. + +This is a simplified version that doesn't require GPU or IK solvers (pyroki/curobo). +It generates random/sinusoidal joint motions directly. + +Usage: + python get_started/rerun/save_trajectory_simple.py --sim mujoco --output trajectory.rrd + + # Replay the saved recording: + rerun trajectory.rrd +""" + +from __future__ import annotations + +import math +from typing import Literal + +try: + import isaacgym # noqa: F401 +except ImportError: + pass + +import rootutils +import torch +import tyro +from loguru import logger as log +from rich.logging import RichHandler + +rootutils.setup_root(__file__, pythonpath=True) +log.configure(handlers=[{"sink": RichHandler(), "format": "{message}"}]) + +from metasim.constants import PhysicStateType +from metasim.scenario.cameras import PinholeCameraCfg +from metasim.scenario.objects import ( + PrimitiveCubeCfg, + PrimitiveSphereCfg, + RigidObjCfg, +) +from metasim.scenario.scenario import ScenarioCfg +from metasim.utils import configclass +from metasim.utils.hf_util import check_and_download_recursive +from metasim.utils.setup_util import get_handler +from metasim.utils.state import state_tensor_to_nested + + +@configclass +class Args: + """Arguments for saving trajectory recording (CPU-only, no IK solver needed).""" + + robot: str = "franka" + """Robot to use.""" + + sim: Literal[ + "isaacsim", + "isaacgym", + "isaaclab", + "genesis", + "pybullet", + "sapien2", + "sapien3", + "mujoco", + ] = "mujoco" + """Simulator backend.""" + + num_envs: int = 1 + """Number of parallel environments.""" + + output: str = "trajectory.rrd" + """Output path for the .rrd recording file.""" + + num_steps: int = 200 + """Number of simulation steps to record.""" + + spawn_viewer: bool = False + """Whether to spawn Rerun viewer during recording.""" + + motion_type: Literal["sinusoidal", "random"] = "sinusoidal" + """Type of joint motion to generate.""" + + +def extract_states_from_obs(obs, handler, key): + """Extract states from observation tensor.""" + env_states = state_tensor_to_nested(handler, obs) + result = {} + if env_states and len(env_states) > 0: + state = env_states[0] + if key in state: + for name, item in state[key].items(): + state_dict = {} + if "pos" in item and item["pos"] is not None: + state_dict["pos"] = ( + item["pos"].cpu().numpy().tolist() if hasattr(item["pos"], "cpu") else list(item["pos"]) + ) + if "rot" in item and item["rot"] is not None: + state_dict["rot"] = ( + item["rot"].cpu().numpy().tolist() if hasattr(item["rot"], "cpu") else list(item["rot"]) + ) + if "dof_pos" in item and item["dof_pos"] is not None: + state_dict["dof_pos"] = item["dof_pos"] + result[name] = state_dict + return result + + +def download_urdf_files(scenario): + """Download URDF files for visualization.""" + urdf_paths = [] + for obj in scenario.objects: + if hasattr(obj, "urdf_path") and obj.urdf_path: + urdf_paths.append(obj.urdf_path) + for robot in scenario.robots: + if hasattr(robot, "urdf_path") and robot.urdf_path: + urdf_paths.append(robot.urdf_path) + if urdf_paths: + log.info(f"Downloading {len(urdf_paths)} URDF files...") + check_and_download_recursive(urdf_paths, n_processes=16) + + +def generate_franka_joint_targets(step: int, num_steps: int, motion_type: str) -> dict: + """Generate joint targets for Franka robot without IK solver. + + Args: + step: Current step number + num_steps: Total number of steps + motion_type: "sinusoidal" or "random" + + Returns: + Dictionary mapping joint names to target positions + """ + # Franka joint limits (approximate, in radians) + joint_limits = { + "panda_joint1": (-2.8973, 2.8973), + "panda_joint2": (-1.7628, 1.7628), + "panda_joint3": (-2.8973, 2.8973), + "panda_joint4": (-3.0718, -0.0698), + "panda_joint5": (-2.8973, 2.8973), + "panda_joint6": (-0.0175, 3.7525), + "panda_joint7": (-2.8973, 2.8973), + } + + # Default rest positions + rest_positions = { + "panda_joint1": 0.0, + "panda_joint2": -0.785398, + "panda_joint3": 0.0, + "panda_joint4": -2.356194, + "panda_joint5": 0.0, + "panda_joint6": 1.570796, + "panda_joint7": 0.785398, + } + + t = step / num_steps # Normalized time [0, 1] + + targets = {} + + if motion_type == "sinusoidal": + # Smooth sinusoidal motion around rest positions + # Each joint oscillates with different frequencies and phases + for i, (joint_name, (lower, upper)) in enumerate(joint_limits.items()): + rest = rest_positions[joint_name] + amplitude = min(0.5, (upper - lower) * 0.15) # Small amplitude for safety + frequency = 1.0 + i * 0.3 # Different frequency for each joint + phase = i * 0.5 # Phase offset + + targets[joint_name] = rest + amplitude * math.sin(2 * math.pi * frequency * t + phase) + + elif motion_type == "random": + # Random walk around rest positions + import random + + random.seed(step) # Reproducible random motion + + for joint_name, (lower, upper) in joint_limits.items(): + rest = rest_positions[joint_name] + # Random offset within 20% of range, smoothed + offset_range = (upper - lower) * 0.1 + offset = random.uniform(-offset_range, offset_range) + + # Smooth it by mixing with rest position based on time + smooth_factor = 0.5 + 0.5 * math.sin(2 * math.pi * t) + targets[joint_name] = rest + offset * smooth_factor + + # Keep fingers at fixed position + targets["panda_finger_joint1"] = 0.04 + targets["panda_finger_joint2"] = 0.04 + + return targets + + +def main(): + args = tyro.cli(Args) + + log.info(f"Recording trajectory to: {args.output}") + log.info(f"Simulation steps: {args.num_steps}") + log.info(f"Motion type: {args.motion_type}") + log.info("NOTE: This is the CPU-only version (no GPU/IK solver required)") + + # ======================================================================== + # Setup Scenario + # ======================================================================== + scenario = ScenarioCfg( + robots=[args.robot], + simulator=args.sim, + headless=True, + num_envs=args.num_envs, + ) + + scenario.cameras = [ + PinholeCameraCfg( + name="camera", + width=640, + height=480, + pos=(1.5, -1.5, 1.5), + look_at=(0.0, 0.0, 0.0), + ) + ] + + scenario.objects = [ + PrimitiveCubeCfg( + name="cube", + size=(0.06, 0.06, 0.06), + color=[1.0, 0.3, 0.3], + physics=PhysicStateType.RIGIDBODY, + ), + PrimitiveSphereCfg( + name="sphere", + radius=0.04, + color=[0.3, 0.3, 1.0], + physics=PhysicStateType.RIGIDBODY, + ), + RigidObjCfg( + name="bbq_sauce", + scale=(1.0, 1.0, 1.0), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/assets/libero/COMMON/stable_hope_objects/bbq_sauce/usd/bbq_sauce.usd", + urdf_path="roboverse_data/assets/libero/COMMON/stable_hope_objects/bbq_sauce/urdf/bbq_sauce.urdf", + mjcf_path="roboverse_data/assets/libero/COMMON/stable_hope_objects/bbq_sauce/mjcf/bbq_sauce.xml", + ), + ] + + handler = get_handler(scenario) + + # Set initial states + init_states = [ + { + "objects": { + "cube": { + "pos": torch.tensor([0.4, 0.2, 0.03]), + "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), + }, + "sphere": { + "pos": torch.tensor([0.5, -0.2, 0.04]), + "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), + }, + "bbq_sauce": { + "pos": torch.tensor([0.6, 0.0, 0.14]), + "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), + }, + }, + "robots": { + "franka": { + "pos": torch.tensor([0.0, 0.0, 0.0]), + "rot": torch.tensor([1.0, 0.0, 0.0, 0.0]), + "dof_pos": { + "panda_joint1": 0.0, + "panda_joint2": -0.785398, + "panda_joint3": 0.0, + "panda_joint4": -2.356194, + "panda_joint5": 0.0, + "panda_joint6": 1.570796, + "panda_joint7": 0.785398, + "panda_finger_joint1": 0.04, + "panda_finger_joint2": 0.04, + }, + }, + }, + } + ] + + handler.set_states(init_states * scenario.num_envs) + obs = handler.get_states(mode="tensor") + + # ======================================================================== + # Setup Rerun Recording + # ======================================================================== + from metasim.utils.rerun.rerun_util import RerunVisualizer + + download_urdf_files(scenario) + + visualizer = RerunVisualizer( + app_name="Trajectory Recording (CPU)", + spawn=args.spawn_viewer, + save_path=args.output, + ) + visualizer.add_frame("world/origin") + + # Initial visualization + default_object_states = extract_states_from_obs(obs, handler, "objects") + default_robot_states = extract_states_from_obs(obs, handler, "robots") + visualizer.visualize_scenario_items(scenario.objects, default_object_states) + visualizer.visualize_scenario_items(scenario.robots, default_robot_states) + + # ======================================================================== + # Run Simulation and Record (No IK solver needed!) + # ======================================================================== + log.info("Starting trajectory recording...") + + robot = scenario.robots[0] + + for step in range(args.num_steps): + visualizer.set_time(step) + + # Generate joint targets directly (no IK needed) + joint_targets = generate_franka_joint_targets(step, args.num_steps, args.motion_type) + + # Convert to action format expected by handler (needs "dof_pos_target" key) + actions = {robot.name: {"dof_pos_target": joint_targets}} + + handler.set_dof_targets(actions) + handler.simulate() + obs = handler.get_states(mode="tensor") + + # Settle physics on first step + if step == 0: + for _ in range(30): + handler.simulate() + obs = handler.get_states(mode="tensor") + + # Update visualization + object_states = extract_states_from_obs(obs, handler, "objects") + robot_states = extract_states_from_obs(obs, handler, "robots") + + for name, state in object_states.items(): + visualizer.update_item_pose(name, state) + for name, state in robot_states.items(): + visualizer.update_item_pose(name, state) + + if step % 20 == 0: + log.info(f"Recording step {step}/{args.num_steps}") + + visualizer.close() + + log.info("") + log.info("=" * 60) + log.info("Recording Complete!") + log.info("=" * 60) + log.info(f"Saved to: {args.output}") + log.info(f"Total steps: {args.num_steps}") + log.info("") + log.info("To replay the recording:") + log.info(f" rerun {args.output}") + log.info("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/get_started/rl/0_ppo.py b/get_started/rl/0_ppo.py index edc41ae93..060771af5 100644 --- a/get_started/rl/0_ppo.py +++ b/get_started/rl/0_ppo.py @@ -36,9 +36,10 @@ class Args: task: str = "reach_origin" robot: str = "franka" num_envs: int = 128 - sim: Literal["isaacsim", "isaaclab", "isaacgym", "mujoco", "genesis", "mjx"] = "mjx" - headless: bool = False - enable_viser: bool = False # Enable real-time 3D visualization with Viser + sim: Literal["isaacsim", "isaaclab", "isaacgym", "mujoco", "genesis", "mjx"] = "isaacsim" + headless: bool = True + enable_viser: bool = True # Enable real-time 3D visualization with Viser + enable_rerun: bool = True # Enable real-time 3D visualization with Rerun args = tyro.cli(Args) @@ -131,11 +132,18 @@ def train_ppo(): device = torch.device("cuda" if torch.cuda.is_available() else "cpu") env = task_cls(scenario=scenario) - # Optionally wrap with Viser visualization - if args.enable_viser: - from metasim.utils.viser.viser_env_wrapper import TaskViserWrapper + # Optionally wrap with visualization + if args.enable_viser or args.enable_rerun: + from metasim.utils.viz_task_wrapper import TaskVizWrapper - env = TaskViserWrapper(env) + env = TaskVizWrapper( + env, + use_rerun=args.enable_rerun, + use_viser=args.enable_viser, + rerun_app_name="PPO Training", + viser_port=8080, + update_freq=10, + ) # # Create VecEnv wrapper for SB3 env = VecEnvWrapper(env) @@ -177,11 +185,18 @@ def train_ppo(): env_inference = task_cls(scenario_inference, device=device) - # Optionally wrap inference environment with Viser visualization - if args.enable_viser: - from metasim.utils.viser.viser_env_wrapper import TaskViserWrapper - - env_inference = TaskViserWrapper(env_inference) + # Optionally wrap inference environment with visualization + if args.enable_viser or args.enable_rerun: + from metasim.utils.viz_task_wrapper import TaskVizWrapper + + env_inference = TaskVizWrapper( + env_inference, + use_rerun=args.enable_rerun, + use_viser=args.enable_viser, + rerun_app_name="PPO Inference", + viser_port=8080, + update_freq=1, + ) env_inference = VecEnvWrapper(env_inference) diff --git a/get_started/rl/0_ppo_gym_style.py b/get_started/rl/0_ppo_gym_style.py index eeac8d6e1..32896baf5 100644 --- a/get_started/rl/0_ppo_gym_style.py +++ b/get_started/rl/0_ppo_gym_style.py @@ -42,6 +42,7 @@ class Args: headless: bool = False device: str = "cuda" enable_viser: bool = False # Enable real-time 3D visualization with Viser + enable_rerun: bool = False # Enable real-time 3D visualization with Rerun args = tyro.cli(Args) @@ -148,11 +149,18 @@ def train_ppo(): device=args.device, ) - # Optionally wrap with Viser visualization - if args.enable_viser: - from metasim.utils.viser.viser_env_wrapper import TaskViserWrapper - - env = TaskViserWrapper(env) + # Optionally wrap with visualization + if args.enable_viser or args.enable_rerun: + from metasim.utils.viz_task_wrapper import TaskVizWrapper + + env = TaskVizWrapper( + env, + use_rerun=args.enable_rerun, + use_viser=args.enable_viser, + rerun_app_name="PPO Training", + viser_port=8080, + update_freq=10, + ) # Create VecEnv wrapper for SB3 env = VecEnvWrapper(env) @@ -195,11 +203,18 @@ def train_ppo(): device=args.device, ) - # Optionally wrap inference environment with Viser visualization - if args.enable_viser: - from metasim.utils.viser.viser_env_wrapper import TaskViserWrapper - - env_inference = TaskViserWrapper(env_inference) + # Optionally wrap inference environment with visualization + if args.enable_viser or args.enable_rerun: + from metasim.utils.viz_task_wrapper import TaskVizWrapper + + env_inference = TaskVizWrapper( + env_inference, + use_rerun=args.enable_rerun, + use_viser=args.enable_viser, + rerun_app_name="PPO Inference", + viser_port=8080, + update_freq=1, + ) env_inference = VecEnvWrapper(env_inference) diff --git a/metasim/sim/base.py b/metasim/sim/base.py index 1ee766b00..2c39b6f1a 100644 --- a/metasim/sim/base.py +++ b/metasim/sim/base.py @@ -12,12 +12,13 @@ from metasim.queries.base import BaseQueryType from metasim.types import Action, DictEnvState, TensorState +from metasim.utils.gs_util import quaternion_multiply from metasim.utils.state import list_state_to_tensor, state_tensor_to_nested # from metasim.utils.hf_util import FileDownloader try: - from robo_splatter.models.basic import RenderConfig - from robo_splatter.models.gaussians import VanillaGaussians + from robo_splatter.models.basic import GSInstance, RenderConfig + from robo_splatter.models.gaussians import RigidsGaussians from robo_splatter.render.scenes import Scene ROBO_SPLATTER_AVAILABLE = True @@ -280,18 +281,13 @@ def _get_camera_params(self, camera): def _build_gs_background(self): """Initialize GS background renderer if enabled in scenario config.""" if self.scenario.gs_scene is None or not self.scenario.gs_scene.with_gs_background: + self.gs_background = None return if not ROBO_SPLATTER_AVAILABLE: log.error("GS background enabled but RoboSplatter not available.") + self.gs_background = None return - - try: - from metasim.utils.gs_util import quaternion_multiply - except ImportError: - log.error("quaternion_multiply not available from gs_util") - return - # Parse pose transformation if self.scenario.gs_scene.gs_background_pose_tum is not None: x, y, z, qx, qy, qz, qw = self.scenario.gs_scene.gs_background_pose_tum @@ -300,15 +296,14 @@ def _build_gs_background(self): # Apply coordinate transform qx, qy, qz, qw = quaternion_multiply([qx, qy, qz, qw], [0.7071, 0, 0, 0.7071]) - init_pose = torch.tensor([x, y, z, qx, qy, qz, qw]) + init_pose = torch.tensor([x, y, z, qx, qy, qz, qw], dtype=torch.float32).cpu() # Load GS model - gs_model = VanillaGaussians( - model_path=self.scenario.gs_scene.gs_background_path, device="cuda" if torch.cuda.is_available() else "cpu" + gs_model = RigidsGaussians( + instances={0: GSInstance(gs_model_path=self.scenario.gs_scene.gs_background_path, init_pose=init_pose)}, + device="cuda" if torch.cuda.is_available() else "cpu", ) - gs_model.apply_global_transform(global_pose=init_pose) - - self.gs_background = Scene(render_config=RenderConfig(), background_models=gs_model) + self.gs_background = Scene(render_config=RenderConfig(), foreground_models=gs_model) @property def num_envs(self) -> int: diff --git a/metasim/sim/genesis/genesis.py b/metasim/sim/genesis/genesis.py index 7d07b43f1..294f481e0 100644 --- a/metasim/sim/genesis/genesis.py +++ b/metasim/sim/genesis/genesis.py @@ -26,6 +26,7 @@ from metasim.scenario.scenario import ScenarioCfg from metasim.sim import BaseSimHandler from metasim.types import Action, DictEnvState +from metasim.utils.gs_util import alpha_blend_rgba from metasim.utils.state import CameraState, ObjectState, RobotState, TensorState # Apply IGL compatibility patch @@ -49,6 +50,7 @@ def _patched_compute_sd(self, query_points): # Optional: RoboSplatter imports for GS background rendering try: from robo_splatter.models.camera import Camera as SplatCamera + from robo_splatter.render.scenes import SceneRenderType ROBO_SPLATTER_AVAILABLE = True except ImportError: @@ -288,13 +290,10 @@ def _get_states(self, env_ids: list[int] | None = None) -> list[DictEnvState]: depth_t = torch.as_tensor(depth) # GS background blending - if ( - self.scenario.gs_scene is not None - and self.scenario.gs_scene.with_gs_background - and ROBO_SPLATTER_AVAILABLE - ): - from metasim.utils.gs_util import alpha_blend_rgba - + if self.scenario.gs_scene is not None and self.scenario.gs_scene.with_gs_background: + assert ROBO_SPLATTER_AVAILABLE, ( + "RoboSplatter is not available. GS background rendering will be disabled." + ) # Get camera parameters Ks, c2w = self._get_camera_params(camera) @@ -306,7 +305,7 @@ def _get_states(self, env_ids: list[int] | None = None) -> list[DictEnvState]: image_width=int(camera.width), device="cuda" if torch.cuda.is_available() else "cpu", ) - gs_result = self.gs_background.render(gs_cam) + gs_result = self.gs_background.render(gs_cam, render_type=SceneRenderType.FOREGROUND) gs_result.to_numpy() # Create foreground mask from segmentation diff --git a/metasim/sim/isaacgym/isaacgym.py b/metasim/sim/isaacgym/isaacgym.py index d102858ea..cf0a38050 100644 --- a/metasim/sim/isaacgym/isaacgym.py +++ b/metasim/sim/isaacgym/isaacgym.py @@ -11,6 +11,7 @@ # Optional: RoboSplatter imports for GS background rendering try: from robo_splatter.models.camera import Camera as SplatCamera + from robo_splatter.render.scenes import SceneRenderType ROBO_SPLATTER_AVAILABLE = True except ImportError: @@ -239,7 +240,7 @@ def _apply_gs_background_rendering(self, camera_states, env_ids): image_width=width, device=self.device, ) - gs_result = self.gs_background.render(gs_cam) + gs_result = self.gs_background.render(gs_cam, render_type=SceneRenderType.FOREGROUND) # Get GS background and normalize to tensors on device (handle numpy or torch) gs_rgb = gs_result.rgb[0].to(self.device) @@ -773,11 +774,8 @@ def _get_states(self, env_ids: list[int] | None = None) -> list[DictEnvState]: # Apply GS background rendering if enabled # TODO: Render with batch parallelization for efficiency - if ( - self.scenario.gs_scene is not None - and self.scenario.gs_scene.with_gs_background - and self.gs_background is not None - ): + if self.scenario.gs_scene.with_gs_background and self.gs_background is not None: + assert ROBO_SPLATTER_AVAILABLE, "RoboSplatter is not available. GS background rendering will be disabled." camera_states = self._apply_gs_background_rendering(camera_states, env_ids) extras = self.get_extra() # extra observations diff --git a/metasim/sim/isaacsim/isaacsim.py b/metasim/sim/isaacsim/isaacsim.py index 6b8a5a683..7207e4314 100644 --- a/metasim/sim/isaacsim/isaacsim.py +++ b/metasim/sim/isaacsim/isaacsim.py @@ -30,12 +30,13 @@ from metasim.sim import BaseSimHandler from metasim.types import DictEnvState from metasim.utils.dict import deep_get +from metasim.utils.gs_util import alpha_blend_rgba_torch from metasim.utils.state import CameraState, ObjectState, RobotState, TensorState from metasim.utils.terrain_utils import TerrainGenerator -# Optional: RoboSplatter imports for GS background rendering try: from robo_splatter.models.camera import Camera as SplatCamera + from robo_splatter.render.scenes import SceneRenderType ROBO_SPLATTER_AVAILABLE = True except ImportError: @@ -159,9 +160,11 @@ def _update_camera_pose(self) -> None: # set look at position using isaaclab's api if camera.mount_to is None: camera_inst = self.scene.sensors[camera.name] - position_tensor = torch.tensor(camera.pos, device=self.device).unsqueeze(0) + position_tensor = torch.tensor(camera.pos, device=self.device, dtype=torch.float32).unsqueeze(0) position_tensor = position_tensor.repeat(self.num_envs, 1) - camera_lookat_tensor = torch.tensor(camera.look_at, device=self.device).unsqueeze(0) + camera_lookat_tensor = torch.tensor( + camera.look_at, device=self.device, dtype=torch.float32 + ).unsqueeze(0) camera_lookat_tensor = camera_lookat_tensor.repeat(self.num_envs, 1) camera_inst.set_world_poses_from_view(position_tensor, camera_lookat_tensor) # log.debug(f"Updated camera {camera.name} pose: pos={camera.pos}, look_at={camera.look_at}") @@ -332,6 +335,76 @@ def _set_states(self, states: list[DictEnvState] | TensorState, env_ids: list[in else: raise Exception("Unsupported state type, must be DictEnvState or TensorState") + def _get_foreground_mask( + self, + instance_seg_data: torch.Tensor | None, + instance_seg_id2label: dict[int, str] | None, + instance_id_seg_data: torch.Tensor | None, + instance_id_seg_id2label: dict[int, str] | None, + ) -> torch.Tensor | None: + """ + Create foreground mask by excluding terrain/ground from instance segmentation data. + + Args: + instance_seg_data: Instance segmentation data (semantic level). + instance_seg_id2label: Mapping from instance IDs to labels for instance_seg_data. + instance_id_seg_data: Instance ID segmentation data (instance level, more precise). + instance_id_seg_id2label: Mapping from instance IDs to labels for instance_id_seg_data. + + Returns: + Foreground mask tensor: 1 for objects (not terrain), 0 for terrain/background. + Returns None if no instance segmentation data is available. + """ + foreground_mask = None + + # Use instance_id_seg_data if available (more precise), otherwise use instance_seg_data + if instance_id_seg_data is not None and instance_id_seg_id2label is not None: + # Find terrain IDs from labels + terrain_ids = { + id + for id, label in instance_id_seg_id2label.items() + if any(kw in label.lower() for kw in ["ground", "terrain", "floor", "world/ground"]) + } + unique_ids = torch.unique(instance_id_seg_data) + + if terrain_ids: + # Object mask: 1 for objects (not terrain), 0 for terrain/background + foreground_mask = torch.ones_like(instance_id_seg_data, dtype=torch.float32) + for terrain_id in terrain_ids: + if terrain_id in unique_ids: + foreground_mask[instance_id_seg_data == terrain_id] = 0.0 + # Exclude background (id == 0) if it exists + if 0 in unique_ids: + foreground_mask[instance_id_seg_data == 0] = 0.0 + elif instance_seg_data is not None and instance_seg_id2label is not None: + # Fallback to instance_seg_data + terrain_ids = { + id + for id, label in instance_seg_id2label.items() + if any(kw in label.lower() for kw in ["ground", "terrain", "floor", "world/ground"]) + } + unique_ids = torch.unique(instance_seg_data) + + if terrain_ids: + foreground_mask = torch.ones_like(instance_seg_data, dtype=torch.float32) + for terrain_id in terrain_ids: + if terrain_id in unique_ids: + foreground_mask[instance_seg_data == terrain_id] = 0.0 + # Exclude background (id == 0) if it exists + if 0 in unique_ids: + foreground_mask[instance_seg_data == 0] = 0.0 + + # Fallback: if no terrain IDs found or mask is all zeros, use simple foreground mask + if foreground_mask is None or (foreground_mask is not None and foreground_mask.sum() == 0): + if instance_id_seg_data is not None: + foreground_mask = (instance_id_seg_data > 0).float() + elif instance_seg_data is not None: + foreground_mask = (instance_seg_data > 0).float() + else: + log.warning("No instance segmentation data available for foreground mask") + + return foreground_mask + def _get_states(self, env_ids: list[int] | None = None) -> TensorState: if env_ids is None: env_ids = list(range(self.num_envs)) @@ -415,9 +488,17 @@ def _get_states(self, env_ids: list[int] | None = None) -> TensorState: if ( self.scenario.gs_scene is not None and self.scenario.gs_scene.with_gs_background - and ROBO_SPLATTER_AVAILABLE and rgb_data is not None ): + assert ROBO_SPLATTER_AVAILABLE, ( + "RoboSplatter is not available. GS background rendering will be disabled." + ) + + foreground_mask = self._get_foreground_mask( + instance_seg_data, instance_seg_id2label, instance_id_seg_data, instance_id_seg_id2label + ) + assert foreground_mask is not None, "Foreground mask is None" + # Get camera parameters (already as torch tensors on device) Ks_t, c2w_t = self._get_camera_params(camera, camera_inst) @@ -430,33 +511,27 @@ def _get_states(self, env_ids: list[int] | None = None) -> TensorState: device=self.device, ) - gs_result = self.gs_background.render(gs_cam) - # Create foreground mask from instance segmentation - if instance_seg_data is not None: - from metasim.utils.gs_util import alpha_blend_rgba_torch - - # Get foreground mask from instance segmentation - foreground_mask = (instance_seg_data > 0).float() # Shape: (envs, H, W) - - # Get RGB Blending with GS background - sim_rgb = rgb_data.float() / 255.0 # Normalize to [0, 1], Shape: (envs, H, W, 3) - gs_rgb = gs_result.rgb # Shape: (envs, H, W, 3), BGR order - - if isinstance(gs_rgb, np.ndarray): - gs_rgb = torch.from_numpy(gs_rgb) - gs_rgb = gs_rgb.to(self.device) - blended_rgb = alpha_blend_rgba_torch(sim_rgb, gs_rgb, foreground_mask) - rgb_data = (blended_rgb * 255.0).clamp(0, 255).to(torch.uint8).unsqueeze(0) - - # Get Depth Blending with GS background - sim_depth = depth_data.squeeze(-1) # Shape: (envs, H, W, 1) -> (envs, H, W) - bg_depth = gs_result.depth.squeeze(-1) # Shape: (envs, H, W, 1) -> (envs, H, W) - if isinstance(bg_depth, np.ndarray): - bg_depth = torch.from_numpy(bg_depth) - bg_depth = bg_depth.to(self.device) - # Use torch.where for depth composition - depth_comp = torch.where(foreground_mask > 0.5, sim_depth, bg_depth) - depth_data = depth_comp.unsqueeze(0).unsqueeze(-1) + gs_result = self.gs_background.render(gs_cam, render_type=SceneRenderType.FOREGROUND) + + # Get RGB Blending with GS background + sim_rgb = rgb_data.float() / 255.0 # Normalize to [0, 1], Shape: (envs, H, W, 3) + gs_rgb = gs_result.rgb # Shape: (envs, H, W, 3), BGR order + + if isinstance(gs_rgb, np.ndarray): + gs_rgb = torch.from_numpy(gs_rgb) + gs_rgb = gs_rgb.to(self.device) + blended_rgb = alpha_blend_rgba_torch(sim_rgb, gs_rgb, foreground_mask) + rgb_data = (blended_rgb * 255.0).clamp(0, 255).to(torch.uint8).unsqueeze(0) + + # Get Depth Blending with GS background + sim_depth = depth_data.squeeze(-1) # Shape: (envs, H, W, 1) -> (envs, H, W) + bg_depth = gs_result.depth.squeeze(-1) # Shape: (envs, H, W, 1) -> (envs, H, W) + if isinstance(bg_depth, np.ndarray): + bg_depth = torch.from_numpy(bg_depth) + bg_depth = bg_depth.to(self.device) + # Use torch.where for depth composition + depth_comp = torch.where(foreground_mask > 0.5, sim_depth, bg_depth) + depth_data = depth_comp.unsqueeze(0).unsqueeze(-1) camera_states[camera.name] = CameraState( rgb=rgb_data, @@ -828,9 +903,9 @@ def _load_terrain(self) -> None: albedo_brightness=1.2, ), ) + terrain_config.num_envs = self.scene.cfg.num_envs terrain_config.env_spacing = self.scene.cfg.env_spacing - self.terrain = terrain_config.class_type(terrain_config) self.terrain.env_origins = self.terrain.terrain_origins if ground_cfg is not None: diff --git a/metasim/sim/mujoco/mujoco.py b/metasim/sim/mujoco/mujoco.py index cebba7b7e..e6deb90bd 100644 --- a/metasim/sim/mujoco/mujoco.py +++ b/metasim/sim/mujoco/mujoco.py @@ -62,9 +62,12 @@ def _safe_glctx_del(self): log.warning("Mujoco Viewer not available. Please check your OPENGL environment.") pass +from metasim.utils.gs_util import alpha_blend_rgba + # Optional: RoboSplatter imports for GS background rendering try: from robo_splatter.models.camera import Camera as SplatCamera + from robo_splatter.render.scenes import SceneRenderType ROBO_SPLATTER_AVAILABLE = True except ImportError: @@ -284,6 +287,21 @@ def _apply_scale_to_mjcf(self, mjcf_model, scale): if len(pos) >= 3: joint.pos = [pos[0] * scale_x, pos[1] * scale_y, pos[2] * scale_z] + # Apply scale to mesh elements (for visual meshes) + for mesh in mjcf_model.find_all("mesh"): + if hasattr(mesh, "scale") and mesh.scale is not None: + mesh_scale = list(mesh.scale) + if len(mesh_scale) >= 3: + mesh.scale = [ + mesh_scale[0] * scale_x, + mesh_scale[1] * scale_y, + mesh_scale[2] * scale_z, + ] + elif len(mesh_scale) == 1: + # Uniform scale + uniform_scale = max(scale_x, scale_y, scale_z) + mesh.scale = [mesh_scale[0] * uniform_scale] + def _set_framebuffer_size(self, mjcf_model, width, height): visual_elem = mjcf_model.visual global_elem = None @@ -682,10 +700,82 @@ def _get_states(self, env_ids: list[int] | None = None) -> list[dict]: else: camera_id = f"{camera.name}_custom" + rgb = None depth = None + if "rgb" in camera.data_types: + if sys.platform == "darwin": + with self._mj_lock: # optional but safer + # match renderer size to camera if needed + if self.renderer is None or (self.renderer.width, self.renderer.height) != ( + camera.width, + camera.height, + ): + self.renderer = mujoco.Renderer(self._mj_model, width=camera.width, height=camera.height) + # mirror state and render + self._mirror_state_to_native() + self.renderer.update_scene(self._mj_data, camera=camera_id) + rgb_np = self.renderer.render() + rgb = torch.from_numpy(rgb_np.copy()).unsqueeze(0) + elif sys.platform == "win32": + rgb_np = self.physics.render( + width=camera.width, height=camera.height, camera_id=camera_id, depth=False + ) + # Ensure numpy array -> torch tensor with shape (1, H, W, C) + rgb = torch.from_numpy(np.ascontiguousarray(rgb_np)).unsqueeze(0) + else: + rgb_np = self.physics.render( + width=camera.width, height=camera.height, camera_id=camera_id, depth=False + ) + rgb = torch.from_numpy(np.ascontiguousarray(rgb_np)).unsqueeze(0) + + if "depth" in camera.data_types: + if sys.platform == "darwin": + with self._mj_lock: + # Ensure renderer matches the camera size + if self.renderer is None or (self.renderer.width, self.renderer.height) != ( + camera.width, + camera.height, + ): + self.renderer = mujoco.Renderer(self._mj_model, width=camera.width, height=camera.height) + + # Keep native model/data in sync with dm_control physics + self._mirror_state_to_native() + self.renderer.update_scene(self._mj_data, camera=camera_id) + + # --- Cross-version depth rendering for mujoco.Renderer --- + if hasattr(self.renderer, "enable_depth_rendering"): + # Newer MuJoCo (>= 3.2/3.3): enable depth mode, render(), then disable. + self.renderer.enable_depth_rendering() + depth_np = self.renderer.render() + self.renderer.disable_depth_rendering() + elif hasattr(mujoco, "RenderMode"): + # Some 3.x builds expose RenderMode enum on mujoco + depth_np = self.renderer.render(render_mode=mujoco.RenderMode.DEPTH) + else: + # Very old fallback: some builds returned (rgb, depth) as a tuple. + # If this still fails in your env, we’ll need a dedicated mjr_readPixels path. + maybe = self.renderer.render() + if isinstance(maybe, tuple) and len(maybe) == 2: + _, depth_np = maybe + else: + raise RuntimeError("Depth rendering not supported by this mujoco.Renderer build.") + depth = torch.from_numpy(depth_np.copy()).unsqueeze(0) + elif sys.platform == "win32": + depth_np = self.physics.render( + width=camera.width, height=camera.height, camera_id=camera_id, depth=True + ) + depth = torch.from_numpy(np.ascontiguousarray(depth_np)).unsqueeze(0) + else: + depth_np = self.physics.render( + width=camera.width, height=camera.height, camera_id=camera_id, depth=True + ) + depth = torch.from_numpy(np.ascontiguousarray(depth_np)).unsqueeze(0) + # Additional GS background rendering and blending (if enabled) if self.scenario.gs_scene is not None and self.scenario.gs_scene.with_gs_background: - from metasim.utils.gs_util import alpha_blend_rgba + assert ROBO_SPLATTER_AVAILABLE, ( + "RoboSplatter is not available. GS background rendering will be disabled." + ) # Extract camera parameters Ks, c2w = self._get_camera_params(camera_id, camera) @@ -698,7 +788,7 @@ def _get_states(self, env_ids: list[int] | None = None) -> list[dict]: image_width=camera.width, device="cuda" if torch.cuda.is_available() else "cpu", ) - gs_result = self.gs_background.render(gs_cam) + gs_result = self.gs_background.render(gs_cam, render_type=SceneRenderType.FOREGROUND) gs_result.to_numpy() # Get semantic segmentation (geom IDs and object IDs) @@ -711,102 +801,25 @@ def _get_states(self, env_ids: list[int] | None = None) -> list[dict]: seg_mask = np.where(foreground_mask, 255, 0).astype(np.uint8) if "rgb" in camera.data_types: - # Get MuJoCo simulation rendering - sim_rgb = self.physics.render( - width=camera.width, height=camera.height, camera_id=camera_id, depth=False, segmentation=False - ) # Blend RGB: foreground objects over GS background - sim_color = (sim_rgb * 255).astype(np.uint8) if sim_rgb.max() <= 1.0 else sim_rgb.astype(np.uint8) + sim_color = (rgb_np * 255).astype(np.uint8) if rgb_np.max() <= 1.0 else rgb_np.astype(np.uint8) foreground = np.concatenate([sim_color, seg_mask[..., None]], axis=-1) background = gs_result.rgb.squeeze(0) blended_rgb = alpha_blend_rgba(foreground, background) - rgb = torch.from_numpy(np.array(blended_rgb.copy())) + rgb = torch.from_numpy(np.ascontiguousarray(blended_rgb.copy())).unsqueeze(0) if "depth" in camera.data_types: - sim_depth = self.physics.render( - width=camera.width, height=camera.height, camera_id=camera_id, depth=True, segmentation=False - ) # Compose depth: use simulation depth for foreground, GS depth for background bg_depth = gs_result.depth.squeeze(0) if bg_depth.ndim == 3 and bg_depth.shape[-1] == 1: bg_depth = bg_depth[..., 0] - depth_comp = np.where(foreground_mask, sim_depth, bg_depth) - depth = torch.from_numpy(depth_comp.copy()) - - else: - if "rgb" in camera.data_types: + depth_comp = np.where(foreground_mask, depth_np, bg_depth) if sys.platform == "darwin": - with self._mj_lock: # optional but safer - # match renderer size to camera if needed - if self.renderer is None or (self.renderer.width, self.renderer.height) != ( - camera.width, - camera.height, - ): - self.renderer = mujoco.Renderer( - self._mj_model, width=camera.width, height=camera.height - ) - # mirror state and render - self._mirror_state_to_native() - self.renderer.update_scene(self._mj_data, camera=camera_id) - rgb_np = self.renderer.render() - rgb = torch.from_numpy(rgb_np.copy()).unsqueeze(0) - elif sys.platform == "win32": - rgb_np = self.physics.render( - width=camera.width, height=camera.height, camera_id=camera_id, depth=False - ) - # Ensure numpy array -> torch tensor with shape (1, H, W, C) - rgb = torch.from_numpy(np.ascontiguousarray(rgb_np)).unsqueeze(0) - else: - rgb_np = self.physics.render( - width=camera.width, height=camera.height, camera_id=camera_id, depth=False - ) - rgb = torch.from_numpy(np.ascontiguousarray(rgb_np)).unsqueeze(0) - if "depth" in camera.data_types: - if sys.platform == "darwin": - with self._mj_lock: - # Ensure renderer matches the camera size - if self.renderer is None or (self.renderer.width, self.renderer.height) != ( - camera.width, - camera.height, - ): - self.renderer = mujoco.Renderer( - self._mj_model, width=camera.width, height=camera.height - ) - - # Keep native model/data in sync with dm_control physics - self._mirror_state_to_native() - self.renderer.update_scene(self._mj_data, camera=camera_id) - - # --- Cross-version depth rendering for mujoco.Renderer --- - if hasattr(self.renderer, "enable_depth_rendering"): - # Newer MuJoCo (>= 3.2/3.3): enable depth mode, render(), then disable. - self.renderer.enable_depth_rendering() - depth_np = self.renderer.render() - self.renderer.disable_depth_rendering() - elif hasattr(mujoco, "RenderMode"): - # Some 3.x builds expose RenderMode enum on mujoco - depth_np = self.renderer.render(render_mode=mujoco.RenderMode.DEPTH) - else: - # Very old fallback: some builds returned (rgb, depth) as a tuple. - # If this still fails in your env, we’ll need a dedicated mjr_readPixels path. - maybe = self.renderer.render() - if isinstance(maybe, tuple) and len(maybe) == 2: - _, depth_np = maybe - else: - raise RuntimeError("Depth rendering not supported by this mujoco.Renderer build.") - depth = torch.from_numpy(depth_np.copy()).unsqueeze(0) - elif sys.platform == "win32": - depth_np = self.physics.render( - width=camera.width, height=camera.height, camera_id=camera_id, depth=True - ) - depth = torch.from_numpy(np.ascontiguousarray(depth_np)).unsqueeze(0) + depth = torch.from_numpy(depth_comp.copy()).unsqueeze(0) else: - depth_np = self.physics.render( - width=camera.width, height=camera.height, camera_id=camera_id, depth=True - ) - depth = torch.from_numpy(np.ascontiguousarray(depth_np)).unsqueeze(0) - state = CameraState(rgb=locals().get("rgb", None), depth=locals().get("depth", None)) + depth = torch.from_numpy(np.ascontiguousarray(depth_comp.copy())).unsqueeze(0) + state = CameraState(rgb=locals().get("rgb", None), depth=locals().get("depth", None)) camera_states[camera.name] = state extras = self.get_extra() return TensorState(objects=object_states, robots=robot_states, cameras=camera_states, extras=extras) diff --git a/metasim/sim/pybullet/pybullet.py b/metasim/sim/pybullet/pybullet.py index a89801e1c..5ee67dfe4 100644 --- a/metasim/sim/pybullet/pybullet.py +++ b/metasim/sim/pybullet/pybullet.py @@ -22,12 +22,15 @@ from metasim.scenario.scenario import ScenarioCfg from metasim.sim import BaseSimHandler from metasim.types import Action, DictEnvState + +# Optional: RoboSplatter imports for GS background rendering +from metasim.utils.gs_util import alpha_blend_rgba from metasim.utils.math import convert_quat from metasim.utils.state import CameraState, ObjectState, RobotState, TensorState, adapt_actions_to_dict -# Optional: RoboSplatter imports for GS background rendering try: from robo_splatter.models.camera import Camera as SplatCamera + from robo_splatter.render.scenes import SceneRenderType ROBO_SPLATTER_AVAILABLE = True except ImportError: @@ -353,6 +356,7 @@ def launch(self) -> None: super().launch() self._build_pybullet() if self.scenario.gs_scene is not None and self.scenario.gs_scene.with_gs_background: + assert ROBO_SPLATTER_AVAILABLE, "RoboSplatter is not available. GS background rendering will be disabled." self._build_gs_background() self.already_disconnect = False @@ -457,7 +461,9 @@ def _get_states(self, env_ids=None) -> list[DictEnvState]: segmentation_mask = np.reshape(img_arr[4], (height, width)) if self.scenario.gs_scene is not None and self.scenario.gs_scene.with_gs_background: - from metasim.utils.gs_util import alpha_blend_rgba + assert ROBO_SPLATTER_AVAILABLE, ( + "RoboSplatter is not available. GS background rendering will be disabled." + ) # Extract camera parameters from PyBullet Ks, c2w = self._get_camera_params(view_matrix, projection_matrix, width, height) @@ -470,7 +476,7 @@ def _get_states(self, env_ids=None) -> list[DictEnvState]: image_width=width, device="cuda" if torch.cuda.is_available() else "cpu", ) - gs_result = self.gs_background.render(gs_cam) + gs_result = self.gs_background.render(gs_cam, render_type=SceneRenderType.FOREGROUND) gs_result.to_numpy() # Create foreground mask: exclude background (-1) and ground plane diff --git a/metasim/sim/sapien/sapien3.py b/metasim/sim/sapien/sapien3.py index 9f0214d4a..95d4cc660 100644 --- a/metasim/sim/sapien/sapien3.py +++ b/metasim/sim/sapien/sapien3.py @@ -34,6 +34,7 @@ from metasim.scenario.scenario import ScenarioCfg from metasim.sim import BaseSimHandler from metasim.types import Action, DictEnvState +from metasim.utils.gs_util import alpha_blend_rgba from metasim.utils.math import quat_from_euler_np from metasim.utils.state import CameraState, ObjectState, RobotState, TensorState, adapt_actions_to_dict @@ -42,6 +43,7 @@ # Optional: RoboSplatter imports for GS background rendering try: from robo_splatter.models.camera import Camera as SplatCamera + from robo_splatter.render.scenes import SceneRenderType ROBO_SPLATTER_AVAILABLE = True except ImportError: @@ -478,6 +480,7 @@ def launch(self) -> None: super().launch() self._build_sapien() if self.scenario.gs_scene is not None and self.scenario.gs_scene.with_gs_background: + assert ROBO_SPLATTER_AVAILABLE, "RoboSplatter is not available. GS background rendering will be disabled." self._build_gs_background() def close(self): @@ -572,8 +575,9 @@ def _get_states(self, env_ids=None) -> list[DictEnvState]: cam_inst = self.camera_ids[camera.name] if self.scenario.gs_scene is not None and self.scenario.gs_scene.with_gs_background: - from metasim.utils.gs_util import alpha_blend_rgba - + assert ROBO_SPLATTER_AVAILABLE, ( + "RoboSplatter is not available. GS background rendering will be disabled." + ) # Build RoboSplatter camera from SAPIEN pose and scenario intrinsics, then render GS gs_cam = SplatCamera.init_from_pose_list( pose_list=cam_inst.get_model_matrix(), @@ -583,7 +587,7 @@ def _get_states(self, env_ids=None) -> list[DictEnvState]: device="cuda" if torch.cuda.is_available() else "cpu", ) - gs_result = self.gs_background.render(gs_cam) + gs_result = self.gs_background.render(gs_cam, render_type=SceneRenderType.FOREGROUND) gs_result.to_numpy() seg_labels = cam_inst.get_picture("Segmentation") diff --git a/metasim/utils/obs_utils.py b/metasim/utils/obs_utils.py index 45730159f..a3a02f201 100644 --- a/metasim/utils/obs_utils.py +++ b/metasim/utils/obs_utils.py @@ -51,12 +51,12 @@ def add(self, state: TensorState): image = (image * 255).astype(np.uint8) self.images.append(image) - def save(self): + def save(self, fps: int = 30): """Save the images or videos.""" if self.video_path is not None and self.images: log.info(f"Saving video of {len(self.images)} frames to {self.video_path}") os.makedirs(os.path.dirname(self.video_path), exist_ok=True) - iio.mimsave(self.video_path, self.images, fps=30) + iio.mimsave(self.video_path, self.images, fps=fps) try: diff --git a/metasim/utils/rerun/README.md b/metasim/utils/rerun/README.md new file mode 100644 index 000000000..5809c217f --- /dev/null +++ b/metasim/utils/rerun/README.md @@ -0,0 +1,107 @@ +# TaskRerunWrapper Usage Guide + +TaskRerunWrapper is a wrapper that adds real-time Rerun visualization to RLTaskEnv. + +## Main Features + +- Automatic Rerun visualization setup +- Real-time robot and object state updates +- Only renders the first environment (to avoid multi-environment complexity) +- Transparent proxy for all environment attributes and methods +- Error handling: visualization failures don't affect training + +## Usage + +### 1. Basic Usage + +```python +from metasim.task.registry import get_task_class +from metasim.utils.rerun.rerun_env_wrapper import TaskRerunWrapper + +# Create environment +task_cls = get_task_class('reach_origin') +scenario = task_cls.scenario.update( + robots=['franka'], + simulator='mujoco', + num_envs=1024, + headless=True, + cameras=[] +) + +env = task_cls(scenario, device='cuda') + +# Wrap environment to enable visualization +rerun_env = TaskRerunWrapper(env, app_name="RL Training", update_freq=10) + +# Use normally +obs = rerun_env.reset() +for _ in range(100): + actions = policy(obs) # your policy + obs, reward, terminated, timeout, info = rerun_env.step(actions) + if terminated.any() or timeout.any(): + obs = rerun_env.reset() + +rerun_env.close() +``` + +### 2. Integration with fast_td3 + +Enable visualization in fast_td3: + +```python +CONFIG = { + "sim": "mujoco", + "robots": ["franka"], + "task": "reach_origin", + "headless": True, + "use_rerun": True, # Enable Rerun visualization + # ... other config +} +``` + +## How It Works + +1. **On Initialization**: + - Creates RerunVisualizer instance + - Downloads necessary URDF files + - Visualizes all robots and objects in the scene + - Opens the Rerun viewer + +2. **During Runtime**: + - Updates visualization after each `reset()` and `step()` (based on update_freq) + - Extracts first environment's state + - Updates all robot and object positions/orientations + +3. **Error Handling**: + - Visualization failures don't affect training + - Errors are logged but don't raise exceptions + +## Comparison with TaskViserWrapper + +| Feature | TaskRerunWrapper | TaskViserWrapper | +|---------|-----------------|------------------| +| Viewer | Desktop app | Web browser (port 8080) | +| Timeline | Built-in scrubbing | No | +| Recording | Native .rrd files | No | +| Interactive Controls | No | Joint/IK sliders | + +## API Reference + +### TaskRerunWrapper + +```python +TaskRerunWrapper( + task_env, # RLTaskEnv or similar + app_name="RoboVerse Training", # Rerun application name + update_freq=10, # Update visualization every N steps + spawn=True, # Auto-open Rerun viewer +) +``` + +### Methods + +- `step(action)` - Step environment and update visualization +- `reset(**kwargs)` - Reset environment and update visualization +- `close()` - Close environment and visualizer +- All other methods/attributes proxied to wrapped environment + diff --git a/metasim/utils/rerun/__init__.py b/metasim/utils/rerun/__init__.py new file mode 100644 index 000000000..1c97196c8 --- /dev/null +++ b/metasim/utils/rerun/__init__.py @@ -0,0 +1,5 @@ +"""Rerun visualization utilities for RoboVerse.""" + +from metasim.utils.rerun.rerun_util import RerunVisualizer + +__all__ = ["RerunVisualizer"] diff --git a/metasim/utils/rerun/rerun_env_wrapper.py b/metasim/utils/rerun/rerun_env_wrapper.py new file mode 100644 index 000000000..0975ffec3 --- /dev/null +++ b/metasim/utils/rerun/rerun_env_wrapper.py @@ -0,0 +1,209 @@ +"""Rerun environment wrapper for RoboVerse. + +This module provides a wrapper for RLTaskEnv that adds real-time Rerun visualization +during training and evaluation. +""" + +from __future__ import annotations + +import logging + +logger = logging.getLogger(__name__) + +try: + import rerun as rr # noqa: F401 + + RERUN_AVAILABLE = True +except ImportError: + RERUN_AVAILABLE = False + + +class TaskRerunWrapper: + """Simple wrapper for RLTaskEnv with real-time Rerun visualization. + + Only renders the first environment for simplicity. Designed for RL training integration. + + Args: + task_env: RLTaskEnv or similar environment with handler + app_name: Application name for Rerun (default: "RoboVerse Training") + update_freq: Update visualization every N steps to reduce resource usage (default: 10) + spawn: Whether to spawn the Rerun viewer automatically (default: True) + """ + + def __init__( + self, + task_env, + app_name: str = "RoboVerse Training", + update_freq: int = 10, + spawn: bool = True, + ): + if not RERUN_AVAILABLE: + raise ImportError("rerun-sdk is required. Install with: pip install rerun-sdk") + + self.env = task_env + self.update_freq = update_freq + self.step_count = 0 + self.handler = self._get_handler() + self.visualizer = None + + self._setup_visualization(app_name, spawn) + + def _get_handler(self): + """Get the actual handler object, supporting both gym and non-gym environments.""" + # Try gym environment path first (gym_vec.task_env.handler) + if hasattr(self.env, "task_env"): + if hasattr(self.env.task_env, "handler") and self.env.task_env.handler is not None: + return self.env.task_env.handler + + # Fall back to direct handler access (env.handler) + if hasattr(self.env, "handler") and self.env.handler is not None: + return self.env.handler + + return None + + def _setup_visualization(self, app_name: str, spawn: bool): + """Setup Rerun visualization.""" + if self.handler is None: + logger.warning("No handler found, skipping visualization setup") + return + + try: + from metasim.utils.rerun.rerun_util import RerunVisualizer + from metasim.utils.state import state_tensor_to_nested + + # Download URDF files + self._download_urdf_files() + + # Initialize visualizer + self.visualizer = RerunVisualizer(app_name=app_name, spawn=spawn) + + # Get initial states + obs = self.handler.get_states(mode="tensor") + env_states = state_tensor_to_nested(self.handler, obs) + + if env_states and len(env_states) > 0: + state = env_states[0] + object_states = state.get("objects", {}) + robot_states = state.get("robots", {}) + + # Visualize objects + for obj in self.handler.objects: + obj_state = self._extract_state(object_states.get(obj.name, {})) + self.visualizer.visualize_item(obj, obj.name, obj_state) + + # Visualize robots + for robot in self.handler.robots: + robot_state = self._extract_state(robot_states.get(robot.name, {})) + self.visualizer.visualize_item(robot, robot.name, robot_state) + + logger.info("Rerun visualization initialized") + + except Exception as e: + logger.error(f"Failed to setup visualization: {e}") + import traceback + + traceback.print_exc() + self.visualizer = None + + def _download_urdf_files(self): + """Download URDF files for all objects and robots.""" + from metasim.utils.hf_util import check_and_download_recursive + + urdf_paths = [] + + for obj in self.handler.objects: + if hasattr(obj, "urdf_path") and obj.urdf_path: + urdf_paths.append(obj.urdf_path) + + for robot in self.handler.robots: + if hasattr(robot, "urdf_path") and robot.urdf_path: + urdf_paths.append(robot.urdf_path) + + if urdf_paths: + check_and_download_recursive(urdf_paths, n_processes=16) + + def _extract_state(self, state_dict: dict) -> dict: + """Extract state from nested dict format.""" + result = {} + + if "pos" in state_dict and state_dict["pos"] is not None: + pos = state_dict["pos"] + if hasattr(pos, "cpu"): + pos = pos.cpu().numpy() + result["pos"] = list(pos) + + if "rot" in state_dict and state_dict["rot"] is not None: + rot = state_dict["rot"] + if hasattr(rot, "cpu"): + rot = rot.cpu().numpy() + result["rot"] = list(rot) + + if "dof_pos" in state_dict and state_dict["dof_pos"] is not None: + result["dof_pos"] = state_dict["dof_pos"] + + return result + + def _update_visualization(self): + """Update visualization with current state.""" + if self.visualizer is None or self.handler is None: + return + + try: + from metasim.utils.state import state_tensor_to_nested + + obs = self.handler.get_states(mode="tensor") + env_states = state_tensor_to_nested(self.handler, obs) + + if env_states and len(env_states) > 0: + state = env_states[0] + object_states = state.get("objects", {}) + robot_states = state.get("robots", {}) + + # Update objects + for obj in self.handler.objects: + if obj.name in object_states: + obj_state = self._extract_state(object_states[obj.name]) + self.visualizer.update_item_pose(obj.name, obj_state) + + # Update robots + for robot in self.handler.robots: + if robot.name in robot_states: + robot_state = self._extract_state(robot_states[robot.name]) + self.visualizer.update_item_pose(robot.name, robot_state) + + # Update time step + self.visualizer.set_time(self.step_count) + + except Exception as e: + logger.debug(f"Visualization update failed: {e}") + + def step(self, action): + """Step the environment and update visualization.""" + result = self.env.step(action) + self.step_count += 1 + + if self.step_count % self.update_freq == 0: + self._update_visualization() + + return result + + def reset(self, **kwargs): + """Reset the environment and update visualization.""" + result = self.env.reset(**kwargs) + self._update_visualization() + return result + + def close(self): + """Close environment and visualizer.""" + if self.visualizer is not None: + self.visualizer.close() + if hasattr(self.env, "close"): + self.env.close() + + def __len__(self) -> int: + """Return number of environments.""" + return self.env.num_envs if hasattr(self.env, "num_envs") else 1 + + def __getattr__(self, name): + """Proxy all other attributes to the wrapped environment.""" + return getattr(self.env, name) diff --git a/metasim/utils/rerun/rerun_util.py b/metasim/utils/rerun/rerun_util.py new file mode 100644 index 000000000..92d6a055b --- /dev/null +++ b/metasim/utils/rerun/rerun_util.py @@ -0,0 +1,1462 @@ +"""Rerun visualization utilities for RoboVerse demos. + +This module provides a unified RerunVisualizer class for interactive 3D visualization +of robots, objects, and trajectories using the Rerun SDK. + +Rerun is an open-source SDK for logging, storing, querying, and visualizing +multimodal data. It supports URDF, OBJ meshes, and various 3D primitives. +""" + +from __future__ import annotations + +import logging +import math +import os +import xml.etree.ElementTree as ET +from pathlib import Path + +import numpy as np + +try: + import rerun as rr + + RERUN_AVAILABLE = True +except ImportError: + RERUN_AVAILABLE = False + rr = None + +try: + import trimesh + + TRIMESH_AVAILABLE = True +except ImportError: + TRIMESH_AVAILABLE = False + trimesh = None + +try: + import yourdfpy + + YOURDFPY_AVAILABLE = True +except ImportError: + YOURDFPY_AVAILABLE = False + yourdfpy = None + +from metasim.scenario.objects import ( + ArticulationObjCfg, + PrimitiveCubeCfg, + PrimitiveCylinderCfg, + PrimitiveSphereCfg, + RigidObjCfg, +) +from metasim.scenario.robot import RobotCfg as BaseRobotCfg + +# Configure logging - use loguru if available, fallback to standard logging +try: + from loguru import logger +except ImportError: + logger = logging.getLogger(__name__) + logging.basicConfig(level=logging.INFO, format="[%(levelname)s] %(message)s") + +# Constants +QUAT_NORMALIZE_EPSILON = 1e-6 + + +def normalize_quaternion(quat: list[float]) -> list[float]: + """Normalize a quaternion to unit length. + + Args: + quat: Quaternion in [w, x, y, z] format + + Returns: + Normalized quaternion in [w, x, y, z] format + """ + quat_norm = math.sqrt(sum(q * q for q in quat)) + if quat_norm > QUAT_NORMALIZE_EPSILON: + return [q / quat_norm for q in quat] + else: + return [1.0, 0.0, 0.0, 0.0] # Identity quaternion + + +def resolve_mesh_path(filename: str, urdf_dir: Path) -> str | None: + """Resolve mesh path from URDF to absolute path. + + Args: + filename: Mesh filename from URDF (may be relative or package://) + urdf_dir: Directory containing the URDF file + + Returns: + Absolute path to mesh file, or None if not found + """ + if os.path.isabs(filename) and os.path.exists(filename): + return filename + + candidates = [] + + # Handle package:// URIs - package root is usually the URDF directory + if filename.startswith("package://"): + rel_path = filename.split("package://", 1)[1] + candidates = [ + urdf_dir / rel_path, # Most common: package:// relative to URDF dir + urdf_dir.parent / rel_path, + urdf_dir.parent.parent / rel_path, + ] + else: + # Build list of candidate paths for relative filenames + base_name = Path(filename).name + candidates = [ + urdf_dir / filename, + urdf_dir / "meshes" / filename, + urdf_dir / "meshes" / base_name, + urdf_dir / "meshes" / "visual" / filename, + urdf_dir / "meshes" / "visual" / base_name, + urdf_dir / "meshes" / "collision" / filename, + urdf_dir / "visual" / filename, + urdf_dir / "visual" / base_name, + urdf_dir / "collision" / filename, + urdf_dir.parent / "meshes" / filename, + urdf_dir.parent / "meshes" / base_name, + urdf_dir.parent / "meshes" / "visual" / filename, + urdf_dir.parent / "meshes" / "visual" / base_name, + ] + + # Also try resolving relative paths like ../meshes/... + try: + resolved = (urdf_dir / filename).resolve() + if resolved not in candidates: + candidates.append(resolved) + except Exception: + pass + + for cand in candidates: + try: + resolved = cand.resolve() if not cand.is_absolute() else cand + if resolved.exists(): + logger.debug(f"Resolved mesh: {filename} -> {resolved}") + return str(resolved) + except Exception: + continue + + logger.warning(f"Could not resolve mesh path: {filename} from {urdf_dir}") + logger.debug(f"Tried candidates: {[str(c) for c in candidates[:5]]}") + return None + + +def parse_urdf_full(urdf_path: str) -> dict: + """Parse URDF file extracting links, joints, and kinematic tree. + + Args: + urdf_path: Path to URDF file + + Returns: + Dictionary with: + - 'links': dict of link_name -> list of visual geometries + - 'joints': dict of joint_name -> joint info (parent, child, origin, type, axis) + - 'parent_map': dict of child_link -> parent_link + - 'root_link': name of the root link + """ + urdf_dir = Path(urdf_path).parent + tree = ET.parse(urdf_path) + root = tree.getroot() + + links = {} + joints = {} + parent_map = {} # child_link -> parent_link + child_links = set() + all_links = set() + + # Parse all links + for link in root.findall(".//link"): + link_name = link.get("name", "unnamed") + all_links.add(link_name) + visuals = [] + + for visual in link.findall("visual"): + visual_info = {"origin": np.eye(4)} + + # Parse origin + origin = visual.find("origin") + if origin is not None: + xyz = origin.get("xyz", "0 0 0").split() + rpy = origin.get("rpy", "0 0 0").split() + visual_info["origin"] = _make_transform([float(x) for x in xyz], [float(r) for r in rpy]) + + # Parse geometry + geometry = visual.find("geometry") + if geometry is not None: + mesh = geometry.find("mesh") + box = geometry.find("box") + sphere = geometry.find("sphere") + cylinder = geometry.find("cylinder") + + if mesh is not None: + filename = mesh.get("filename", "") + scale = mesh.get("scale", "1 1 1").split() + resolved_path = resolve_mesh_path(filename, urdf_dir) + if resolved_path: + visual_info["type"] = "mesh" + visual_info["filename"] = resolved_path + visual_info["scale"] = [float(s) for s in scale] + elif box is not None: + size = box.get("size", "1 1 1").split() + visual_info["type"] = "box" + visual_info["size"] = [float(s) for s in size] + elif sphere is not None: + radius = float(sphere.get("radius", "1")) + visual_info["type"] = "sphere" + visual_info["radius"] = radius + elif cylinder is not None: + radius = float(cylinder.get("radius", "1")) + length = float(cylinder.get("length", "1")) + visual_info["type"] = "cylinder" + visual_info["radius"] = radius + visual_info["length"] = length + + # Parse material color + material = visual.find("material") + if material is not None: + color_elem = material.find("color") + if color_elem is not None: + rgba = color_elem.get("rgba", "0.8 0.8 0.8 1").split() + visual_info["color"] = [float(c) for c in rgba] + else: + visual_info["color"] = [0.8, 0.8, 0.8, 1.0] + else: + visual_info["color"] = [0.8, 0.8, 0.8, 1.0] + + if "type" in visual_info: + visuals.append(visual_info) + + links[link_name] = visuals + + # Parse all joints + for joint in root.findall(".//joint"): + joint_name = joint.get("name", "unnamed") + joint_type = joint.get("type", "fixed") + + parent_elem = joint.find("parent") + child_elem = joint.find("child") + + if parent_elem is None or child_elem is None: + continue + + parent_link = parent_elem.get("link") + child_link = child_elem.get("link") + + # Parse joint origin + origin = joint.find("origin") + if origin is not None: + xyz = origin.get("xyz", "0 0 0").split() + rpy = origin.get("rpy", "0 0 0").split() + joint_origin = _make_transform([float(x) for x in xyz], [float(r) for r in rpy]) + else: + joint_origin = np.eye(4) + + # Parse joint axis + axis_elem = joint.find("axis") + if axis_elem is not None: + axis = [float(x) for x in axis_elem.get("xyz", "0 0 1").split()] + else: + axis = [0, 0, 1] + + joints[joint_name] = { + "parent": parent_link, + "child": child_link, + "origin": joint_origin, + "type": joint_type, + "axis": axis, + } + + parent_map[child_link] = (parent_link, joint_name) + child_links.add(child_link) + + # Find root link (links that are not children of any joint) + root_links = all_links - child_links + root_link = next(iter(root_links)) if root_links else (next(iter(all_links)) if all_links else None) + + return { + "links": links, + "joints": joints, + "parent_map": parent_map, + "root_link": root_link, + } + + +def compute_link_transforms(urdf_data: dict, dof_pos: dict | None = None) -> dict: + """Compute world transforms for each link using forward kinematics. + + Args: + urdf_data: Output from parse_urdf_full() + dof_pos: Optional dict mapping joint_name -> joint position (radians) + + Returns: + Dictionary mapping link_name -> 4x4 world transform matrix + """ + if dof_pos is None: + dof_pos = {} + + links = urdf_data["links"] + joints = urdf_data["joints"] + parent_map = urdf_data["parent_map"] + root_link = urdf_data["root_link"] + + link_transforms = {} + + def compute_link_transform(link_name: str) -> np.ndarray: + """Recursively compute the world transform for a link.""" + if link_name in link_transforms: + return link_transforms[link_name] + + if link_name == root_link or link_name not in parent_map: + # Root link is at identity + link_transforms[link_name] = np.eye(4) + return link_transforms[link_name] + + parent_link, joint_name = parent_map[link_name] + parent_transform = compute_link_transform(parent_link) + + joint_info = joints[joint_name] + joint_origin = joint_info["origin"] + joint_type = joint_info["type"] + axis = joint_info["axis"] + + # Get joint position + joint_pos = dof_pos.get(joint_name, 0.0) + + # Compute joint transform based on type + if joint_type == "revolute" or joint_type == "continuous": + # Rotation about axis + joint_transform = _axis_angle_to_transform(axis, joint_pos) + elif joint_type == "prismatic": + # Translation along axis + joint_transform = np.eye(4) + joint_transform[:3, 3] = np.array(axis) * joint_pos + else: + # Fixed joint + joint_transform = np.eye(4) + + # Link transform = parent * joint_origin * joint_rotation + link_transforms[link_name] = parent_transform @ joint_origin @ joint_transform + return link_transforms[link_name] + + # Compute transform for all links + for link_name in links: + compute_link_transform(link_name) + + return link_transforms + + +def _axis_angle_to_transform(axis: list, angle: float) -> np.ndarray: + """Convert axis-angle to 4x4 transformation matrix (rotation only). + + Uses Rodrigues' rotation formula. + """ + axis = np.array(axis, dtype=float) + axis = axis / (np.linalg.norm(axis) + 1e-10) + + K = np.array([[0, -axis[2], axis[1]], [axis[2], 0, -axis[0]], [-axis[1], axis[0], 0]]) + + R = np.eye(3) + np.sin(angle) * K + (1 - np.cos(angle)) * (K @ K) + + T = np.eye(4) + T[:3, :3] = R + return T + + +def parse_urdf_simple(urdf_path: str) -> dict: + """Simple URDF parser that extracts link visual geometries. + + This is a fallback when yourdfpy is not available. + + Args: + urdf_path: Path to URDF file + + Returns: + Dictionary with link names as keys and list of visual geometries as values + """ + urdf_data = parse_urdf_full(urdf_path) + return urdf_data["links"] + + +def _make_transform(xyz: list, rpy: list) -> np.ndarray: + """Create 4x4 transformation matrix from xyz and rpy.""" + T = np.eye(4) + T[:3, 3] = xyz + + # Roll, pitch, yaw to rotation matrix + roll, pitch, yaw = rpy + cr, sr = np.cos(roll), np.sin(roll) + cp, sp = np.cos(pitch), np.sin(pitch) + cy, sy = np.cos(yaw), np.sin(yaw) + + T[0, 0] = cy * cp + T[0, 1] = cy * sp * sr - sy * cr + T[0, 2] = cy * sp * cr + sy * sr + T[1, 0] = sy * cp + T[1, 1] = sy * sp * sr + cy * cr + T[1, 2] = sy * sp * cr - cy * sr + T[2, 0] = -sp + T[2, 1] = cp * sr + T[2, 2] = cp * cr + + return T + + +class RerunVisualizer: + """Interactive 3D visualizer for robots and objects using Rerun. + + This class provides visualization capabilities including: + - Loading and displaying URDF models (robots and objects) + - Primitive shape visualization (cubes, spheres, cylinders) + - OBJ mesh visualization + - Dynamic pose updates during simulation + - Trajectory playback + + Args: + app_name: Application name for Rerun (default: "RoboVerse") + spawn: Whether to spawn the Rerun viewer automatically (default: True) + connect: Whether to connect to an existing Rerun viewer (default: False) + save_path: Optional path to save .rrd recording file + use_normals: Whether to compute and include vertex normals for meshes (default: True) + """ + + def __init__( + self, + app_name: str = "RoboVerse", + spawn: bool = True, + connect: bool = False, + save_path: str | None = None, + use_normals: bool = True, + ) -> None: + if not RERUN_AVAILABLE: + raise ImportError("rerun-sdk is required. Install with: pip install rerun-sdk") + + self.app_name = app_name + self.use_normals = use_normals + self._urdf_models = {} # name -> urdf object + self._item_states = {} # name -> current state dict + self._item_configs = {} # name -> config object + self._initial_configs = {} # name -> initial joint config + self._joint_names_cache = {} # name -> list of joint names + self._time_step = 0 + + # Initialize Rerun + rr.init(app_name) + + if save_path: + rr.save(save_path) + logger.info(f"Recording to {save_path}") + + if connect: + rr.connect() + logger.info("Connected to existing Rerun viewer") + elif spawn: + rr.spawn() + logger.info("Spawned Rerun viewer") + + # Set up 3D view with ground plane + self._setup_scene() + + def _setup_scene(self) -> None: + """Set up the initial 3D scene with coordinate system and ground plane.""" + # Log world coordinate frame + rr.log( + "world", + rr.ViewCoordinates.RIGHT_HAND_Z_UP, + static=True, + ) + + # Add ground plane as a grid of lines + self._add_ground_grid() + + def _add_ground_grid(self, size: float = 5.0, divisions: int = 20) -> None: + """Add a ground plane to the scene. + + Args: + size: Half-size of the ground plane + divisions: Number of divisions (unused, kept for API compatibility) + """ + # Add solid ground plane only (no grid lines) + if TRIMESH_AVAILABLE: + try: + # Create a flat box for the ground (very thin) + ground = trimesh.creation.box(extents=[size * 2, size * 2, 0.01]) + # Move it slightly below z=0 so objects sit on top + ground.vertices[:, 2] -= 0.005 + vertices = np.array(ground.vertices, dtype=np.float32) + faces = np.array(ground.faces, dtype=np.uint32) + ground_color = [220, 220, 220] # Light gray + rr.log( + "world/ground_plane", + rr.Mesh3D( + vertex_positions=vertices, + triangle_indices=faces, + vertex_colors=[ground_color] * len(vertices), + ), + static=True, + ) + except Exception as e: + logger.debug(f"Failed to create ground plane mesh: {e}") + + def add_frame(self, name: str) -> None: + """Add a coordinate frame to the scene. + + Args: + name: Name of the frame + """ + # Log axes as arrows + origin = [0, 0, 0] + axis_length = 0.1 + + rr.log( + f"{name}/x_axis", + rr.Arrows3D( + origins=[origin], + vectors=[[axis_length, 0, 0]], + colors=[[255, 0, 0]], # Red + ), + static=True, + ) + rr.log( + f"{name}/y_axis", + rr.Arrows3D( + origins=[origin], + vectors=[[0, axis_length, 0]], + colors=[[0, 255, 0]], # Green + ), + static=True, + ) + rr.log( + f"{name}/z_axis", + rr.Arrows3D( + origins=[origin], + vectors=[[0, 0, axis_length]], + colors=[[0, 0, 255]], # Blue + ), + static=True, + ) + + def visualize_scenario_items(self, items: list | dict, item_states: dict | None = None) -> None: + """Visualize a collection of scenario items (objects or robots). + + Args: + items: List or dict of item configurations to visualize + item_states: Optional dict mapping item names to their states (pos, rot, etc.) + """ + if item_states is None: + item_states = {} + if isinstance(items, list): + for item_cfg in items: + item_name = item_cfg.name + item_state = item_states.get(item_name, {}) + self.visualize_item(item_cfg, item_name, item_state) + elif isinstance(items, dict): + for item_name, item_cfg in items.items(): + item_state = item_states.get(item_name, {}) + self.visualize_item(item_cfg, item_name, item_state) + else: + logger.warning(f"Unsupported items type {type(items)}") + + def visualize_item( + self, + cfg: PrimitiveCubeCfg + | PrimitiveSphereCfg + | PrimitiveCylinderCfg + | RigidObjCfg + | ArticulationObjCfg + | BaseRobotCfg, + name: str, + state: dict | None = None, + ) -> None: + """Visualize a single item (robot or object) in the 3D scene. + + Args: + cfg: Configuration object for the item + name: Name identifier for the item + state: Optional state dictionary containing position, rotation, and joint positions + """ + logger.debug(f"[Rerun] Visualizing {name} with state: {state}") + + # Store config and state + self._item_configs[name] = cfg + if state: + self._item_states[name] = state + + # Get position and rotation + position = self._get_position(cfg, state) + rotation = self._get_rotation(cfg, state) + + # Handle different item types + if isinstance(cfg, (RigidObjCfg, ArticulationObjCfg, BaseRobotCfg)) and getattr(cfg, "urdf_path", None): + self._visualize_urdf(cfg, name, position, rotation, state) + elif isinstance(cfg, PrimitiveCubeCfg): + self._visualize_cube(cfg, name, position, rotation) + elif isinstance(cfg, PrimitiveSphereCfg): + self._visualize_sphere(cfg, name, position, rotation) + elif isinstance(cfg, PrimitiveCylinderCfg): + self._visualize_cylinder(cfg, name, position, rotation) + else: + logger.warning(f"Unsupported object type {type(cfg)} for {name}") + + def _get_position(self, cfg, state: dict | None) -> tuple[float, float, float]: + """Extract position from state or config defaults.""" + if state and "pos" in state and state["pos"] is not None: + pos = state["pos"] + if isinstance(pos, (list, tuple)) and len(pos) >= 3: + return (float(pos[0]), float(pos[1]), float(pos[2])) + return cfg.default_position + + def _get_rotation(self, cfg, state: dict | None) -> tuple[float, float, float, float]: + """Extract rotation (wxyz quaternion) from state or config defaults.""" + if state and "rot" in state and state["rot"] is not None: + rot = state["rot"] + if isinstance(rot, (list, tuple)) and len(rot) >= 4: + return (float(rot[0]), float(rot[1]), float(rot[2]), float(rot[3])) + return cfg.default_orientation + + def _visualize_urdf( + self, + cfg, + name: str, + position: tuple, + rotation: tuple, + state: dict | None, + ) -> None: + """Visualize a URDF model with proper kinematic chain. + + Args: + cfg: Configuration object with urdf_path + name: Name identifier + position: (x, y, z) position + rotation: (w, x, y, z) quaternion + state: Optional state with dof_pos + """ + urdf_path = cfg.urdf_path + + # Try to resolve the URDF path + urdf_path_resolved = Path(urdf_path) + if not urdf_path_resolved.exists(): + # Try common base directories + for base in [Path.cwd(), Path(__file__).parent.parent.parent.parent.parent]: + candidate = base / urdf_path + if candidate.exists(): + urdf_path_resolved = candidate + break + + if not urdf_path_resolved.exists(): + logger.warning(f"URDF file not found: {urdf_path} (resolved: {urdf_path_resolved})") + # Create a simple placeholder box for missing URDFs + self._visualize_placeholder(name, position, rotation) + return + + # Get scale - convert to tuple of floats + scale = cfg.scale if hasattr(cfg, "scale") else (1.0, 1.0, 1.0) + if isinstance(scale, (int, float)): + scale = (float(scale), float(scale), float(scale)) + else: + scale = tuple(float(s) for s in scale) + + try: + # Parse URDF with full kinematic chain + logger.info(f"Loading URDF for {name}: {urdf_path_resolved}") + urdf_data = parse_urdf_full(str(urdf_path_resolved)) + link_visuals = urdf_data["links"] + + if not link_visuals: + logger.warning(f"No visual geometries found in URDF: {urdf_path_resolved}") + self._visualize_placeholder(name, position, rotation) + return + + # Count total visuals for logging + total_visuals = sum(len(v) for v in link_visuals.values()) + logger.info(f"Found {len(link_visuals)} links with {total_visuals} visual geometries") + + # Get joint positions from state + dof_pos = {} + if state and "dof_pos" in state and state["dof_pos"]: + dof_pos = state["dof_pos"] + # Convert tensor values to floats if needed + for k, v in dof_pos.items(): + if hasattr(v, "item"): + dof_pos[k] = v.item() + elif hasattr(v, "cpu"): + dof_pos[k] = v.cpu().numpy().item() + + # Compute forward kinematics to get link world transforms + link_transforms = compute_link_transforms(urdf_data, dof_pos) + + # Build root transform matrix from position and rotation + root_transform = np.eye(4) + root_transform[:3, 3] = position + # Convert quaternion (w,x,y,z) to rotation matrix + root_transform[:3, :3] = self._quaternion_to_rotation_matrix(rotation) + + # Log the root entity with robot base transform + rr.log( + f"world/{name}", + rr.Transform3D( + translation=position, + rotation=rr.Quaternion(xyzw=[rotation[1], rotation[2], rotation[3], rotation[0]]), + ), + ) + + # Log each link's visual geometries with transforms relative to robot root + # Since world/{name} already has the root transform, we only apply link_local_transform + visuals_logged = 0 + for link_name, visuals in link_visuals.items(): + if not visuals: + continue + + # Get link's local transform in URDF frame (relative to robot root) + link_local_transform = link_transforms.get(link_name, np.eye(4)) + + # Extract position and rotation for the link (relative to robot root, not world) + link_pos = link_local_transform[:3, 3].tolist() + link_rot = self._rotation_matrix_to_quaternion(link_local_transform[:3, :3]) + + # Log link transform relative to robot root + rr.log( + f"world/{name}/{link_name}", + rr.Transform3D( + translation=link_pos, + rotation=rr.Quaternion(xyzw=[link_rot[1], link_rot[2], link_rot[3], link_rot[0]]), + ), + ) + + for i, visual_info in enumerate(visuals): + self._log_visual_with_scale( + f"world/{name}/{link_name}/visual_{i}", + visual_info, + scale, + ) + visuals_logged += 1 + + logger.info(f"Successfully visualized URDF {name}: {visuals_logged} visuals logged") + + # Store URDF data for later updates + self._urdf_models[name] = { + "urdf_data": urdf_data, + "scale": scale, + "root_transform": root_transform, + } + + except Exception as e: + logger.error(f"Error visualizing URDF {name}: {e}") + import traceback + + traceback.print_exc() + # Create placeholder on error + self._visualize_placeholder(name, position, rotation) + + def _quaternion_to_rotation_matrix(self, quat: tuple) -> np.ndarray: + """Convert quaternion (w, x, y, z) to 3x3 rotation matrix.""" + w, x, y, z = quat + + # Normalize + norm = np.sqrt(w * w + x * x + y * y + z * z) + if norm > 1e-10: + w, x, y, z = w / norm, x / norm, y / norm, z / norm + + R = np.array([ + [1 - 2 * y * y - 2 * z * z, 2 * x * y - 2 * w * z, 2 * x * z + 2 * w * y], + [2 * x * y + 2 * w * z, 1 - 2 * x * x - 2 * z * z, 2 * y * z - 2 * w * x], + [2 * x * z - 2 * w * y, 2 * y * z + 2 * w * x, 1 - 2 * x * x - 2 * y * y], + ]) + return R + + def _visualize_placeholder(self, name: str, position: tuple, rotation: tuple) -> None: + """Create a placeholder visualization for missing/invalid URDFs.""" + logger.info(f"Creating placeholder visualization for {name}") + rr.log( + f"world/{name}", + rr.Transform3D( + translation=position, + rotation=rr.Quaternion(xyzw=[rotation[1], rotation[2], rotation[3], rotation[0]]), + ), + ) + # Purple box as placeholder + rr.log( + f"world/{name}/placeholder", + rr.Boxes3D( + half_sizes=[[0.05, 0.05, 0.05]], + colors=[[128, 0, 128]], # Purple + ), + ) + + def _log_visual_with_scale( + self, + entity_path: str, + visual_info: dict, + global_scale: tuple, + ) -> None: + """Log a visual geometry with global scale applied to vertices/sizes. + + Args: + entity_path: Rerun entity path + visual_info: Dictionary with type, origin, color, and geometry params + global_scale: (sx, sy, sz) global scale to apply + """ + origin = visual_info.get("origin", np.eye(4)) + # Don't scale the visual's local position - only mesh vertices are scaled + pos = origin[:3, 3].tolist() + rot_matrix = origin[:3, :3] + quat = self._rotation_matrix_to_quaternion(rot_matrix) + + # Get color (RGBA 0-1 range) + rgba = visual_info.get("color", [0.8, 0.8, 0.8, 1.0]) + color = [int(c * 255) for c in rgba[:3]] + + visual_type = visual_info.get("type") + + # Log transform for this visual + rr.log( + entity_path, + rr.Transform3D( + translation=pos, + rotation=rr.Quaternion(xyzw=[quat[1], quat[2], quat[3], quat[0]]), + ), + ) + + if visual_type == "mesh": + mesh_path = visual_info.get("filename") + mesh_scale = visual_info.get("scale", [1, 1, 1]) + + if not mesh_path: + logger.debug(f"No mesh path for {entity_path}") + return + + if not os.path.exists(mesh_path): + logger.debug(f"Mesh file not found: {mesh_path}") + rr.log( + entity_path, + rr.Boxes3D( + half_sizes=[[0.01, 0.01, 0.01]], + colors=[[255, 0, 255]], + ), + ) + return + + ext = Path(mesh_path).suffix.lower() + mesh_loaded = False + + if TRIMESH_AVAILABLE: + try: + mesh = None + texture_info_data = None # Store texture info for later + + # First, try to load with textures for OBJ files + if ext == ".obj": + try: + # Load without force="mesh" to get textures + mesh_with_tex = trimesh.load(mesh_path) + if isinstance(mesh_with_tex, trimesh.Scene): + # Extract texture info before concatenating + for geom_name, geom in mesh_with_tex.geometry.items(): + if hasattr(geom, "visual") and geom.visual is not None: + vis = geom.visual + if hasattr(vis, "uv") and vis.uv is not None and hasattr(vis, "material"): + mat = vis.material + if hasattr(mat, "image") and mat.image is not None: + texture_info_data = { + "uv": np.array(vis.uv, dtype=np.float32), + "image": np.array(mat.image), + } + break + mesh = mesh_with_tex.dump(concatenate=True) + elif hasattr(mesh_with_tex, "visual"): + vis = mesh_with_tex.visual + if hasattr(vis, "uv") and vis.uv is not None and hasattr(vis, "material"): + mat = vis.material + if hasattr(mat, "image") and mat.image is not None: + texture_info_data = { + "uv": np.array(vis.uv, dtype=np.float32), + "image": np.array(mat.image), + } + mesh = mesh_with_tex + except Exception: + mesh = None + + # Fall back to force="mesh" for reliable geometry loading + if mesh is None or not hasattr(mesh, "vertices"): + if ext == ".dae": + mesh = trimesh.load(mesh_path, force="mesh", resolver=None) + else: + mesh = trimesh.load(mesh_path, force="mesh") + + if isinstance(mesh, trimesh.Scene): + mesh = mesh.dump(concatenate=True) + + if hasattr(mesh, "vertices") and hasattr(mesh, "faces"): + vertices = np.array(mesh.vertices, dtype=np.float32) + + # Apply URDF mesh scale first + if mesh_scale != [1, 1, 1]: + vertices = vertices * np.array(mesh_scale, dtype=np.float32) + + # Apply global scale to vertices + vertices = vertices * np.array(global_scale, dtype=np.float32) + + faces = np.array(mesh.faces, dtype=np.uint32) + + if len(vertices) > 0 and len(faces) > 0: + mesh3d_kwargs = { + "vertex_positions": vertices, + "triangle_indices": faces, + } + + # Compute vertex normals for better lighting + if self.use_normals: + try: + mesh.fix_normals() + if hasattr(mesh, "_cache"): + mesh._cache.clear() + normals = mesh.vertex_normals.astype(np.float32) + norms = np.linalg.norm(normals, axis=1, keepdims=True) + norms[norms == 0] = 1.0 + normals = normals / norms + mesh3d_kwargs["vertex_normals"] = normals + except Exception as e: + logger.debug(f"Failed to compute normals for {mesh_path}: {e}") + + has_texture = False + + # Try to use pre-extracted texture info + if texture_info_data is not None: + uv = texture_info_data["uv"] + texture_image = texture_info_data["image"] + if len(uv) == len(vertices) and texture_image.size > 0: + mesh3d_kwargs["vertex_texcoords"] = uv + if len(texture_image.shape) == 2: + texture_image = np.stack([texture_image] * 3, axis=-1) + mesh3d_kwargs["albedo_texture"] = texture_image + has_texture = True + logger.debug(f"Loaded texture for {mesh_path}: {texture_image.shape}") + + # Fall back to vertex colors + if not has_texture: + if hasattr(mesh, "visual") and hasattr(mesh.visual, "vertex_colors"): + vc = mesh.visual.vertex_colors + if vc is not None and len(vc) == len(vertices): + mesh3d_kwargs["vertex_colors"] = vc[:, :3].tolist() + else: + mesh3d_kwargs["vertex_colors"] = [color] * len(vertices) + else: + mesh3d_kwargs["vertex_colors"] = [color] * len(vertices) + + rr.log(entity_path, rr.Mesh3D(**mesh3d_kwargs)) + mesh_loaded = True + texture_info = " with texture" if has_texture else "" + normals_info = ( + " with normals" if self.use_normals and "vertex_normals" in mesh3d_kwargs else "" + ) + logger.debug( + f"Loaded mesh: {mesh_path} ({len(vertices)} verts, scale={global_scale}){texture_info}{normals_info}" + ) + except Exception as e: + logger.warning(f"trimesh failed for {mesh_path}: {e}") + + if not mesh_loaded and ext in [".obj", ".glb", ".gltf", ".stl", ".dae"]: + try: + # Asset3D can load textures automatically for supported formats + if global_scale != (1.0, 1.0, 1.0): + logger.warning( + f"Cannot apply scale to Asset3D: {mesh_path}. Install trimesh for proper scaling." + ) + rr.log( + entity_path, + rr.Asset3D(path=mesh_path), + ) + mesh_loaded = True + logger.debug(f"Loaded mesh as Asset3D: {mesh_path}") + except Exception as e: + logger.debug(f"Asset3D failed for {mesh_path}: {e}") + + if not mesh_loaded: + logger.warning(f"Could not load mesh: {mesh_path} (format: {ext})") + rr.log( + entity_path, + rr.Boxes3D( + half_sizes=[[0.02, 0.02, 0.02]], + colors=[[255, 128, 0]], + ), + ) + return + + elif visual_type == "box": + size = visual_info.get("size", [1, 1, 1]) + # Apply global scale to box size + scaled_size = [s * gs for s, gs in zip(size, global_scale)] + + # Use Mesh3D for solid rendering + if TRIMESH_AVAILABLE: + try: + box = trimesh.creation.box(extents=scaled_size) + vertices = np.array(box.vertices, dtype=np.float32) + faces = np.array(box.faces, dtype=np.uint32) + rr.log( + entity_path, + rr.Mesh3D( + vertex_positions=vertices, + triangle_indices=faces, + vertex_colors=[color] * len(vertices), + ), + ) + return + except Exception: + pass + + # Fallback + half_sizes = [s / 2 for s in scaled_size] + rr.log( + entity_path, + rr.Boxes3D( + half_sizes=[half_sizes], + colors=[color], + ), + ) + + elif visual_type == "sphere": + radius = visual_info.get("radius", 1.0) + # Apply average scale to sphere radius + avg_scale = sum(global_scale) / 3.0 + scaled_radius = radius * avg_scale + + # Use Mesh3D for solid rendering + if TRIMESH_AVAILABLE: + try: + sphere = trimesh.creation.icosphere(subdivisions=3, radius=scaled_radius) + vertices = np.array(sphere.vertices, dtype=np.float32) + faces = np.array(sphere.faces, dtype=np.uint32) + rr.log( + entity_path, + rr.Mesh3D( + vertex_positions=vertices, + triangle_indices=faces, + vertex_colors=[color] * len(vertices), + ), + ) + return + except Exception: + pass + + # Fallback + rr.log( + entity_path, + rr.Ellipsoids3D( + half_sizes=[[scaled_radius, scaled_radius, scaled_radius]], + colors=[color], + ), + ) + + elif visual_type == "cylinder": + radius = visual_info.get("radius", 1.0) + length = visual_info.get("length", 1.0) + + # Apply scale (xy for radius, z for length) + scaled_radius = radius * (global_scale[0] + global_scale[1]) / 2.0 + scaled_length = length * global_scale[2] + + if TRIMESH_AVAILABLE: + try: + cylinder = trimesh.creation.cylinder(radius=scaled_radius, height=scaled_length) + vertices = np.array(cylinder.vertices) + faces = np.array(cylinder.faces) + rr.log( + entity_path, + rr.Mesh3D( + vertex_positions=vertices, + triangle_indices=faces, + vertex_colors=[color] * len(vertices), + ), + ) + except Exception as e: + logger.warning(f"Failed to create cylinder mesh: {e}") + else: + rr.log( + entity_path, + rr.Ellipsoids3D( + half_sizes=[[scaled_radius, scaled_radius, scaled_length / 2]], + colors=[color], + ), + ) + + def _log_visual_simple( + self, + entity_path: str, + visual_info: dict, + ) -> None: + """Log a visual geometry using the simple URDF parser output (no global scale). + + Args: + entity_path: Rerun entity path + visual_info: Dictionary with type, origin, color, and geometry params + """ + # Delegate to scaled version with identity scale + self._log_visual_with_scale(entity_path, visual_info, (1.0, 1.0, 1.0)) + + def _rotation_matrix_to_quaternion(self, R: np.ndarray) -> tuple: + """Convert rotation matrix to quaternion (w, x, y, z).""" + trace = np.trace(R) + if trace > 0: + s = 0.5 / np.sqrt(trace + 1.0) + w = 0.25 / s + x = (R[2, 1] - R[1, 2]) * s + y = (R[0, 2] - R[2, 0]) * s + z = (R[1, 0] - R[0, 1]) * s + elif R[0, 0] > R[1, 1] and R[0, 0] > R[2, 2]: + s = 2.0 * np.sqrt(1.0 + R[0, 0] - R[1, 1] - R[2, 2]) + w = (R[2, 1] - R[1, 2]) / s + x = 0.25 * s + y = (R[0, 1] + R[1, 0]) / s + z = (R[0, 2] + R[2, 0]) / s + elif R[1, 1] > R[2, 2]: + s = 2.0 * np.sqrt(1.0 + R[1, 1] - R[0, 0] - R[2, 2]) + w = (R[0, 2] - R[2, 0]) / s + x = (R[0, 1] + R[1, 0]) / s + y = 0.25 * s + z = (R[1, 2] + R[2, 1]) / s + else: + s = 2.0 * np.sqrt(1.0 + R[2, 2] - R[0, 0] - R[1, 1]) + w = (R[1, 0] - R[0, 1]) / s + x = (R[0, 2] + R[2, 0]) / s + y = (R[1, 2] + R[2, 1]) / s + z = 0.25 * s + + # Normalize + norm = np.sqrt(w * w + x * x + y * y + z * z) + return (w / norm, x / norm, y / norm, z / norm) + + def _visualize_cube( + self, + cfg: PrimitiveCubeCfg, + name: str, + position: tuple, + rotation: tuple, + ) -> None: + """Visualize a cube primitive.""" + # cfg.size is the full edge length + size = list(cfg.size) + + color = [200, 200, 200] + if hasattr(cfg, "color") and cfg.color: + color = [int(c * 255) if c <= 1.0 else int(c) for c in cfg.color[:3]] + + # Log transform to parent entity + rr.log( + f"world/{name}", + rr.Transform3D( + translation=position, + rotation=rr.Quaternion(xyzw=[rotation[1], rotation[2], rotation[3], rotation[0]]), + ), + ) + + # Use Mesh3D for solid surface rendering instead of Boxes3D (which renders as wireframe) + # Log geometry to child entity so it inherits parent transform + if TRIMESH_AVAILABLE: + try: + # Create box mesh with trimesh - extents is the full size + box = trimesh.creation.box(extents=size) + vertices = np.array(box.vertices, dtype=np.float32) + faces = np.array(box.faces, dtype=np.uint32) + rr.log( + f"world/{name}/visual", + rr.Mesh3D( + vertex_positions=vertices, + triangle_indices=faces, + vertex_colors=[color] * len(vertices), + ), + ) + return + except Exception as e: + logger.debug(f"Failed to create cube mesh: {e}") + + # Fallback to Boxes3D - uses half_size + half_size = [s / 2 for s in size] + rr.log( + f"world/{name}/visual", + rr.Boxes3D( + half_sizes=[half_size], + colors=[color], + fill_mode=rr.components.FillMode.Solid, + ), + ) + + def _visualize_sphere( + self, + cfg: PrimitiveSphereCfg, + name: str, + position: tuple, + rotation: tuple, + ) -> None: + """Visualize a sphere primitive.""" + # cfg.radius is the radius (half the diameter) + radius = cfg.radius + + color = [200, 200, 200] + if hasattr(cfg, "color") and cfg.color: + color = [int(c * 255) if c <= 1.0 else int(c) for c in cfg.color[:3]] + + # Log transform to parent entity + rr.log( + f"world/{name}", + rr.Transform3D( + translation=position, + rotation=rr.Quaternion(xyzw=[rotation[1], rotation[2], rotation[3], rotation[0]]), + ), + ) + + # Use Mesh3D for solid surface rendering instead of Ellipsoids3D (which renders as wireframe) + # Log geometry to child entity so it inherits parent transform + if TRIMESH_AVAILABLE: + try: + # Create sphere mesh with trimesh (subdivisions=3 gives good quality) + sphere = trimesh.creation.icosphere(subdivisions=3, radius=radius) + vertices = np.array(sphere.vertices, dtype=np.float32) + faces = np.array(sphere.faces, dtype=np.uint32) + rr.log( + f"world/{name}/visual", + rr.Mesh3D( + vertex_positions=vertices, + triangle_indices=faces, + vertex_colors=[color] * len(vertices), + ), + ) + return + except Exception as e: + logger.debug(f"Failed to create sphere mesh: {e}") + + # Fallback to Ellipsoids3D + rr.log( + f"world/{name}/visual", + rr.Ellipsoids3D( + half_sizes=[[radius, radius, radius]], + colors=[color], + fill_mode=rr.components.FillMode.Solid, + ), + ) + + def _visualize_cylinder( + self, + cfg: PrimitiveCylinderCfg, + name: str, + position: tuple, + rotation: tuple, + ) -> None: + """Visualize a cylinder primitive.""" + radius = cfg.radius + height = cfg.height + + color = [200, 200, 200] + if hasattr(cfg, "color") and cfg.color: + color = [int(c * 255) if c <= 1.0 else int(c) for c in cfg.color[:3]] + + # Log transform to parent entity + rr.log( + f"world/{name}", + rr.Transform3D( + translation=position, + rotation=rr.Quaternion(xyzw=[rotation[1], rotation[2], rotation[3], rotation[0]]), + ), + ) + + # Log geometry to child entity so it inherits parent transform + if TRIMESH_AVAILABLE: + # Create cylinder mesh + cylinder = trimesh.creation.cylinder(radius=radius, height=height) + vertices = np.array(cylinder.vertices) + faces = np.array(cylinder.faces) + + rr.log( + f"world/{name}/visual", + rr.Mesh3D( + vertex_positions=vertices, + triangle_indices=faces, + vertex_colors=[color] * len(vertices), + ), + ) + else: + # Fallback: use ellipsoid approximation + rr.log( + f"world/{name}/visual", + rr.Ellipsoids3D( + half_sizes=[[radius, radius, height / 2]], + colors=[color], + ), + ) + + def update_item_pose(self, name: str, state: dict) -> None: + """Update the pose of a visualized item. + + Args: + name: Name of the item to update + state: State dict containing 'pos', 'rot', and optionally 'dof_pos' + """ + self._item_states[name] = state + + position = None + rotation = None + + if "pos" in state and state["pos"] is not None: + pos = state["pos"] + if hasattr(pos, "cpu"): + pos = pos.cpu().numpy() + position = list(pos) + + if "rot" in state and state["rot"] is not None: + rot = state["rot"] + if hasattr(rot, "cpu"): + rot = rot.cpu().numpy() + rotation = list(rot) + + # Update base transform + if position is not None or rotation is not None: + transform_args = {} + if position is not None: + transform_args["translation"] = position + if rotation is not None: + # rotation is [w, x, y, z], rerun wants xyzw + transform_args["rotation"] = rr.Quaternion(xyzw=[rotation[1], rotation[2], rotation[3], rotation[0]]) + + rr.log(f"world/{name}", rr.Transform3D(**transform_args)) + + # Update articulated joints if we have URDF data and dof_pos + if name in self._urdf_models and "dof_pos" in state and state["dof_pos"]: + self._update_urdf_joints(name, state) + + def _update_urdf_joints(self, name: str, state: dict) -> None: + """Update joint positions for an articulated URDF model. + + Args: + name: Name of the item + state: State dict with 'pos', 'rot', 'dof_pos' + """ + urdf_info = self._urdf_models.get(name) + if not urdf_info: + return + + urdf_data = urdf_info["urdf_data"] + + # Get joint positions + dof_pos = state.get("dof_pos", {}) + if not dof_pos: + return + + # Convert tensor values to floats + dof_pos_float = {} + for k, v in dof_pos.items(): + if hasattr(v, "item"): + dof_pos_float[k] = v.item() + elif hasattr(v, "cpu"): + dof_pos_float[k] = v.cpu().numpy().item() + else: + dof_pos_float[k] = float(v) + + # Compute new link transforms using forward kinematics + link_transforms = compute_link_transforms(urdf_data, dof_pos_float) + + # Update each link's transform (relative to robot root, since world/{name} has root transform) + for link_name in urdf_data["links"]: + link_local_transform = link_transforms.get(link_name, np.eye(4)) + + # Use local transform relative to robot root (not world transform) + link_pos = link_local_transform[:3, 3].tolist() + link_rot = self._rotation_matrix_to_quaternion(link_local_transform[:3, :3]) + + rr.log( + f"world/{name}/{link_name}", + rr.Transform3D( + translation=link_pos, + rotation=rr.Quaternion(xyzw=[link_rot[1], link_rot[2], link_rot[3], link_rot[0]]), + ), + ) + + def set_time(self, time_step: int) -> None: + """Set the current time step for timeline-based visualization. + + Args: + time_step: Current simulation step + """ + self._time_step = time_step + rr.set_time_sequence("step", time_step) + + def log_trajectory_point( + self, + name: str, + position: list | np.ndarray, + color: list | None = None, + ) -> None: + """Log a single trajectory point. + + Args: + name: Name of the trajectory entity + position: 3D position [x, y, z] + color: Optional RGB color [r, g, b] + """ + if color is None: + color = [255, 165, 0] # Orange + + rr.log( + f"world/trajectories/{name}", + rr.Points3D( + positions=[position], + colors=[color], + radii=[0.01], + ), + ) + + def log_trajectory( + self, + name: str, + positions: list | np.ndarray, + color: list | None = None, + ) -> None: + """Log a complete trajectory as a line strip. + + Args: + name: Name of the trajectory entity + positions: List of 3D positions [[x, y, z], ...] + color: Optional RGB color [r, g, b] + """ + if color is None: + color = [255, 165, 0] # Orange + + rr.log( + f"world/trajectories/{name}", + rr.LineStrips3D( + [positions], + colors=[color], + ), + static=True, + ) + + def log_camera_image( + self, + camera_name: str, + rgb: np.ndarray, + depth: np.ndarray | None = None, + ) -> None: + """Log camera images. + + Args: + camera_name: Name of the camera + rgb: RGB image array (H, W, 3) + depth: Optional depth image array (H, W) + """ + rr.log(f"cameras/{camera_name}/rgb", rr.Image(rgb)) + + if depth is not None: + rr.log(f"cameras/{camera_name}/depth", rr.DepthImage(depth)) + + def clear(self) -> None: + """Clear all logged data.""" + # Clear internal caches + self._urdf_models.clear() + self._item_states.clear() + self._item_configs.clear() + self._initial_configs.clear() + self._joint_names_cache.clear() + self._time_step = 0 + + def close(self) -> None: + """Close the visualizer and clean up resources.""" + self.clear() + logger.info("Rerun visualizer closed") diff --git a/metasim/utils/viser/viser_env_wrapper.py b/metasim/utils/viser/viser_env_wrapper.py index 3d2b67b9d..ee55d6a6b 100644 --- a/metasim/utils/viser/viser_env_wrapper.py +++ b/metasim/utils/viser/viser_env_wrapper.py @@ -175,41 +175,10 @@ def close(self): if self.env: self.env.close() - # Delegate other methods to wrapped environment - def __getattr__(self, name: str) -> Any: - """Delegate attribute access to wrapped environment.""" - return getattr(self.env, name) - def __len__(self) -> int: """Return number of environments.""" return self.env.num_envs if hasattr(self.env, "num_envs") else 1 - @property - def num_envs(self) -> int: - """Number of environments.""" - return self.env.num_envs if hasattr(self.env, "num_envs") else 1 - - @property - def num_actions(self) -> int: - """Number of actions.""" - return self.env.num_actions if hasattr(self.env, "num_actions") else 0 - - @property - def num_obs(self) -> int: - """Number of observations.""" - return self.env.num_obs if hasattr(self.env, "num_obs") else 0 - - @property - def max_episode_steps(self) -> int: - """Maximum episode steps.""" - return self.env.max_episode_steps if hasattr(self.env, "max_episode_steps") else 1000 - - @property - def action_space(self): - """Action space.""" - return self.env.action_space if hasattr(self.env, "action_space") else None - - @property - def observation_space(self): - """Observation space.""" - return self.env.observation_space if hasattr(self.env, "observation_space") else None + def __getattr__(self, name: str) -> Any: + """Delegate attribute access to wrapped environment.""" + return getattr(self.env, name) diff --git a/metasim/utils/viz_handler_wrapper.py b/metasim/utils/viz_handler_wrapper.py new file mode 100644 index 000000000..129ca7cbd --- /dev/null +++ b/metasim/utils/viz_handler_wrapper.py @@ -0,0 +1,321 @@ +"""Handler wrapper for automatic visualization updates. + +This module provides a wrapper for BaseSimHandler that automatically updates +Rerun and Viser visualizations when set_states or simulate is called. +""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from metasim.sim.base import BaseSimHandler + from metasim.types import DictEnvState, TensorState + +logger = logging.getLogger(__name__) + +# Check for Rerun availability +try: + import rerun as rr # noqa: F401 + + RERUN_AVAILABLE = True +except ImportError: + RERUN_AVAILABLE = False + +# Check for Viser availability +try: + import viser # noqa: F401 + + VISER_AVAILABLE = True +except ImportError: + VISER_AVAILABLE = False + + +class VizHandlerWrapper: + """Wrapper for BaseSimHandler that automatically updates visualization. + + This wrapper intercepts set_states and simulate calls to automatically + update Rerun and/or Viser visualizations. This allows visualization to + work at the handler level without needing to wrap the environment. + + Args: + handler: BaseSimHandler instance to wrap + use_rerun: Whether to enable Rerun visualization (default: False) + use_viser: Whether to enable Viser visualization (default: False) + rerun_app_name: Application name for Rerun (default: "RoboVerse Handler") + rerun_spawn: Whether to spawn Rerun viewer automatically (default: True) + viser_port: Port for Viser server (default: 8080) + update_freq: Update visualization every N calls to reduce resource usage (default: 1) + """ + + def __init__( + self, + handler: BaseSimHandler, + use_rerun: bool = False, + use_viser: bool = False, + rerun_app_name: str = "RoboVerse Handler", + rerun_spawn: bool = True, + viser_port: int = 8080, + update_freq: int = 1, + ): + self.handler = handler + self.use_rerun = use_rerun + self.use_viser = use_viser + self.update_freq = update_freq + self.call_count = 0 + + # Initialize visualizers + self.rerun_visualizer = None + self.viser_visualizer = None + + if use_rerun: + self._setup_rerun(rerun_app_name, rerun_spawn) + + if use_viser: + self._setup_viser(viser_port) + + def _setup_rerun(self, app_name: str, spawn: bool): + """Setup Rerun visualization.""" + if not RERUN_AVAILABLE: + logger.warning("Rerun requested but rerun-sdk not available. Install with: pip install rerun-sdk") + return + + try: + from metasim.utils.rerun.rerun_util import RerunVisualizer + from metasim.utils.state import state_tensor_to_nested + + # Download URDF files + self._download_urdf_files() + + # Initialize visualizer + self.rerun_visualizer = RerunVisualizer(app_name=app_name, spawn=spawn) + + # Get initial states and visualize + obs = self.handler.get_states(mode="tensor") + env_states = state_tensor_to_nested(self.handler, obs) + + if env_states and len(env_states) > 0: + state = env_states[0] + object_states = state.get("objects", {}) + robot_states = state.get("robots", {}) + + # Visualize objects + for obj in self.handler.objects: + obj_state = self._extract_state(object_states.get(obj.name, {})) + self.rerun_visualizer.visualize_item(obj, obj.name, obj_state) + + # Visualize robots + for robot in self.handler.robots: + robot_state = self._extract_state(robot_states.get(robot.name, {})) + self.rerun_visualizer.visualize_item(robot, robot.name, robot_state) + + logger.info("Rerun visualization initialized for handler") + + except Exception as e: + logger.error(f"Failed to setup Rerun visualization: {e}") + import traceback + + traceback.print_exc() + self.rerun_visualizer = None + + def _setup_viser(self, port: int): + """Setup Viser visualization.""" + if not VISER_AVAILABLE: + logger.warning("Viser requested but viser not available. Install with: pip install viser") + return + + try: + from metasim.utils.viser.viser_util import ViserVisualizer + + self.viser_visualizer = ViserVisualizer(port=port) + self.viser_visualizer.add_grid() + self.viser_visualizer.add_frame("/world_frame") + + # Download URDF files + self._download_urdf_files() + + # Get initial states + obs = self.handler.get_states(mode="tensor") + + # Extract initial states for first environment only + if hasattr(obs, "objects") and hasattr(obs, "robots"): + default_object_states = self._extract_states_from_obs(obs, "objects") + default_robot_states = self._extract_states_from_obs(obs, "robots") + + # Visualize all objects and robots + if self.handler.objects: + self.viser_visualizer.visualize_scenario_items(self.handler.objects, default_object_states) + + if self.handler.robots: + self.viser_visualizer.visualize_scenario_items(self.handler.robots, default_robot_states) + + # Setup camera + self.viser_visualizer.enable_camera_controls( + initial_position=[1.5, -1.5, 1.5], + render_width=1024, + render_height=1024, + look_at_position=[0, 0, 0], + initial_fov=71.28, + ) + + logger.info("Viser visualization initialized for handler") + + except Exception as e: + logger.error(f"Failed to setup Viser visualization: {e}") + import traceback + + traceback.print_exc() + self.viser_visualizer = None + + def _download_urdf_files(self): + """Download URDF files for all objects and robots.""" + from metasim.utils.hf_util import check_and_download_recursive + + urdf_paths = [] + + for obj in self.handler.objects: + if hasattr(obj, "urdf_path") and obj.urdf_path: + urdf_paths.append(obj.urdf_path) + + for robot in self.handler.robots: + if hasattr(robot, "urdf_path") and robot.urdf_path: + urdf_paths.append(robot.urdf_path) + + if urdf_paths: + check_and_download_recursive(urdf_paths, n_processes=16) + + def _extract_state(self, state_dict: dict) -> dict: + """Extract state from nested dict format.""" + result = {} + + if "pos" in state_dict and state_dict["pos"] is not None: + pos = state_dict["pos"] + if hasattr(pos, "cpu"): + pos = pos.cpu().numpy() + result["pos"] = list(pos) + + if "rot" in state_dict and state_dict["rot"] is not None: + rot = state_dict["rot"] + if hasattr(rot, "cpu"): + rot = rot.cpu().numpy() + result["rot"] = list(rot) + + if "dof_pos" in state_dict and state_dict["dof_pos"] is not None: + result["dof_pos"] = state_dict["dof_pos"] + + return result + + def _extract_states_from_obs(self, obs, key): + """Extract states from observation tensor (first environment only). + + Args: + obs: TensorState observation + key: "objects" or "robots" + + Returns: + dict[name] = {"pos": ..., "rot": ..., "dof_pos": ...} + """ + if not hasattr(obs, key): + return {} + + result = {} + items = getattr(obs, key) + + for name, item in items.items(): + state_dict = {} + + # Extract position and rotation from root_state (first 7 values of first env) + if hasattr(item, "root_state") and item.root_state is not None: + root_state = item.root_state[0] # First environment only + state_dict["pos"] = root_state[:3].cpu().numpy().tolist() + state_dict["rot"] = root_state[3:7].cpu().numpy().tolist() + + # Extract joint positions (first environment only) + if hasattr(item, "joint_pos") and item.joint_pos is not None: + joint_names = self.handler._get_joint_names(name, sort=True) + state_dict["dof_pos"] = {joint_names[i]: item.joint_pos[0, i].item() for i in range(len(joint_names))} + + result[name] = state_dict + + return result + + def _update_visualization(self): + """Update visualization with current state.""" + self.call_count += 1 + + # Skip if not time to update + if self.call_count % self.update_freq != 0: + return + + try: + from metasim.utils.state import state_tensor_to_nested + + obs = self.handler.get_states(mode="tensor") + + # Update Rerun visualization + if self.rerun_visualizer is not None: + env_states = state_tensor_to_nested(self.handler, obs) + + if env_states and len(env_states) > 0: + state = env_states[0] + object_states = state.get("objects", {}) + robot_states = state.get("robots", {}) + + # Update objects + for obj in self.handler.objects: + if obj.name in object_states: + obj_state = self._extract_state(object_states[obj.name]) + self.rerun_visualizer.update_item_pose(obj.name, obj_state) + + # Update robots + for robot in self.handler.robots: + if robot.name in robot_states: + robot_state = self._extract_state(robot_states[robot.name]) + self.rerun_visualizer.update_item_pose(robot.name, robot_state) + + # Update time step + self.rerun_visualizer.set_time(self.call_count) + + # Update Viser visualization + if self.viser_visualizer is not None: + if hasattr(obs, "objects") and hasattr(obs, "robots"): + # Update objects from first environment + if self.handler.objects: + object_states = self._extract_states_from_obs(obs, "objects") + for name, state in object_states.items(): + self.viser_visualizer.update_item_pose(name, state) + + # Update robots from first environment + if self.handler.robots: + robot_states = self._extract_states_from_obs(obs, "robots") + for name, state in robot_states.items(): + self.viser_visualizer.update_item_pose(name, state) + + self.viser_visualizer.refresh_camera_view() + + except Exception as e: + logger.debug(f"Visualization update failed: {e}") + + def set_states(self, states: TensorState | list[DictEnvState], env_ids: list[int] | None = None) -> None: + """Set states and update visualization.""" + self.handler.set_states(states, env_ids) + self._update_visualization() + + def simulate(self): + """Simulate and update visualization.""" + self.handler.simulate() + self._update_visualization() + + def close(self): + """Close handler and visualizers.""" + if self.rerun_visualizer is not None: + self.rerun_visualizer.close() + # Note: ViserVisualizer doesn't have a close method + # The server will be cleaned up when the object is garbage collected + if hasattr(self.handler, "close"): + self.handler.close() + + def __getattr__(self, name: str): + """Proxy all other attributes to the wrapped handler.""" + return getattr(self.handler, name) diff --git a/metasim/utils/viz_task_wrapper.py b/metasim/utils/viz_task_wrapper.py new file mode 100644 index 000000000..21265fc75 --- /dev/null +++ b/metasim/utils/viz_task_wrapper.py @@ -0,0 +1,364 @@ +"""Unified visualization wrapper for RLTaskEnv. + +This module provides a unified wrapper for RLTaskEnv that supports both Rerun and Viser +visualization simultaneously or individually. +""" + +from __future__ import annotations + +import logging +from typing import Any + +logger = logging.getLogger(__name__) + +# Check for Rerun availability +try: + import rerun as rr # noqa: F401 + + RERUN_AVAILABLE = True +except ImportError: + RERUN_AVAILABLE = False + +# Check for Viser availability +try: + import viser # noqa: F401 + + VISER_AVAILABLE = True +except ImportError: + VISER_AVAILABLE = False + + +class TaskVizWrapper: + """Unified wrapper for RLTaskEnv with Rerun and/or Viser visualization. + + This wrapper supports both Rerun and Viser visualization simultaneously or individually. + Only renders the first environment for simplicity. Designed for RL training integration. + + Args: + task_env: RLTaskEnv or similar environment with handler + use_rerun: Whether to enable Rerun visualization (default: False) + use_viser: Whether to enable Viser visualization (default: False) + rerun_app_name: Application name for Rerun (default: "RoboVerse Training") + rerun_spawn: Whether to spawn Rerun viewer automatically (default: True) + viser_port: Port for Viser server (default: 8080) + update_freq: Update visualization every N steps to reduce resource usage (default: 10) + """ + + def __init__( + self, + task_env, + use_rerun: bool = False, + use_viser: bool = False, + rerun_app_name: str = "RoboVerse Training", + rerun_spawn: bool = True, + viser_port: int = 8080, + update_freq: int = 10, + ): + if use_rerun and not RERUN_AVAILABLE: + raise ImportError("rerun-sdk is required. Install with: pip install rerun-sdk") + + if use_viser and not VISER_AVAILABLE: + raise ImportError("viser is required. Install with: pip install viser") + + if not use_rerun and not use_viser: + logger.warning("Neither Rerun nor Viser is enabled. Wrapper will have no effect.") + + self.env = task_env + self.use_rerun = use_rerun + self.use_viser = use_viser + self.update_freq = update_freq + self.step_count = 0 + self.handler = self._get_handler() + + # Visualizers + self.rerun_visualizer = None + self.viser_visualizer = None + + # Setup visualizations + if use_rerun: + self._setup_rerun(rerun_app_name, rerun_spawn) + + if use_viser: + self._setup_viser(viser_port) + + def _get_handler(self): + """Get the actual handler object, supporting both gym and non-gym environments.""" + # Try gym environment path first (gym_vec.task_env.handler) + if hasattr(self.env, "task_env"): + if hasattr(self.env.task_env, "handler") and self.env.task_env.handler is not None: + return self.env.task_env.handler + + # Fall back to direct handler access (env.handler) + if hasattr(self.env, "handler") and self.env.handler is not None: + return self.env.handler + + return None + + def _setup_rerun(self, app_name: str, spawn: bool): + """Setup Rerun visualization.""" + if self.handler is None: + logger.warning("No handler found, skipping Rerun visualization setup") + return + + try: + from metasim.utils.rerun.rerun_util import RerunVisualizer + from metasim.utils.state import state_tensor_to_nested + + # Download URDF files + self._download_urdf_files() + + # Initialize visualizer + self.rerun_visualizer = RerunVisualizer(app_name=app_name, spawn=spawn) + + # Get initial states + obs = self.handler.get_states(mode="tensor") + env_states = state_tensor_to_nested(self.handler, obs) + + if env_states and len(env_states) > 0: + state = env_states[0] + object_states = state.get("objects", {}) + robot_states = state.get("robots", {}) + + # Visualize objects + for obj in self.handler.objects: + obj_state = self._extract_state(object_states.get(obj.name, {})) + self.rerun_visualizer.visualize_item(obj, obj.name, obj_state) + + # Visualize robots + for robot in self.handler.robots: + robot_state = self._extract_state(robot_states.get(robot.name, {})) + self.rerun_visualizer.visualize_item(robot, robot.name, robot_state) + + logger.info("Rerun visualization initialized") + + except Exception as e: + logger.error(f"Failed to setup Rerun visualization: {e}") + import traceback + + traceback.print_exc() + self.rerun_visualizer = None + + def _setup_viser(self, port: int): + """Setup Viser visualization.""" + if self.handler is None: + logger.warning("No handler found, skipping Viser visualization setup") + return + + try: + from metasim.utils.viser.viser_util import ViserVisualizer + + self.viser_visualizer = ViserVisualizer(port=port) + self.viser_visualizer.add_grid() + self.viser_visualizer.add_frame("/world_frame") + + # Download URDF files for visualization + self._download_urdf_files() + + # Get initial states using handler + obs = self.handler.get_states(mode="tensor") + + # Extract initial states for first environment only + if hasattr(obs, "objects") and hasattr(obs, "robots"): + default_object_states = self._extract_states_from_obs(obs, "objects") + default_robot_states = self._extract_states_from_obs(obs, "robots") + + # Visualize all objects and robots + # Try to use scenario first, fall back to handler + objects = None + robots = None + if hasattr(self.env, "scenario"): + objects = getattr(self.env.scenario, "objects", None) + robots = getattr(self.env.scenario, "robots", None) + + if objects is None and self.handler.objects: + objects = self.handler.objects + if robots is None and self.handler.robots: + robots = self.handler.robots + + if objects: + self.viser_visualizer.visualize_scenario_items(objects, default_object_states) + + if robots: + self.viser_visualizer.visualize_scenario_items(robots, default_robot_states) + + # Setup camera + self.viser_visualizer.enable_camera_controls( + initial_position=[1.5, -1.5, 1.5], + render_width=1024, + render_height=1024, + look_at_position=[0, 0, 0], + initial_fov=71.28, + ) + + logger.info("Viser visualization initialized") + + except Exception as e: + logger.error(f"Failed to setup Viser visualization: {e}") + import traceback + + traceback.print_exc() + self.viser_visualizer = None + + def _download_urdf_files(self): + """Download URDF files for all objects and robots.""" + from metasim.utils.hf_util import check_and_download_recursive + + urdf_paths = [] + + for obj in self.handler.objects: + if hasattr(obj, "urdf_path") and obj.urdf_path: + urdf_paths.append(obj.urdf_path) + + for robot in self.handler.robots: + if hasattr(robot, "urdf_path") and robot.urdf_path: + urdf_paths.append(robot.urdf_path) + + if urdf_paths: + check_and_download_recursive(urdf_paths, n_processes=16) + + def _extract_state(self, state_dict: dict) -> dict: + """Extract state from nested dict format.""" + result = {} + + if "pos" in state_dict and state_dict["pos"] is not None: + pos = state_dict["pos"] + if hasattr(pos, "cpu"): + pos = pos.cpu().numpy() + result["pos"] = list(pos) + + if "rot" in state_dict and state_dict["rot"] is not None: + rot = state_dict["rot"] + if hasattr(rot, "cpu"): + rot = rot.cpu().numpy() + result["rot"] = list(rot) + + if "dof_pos" in state_dict and state_dict["dof_pos"] is not None: + result["dof_pos"] = state_dict["dof_pos"] + + return result + + def _extract_states_from_obs(self, obs, key): + """Extract states from observation tensor (first environment only). + + Args: + obs: TensorState observation + key: "objects" or "robots" + + Returns: + dict[name] = {"pos": ..., "rot": ..., "dof_pos": ...} + """ + if not hasattr(obs, key): + return {} + + result = {} + items = getattr(obs, key) + + for name, item in items.items(): + state_dict = {} + + # Extract position and rotation from root_state (first 7 values of first env) + if hasattr(item, "root_state") and item.root_state is not None: + root_state = item.root_state[0] # First environment only + state_dict["pos"] = root_state[:3].cpu().numpy().tolist() + state_dict["rot"] = root_state[3:7].cpu().numpy().tolist() + + # Extract joint positions (first environment only) + if hasattr(item, "joint_pos") and item.joint_pos is not None: + if self.handler is not None: + joint_names = self.handler._get_joint_names(name, sort=True) + state_dict["dof_pos"] = { + joint_names[i]: item.joint_pos[0, i].item() for i in range(len(joint_names)) + } + + result[name] = state_dict + + return result + + def _update_visualization(self): + """Update visualization with current state.""" + if self.handler is None: + return + + try: + from metasim.utils.state import state_tensor_to_nested + + obs = self.handler.get_states(mode="tensor") + + # Update Rerun visualization + if self.rerun_visualizer is not None: + env_states = state_tensor_to_nested(self.handler, obs) + + if env_states and len(env_states) > 0: + state = env_states[0] + object_states = state.get("objects", {}) + robot_states = state.get("robots", {}) + + # Update objects + for obj in self.handler.objects: + if obj.name in object_states: + obj_state = self._extract_state(object_states[obj.name]) + self.rerun_visualizer.update_item_pose(obj.name, obj_state) + + # Update robots + for robot in self.handler.robots: + if robot.name in robot_states: + robot_state = self._extract_state(robot_states[robot.name]) + self.rerun_visualizer.update_item_pose(robot.name, robot_state) + + # Update time step + self.rerun_visualizer.set_time(self.step_count) + + # Update Viser visualization + if self.viser_visualizer is not None: + if hasattr(obs, "objects") and hasattr(obs, "robots"): + # Update objects from first environment + object_states = self._extract_states_from_obs(obs, "objects") + for name, state in object_states.items(): + self.viser_visualizer.update_item_pose(name, state) + + # Update robots from first environment + robot_states = self._extract_states_from_obs(obs, "robots") + for name, state in robot_states.items(): + self.viser_visualizer.update_item_pose(name, state) + + self.viser_visualizer.refresh_camera_view() + + except Exception as e: + logger.debug(f"Visualization update failed: {e}") + + def step(self, action): + """Step the environment and update visualization.""" + result = self.env.step(action) + self.step_count += 1 + + if self.step_count % self.update_freq == 0: + self._update_visualization() + + return result + + def reset(self, **kwargs): + """Reset the environment and update visualization.""" + result = self.env.reset(**kwargs) + self._update_visualization() + return result + + def render(self, mode="human"): + """Render the environment.""" + return None + + def close(self): + """Close environment and visualizers.""" + if self.rerun_visualizer is not None: + self.rerun_visualizer.close() + # Note: ViserVisualizer doesn't have a close method + # The server will be cleaned up when the object is garbage collected + if hasattr(self.env, "close"): + self.env.close() + + def __len__(self) -> int: + """Return number of environments.""" + return self.env.num_envs if hasattr(self.env, "num_envs") else 1 + + def __getattr__(self, name: str) -> Any: + """Proxy all other attributes to the wrapped environment.""" + return getattr(self.env, name) diff --git a/pyproject.toml b/pyproject.toml index 216c9f586..6845a307a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,9 +44,8 @@ genesis = [ ] robosplatter = [ - "robo-splatter @ git+https://github.com/HorizonRobotics/RoboSplatter.git", + "robo-splatter @ git+https://github.com/HorizonRobotics/RoboSplatter.git@v0.1.2", "setuptools==59.5.0" - ] [tool.uv] diff --git a/scripts/advanced/teleop_keyboard.py b/scripts/advanced/teleop_keyboard.py index e5a15835c..8b79913d3 100644 --- a/scripts/advanced/teleop_keyboard.py +++ b/scripts/advanced/teleop_keyboard.py @@ -51,8 +51,9 @@ class Args: ik_solver: Literal["curobo", "pyroki"] = "pyroki" no_gnd: bool = False - ## Viser Visualization + ## Visualization enable_viser: bool = True # Enable real-time Viser 3D visualization + enable_rerun: bool = False # Enable real-time Rerun 3D visualization viser_port: int = 8080 # Port for Viser server ## Display @@ -195,12 +196,22 @@ def main(): device = torch.device("cpu") env = task_cls(scenario, device=device) - # Optionally wrap with Viser visualization - if args.enable_viser: - from metasim.utils.viser.viser_env_wrapper import TaskViserWrapper - - env = TaskViserWrapper(env, port=args.viser_port) - log.info(f"Viser visualization enabled on port {args.viser_port}") + # Optionally wrap with visualization + if args.enable_viser or args.enable_rerun: + from metasim.utils.viz_task_wrapper import TaskVizWrapper + + env = TaskVizWrapper( + env, + use_rerun=args.enable_rerun, + use_viser=args.enable_viser, + rerun_app_name="Teleop Keyboard", + viser_port=args.viser_port, + update_freq=1, + ) + if args.enable_viser: + log.info(f"Viser visualization enabled on port {args.viser_port}") + if args.enable_rerun: + log.info("Rerun visualization enabled") toc = time.time() log.trace(f"Time to launch: {toc - tic:.2f}s") From d7451b434253de75bb29ad2b7d728f9763a1ebdd Mon Sep 17 00:00:00 2001 From: JIAjindou <96030696+JIAjindou@users.noreply.github.com> Date: Tue, 16 Dec 2025 11:03:51 +0800 Subject: [PATCH 31/50] [fix] act running issue and DR level command (#727) (#728) * [fix] act running * [fix] control dr level in il_run * [fix] control dr level in il_run * [fix] control dr level in il_run --- roboverse_learn/il/collect_demo.sh | 10 ++++----- .../il/configs/eval_config/default_eval.yaml | 4 +--- roboverse_learn/il/il_run.sh | 22 ++++++++++++++++--- .../il/policies/act/act_eval_runner.py | 2 +- .../il/policies/act/detr/util/__init__.py | 3 +++ 5 files changed, 29 insertions(+), 12 deletions(-) diff --git a/roboverse_learn/il/collect_demo.sh b/roboverse_learn/il/collect_demo.sh index 1f9d68424..06429e316 100755 --- a/roboverse_learn/il/collect_demo.sh +++ b/roboverse_learn/il/collect_demo.sh @@ -2,13 +2,13 @@ export CUDA_VISIBLE_DEVICES=0 ## Parameters -task_name_set=stack_cube -random_level=0 # 0: No randomization 1: Randomize visual material 2: Randomize camera pose 3: Randomize object reflection and lighting +task_name_set=close_box +random_level=0 # Randomization level: 0=None, 1=Scene+Material, 2=+Light, 3=+Camera num_envs=1 # Number of parallel environments demo_start_idx=0 # Index of the first demo to collect -sim_set=mujoco +sim_set=isaacsim cust_name=test -num_demo_success=100 # Number of successful demonstrations to collect +num_demo_success=100 expert_data_num=100 @@ -30,7 +30,7 @@ python ./scripts/advanced/collect_demo.py \ --demo_start_idx=${demo_start_idx} \ --num_demo_success ${num_demo_success} \ --cust_name=${cust_name} \ -#--enable_randomization +--level=${random_level} ## Convert demonstration data python ./roboverse_learn/il/data2zarr_dp.py \ diff --git a/roboverse_learn/il/configs/eval_config/default_eval.yaml b/roboverse_learn/il/configs/eval_config/default_eval.yaml index 8633a210f..d2ce4fdae 100644 --- a/roboverse_learn/il/configs/eval_config/default_eval.yaml +++ b/roboverse_learn/il/configs/eval_config/default_eval.yaml @@ -1,8 +1,6 @@ eval_args: _target_: roboverse_learn.il.utils.eval_args.Args - # random: - # _target_: metasim.cfg.randomization.RandomizationCfg - # level: 0 + level: 0 task: "" max_step: 200 num_envs: 1 diff --git a/roboverse_learn/il/il_run.sh b/roboverse_learn/il/il_run.sh index 4e0056eb2..321a094a2 100644 --- a/roboverse_learn/il/il_run.sh +++ b/roboverse_learn/il/il_run.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Usage: bash roboverse_learn/il/il_run.sh --task_name_set close_box --policy_name ddpm_dit --demo_num 100 --sim_set mujoco +# Usage: bash roboverse_learn/il/il_run.sh --task_name_set close_box --policy_name ddpm_dit --dr_level_eval 2 -- train_enable False task_name_set="close_box" # Tasks, e.g., close_box, stack_cube, pick_cube policy_name="ddpm_dit" # IL policy, opts: ddpm_unet, ddpm_dit, ddim_unet, fm_unet, fm_dit, vita, act, score @@ -11,7 +11,6 @@ train_enable=True eval_enable=True # Training parameters -level=0 num_epochs=100 seed=42 gpu=0 @@ -21,6 +20,10 @@ delta_ee=0 eval_num_envs=1 eval_max_step=300 +# Domain Randomization Level +dr_level_collect=0 +dr_level_eval=0 + # Parse parameters while [[ $# -gt 0 ]]; do case "$1" in @@ -48,6 +51,14 @@ while [[ $# -gt 0 ]]; do eval_enable="$2" shift 2 ;; + --dr_level_collect) + dr_level_collect="$2" + shift 2 + ;; + --dr_level_eval) + dr_level_eval="$2" + shift 2 + ;; --num_epochs) num_epochs="$2" shift 2 @@ -70,6 +81,7 @@ sed -i "s/^task_name_set=.*/task_name_set=$task_name_set/" ./roboverse_learn/il/ sed -i "s/^sim_set=.*/sim_set=$sim_set/" ./roboverse_learn/il/collect_demo.sh sed -i "s/^num_demo_success=.*/num_demo_success=$demo_num/" ./roboverse_learn/il/collect_demo.sh sed -i "s/^expert_data_num=.*/expert_data_num=$demo_num/" ./roboverse_learn/il/collect_demo.sh +sed -i "s/^random_level=.*/random_level=$dr_level_collect/" ./roboverse_learn/il/collect_demo.sh bash ./roboverse_learn/il/collect_demo.sh # Map policy_name to model config @@ -82,6 +94,9 @@ if [ "${policy_name}" = "act" ]; then sed -i "s/^task_name_set=.*/task_name_set=$task_name_set/" ./roboverse_learn/il/policies/act/act_run.sh sed -i "s/^sim_set=.*/sim_set=$sim_set/" ./roboverse_learn/il/policies/act/act_run.sh sed -i "s/^expert_data_num=.*/expert_data_num=$demo_num/" ./roboverse_learn/il/policies/act/act_run.sh + sed -i "s/^train_enable=.*/train_enable=$train_enable/" ./roboverse_learn/il/policies/act/act_run.sh + sed -i "s/^eval_enable=.*/eval_enable=$eval_enable/" ./roboverse_learn/il/policies/act/act_run.sh + sed -i "s/^eval_level=.*/eval_level=$dr_level_eval/" ./roboverse_learn/il/policies/act/act_run.sh bash ./roboverse_learn/il/policies/act/act_run.sh echo "=== Completed all data collection, training, and evaluation ===" exit 0 @@ -104,7 +119,7 @@ fi export policy_name="${policy_name}" python ${main_script} --config-name=${config_name}.yaml \ task_name=${task_name_set} \ -"dataset_config.zarr_path=./data_policy/${task_name_set}FrankaL${level}_${extra}_${demo_num}.zarr" \ +"dataset_config.zarr_path=./data_policy/${task_name_set}FrankaL${dr_level_collect}_${extra}_${demo_num}.zarr" \ train_config.training_params.seed=${seed} \ train_config.training_params.num_epochs=${num_epochs} \ train_config.training_params.device=${gpu} \ @@ -115,6 +130,7 @@ eval_config.eval_args.task=${task_name_set} \ eval_config.eval_args.max_step=${eval_max_step} \ eval_config.eval_args.num_envs=${eval_num_envs} \ eval_config.eval_args.sim=${sim_set} \ +eval_config.eval_args.level=${dr_level_eval} \ +eval_config.eval_args.max_demo=${demo_num} \ train_enable=${train_enable} \ eval_enable=${eval_enable} \ diff --git a/roboverse_learn/il/policies/act/act_eval_runner.py b/roboverse_learn/il/policies/act/act_eval_runner.py index 63a6a9ebb..79a1bd331 100755 --- a/roboverse_learn/il/policies/act/act_eval_runner.py +++ b/roboverse_learn/il/policies/act/act_eval_runner.py @@ -358,7 +358,7 @@ class SimpleRenderCfg: import pickle - from roboverse_learn.il.policies.act import ACTPolicy + from roboverse_learn.il.policies.act.policy import ACTPolicy ckpt_path = os.path.join(args.ckpt_path, act_ckpt_name) policy = ACTPolicy(policy_config) diff --git a/roboverse_learn/il/policies/act/detr/util/__init__.py b/roboverse_learn/il/policies/act/detr/util/__init__.py index 168f9979a..f096ec50b 100644 --- a/roboverse_learn/il/policies/act/detr/util/__init__.py +++ b/roboverse_learn/il/policies/act/detr/util/__init__.py @@ -1 +1,4 @@ # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved +from .misc import * +from .box_ops import * +from .plot_utils import * From 07633ae163a4da67d66118eb9b15b4d8f01c5d49 Mon Sep 17 00:00:00 2001 From: JIAjindou <96030696+JIAjindou@users.noreply.github.com> Date: Wed, 17 Dec 2025 09:08:29 +0800 Subject: [PATCH 32/50] [fix] act output path and run command issue (#732) * [fix] unify act path with dp * [fix] unify act path with dp * [fix] act run control * [fix] act output path --- roboverse_learn/il/il_run.sh | 5 +-- .../il/policies/act/act_eval_runner.py | 6 ++-- roboverse_learn/il/policies/act/act_run.sh | 32 +++++++++---------- roboverse_learn/il/policies/act/train.py | 2 +- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/roboverse_learn/il/il_run.sh b/roboverse_learn/il/il_run.sh index 321a094a2..3745910ae 100644 --- a/roboverse_learn/il/il_run.sh +++ b/roboverse_learn/il/il_run.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Usage: bash roboverse_learn/il/il_run.sh --task_name_set close_box --policy_name ddpm_dit --dr_level_eval 2 -- train_enable False +# Usage: bash roboverse_learn/il/il_run.sh --task_name_set close_box --policy_name ddpm_dit --dr_level_eval 2 --train_enable False task_name_set="close_box" # Tasks, e.g., close_box, stack_cube, pick_cube policy_name="ddpm_dit" # IL policy, opts: ddpm_unet, ddpm_dit, ddim_unet, fm_unet, fm_dit, vita, act, score @@ -90,12 +90,13 @@ main_script="./roboverse_learn/il/train.py" # if policy_name is ACT if [ "${policy_name}" = "act" ]; then - echo "=== Running ACT training ===" + echo "=== Running ACT training and evaluation===" sed -i "s/^task_name_set=.*/task_name_set=$task_name_set/" ./roboverse_learn/il/policies/act/act_run.sh sed -i "s/^sim_set=.*/sim_set=$sim_set/" ./roboverse_learn/il/policies/act/act_run.sh sed -i "s/^expert_data_num=.*/expert_data_num=$demo_num/" ./roboverse_learn/il/policies/act/act_run.sh sed -i "s/^train_enable=.*/train_enable=$train_enable/" ./roboverse_learn/il/policies/act/act_run.sh sed -i "s/^eval_enable=.*/eval_enable=$eval_enable/" ./roboverse_learn/il/policies/act/act_run.sh + sed -i "s/^collect_level=.*/collect_level=$dr_level_collect/" ./roboverse_learn/il/policies/act/act_run.sh sed -i "s/^eval_level=.*/eval_level=$dr_level_eval/" ./roboverse_learn/il/policies/act/act_run.sh bash ./roboverse_learn/il/policies/act/act_run.sh echo "=== Completed all data collection, training, and evaluation ===" diff --git a/roboverse_learn/il/policies/act/act_eval_runner.py b/roboverse_learn/il/policies/act/act_eval_runner.py index 79a1bd331..81f9bf4bb 100755 --- a/roboverse_learn/il/policies/act/act_eval_runner.py +++ b/roboverse_learn/il/policies/act/act_eval_runner.py @@ -387,7 +387,7 @@ def post_process(a): max_timesteps = int(max_timesteps * 1) ckpt_name = args.ckpt_path.split("/")[-1] - os.makedirs(f"il_outputs/{args.algo}/{args.task}/{ckpt_name}", exist_ok=True) + os.makedirs(f"il_outputs/{args.algo}/{args.task}/eval/{ckpt_name}_dr{args.level}", exist_ok=True) ## cuRobo controller (commented out - not needed for ACT joint control) # *_, robot_ik = get_curobo_models(scenario.robots[0]) @@ -508,12 +508,12 @@ def post_process(a): step += 1 - images_to_video(image_list, f"il_outputs/{args.algo}/{args.task}/{ckpt_name}/{i}.mp4") + images_to_video(image_list, f"il_outputs/{args.algo}/{args.task}/eval/{ckpt_name}_dr{args.level}/{i}.mp4") success_rate = TotalSuccess / num_eval print("Success Rate: ", success_rate) - result_dir = f"il_outputs/{args.algo}/{args.task}/{ckpt_name}" + result_dir = f"il_outputs/{args.algo}/{args.task}/eval/{ckpt_name}_dr{args.level}" result_file = os.path.join(result_dir, "success_rate.txt") with open(result_file, "w") as f: f.write(f"Success Rate: {success_rate:.4f}\n") diff --git a/roboverse_learn/il/policies/act/act_run.sh b/roboverse_learn/il/policies/act/act_run.sh index 8c54e7d89..f39ec59af 100755 --- a/roboverse_learn/il/policies/act/act_run.sh +++ b/roboverse_learn/il/policies/act/act_run.sh @@ -1,13 +1,13 @@ ## Separate script for training and evaluation -train_enable=true -eval_enable=true +train_enable=True +eval_enable=True ## Parameters task_name_set=close_box -expert_data_num=90 +expert_data_num=100 gpu_id=0 -sim_set=mujoco +sim_set=isaacsim num_epochs=100 obs_space=joint_pos # joint_pos or ee @@ -16,29 +16,34 @@ delta_ee=0 # 0 or 1 (only matters if act_space is ee, 0 means absolute 1 means d alg_name=ACT seed=42 -level=0 +collect_level=2 # ACT hyperparameters -chunk_size=20 +chunk_size=40 kl_weight=10 hidden_dim=512 lr=1e-5 batch_size=8 dim_feedforward=3200 +# Domain Randomization parameters for evaluation +eval_level=2 +eval_scene_mode=0 # 0=Manual, 1=USD Table, 2=USD Scene, 3=Full USD +eval_seed=42 # Randomization seed (optional) + extra="obs:${obs_space}_act:${act_space}" if [ "${delta_ee}" = 1 ]; then extra="${extra}_delta" fi # Training -if [ "${train_enable}" = "true" ]; then +if [ "${train_enable}" = "True" ]; then echo "=== Training ===" export CUDA_VISIBLE_DEVICES=${gpu_id} python -m roboverse_learn.il.policies.act.train \ - --task_name ${task_name_set}_${extra}_chunk${chunk_size} \ + --task_name ${task_name_set} \ --num_episodes ${expert_data_num} \ - --dataset_dir data_policy/${task_name_set}FrankaL${level}_${extra}_${expert_data_num}.zarr \ + --dataset_dir data_policy/${task_name_set}FrankaL${collect_level}_${extra}_${expert_data_num}.zarr \ --policy_class ${alg_name} --kl_weight ${kl_weight} --chunk_size ${chunk_size} \ --hidden_dim ${hidden_dim} --batch_size ${batch_size} --dim_feedforward ${dim_feedforward} \ --num_epochs ${num_epochs} --lr ${lr} --state_dim 9 \ @@ -46,16 +51,11 @@ if [ "${train_enable}" = "true" ]; then fi # Evaluation -if [ "${eval_enable}" = "true" ]; then +if [ "${eval_enable}" = "True" ]; then echo "=== Evaluation ===" # # export TORCH_CUDA_ARCH_LIST="8.9" ckpt_path=$(cat ./roboverse_learn/il/policies/act/ckpt_dir_path.txt) - # Domain Randomization parameters for evaluation - eval_level=3 # 0=None, 1=Scene+Material, 2=+Light, 3=+Camera - eval_scene_mode=2 # 0=Manual, 1=USD Table, 2=USD Scene, 3=Full USD - eval_seed=42 # Randomization seed (optional) - python -m roboverse_learn.il.policies.act.act_eval_runner \ --task ${task_name_set} \ --robot franka \ @@ -64,7 +64,7 @@ if [ "${eval_enable}" = "true" ]; then --algo act \ --ckpt_path ./${ckpt_path} \ --headless True \ - --num_eval 5 \ + --num_eval ${expert_data_num} \ --temporal_agg True \ --chunk_size ${chunk_size} \ --level ${eval_level} \ diff --git a/roboverse_learn/il/policies/act/train.py b/roboverse_learn/il/policies/act/train.py index 1896f8580..fe27a703f 100644 --- a/roboverse_learn/il/policies/act/train.py +++ b/roboverse_learn/il/policies/act/train.py @@ -59,7 +59,7 @@ def main(args): else: raise NotImplementedError - ckpt_dir = f"info/outputs/ACT/{datetime.now().strftime('%Y.%m.%d')}/{datetime.now().strftime('%H.%M.%S')}_{task_name}_{num_episodes}" + ckpt_dir = f"il_outputs/act/{task_name}" # Load metadata from dataset directory metadata_path = os.path.join(dataset_dir, 'metadata.json') From 17001b98a887ea84cb5fa12eaef697a488664d6b Mon Sep 17 00:00:00 2001 From: Daoyuan Zhu <77039357+infinit-luffy@users.noreply.github.com> Date: Thu, 18 Dec 2025 11:02:29 +0800 Subject: [PATCH 33/50] add success checker for calvin (#725) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I believe the change only effects Calvin tasks, thus no need to do any test. * add success check * Refactor BaseCalvinTableTask_A and clean up code * Update scene_B.py * add success check * Update scene_D.py * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * add success check * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update scene_A.py * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update scene_B.py * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update scene_C.py * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update scene_D.py * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: 朱道远 --- roboverse_pack/tasks/calvin/base_table.py | 251 +++++++++++++++++++--- roboverse_pack/tasks/calvin/scene_A.py | 95 +------- roboverse_pack/tasks/calvin/scene_B.py | 92 +------- roboverse_pack/tasks/calvin/scene_C.py | 95 +------- roboverse_pack/tasks/calvin/scene_D.py | 93 +------- 5 files changed, 221 insertions(+), 405 deletions(-) diff --git a/roboverse_pack/tasks/calvin/base_table.py b/roboverse_pack/tasks/calvin/base_table.py index 0a4b4fd02..9cdb16bd6 100644 --- a/roboverse_pack/tasks/calvin/base_table.py +++ b/roboverse_pack/tasks/calvin/base_table.py @@ -1,15 +1,17 @@ from __future__ import annotations -import pickle import xml.etree.ElementTree as ET import gymnasium as gym +import torch from metasim.scenario.objects import ArticulationObjCfg from metasim.scenario.robot import BaseActuatorCfg from metasim.scenario.scenario import ScenarioCfg from metasim.task.base import BaseTaskEnv from metasim.task.registry import register_task +from metasim.types import Termination +from metasim.utils.demo_util import get_traj from metasim.utils.ik_solver import setup_ik_solver from metasim.utils.tensor_util import array_to_tensor from roboverse_pack.robots.franka_with_gripper_extension_cfg import FrankaWithGripperExtensionCfg @@ -93,20 +95,144 @@ def _action_space(self): else: raise NotImplementedError + @staticmethod + def _get_joint_names_from_urdf(urdf_path: str): + try: + tree = ET.parse(urdf_path) + root = tree.getroot() + joint_names = [] + for joint in root.findall("joint"): + if joint.get("type") != "fixed": + joint_names.append(joint.get("name")) + return joint_names + except (ET.ParseError, FileNotFoundError): + return [] + + def _get_initial_states(self): + + init_states, all_actions, all_states = get_traj(self.traj_filepath, self.scenario.robots[0], self.handler) + + # self.done_states = all_states[:][-1] + self.global_init_states = init_states + + self.global_done_states = [traj[-1] for traj in all_states] + + """Return per-env initial states (override in subclasses).""" + return init_states + + def _is_state_equal(self, s1, s2, atol=1e-4): + + if set(s1.keys()) != set(s2.keys()): + return False + + def compare_entity(e1, e2): + + if not torch.allclose(e1["pos"].cpu(), e2["pos"].cpu(), atol=atol): + return False + + if not torch.allclose(e1["rot"].cpu(), e2["rot"].cpu(), atol=atol): + return False + + if "dof_pos" in e1: + dof1 = e1["dof_pos"] + dof2 = e2.get("dof_pos", {}) + + if len(dof1) != len(dof2): + return False + + for joint_name, val1 in dof1.items(): + if joint_name not in dof2: + return False + + val2 = dof2[joint_name] + + if isinstance(val1, torch.Tensor): + val1 = val1.item() + if isinstance(val2, torch.Tensor): + val2 = val2.item() + + if abs(val1 - val2) > atol: + return False + + return True + + objs1 = s1.get("objects", {}) + objs2 = s2.get("objects", {}) + if set(objs1.keys()) != set(objs2.keys()): + return False + + for name in objs1: + if not compare_entity(objs1[name], objs2[name]): + return False + + robots1 = s1.get("robots", {}) + robots2 = s2.get("robots", {}) + if set(robots1.keys()) != set(robots2.keys()): + return False + + for name in robots1: + if not compare_entity(robots1[name], robots2[name]): + return False + + return True + + def reset(self, states, env_ids=None): + + if env_ids is None: + env_ids = list(range(self.num_envs)) + + if hasattr(env_ids, "cpu"): + env_ids = env_ids.cpu().numpy() + + if not hasattr(self, "done_states") or self.done_states is None: + self.done_states = [None] * self.num_envs + + incoming_done_states = [] + + for s in states: + found_idx = -1 + for i, global_s in enumerate(self.global_init_states): + # import ipdb; ipdb.set_trace() + if self._is_state_equal(s, global_s): + found_idx = i + break + + if found_idx != -1: + incoming_done_states.append(self.global_done_states[found_idx]) + else: + # print("Warning: State not found in global cache!") + incoming_done_states.append(None) + + for i, env_id in enumerate(env_ids): + self.done_states[env_id] = incoming_done_states[i] + + return super().reset(states=states, env_ids=env_ids) + def step(self, action): try: if isinstance(action, list) and action and isinstance(action[0], dict): robot_name = self.scenario.robots[0].name + extracted_actions = [env_act[robot_name] for env_act in action] + import torch + + if isinstance(extracted_actions[0], torch.Tensor): + action = torch.stack(extracted_actions) + else: + action = torch.tensor(extracted_actions, device=self.device) + elif isinstance(action, dict): + robot_name = self.scenario.robots[0].name + if robot_name in action: + action = action[robot_name] - # Check if the robot's name is a key in the dictionary. - if robot_name in action[0]: - action = action[0][robot_name] - except: + except Exception as e: pass if self.scenario.robots[0].control_type == "joint_position": assert action.shape[-1] == 9, f"Expected action shape (9,), got {action.shape}" - return super().step(action) + + obs, reward, success, time_out, extras = super().step(action) + + return obs, reward, success, time_out, extras elif self.scenario.robots[0].control_type == "ee_pose": action = array_to_tensor(action, device=self.device).float() @@ -127,38 +253,95 @@ def step(self, action): return_dict=False, ) - return super().step(actions) + obs, reward, success, time_out, extras = super().step(actions) + + return obs, reward, success, time_out, extras else: raise NotImplementedError - @staticmethod - def _get_joint_names_from_urdf(urdf_path: str): - try: - tree = ET.parse(urdf_path) - root = tree.getroot() - joint_names = [] - for joint in root.findall("joint"): - if joint.get("type") != "fixed": - joint_names.append(joint.get("name")) - return joint_names - except (ET.ParseError, FileNotFoundError): - return [] + def _terminated(self, env_states) -> Termination: - def _get_initial_states(self): - path = self.traj_filepath - if path.endswith(".pkl"): - with open(path, "rb") as f: - data = pickle.load(f) - init_state = data["franka"][0]["reset_state"] + success = self.check_state_tolerance(env_states) - """Return per-env initial states (override in subclasses).""" - return init_state + return success - def _action_space(self): - if self.scenario.robots[0].control_type == "joint_position": - return gym.spaces.Box(low=-1.0, high=1.0, shape=(9,), dtype=float) - elif self.scenario.robots[0].control_type == "ee_pose": - return gym.spaces.Box(low=-1.0, high=1.0, shape=(8,), dtype=float) - else: - raise NotImplementedError + def check_state_tolerance(self, current_state, pos_tol=0.05, rot_tol=0.1, joint_tol=0.1, verbose=False): + + num_envs = self.num_envs + device = self.device + + all_success = torch.ones(num_envs, dtype=torch.bool, device=device) + + ref_target_dict = self.done_states[0] + + if "objects" in ref_target_dict: + for obj_name in ref_target_dict["objects"].keys(): + obj_state = current_state.objects[obj_name].root_state + curr_pos = obj_state[:, :3] # (num_envs, 3) + curr_rot = obj_state[:, 3:7] # (num_envs, 4) + target_pos_list = [s["objects"][obj_name]["pos"] for s in self.done_states] + target_rot_list = [s["objects"][obj_name]["rot"] for s in self.done_states] + target_pos = torch.stack(target_pos_list).to(device) # (num_envs, 3) + target_rot = torch.stack(target_rot_list).to(device) # (num_envs, 4) + + pos_err = torch.norm(curr_pos - target_pos, dim=1) # (num_envs,) + quat_dot = torch.sum(curr_rot * target_rot, dim=1).abs() + quat_dot = torch.clamp(quat_dot, 0.0, 1.0) + rot_err = 2.0 * torch.acos(quat_dot) # (num_envs,) + + obj_success = (pos_err < pos_tol) & (rot_err < rot_tol) + all_success = all_success & obj_success + + if verbose: + failed_indices = torch.nonzero(~obj_success).flatten() + if len(failed_indices) > 0: + idx = failed_indices[0] + # print(f"[Env {idx}] Obj '{obj_name}' Fail: PosErr={pos_err[idx]:.3f}, RotErr={rot_err[idx]:.3f}") + + franka_joint_order = [ + "panda_finger_joint1", + "panda_finger_joint2", + "panda_joint1", + "panda_joint2", + "panda_joint3", + "panda_joint4", + "panda_joint5", + "panda_joint6", + "panda_joint7", + ] + + if "robots" in ref_target_dict: + for robot_name in ref_target_dict["robots"].keys(): + curr_joints = current_state.robots[robot_name].joint_pos + target_joints_list = [] + joints_to_check_indices = [] + target_vals_batch = [] + ref_dof_dict = ref_target_dict["robots"][robot_name]["dof_pos"] + + for col_idx, joint_name in enumerate(franka_joint_order): + if joint_name in ref_dof_dict: + joints_to_check_indices.append(col_idx) + + if not joints_to_check_indices: + continue + + for env_i in range(num_envs): + env_target_dict = self.done_states[env_i]["robots"][robot_name]["dof_pos"] + row_vals = [env_target_dict[franka_joint_order[idx]] for idx in joints_to_check_indices] + target_vals_batch.append(row_vals) + + target_joints = torch.tensor(target_vals_batch, device=device, dtype=torch.float32) # (num_envs, k) + curr_joints_subset = curr_joints[:, joints_to_check_indices] # (num_envs, k) + joint_diff = torch.abs(curr_joints_subset - target_joints) + max_joint_err, _ = torch.max(joint_diff, dim=1) # (num_envs,) + robot_success = max_joint_err < joint_tol + all_success = all_success & robot_success + + if verbose: + failed_indices = torch.nonzero(~robot_success).flatten() + if len(failed_indices) > 0: + idx = failed_indices[0] + # print(f"[Env {idx}] Robot '{robot_name}' Fail: MaxJointErr={max_joint_err[idx]:.3f}") + + return all_success diff --git a/roboverse_pack/tasks/calvin/scene_A.py b/roboverse_pack/tasks/calvin/scene_A.py index c7174693d..24260e320 100644 --- a/roboverse_pack/tasks/calvin/scene_A.py +++ b/roboverse_pack/tasks/calvin/scene_A.py @@ -24,83 +24,6 @@ @register_task("calvin.base_table_A") class BaseCalvinTableTask_A(BaseCalvinTableTask): - # scenario = ScenarioCfg( - # robots=[ - # FrankaWithGripperExtensionCfg( - # name="franka", - # default_position=[-0.34, -0.46, 0.24], - # default_orientation=[1, 0, 0, 0], - # actuators={ - # "panda_joint1": BaseActuatorCfg(velocity_limit=2.175, torque_limit=87, stiffness=280, damping=10), - # "panda_joint2": BaseActuatorCfg(velocity_limit=2.175, torque_limit=87, stiffness=280, damping=10), - # "panda_joint3": BaseActuatorCfg(velocity_limit=2.175, torque_limit=87, stiffness=280, damping=10), - # "panda_joint4": BaseActuatorCfg(velocity_limit=2.175, torque_limit=87, stiffness=280, damping=10), - # "panda_joint5": BaseActuatorCfg(velocity_limit=2.61, torque_limit=12.0, stiffness=200, damping=5), - # "panda_joint6": BaseActuatorCfg(velocity_limit=2.61, torque_limit=12.0, stiffness=200, damping=5), - # "panda_joint7": BaseActuatorCfg(velocity_limit=2.61, torque_limit=12.0, stiffness=200, damping=5), - # "panda_finger_joint1": BaseActuatorCfg( - # velocity_limit=0.2, torque_limit=20.0, is_ee=True, stiffness=30000, damping=1000 - # ), - # "panda_finger_joint2": BaseActuatorCfg( - # velocity_limit=0.2, torque_limit=20.0, is_ee=True, stiffness=30000, damping=1000 - # ), - # }, - # default_joint_positions={ - # "panda_joint1": -1.21779206, - # "panda_joint2": 1.03987646, - # "panda_joint3": 2.11978261, - # "panda_joint4": -2.34205014, - # "panda_joint5": -0.87015947, - # "panda_joint6": 1.64119353, - # "panda_joint7": 0.55344866, - # "panda_finger_joint1": 0.04, - # "panda_finger_joint2": 0.04, - # }, - # control_type="joint_position", - # fix_base_link=True, - # urdf_path="roboverse_data/robots/franka_calvin/panda_longer_finger.urdf", - # # urdf_path="/home/dyz/RoboVerse/calvin/calvin_env/calvin_env/data/franka_panda/panda.urdf", - # usd_path=None, - # mjcf_path=None, - # mjx_mjcf_path=None, - # ) - # ], - # objects=[ - # ArticulationObjCfg( - # name="table", - # scale=0.8, - # default_position=[0, 0, 0], - # default_orientation=[1, 0, 0, 0], - # fix_base_link=True, - # urdf_path="roboverse_data/assets/calvin/calvin_table_A/urdf/calvin_table_A.urdf", - # ), - # RigidObjCfg( - # name="pink_cube", - # scale=0.8, - # default_position=[1.28661989e-01, -3.77756105e-02, 4.59989266e-01 + 0.01], - # default_orientation=quat_from_euler_np(1.10200730e-04, 3.19760378e-05, -3.94522179e-01), - # fix_base_link=False, - # urdf_path="roboverse_data/assets/calvin/block_pink_small.urdf", - # ), - # RigidObjCfg( - # name="blue_cube", - # scale=0.8, - # default_position=[-2.83642665e-01, 8.05351014e-02, 4.60989238e-01 + 0.01], - # default_orientation=quat_from_euler_np(-1.10251078e-05, -5.25663348e-05, -9.06438129e-01), - # fix_base_link=False, - # urdf_path="roboverse_data/assets/calvin/block_blue_big.urdf", - # ), - # RigidObjCfg( - # name="red_cube", - # scale=0.8, - # default_position=[2.32403619e-01, -4.04295856e-02, 4.59990009e-01 + 0.01], - # default_orientation=quat_from_euler_np(4.12287744e-08, -8.05700103e-09, -2.17741510e00), - # fix_base_link=False, - # urdf_path="roboverse_data/assets/calvin/block_red_middle.urdf", - # ), - # ], - # decimation=8, - # ) def __init__(self, *args, **kwargs): self.scenario.objects = [ ArticulationObjCfg( @@ -111,15 +34,6 @@ def __init__(self, *args, **kwargs): fix_base_link=True, urdf_path="roboverse_data/assets/calvin/calvin_table_A/urdf/calvin_table_A.urdf", extra_resources=[ - # dark_wood__black_handle.png - # dark_wood__gray_handle.png - # dark_wood.png - # light_wood__black_handle.png - # light_wood__gray_handle.png - # light_wood.png - # wood__black_handle.png - # wood__gray_handle.png - # wood.png "roboverse_data/assets/calvin/calvin_table_A/textures/dark_wood__black_handle.png", "roboverse_data/assets/calvin/calvin_table_A/textures/dark_wood__gray_handle.png", "roboverse_data/assets/calvin/calvin_table_A/textures/dark_wood.png", @@ -129,11 +43,6 @@ def __init__(self, *args, **kwargs): "roboverse_data/assets/calvin/calvin_table_A/textures/wood__black_handle.png", "roboverse_data/assets/calvin/calvin_table_A/textures/wood__gray_handle.png", "roboverse_data/assets/calvin/calvin_table_A/textures/wood.png", - # base_link.mtl - # drawer_link.mtl - # plank_link.mtl - # switch_link.mtl - # slide_link.mtl "roboverse_data/assets/calvin/calvin_table_A/meshes/base_link.mtl", "roboverse_data/assets/calvin/calvin_table_A/meshes/drawer_link.mtl", "roboverse_data/assets/calvin/calvin_table_A/meshes/plank_link.mtl", @@ -180,6 +89,4 @@ def __init__(self, *args, **kwargs): ] super().__init__(*args, **kwargs) - traj_filepath = ( - "roboverse_data/trajs/calvin/env_A_out/episode_chunk_132_1802438_1809815/trajectory_env_A_1237_v2.pkl" - ) + traj_filepath = "roboverse_data/trajs/calvin/calvin_traj_ann/env_A_out/task_0_v2.pkl" diff --git a/roboverse_pack/tasks/calvin/scene_B.py b/roboverse_pack/tasks/calvin/scene_B.py index 7b6244571..e005b154d 100644 --- a/roboverse_pack/tasks/calvin/scene_B.py +++ b/roboverse_pack/tasks/calvin/scene_B.py @@ -24,82 +24,6 @@ @register_task("calvin.base_table_B") class BaseCalvinTableTask_B(BaseCalvinTableTask): - # scenario = ScenarioCfg( - # robots=[ - # FrankaWithGripperExtensionCfg( - # name="franka", - # default_position=[-0.34, -0.46, 0.24], - # default_orientation=[1, 0, 0, 0], - # actuators={ - # "panda_joint1": BaseActuatorCfg(velocity_limit=2.175, torque_limit=87, stiffness=280, damping=10), - # "panda_joint2": BaseActuatorCfg(velocity_limit=2.175, torque_limit=87, stiffness=280, damping=10), - # "panda_joint3": BaseActuatorCfg(velocity_limit=2.175, torque_limit=87, stiffness=280, damping=10), - # "panda_joint4": BaseActuatorCfg(velocity_limit=2.175, torque_limit=87, stiffness=280, damping=10), - # "panda_joint5": BaseActuatorCfg(velocity_limit=2.61, torque_limit=12.0, stiffness=200, damping=5), - # "panda_joint6": BaseActuatorCfg(velocity_limit=2.61, torque_limit=12.0, stiffness=200, damping=5), - # "panda_joint7": BaseActuatorCfg(velocity_limit=2.61, torque_limit=12.0, stiffness=200, damping=5), - # "panda_finger_joint1": BaseActuatorCfg( - # velocity_limit=0.2, torque_limit=20.0, is_ee=True, stiffness=30000, damping=1000 - # ), - # "panda_finger_joint2": BaseActuatorCfg( - # velocity_limit=0.2, torque_limit=20.0, is_ee=True, stiffness=30000, damping=1000 - # ), - # }, - # default_joint_positions={ - # "panda_joint1": -1.21779206, - # "panda_joint2": 1.03987646, - # "panda_joint3": 2.11978261, - # "panda_joint4": -2.34205014, - # "panda_joint5": -0.87015947, - # "panda_joint6": 1.64119353, - # "panda_joint7": 0.55344866, - # "panda_finger_joint1": 0.04, - # "panda_finger_joint2": 0.04, - # }, - # control_type="joint_position", - # fix_base_link=True, - # urdf_path="roboverse_data/robots/franka_calvin/panda_longer_finger.urdf", - # usd_path=None, - # mjcf_path=None, - # mjx_mjcf_path=None, - # ) - # ], - # objects=[ - # ArticulationObjCfg( - # name="table", - # scale=0.8, - # default_position=[0, 0, 0], - # default_orientation=[1, 0, 0, 0], - # fix_base_link=True, - # urdf_path="roboverse_data/assets/calvin/calvin_table_B/urdf/calvin_table_B.urdf", - # ), - # RigidObjCfg( - # name="pink_cube", - # scale=0.8, - # default_position=[1.28661989e-01, -3.77756105e-02, 4.59989266e-01 + 0.01], - # default_orientation=quat_from_euler_np(1.10200730e-04, 3.19760378e-05, -3.94522179e-01), - # fix_base_link=False, - # urdf_path="roboverse_data/assets/calvin/block_pink_middle.urdf", - # ), - # RigidObjCfg( - # name="blue_cube", - # scale=0.8, - # default_position=[-2.83642665e-01, 8.05351014e-02, 4.60989238e-01 + 0.01], - # default_orientation=quat_from_euler_np(-1.10251078e-05, -5.25663348e-05, -9.06438129e-01), - # fix_base_link=False, - # urdf_path="roboverse_data/assets/calvin/block_blue_big.urdf", - # ), - # RigidObjCfg( - # name="red_cube", - # scale=0.8, - # default_position=[2.32403619e-01, -4.04295856e-02, 4.59990009e-01 + 0.01], - # default_orientation=quat_from_euler_np(4.12287744e-08, -8.05700103e-09, -2.17741510e00), - # fix_base_link=False, - # urdf_path="roboverse_data/assets/calvin/block_red_small.urdf", - # ), - # ], - # decimation=8, - # ) def __init__(self, *args, **kwargs): self.scenario.objects = [ ArticulationObjCfg( @@ -110,15 +34,6 @@ def __init__(self, *args, **kwargs): fix_base_link=True, urdf_path="roboverse_data/assets/calvin/calvin_table_B/urdf/calvin_table_B.urdf", extra_resources=[ - # dark_wood__black_handle.png - # dark_wood__gray_handle.png - # dark_wood.png - # light_wood__black_handle.png - # light_wood__gray_handle.png - # light_wood.png - # wood__black_handle.png - # wood__gray_handle.png - # wood.png "roboverse_data/assets/calvin/calvin_table_B/textures/dark_wood__black_handle.png", "roboverse_data/assets/calvin/calvin_table_B/textures/dark_wood__gray_handle.png", "roboverse_data/assets/calvin/calvin_table_B/textures/dark_wood.png", @@ -128,11 +43,6 @@ def __init__(self, *args, **kwargs): "roboverse_data/assets/calvin/calvin_table_B/textures/wood__black_handle.png", "roboverse_data/assets/calvin/calvin_table_B/textures/wood__gray_handle.png", "roboverse_data/assets/calvin/calvin_table_B/textures/wood.png", - # base_link.mtl - # drawer_link.mtl - # plank_link.mtl - # switch_link.mtl - # slide_link.mtl "roboverse_data/assets/calvin/calvin_table_B/meshes/base_link.mtl", "roboverse_data/assets/calvin/calvin_table_B/meshes/drawer_link.mtl", "roboverse_data/assets/calvin/calvin_table_B/meshes/plank_link.mtl", @@ -167,4 +77,4 @@ def __init__(self, *args, **kwargs): ] super().__init__(*args, **kwargs) - traj_filepath = "roboverse_data/trajs/calvin/env_B_out/episode_chunk_36_632862_639064/trajectory_env_B_4370_v2.pkl" + traj_filepath = "roboverse_data/trajs/calvin/calvin_traj_ann/env_B_out/task_100_v2.pkl" diff --git a/roboverse_pack/tasks/calvin/scene_C.py b/roboverse_pack/tasks/calvin/scene_C.py index d1a97094c..396cfdafd 100644 --- a/roboverse_pack/tasks/calvin/scene_C.py +++ b/roboverse_pack/tasks/calvin/scene_C.py @@ -24,83 +24,6 @@ @register_task("calvin.base_table_C") class BaseCalvinTableTask_C(BaseCalvinTableTask): - # scenario = ScenarioCfg( - # robots=[ - # FrankaWithGripperExtensionCfg( - # name="franka", - # default_position=[-0.34, -0.46, 0.24], - # default_orientation=[1, 0, 0, 0], - # actuators={ - # "panda_joint1": BaseActuatorCfg(velocity_limit=2.175, torque_limit=87, stiffness=280, damping=10), - # "panda_joint2": BaseActuatorCfg(velocity_limit=2.175, torque_limit=87, stiffness=280, damping=10), - # "panda_joint3": BaseActuatorCfg(velocity_limit=2.175, torque_limit=87, stiffness=280, damping=10), - # "panda_joint4": BaseActuatorCfg(velocity_limit=2.175, torque_limit=87, stiffness=280, damping=10), - # "panda_joint5": BaseActuatorCfg(velocity_limit=2.61, torque_limit=12.0, stiffness=200, damping=5), - # "panda_joint6": BaseActuatorCfg(velocity_limit=2.61, torque_limit=12.0, stiffness=200, damping=5), - # "panda_joint7": BaseActuatorCfg(velocity_limit=2.61, torque_limit=12.0, stiffness=200, damping=5), - # "panda_finger_joint1": BaseActuatorCfg( - # velocity_limit=0.2, torque_limit=20.0, is_ee=True, stiffness=30000, damping=1000 - # ), - # "panda_finger_joint2": BaseActuatorCfg( - # velocity_limit=0.2, torque_limit=20.0, is_ee=True, stiffness=30000, damping=1000 - # ), - # }, - # default_joint_positions={ - # "panda_joint1": -1.21779206, - # "panda_joint2": 1.03987646, - # "panda_joint3": 2.11978261, - # "panda_joint4": -2.34205014, - # "panda_joint5": -0.87015947, - # "panda_joint6": 1.64119353, - # "panda_joint7": 0.55344866, - # "panda_finger_joint1": 0.04, - # "panda_finger_joint2": 0.04, - # }, - # control_type="joint_position", - # fix_base_link=True, - # urdf_path="roboverse_data/robots/franka_calvin/panda_longer_finger.urdf", - # usd_path=None, - # mjcf_path=None, - # mjx_mjcf_path=None, - # ) - # ], - # objects=[ - # ArticulationObjCfg( - # name="table", - # scale=0.8, - # default_position=[0, 0, 0], - # default_orientation=[1, 0, 0, 0], - # fix_base_link=True, - # urdf_path="roboverse_data/assets/calvin/calvin_table_C/urdf/calvin_table_C.urdf", - # ), - # RigidObjCfg( - # name="pink_cube", - # scale=0.8, - # default_position=[1.28661989e-01, -3.77756105e-02, 4.59989266e-01 + 0.01], - # default_orientation=quat_from_euler_np(1.10200730e-04, 3.19760378e-05, -3.94522179e-01), - # fix_base_link=False, - # urdf_path="roboverse_data/assets/calvin/block_pink_middle.urdf", - # ), - # RigidObjCfg( - # name="blue_cube", - # scale=0.8, - # default_position=[-2.83642665e-01, 8.05351014e-02, 4.60989238e-01 + 0.01], - # default_orientation=quat_from_euler_np(-1.10251078e-05, -5.25663348e-05, -9.06438129e-01), - # fix_base_link=False, - # urdf_path="roboverse_data/assets/calvin/block_blue_small.urdf", - # ), - # RigidObjCfg( - # name="red_cube", - # scale=0.8, - # default_position=[2.32403619e-01, -4.04295856e-02, 4.59990009e-01 + 0.01], - # default_orientation=quat_from_euler_np(4.12287744e-08, -8.05700103e-09, -2.17741510e00), - # fix_base_link=False, - # urdf_path="roboverse_data/assets/calvin/block_red_big.urdf", - # ), - # ], - # decimation=8, - # ) - def __init__(self, *args, **kwargs): self.scenario.objects = [ ArticulationObjCfg( @@ -111,15 +34,6 @@ def __init__(self, *args, **kwargs): fix_base_link=True, urdf_path="roboverse_data/assets/calvin/calvin_table_C/urdf/calvin_table_C.urdf", extra_resources=[ - # dark_wood__black_handle.png - # dark_wood__gray_handle.png - # dark_wood.png - # light_wood__black_handle.png - # light_wood__gray_handle.png - # light_wood.png - # wood__black_handle.png - # wood__gray_handle.png - # wood.png "roboverse_data/assets/calvin/calvin_table_C/textures/dark_wood__black_handle.png", "roboverse_data/assets/calvin/calvin_table_C/textures/dark_wood__gray_handle.png", "roboverse_data/assets/calvin/calvin_table_C/textures/dark_wood.png", @@ -129,11 +43,6 @@ def __init__(self, *args, **kwargs): "roboverse_data/assets/calvin/calvin_table_C/textures/wood__black_handle.png", "roboverse_data/assets/calvin/calvin_table_C/textures/wood__gray_handle.png", "roboverse_data/assets/calvin/calvin_table_C/textures/wood.png", - # base_link.mtl - # drawer_link.mtl - # plank_link.mtl - # slide_link.mtl - # switch_link.mtl "roboverse_data/assets/calvin/calvin_table_C/meshes/base_link.mtl", "roboverse_data/assets/calvin/calvin_table_C/meshes/drawer_link.mtl", "roboverse_data/assets/calvin/calvin_table_C/meshes/plank_link.mtl", @@ -168,6 +77,4 @@ def __init__(self, *args, **kwargs): ] super().__init__(*args, **kwargs) - traj_filepath = ( - "roboverse_data/trajs/calvin/env_C_out/episode_chunk_123_1621143_1647277/trajectory_env_C_1926_v2.pkl" - ) + traj_filepath = "roboverse_data/trajs/calvin/calvin_traj_ann/env_C_out/task_100_v2.pkl" diff --git a/roboverse_pack/tasks/calvin/scene_D.py b/roboverse_pack/tasks/calvin/scene_D.py index 3dc761402..95f07b756 100644 --- a/roboverse_pack/tasks/calvin/scene_D.py +++ b/roboverse_pack/tasks/calvin/scene_D.py @@ -24,83 +24,6 @@ @register_task("calvin.base_table_D") class BaseCalvinTableTask_C(BaseCalvinTableTask): - # scenario = ScenarioCfg( - # robots=[ - # FrankaWithGripperExtensionCfg( - # name="franka", - # default_position=[-0.34, -0.46, 0.24], - # default_orientation=[1, 0, 0, 0], - # actuators={ - # "panda_joint1": BaseActuatorCfg(velocity_limit=2.175, torque_limit=87, stiffness=280, damping=10), - # "panda_joint2": BaseActuatorCfg(velocity_limit=2.175, torque_limit=87, stiffness=280, damping=10), - # "panda_joint3": BaseActuatorCfg(velocity_limit=2.175, torque_limit=87, stiffness=280, damping=10), - # "panda_joint4": BaseActuatorCfg(velocity_limit=2.175, torque_limit=87, stiffness=280, damping=10), - # "panda_joint5": BaseActuatorCfg(velocity_limit=2.61, torque_limit=12.0, stiffness=200, damping=5), - # "panda_joint6": BaseActuatorCfg(velocity_limit=2.61, torque_limit=12.0, stiffness=200, damping=5), - # "panda_joint7": BaseActuatorCfg(velocity_limit=2.61, torque_limit=12.0, stiffness=200, damping=5), - # "panda_finger_joint1": BaseActuatorCfg( - # velocity_limit=0.2, torque_limit=20.0, is_ee=True, stiffness=30000, damping=1000 - # ), - # "panda_finger_joint2": BaseActuatorCfg( - # velocity_limit=0.2, torque_limit=20.0, is_ee=True, stiffness=30000, damping=1000 - # ), - # }, - # default_joint_positions={ - # "panda_joint1": -1.21779206, - # "panda_joint2": 1.03987646, - # "panda_joint3": 2.11978261, - # "panda_joint4": -2.34205014, - # "panda_joint5": -0.87015947, - # "panda_joint6": 1.64119353, - # "panda_joint7": 0.55344866, - # "panda_finger_joint1": 0.04, - # "panda_finger_joint2": 0.04, - # }, - # control_type="joint_position", - # fix_base_link=True, - # urdf_path="roboverse_data/robots/franka_calvin/panda_longer_finger.urdf", - # usd_path=None, - # mjcf_path=None, - # mjx_mjcf_path=None, - # ) - # ], - # objects=[ - # ArticulationObjCfg( - # name="table", - # scale=0.8, - # default_position=[0, 0, 0], - # default_orientation=[1, 0, 0, 0], - # fix_base_link=True, - # urdf_path="roboverse_data/assets/calvin/calvin_table_C/urdf/calvin_table_C.urdf", - # ), - # RigidObjCfg( - # name="pink_cube", - # scale=0.8, - # default_position=[1.28661989e-01, -3.77756105e-02, 4.59989266e-01 + 0.01], - # default_orientation=quat_from_euler_np(1.10200730e-04, 3.19760378e-05, -3.94522179e-01), - # fix_base_link=False, - # urdf_path="roboverse_data/assets/calvin/block_pink_middle.urdf", - # ), - # RigidObjCfg( - # name="blue_cube", - # scale=0.8, - # default_position=[-2.83642665e-01, 8.05351014e-02, 4.60989238e-01 + 0.01], - # default_orientation=quat_from_euler_np(-1.10251078e-05, -5.25663348e-05, -9.06438129e-01), - # fix_base_link=False, - # urdf_path="roboverse_data/assets/calvin/block_blue_small.urdf", - # ), - # RigidObjCfg( - # name="red_cube", - # scale=0.8, - # default_position=[2.32403619e-01, -4.04295856e-02, 4.59990009e-01 + 0.01], - # default_orientation=quat_from_euler_np(4.12287744e-08, -8.05700103e-09, -2.17741510e00), - # fix_base_link=False, - # urdf_path="roboverse_data/assets/calvin/block_red_big.urdf", - # ), - # ], - # decimation=8, - # ) - def __init__(self, *args, **kwargs): self.scenario.objects = [ ArticulationObjCfg( @@ -111,15 +34,6 @@ def __init__(self, *args, **kwargs): fix_base_link=True, urdf_path="roboverse_data/assets/calvin/calvin_table_D/urdf/calvin_table_D.urdf", extra_resources=[ - # dark_wood__black_handle.png - # dark_wood__gray_handle.png - # dark_wood.png - # light_wood__black_handle.png - # light_wood__gray_handle.png - # light_wood.png - # wood__black_handle.png - # wood__gray_handle.png - # wood.png "roboverse_data/assets/calvin/calvin_table_D/textures/dark_wood__black_handle.png", "roboverse_data/assets/calvin/calvin_table_D/textures/dark_wood__gray_handle.png", "roboverse_data/assets/calvin/calvin_table_D/textures/dark_wood.png", @@ -129,11 +43,6 @@ def __init__(self, *args, **kwargs): "roboverse_data/assets/calvin/calvin_table_D/textures/wood__black_handle.png", "roboverse_data/assets/calvin/calvin_table_D/textures/wood__gray_handle.png", "roboverse_data/assets/calvin/calvin_table_D/textures/wood.png", - # base_link.mtl - # drawer_link.mtl - # plank_link.mtl - # slide_link.mtl - # switch_link.mtl "roboverse_data/assets/calvin/calvin_table_D/meshes/base_link.mtl", "roboverse_data/assets/calvin/calvin_table_D/meshes/drawer_link.mtl", "roboverse_data/assets/calvin/calvin_table_D/meshes/plank_link.mtl", @@ -168,4 +77,4 @@ def __init__(self, *args, **kwargs): ] super().__init__(*args, **kwargs) - traj_filepath = "roboverse_data/trajs/calvin/env_D_out/episode_chunk_15_358482_361252/trajectory_env_D_2205_v2.pkl" + traj_filepath = "roboverse_data/trajs/calvin/calvin_traj_ann/env_D_out/task_100_v2.pkl" From 4cbb03ea77df763777495a8a0b2ea16eef320798 Mon Sep 17 00:00:00 2001 From: JIAjindou <96030696+JIAjindou@users.noreply.github.com> Date: Thu, 18 Dec 2025 17:55:41 +0800 Subject: [PATCH 34/50] [fix] VP-score matching (#733) * [fix] VP-score matching * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../il/policies/dp/score_unet_image_policy.py | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/roboverse_learn/il/policies/dp/score_unet_image_policy.py b/roboverse_learn/il/policies/dp/score_unet_image_policy.py index 32cf49592..ff6631482 100644 --- a/roboverse_learn/il/policies/dp/score_unet_image_policy.py +++ b/roboverse_learn/il/policies/dp/score_unet_image_policy.py @@ -102,20 +102,24 @@ def conditional_sample( scheduler.set_timesteps(self.num_inference_steps) for t in scheduler.timesteps: - log.info(f"Using algorithm: Score Matching") + # log.info(f"Using algorithm: Score Matching") trajectory[condition_mask] = condition_data[condition_mask] score = model(trajectory, t, local_cond=local_cond, global_cond=global_cond) - beta_t = scheduler.betas[t] * self.num_inference_steps - # beta_t = torch.tensor(0.02, device=trajectory.device, dtype=trajectory.dtype) + step_idx = (t * (scheduler.config.num_train_timesteps // self.num_inference_steps)).long() + beta_t = scheduler.betas[step_idx].to(trajectory.device).view(-1, 1, 1) + + # VP-SDE update formula: + # dx = [-1/2 * beta(t) * x - beta(t) * score] dt + sqrt(beta(t)) * dW + # x_{t-1} = x_t + 0.5 * beta_t * x_t + beta_t * score + sqrt(beta_t) * noise (npte dt < 0) + drift = 0.5 * beta_t * trajectory + beta_t * score noise = torch.randn_like(trajectory) if t > 0 else torch.zeros_like(trajectory) + diffusion = torch.sqrt(beta_t) * noise - # Langevin dynamics update - # trajectory = trajectory + (beta_t / 2) * score + torch.sqrt(beta_t) * noise - trajectory = trajectory + (beta_t / 2) * score + trajectory = trajectory + drift + diffusion trajectory[condition_mask] = condition_data[condition_mask] return trajectory @@ -220,12 +224,11 @@ def compute_loss(self, batch): ## ||score + noise / sigma_t||^2, ideal score: noise / sigma_t # ideal - unstable - # sigma_t = (torch.sqrt(1 - self.noise_scheduler.alphas_cumprod[timesteps])).view(-1, 1, 1) + sigma_t = (torch.sqrt(1 - self.noise_scheduler.alphas_cumprod[timesteps])).view(-1, 1, 1) # practical - more stable - sigma_t = (1.0 / torch.sqrt(self.noise_scheduler.alphas_cumprod[timesteps])).view(-1, 1, 1) - target = -noise / sigma_t + # sigma_t = (1.0 / torch.sqrt(self.noise_scheduler.alphas_cumprod[timesteps])).view(-1, 1, 1) - loss = F.mse_loss(score, target, reduction="none") + loss = F.mse_loss(score * sigma_t, -noise, reduction="none") loss = loss * loss_mask.type(loss.dtype) loss = reduce(loss, "b ... -> b (...)", "mean") loss = loss.mean() From c2b7267c3879c671813792f61b3ca260e4d693be Mon Sep 17 00:00:00 2001 From: JIAjindou <96030696+JIAjindou@users.noreply.github.com> Date: Fri, 19 Dec 2025 15:27:03 +0800 Subject: [PATCH 35/50] [fix] act sr stabilization (#734) * [fix] act sr stabilization * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../example_pack/tasks/checkers/checkers.py | 2 +- .../il/policies/act/act_eval_runner.py | 13 +- roboverse_learn/il/policies/act/act_run.sh | 10 +- roboverse_learn/il/policies/act/train.py | 356 +++++++++--------- roboverse_learn/il/policies/act/utils.py | 20 +- 5 files changed, 213 insertions(+), 188 deletions(-) diff --git a/metasim/example/example_pack/tasks/checkers/checkers.py b/metasim/example/example_pack/tasks/checkers/checkers.py index aee06e5aa..9a3a0ca24 100644 --- a/metasim/example/example_pack/tasks/checkers/checkers.py +++ b/metasim/example/example_pack/tasks/checkers/checkers.py @@ -88,7 +88,7 @@ class JointPosChecker: def check(self, handler: BaseSimHandler, states: TensorState) -> torch.BoolTensor: dof_pos = get_dof_pos(handler, self.obj_name, self.joint_name) - log.debug(f"Joint {self.joint_name} of object {self.obj_name} has position {tensor_to_str(dof_pos)}") + # log.debug(f"Joint {self.joint_name} of object {self.obj_name} has position {tensor_to_str(dof_pos)}") if self.mode == "ge": return dof_pos >= self.radian_threshold elif self.mode == "le": diff --git a/roboverse_learn/il/policies/act/act_eval_runner.py b/roboverse_learn/il/policies/act/act_eval_runner.py index 81f9bf4bb..63252ceef 100755 --- a/roboverse_learn/il/policies/act/act_eval_runner.py +++ b/roboverse_learn/il/policies/act/act_eval_runner.py @@ -341,7 +341,7 @@ class SimpleRenderCfg: batch_size = 8 dim_feedforward = 3200 lr = 1e-5 - act_ckpt_name = "policy_best.ckpt" + act_ckpt_name = "policy_last.ckpt" policy_config = { "lr": lr, "num_queries": args.chunk_size, @@ -387,7 +387,7 @@ def post_process(a): max_timesteps = int(max_timesteps * 1) ckpt_name = args.ckpt_path.split("/")[-1] - os.makedirs(f"il_outputs/{args.algo}/{args.task}/eval/{ckpt_name}_dr{args.level}", exist_ok=True) + os.makedirs(f"il_outputs/{args.algo}/{args.task}/eval/{ckpt_name}", exist_ok=True) ## cuRobo controller (commented out - not needed for ACT joint control) # *_, robot_ik = get_curobo_models(scenario.robots[0]) @@ -437,7 +437,7 @@ def post_process(a): with torch.no_grad(): while step < MaxStep: - log.debug(f"Step {step}") + # log.debug(f"Step {step}") robot_joint_limits = scenario.robots[0].joint_limits image_list.append(np.array(obs.cameras['camera'].rgb.cpu())[0]) @@ -495,8 +495,8 @@ def post_process(a): TotalSuccess += 1 SuccessOnce[0] = True print(f"Env {i} Success") + break - log.debug(f"TotalSuccess: {TotalSuccess}") SuccessOnce = [SuccessOnce[i] or success[i] for i in range(num_envs)] TimeOut = [TimeOut[i] or time_out[i] for i in range(num_envs)] for TimeOutIndex in range(num_envs): @@ -508,12 +508,13 @@ def post_process(a): step += 1 - images_to_video(image_list, f"il_outputs/{args.algo}/{args.task}/eval/{ckpt_name}_dr{args.level}/{i}.mp4") + log.debug(f"TotalSuccess: {TotalSuccess} | Success Rate: {TotalSuccess / (i + 1):.4f}") + images_to_video(image_list, f"il_outputs/{args.algo}/{args.task}/eval/{ckpt_name}/{i}.mp4") success_rate = TotalSuccess / num_eval print("Success Rate: ", success_rate) - result_dir = f"il_outputs/{args.algo}/{args.task}/eval/{ckpt_name}_dr{args.level}" + result_dir = f"il_outputs/{args.algo}/{args.task}/eval/{ckpt_name}" result_file = os.path.join(result_dir, "success_rate.txt") with open(result_file, "w") as f: f.write(f"Success Rate: {success_rate:.4f}\n") diff --git a/roboverse_learn/il/policies/act/act_run.sh b/roboverse_learn/il/policies/act/act_run.sh index f39ec59af..cff72406a 100755 --- a/roboverse_learn/il/policies/act/act_run.sh +++ b/roboverse_learn/il/policies/act/act_run.sh @@ -16,18 +16,18 @@ delta_ee=0 # 0 or 1 (only matters if act_space is ee, 0 means absolute 1 means d alg_name=ACT seed=42 -collect_level=2 +collect_level=3 # ACT hyperparameters chunk_size=40 kl_weight=10 hidden_dim=512 lr=1e-5 -batch_size=8 +batch_size=32 dim_feedforward=3200 # Domain Randomization parameters for evaluation -eval_level=2 +eval_level=3 eval_scene_mode=0 # 0=Manual, 1=USD Table, 2=USD Scene, 3=Full USD eval_seed=42 # Randomization seed (optional) @@ -47,7 +47,8 @@ if [ "${train_enable}" = "True" ]; then --policy_class ${alg_name} --kl_weight ${kl_weight} --chunk_size ${chunk_size} \ --hidden_dim ${hidden_dim} --batch_size ${batch_size} --dim_feedforward ${dim_feedforward} \ --num_epochs ${num_epochs} --lr ${lr} --state_dim 9 \ - --seed ${seed} + --seed ${seed} \ + --level ${collect_level} fi # Evaluation @@ -56,6 +57,7 @@ if [ "${eval_enable}" = "True" ]; then # # export TORCH_CUDA_ARCH_LIST="8.9" ckpt_path=$(cat ./roboverse_learn/il/policies/act/ckpt_dir_path.txt) + python -m roboverse_learn.il.policies.act.act_eval_runner \ --task ${task_name_set} \ --robot franka \ diff --git a/roboverse_learn/il/policies/act/train.py b/roboverse_learn/il/policies/act/train.py index fe27a703f..106d71b47 100644 --- a/roboverse_learn/il/policies/act/train.py +++ b/roboverse_learn/il/policies/act/train.py @@ -8,29 +8,189 @@ import json from copy import deepcopy from tqdm import tqdm +from omegaconf import OmegaConf from .utils import load_data # data functions from .utils import compute_dict_mean, set_seed, detach_dict # helper functions from .policy import ACTPolicy, CNNMLPPolicy +from roboverse_learn.il.runners.base_runner import BaseRunner import IPython e = IPython.embed from datetime import datetime +class ACTRunner(BaseRunner): + def __init__(self, cfg, output_dir=None): + super().__init__(cfg, output_dir=output_dir) + + # Determine device + self.device = torch.device(cfg.get('device', 'cuda' if torch.cuda.is_available() else 'cpu')) + + # Set seed + set_seed(cfg['seed']) + + # Policy configuration + self.policy_class = cfg['policy_class'] + self.policy_config = cfg['policy_config'] + + # Initialize policy and optimizer + self.policy = self._make_policy() + self.policy.to(self.device) + self.optimizer = self._make_optimizer() + + def _make_policy(self): + if self.policy_class == 'ACT': + policy = ACTPolicy(self.policy_config) + elif self.policy_class == 'CNNMLP': + policy = CNNMLPPolicy(self.policy_config) + else: + raise NotImplementedError + return policy + + def _make_optimizer(self): + if self.policy_class == 'ACT': + optimizer = self.policy.configure_optimizers() + elif self.policy_class == 'CNNMLP': + optimizer = self.policy.configure_optimizers() + else: + raise NotImplementedError + return optimizer + + def forward_pass(self, data): + image_data, qpos_data, action_data, is_pad = data + image_data = image_data.to(self.device) + qpos_data = qpos_data.to(self.device) + action_data = action_data.to(self.device) + is_pad = is_pad.to(self.device) + return self.policy(qpos_data, image_data, action_data, is_pad) + + def train(self): + cfg = self.cfg + num_epochs = cfg['num_epochs'] + ckpt_dir = self.output_dir + seed = cfg['seed'] + + # Load data + train_dataloader, val_dataloader, stats, _ = load_data( + cfg['dataset_dir'], + cfg['num_episodes'], + cfg['camera_names'], + cfg['batch_size'], + cfg['batch_size'] + ) + + # Save dataset stats + if not os.path.isdir(ckpt_dir): + os.makedirs(ckpt_dir) + stats_path = os.path.join(ckpt_dir, f'dataset_stats.pkl') + with open(stats_path, 'wb') as f: + pickle.dump(stats, f) + + # Save config to cfg.yaml + config_path = os.path.join(ckpt_dir, 'cfg.yaml') + with open(config_path, 'w') as f: + # Convert OmegaConf to dict if needed, or just dump the dict + if isinstance(cfg, (dict, list)): + yaml.dump(cfg, f, default_flow_style=False) + else: + yaml.dump(OmegaConf.to_container(cfg), f, default_flow_style=False) + + train_history = [] + validation_history = [] + min_val_loss = np.inf + best_ckpt_info = None + + for epoch in tqdm(range(num_epochs)): + print(f'\nEpoch {epoch}') + # Validation + with torch.inference_mode(): + self.policy.eval() + epoch_dicts = [] + for batch_idx, data in enumerate(val_dataloader): + forward_dict = self.forward_pass(data) + epoch_dicts.append(forward_dict) + epoch_summary = compute_dict_mean(epoch_dicts) + validation_history.append(epoch_summary) + + epoch_val_loss = epoch_summary['loss'] + if epoch_val_loss < min_val_loss: + min_val_loss = epoch_val_loss + best_ckpt_info = (epoch, min_val_loss, deepcopy(self.policy.state_dict())) + + print(f'Val loss: {epoch_val_loss:.5f}') + summary_string = '' + for k, v in epoch_summary.items(): + summary_string += f'{k}: {v.item():.3f} ' + print(summary_string) + + # Training + self.policy.train() + self.optimizer.zero_grad() + epoch_train_dicts = [] + for batch_idx, data in enumerate(train_dataloader): + forward_dict = self.forward_pass(data) + # Backward + loss = forward_dict['loss'] + loss.backward() + self.optimizer.step() + self.optimizer.zero_grad() + epoch_train_dicts.append(forward_dict) + + epoch_train_summary = compute_dict_mean(epoch_train_dicts) + train_history.append(epoch_train_summary) + epoch_train_loss = epoch_train_summary['loss'] + print(f'Train loss: {epoch_train_loss:.5f}') + + summary_string = '' + for k, v in epoch_train_summary.items(): + summary_string += f'{k}: {v.item():.3f} ' + print(summary_string) + + if epoch % 100 == 0: + ckpt_path = os.path.join(ckpt_dir, f'policy_epoch_{epoch}_seed_{seed}.ckpt') + torch.save(self.policy.state_dict(), ckpt_path) + plot_history(train_history, validation_history, epoch, ckpt_dir, seed) + + ckpt_path = os.path.join(ckpt_dir, f'policy_last.ckpt') + torch.save(self.policy.state_dict(), ckpt_path) + + best_epoch, min_val_loss, best_state_dict = best_ckpt_info + ckpt_path = os.path.join(ckpt_dir, f'policy_best.ckpt') + torch.save(best_state_dict, ckpt_path) + print(f'Training finished:\nSeed {seed}, val loss {min_val_loss:.6f} at epoch {best_epoch}') + + # Save training curves + plot_history(train_history, validation_history, num_epochs, ckpt_dir, seed) + + file_path = os.path.join("./roboverse_learn/il/policies/act", "ckpt_dir_path.txt") + with open(file_path, 'w') as f: + f.write(ckpt_dir) + + return best_ckpt_info + + +def plot_history(train_history, validation_history, num_epochs, ckpt_dir, seed): + # save training curves + for key in train_history[0]: + plot_path = os.path.join(ckpt_dir, f'train_val_{key}_seed_{seed}.png') + plt.figure() + train_values = [summary[key].item() for summary in train_history] + val_values = [summary[key].item() for summary in validation_history] + plt.plot(np.linspace(0, num_epochs-1, len(train_history)), train_values, label='train') + plt.plot(np.linspace(0, num_epochs-1, len(validation_history)), val_values, label='validation') + # plt.ylim([-0.1, 1]) + plt.tight_layout() + plt.legend() + plt.title(key) + plt.savefig(plot_path) + print(f'Saved plots to {ckpt_dir}') + + def main(args): - set_seed(1) - # command line parameters + # Prepare configuration policy_class = args['policy_class'] task_name = args['task_name'] - batch_size_train = args['batch_size'] - batch_size_val = args['batch_size'] - num_epochs = args['num_epochs'] - - # get task parameters - dataset_dir = args['dataset_dir'] - num_episodes = args['num_episodes'] - episode_len = args['episode_len'] camera_names = args['camera_names'] # fixed parameters @@ -59,9 +219,11 @@ def main(args): else: raise NotImplementedError - ckpt_dir = f"il_outputs/act/{task_name}" + level = args.get('level', 0) + ckpt_dir = f"il_outputs/act/{task_name}/ckpt/level{level}" # Load metadata from dataset directory + dataset_dir = args['dataset_dir'] metadata_path = os.path.join(dataset_dir, 'metadata.json') dataset_metadata = {} if os.path.exists(metadata_path): @@ -69,9 +231,8 @@ def main(args): dataset_metadata = json.load(f) config = { - 'num_epochs': num_epochs, - 'ckpt_dir': ckpt_dir, - 'episode_len': episode_len, + 'num_epochs': args['num_epochs'], + 'episode_len': args['episode_len'], 'lr': args['lr'], 'policy_class': policy_class, 'policy_config': policy_config, @@ -80,163 +241,20 @@ def main(args): 'temporal_agg': args['temporal_agg'], 'camera_names': camera_names, 'real_robot': True, - 'data': dataset_metadata, # Add the dataset metadata to config + 'data': dataset_metadata, + 'dataset_dir': dataset_dir, + 'num_episodes': args['num_episodes'], + 'batch_size': args['batch_size'], + 'device': args.get('device', 'cuda' if torch.cuda.is_available() else 'cpu') } - train_dataloader, val_dataloader, stats, _ = load_data( - dataset_dir, - num_episodes, - camera_names, - batch_size_train, - batch_size_val - ) - - # save dataset stats - if not os.path.isdir(ckpt_dir): - os.makedirs(ckpt_dir) - stats_path = os.path.join(ckpt_dir, f'dataset_stats.pkl') - with open(stats_path, 'wb') as f: - pickle.dump(stats, f) + # Convert to OmegaConf for compatibility with BaseRunner if needed, + # but BaseRunner doesn't strictly enforce OmegaConf type for simple access. + # However, it's better practice to wrapping it if we were fully migrating. + # For now, passing the dict is fine as long as we access with [] or .get() - # Save config to cfg.yaml - config_path = os.path.join(ckpt_dir, 'cfg.yaml') - with open(config_path, 'w') as f: - yaml.dump(config, f, default_flow_style=False) - - best_ckpt_info = train_bc(train_dataloader, val_dataloader, config) - best_epoch, min_val_loss, best_state_dict = best_ckpt_info - - # save best checkpoint - ckpt_path = os.path.join(ckpt_dir, f'policy_best.ckpt') - torch.save(best_state_dict, ckpt_path) - print(f'Best ckpt, val loss {min_val_loss:.6f} @ epoch{best_epoch}') - - - -def make_policy(policy_class, policy_config): - if policy_class == 'ACT': - policy = ACTPolicy(policy_config) - elif policy_class == 'CNNMLP': - policy = CNNMLPPolicy(policy_config) - else: - raise NotImplementedError - return policy - - -def make_optimizer(policy_class, policy): - if policy_class == 'ACT': - optimizer = policy.configure_optimizers() - elif policy_class == 'CNNMLP': - optimizer = policy.configure_optimizers() - else: - raise NotImplementedError - return optimizer - - -def forward_pass(data, policy): - image_data, qpos_data, action_data, is_pad = data - image_data, qpos_data, action_data, is_pad = image_data.cuda(), qpos_data.cuda(), action_data.cuda(), is_pad.cuda() - return policy(qpos_data, image_data, action_data, is_pad) - - -def train_bc(train_dataloader, val_dataloader, config): - num_epochs = config['num_epochs'] - ckpt_dir = config['ckpt_dir'] - seed = config['seed'] - policy_class = config['policy_class'] - policy_config = config['policy_config'] - - set_seed(seed) - - policy = make_policy(policy_class, policy_config) - policy.cuda() - optimizer = make_optimizer(policy_class, policy) - - train_history = [] - validation_history = [] - min_val_loss = np.inf - best_ckpt_info = None - for epoch in tqdm(range(num_epochs)): - print(f'\nEpoch {epoch}') - # validation - with torch.inference_mode(): - policy.eval() - epoch_dicts = [] - for batch_idx, data in enumerate(val_dataloader): - forward_dict = forward_pass(data, policy) - epoch_dicts.append(forward_dict) - epoch_summary = compute_dict_mean(epoch_dicts) - validation_history.append(epoch_summary) - - epoch_val_loss = epoch_summary['loss'] - if epoch_val_loss < min_val_loss: - min_val_loss = epoch_val_loss - best_ckpt_info = (epoch, min_val_loss, deepcopy(policy.state_dict())) - print(f'Val loss: {epoch_val_loss:.5f}') - summary_string = '' - for k, v in epoch_summary.items(): - summary_string += f'{k}: {v.item():.3f} ' - print(summary_string) - - policy.train() - optimizer.zero_grad() - epoch_train_dicts = [] - for batch_idx, data in enumerate(train_dataloader): - forward_dict = forward_pass(data, policy) - # backward - loss = forward_dict['loss'] - loss.backward() - optimizer.step() - optimizer.zero_grad() - epoch_train_dicts.append(forward_dict) - epoch_train_summary = compute_dict_mean(epoch_train_dicts) - train_history.append(epoch_train_summary) - epoch_train_loss = epoch_train_summary['loss'] - print(f'Train loss: {epoch_train_loss:.5f}') - - summary_string = '' - for k, v in epoch_train_summary.items(): - summary_string += f'{k}: {v.item():.3f} ' - print(summary_string) - - if epoch % 100 == 0: - ckpt_path = os.path.join(ckpt_dir, f'policy_epoch_{epoch}_seed_{seed}.ckpt') - torch.save(policy.state_dict(), ckpt_path) - plot_history(train_history, validation_history, epoch, ckpt_dir, seed) - - ckpt_path = os.path.join(ckpt_dir, f'policy_last.ckpt') - torch.save(policy.state_dict(), ckpt_path) - - best_epoch, min_val_loss, best_state_dict = best_ckpt_info - ckpt_path = os.path.join(ckpt_dir, f'policy_epoch_{best_epoch}_seed_{seed}.ckpt') - torch.save(best_state_dict, ckpt_path) - print(f'Training finished:\nSeed {seed}, val loss {min_val_loss:.6f} at epoch {best_epoch}') - - # save training curves - plot_history(train_history, validation_history, num_epochs, ckpt_dir, seed) - - file_path = os.path.join("./roboverse_learn/il/policies/act", "ckpt_dir_path.txt") - with open(file_path, 'w') as f: - f.write(ckpt_dir) - - return best_ckpt_info - - -def plot_history(train_history, validation_history, num_epochs, ckpt_dir, seed): - # save training curves - for key in train_history[0]: - plot_path = os.path.join(ckpt_dir, f'train_val_{key}_seed_{seed}.png') - plt.figure() - train_values = [summary[key].item() for summary in train_history] - val_values = [summary[key].item() for summary in validation_history] - plt.plot(np.linspace(0, num_epochs-1, len(train_history)), train_values, label='train') - plt.plot(np.linspace(0, num_epochs-1, len(validation_history)), val_values, label='validation') - # plt.ylim([-0.1, 1]) - plt.tight_layout() - plt.legend() - plt.title(key) - plt.savefig(plot_path) - print(f'Saved plots to {ckpt_dir}') + runner = ACTRunner(config, output_dir=ckpt_dir) + runner.train() if __name__ == '__main__': @@ -260,5 +278,7 @@ def plot_history(train_history, validation_history, num_epochs, ckpt_dir, seed): parser.add_argument('--dim_feedforward', action='store', type=int, help='dim_feedforward', required=False) parser.add_argument('--temporal_agg', action='store_true') parser.add_argument('--state_dim', action='store', type=int, help='state_dim', required=False, default=9) + parser.add_argument('--level', action='store', type=str, help='level', required=False, default=0) + parser.add_argument('--device', action='store', type=str, help='device', required=False, default='cuda') main(vars(parser.parse_args())) diff --git a/roboverse_learn/il/policies/act/utils.py b/roboverse_learn/il/policies/act/utils.py index 66e15b33b..b676a2f92 100644 --- a/roboverse_learn/il/policies/act/utils.py +++ b/roboverse_learn/il/policies/act/utils.py @@ -27,14 +27,21 @@ def __init__(self, episode_ids, dataset_dir, camera_names, norm_stats): keys=["head_camera", "state", "action"] ) + # Construct indices for all possible (episode_id, start_ts) pairs + self.indices = [] + for episode_id in self.episode_ids: + episode_slice = self.replay_buffer.get_episode_slice(episode_id) + episode_len = episode_slice.stop - episode_slice.start + for ts in range(episode_len): + self.indices.append((episode_id, ts)) + def __len__(self): - return len(self.episode_ids) + return len(self.indices) def __getitem__(self, index): - sample_full_episode = False # hardcode - # Get episode id from the index - episode_id = self.episode_ids[index] + # Get episode id and start_ts from the pre-calculated indices + episode_id, start_ts = self.indices[index] # Get the episode slice directly episode_slice = self.replay_buffer.get_episode_slice(episode_id) @@ -44,11 +51,6 @@ def __getitem__(self, index): action_sequence = self.replay_buffer["action"][episode_slice] head_camera_sequence = self.replay_buffer["head_camera"][episode_slice] - - if sample_full_episode: - start_ts = 0 - else: - start_ts = np.random.choice(len(state_sequence)) # Take the first frame as the starting point state = state_sequence[start_ts] From 68fc75aa12c87988abbd8b8bf543706b2223edcf Mon Sep 17 00:00:00 2001 From: Daoyuan Zhu <77039357+infinit-luffy@users.noreply.github.com> Date: Sat, 20 Dec 2025 10:58:01 +0800 Subject: [PATCH 36/50] add two tasks (#731) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add two task * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * add two task * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * add two task * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * add two task * add two task --------- Co-authored-by: 朱道远 Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- roboverse_learn/rl/fast_td3/README.md | 112 ++++ .../rl/fast_td3/configs/pick_place_knife.yaml | 99 +++ .../fast_td3/configs/pick_place_teapot.yaml | 100 +++ .../rl/fast_td3/configs/track_knife.yaml | 94 +++ .../rl/fast_td3/configs/track_teapot.yaml | 94 +++ .../approach_grasp_ceramic_teapot.py | 514 +++++++++++++++ .../tasks/pick_place/approach_grasp_knife.py | 613 ++++++++++++++++++ .../tasks/pick_place/track_ceramic_teapot.py | 472 ++++++++++++++ .../tasks/pick_place/track_knife.py | 473 ++++++++++++++ 9 files changed, 2571 insertions(+) create mode 100644 roboverse_learn/rl/fast_td3/configs/pick_place_knife.yaml create mode 100644 roboverse_learn/rl/fast_td3/configs/pick_place_teapot.yaml create mode 100644 roboverse_learn/rl/fast_td3/configs/track_knife.yaml create mode 100644 roboverse_learn/rl/fast_td3/configs/track_teapot.yaml create mode 100644 roboverse_pack/tasks/pick_place/approach_grasp_ceramic_teapot.py create mode 100644 roboverse_pack/tasks/pick_place/approach_grasp_knife.py create mode 100644 roboverse_pack/tasks/pick_place/track_ceramic_teapot.py create mode 100644 roboverse_pack/tasks/pick_place/track_knife.py diff --git a/roboverse_learn/rl/fast_td3/README.md b/roboverse_learn/rl/fast_td3/README.md index a42cf94d3..a24884c44 100644 --- a/roboverse_learn/rl/fast_td3/README.md +++ b/roboverse_learn/rl/fast_td3/README.md @@ -190,3 +190,115 @@ Then run training as usual: ```bash python roboverse_learn/rl/fast_td3/train.py --config your_config.yaml ``` + +## V-HACD (Volumetric Hierarchical Approximate Convex Decomposition) + +Add the following code in RoboVerse/metasim/sim/isaacgym/isaacgym.py + + +```python +# eg RigidObjCfg after line 395 +asset_options.vhacd_enabled = True +asset_options.vhacd_params.resolution = 500000 # +asset_options.vhacd_params.max_convex_hulls = 64 # +asset_options.vhacd_params.max_num_vertices_per_ch = 64 +asset_options.thickness = 0.001 + +``` + + + +## Reconstruct Local Offset from World Coordinates + +This script implements calculate the local offset between the desired grasp position and the object center. + +### Logic Overview +* **Quaternion Format**: `[w, x, y, z]` + +## Implementation Code + +```python +import torch + +def calculate_local_offset(root_pos, root_rot, target_pos): + """ + Inverse: Calculate local offset of Target in Root coordinate system + [W, X, Y, Z] format version + """ + # 1. Calculate world coordinate difference + diff_world = target_pos - root_pos + + # 2. Extract quaternion (format: w, x, y, z) + # Correction: Index 0 is w + w, x, y, z = root_rot[:, 0], root_rot[:, 1], root_rot[:, 2], root_rot[:, 3] + + # 3. Construct inverse rotation (conjugate quaternion: [w, -x, -y, -z]) + # w remains unchanged, vector part is negated + q_vec_inv = torch.stack([-x, -y, -z], dim=1) + w = w.unsqueeze(1) + + # 4. Apply rotation: q_inv * diff * q + # t = 2 * cross(q_vec, v) + t = 2.0 * torch.cross(q_vec_inv, diff_world, dim=1) + + # result = v + w*t + cross(q_vec, t) + local_offset = diff_world + w * t + torch.cross(q_vec_inv, t, dim=1) + + return local_offset + +def get_world_pos(root_pos, root_rot, local_offset): + """ + Forward verification: Calculate world coordinates based on local offset + [W, X, Y, Z] format version + """ + # 1. Extract quaternion (format: w, x, y, z) + # Correction: Index 0 is w + w, x, y, z = root_rot[:, 0], root_rot[:, 1], root_rot[:, 2], root_rot[:, 3] + + # 2. Forward rotation q (w, x, y, z) + # Vector part uses x, y, z directly + q_vec = torch.stack([x, y, z], dim=1) + w = w.unsqueeze(1) + + # Input vector v + v = local_offset + + # 3. Apply rotation: q * v * q_inv + t = 2.0 * torch.cross(q_vec, v, dim=1) + final_vec = v + w * t + torch.cross(q_vec, t, dim=1) + + return root_pos + final_vec + +# --- Example Usage & Verification --- + +if __name__ == "__main__": + # Root Position (e.g., Knife Handle) + knife_pos = torch.tensor([0.201373, -0.330642, 0.779824]).unsqueeze(0) + + # Root Rotation (Quaternion [w, x, y, z]) + # Assuming the data [-0.398, ...] corresponds to wxyz + knife_rot = torch.tensor([-0.398238, 0.035423, -0.027580, -0.916183]).unsqueeze(0) + + # Target Position (World Space) + target_pos = torch.tensor([0.118386, -0.429724, 0.780205]).unsqueeze(0) + + print("-" * 20) + print(f"Original Target Pos: {target_pos}") + + # 1. Calculate local offset + local_offset = calculate_local_offset(knife_pos, knife_rot, target_pos) + print(f"Calculated Local Offset: {local_offset}") + + # 2. Reconstruct world position from local offset + reconstructed_pos = get_world_pos(knife_pos, knife_rot, local_offset) + print(f"Reconstructed World Pos: {reconstructed_pos}") + + # 3. Verify error + error = (reconstructed_pos - target_pos).abs().max().item() + print(f"Max Error: {error}") + + if error < 1e-6: + print("Test PASSED: Round-trip transformation successful.") + else: + print("Test FAILED: Error too large.") + print("-" * 20) \ No newline at end of file diff --git a/roboverse_learn/rl/fast_td3/configs/pick_place_knife.yaml b/roboverse_learn/rl/fast_td3/configs/pick_place_knife.yaml new file mode 100644 index 000000000..b5675802c --- /dev/null +++ b/roboverse_learn/rl/fast_td3/configs/pick_place_knife.yaml @@ -0,0 +1,99 @@ +# Base Configuration for FastTD3 Training +# Default configuration with IsaacGym simulator and H1 humanoid robot + +# ------------------------------------------------------------------------------- +# Environment +# ------------------------------------------------------------------------------- + +# isaac gym setting +# asset_options.vhacd_enabled = True +# asset_options.vhacd_params.resolution = 500000 # +# asset_options.vhacd_params.max_convex_hulls = 64 # +# asset_options.vhacd_params.max_num_vertices_per_ch = 64 +# asset_options.thickness = 0.001 + + +sim: "isaacgym" +robots: ["franka"] +# task: "pick_place.approach_grasp_knife" +task: "pick_place.approach_grasp_knife" +decimation: 4 +train_or_eval: "train" +headless: True + +# ------------------------------------------------------------------------------- +# Seeds & Device +# ------------------------------------------------------------------------------- +seed: 1 +cuda: true +torch_deterministic: true +device_rank: 0 + +# ------------------------------------------------------------------------------- +# Rollout & Timesteps +# ------------------------------------------------------------------------------- +num_envs: 400 +num_eval_envs: 400 +total_timesteps: 2000000 +learning_starts: 10 +num_steps: 1 + +# ------------------------------------------------------------------------------- +# Replay, Batching, Discounting +# ------------------------------------------------------------------------------- +buffer_size: 20480 +batch_size: 32768 +gamma: 0.99 +tau: 0.1 + +# ------------------------------------------------------------------------------- +# Update Schedule +# ------------------------------------------------------------------------------- +policy_frequency: 2 +num_updates: 5 +# ------------------------------------------------------------------------------- +# Optimizer & Network +# ------------------------------------------------------------------------------- +critic_learning_rate: 0.0003 +actor_learning_rate: 0.0003 +weight_decay: 0.1 +critic_hidden_dim: 512 +actor_hidden_dim: 256 +init_scale: 0.01 +num_atoms: 101 + +# ------------------------------------------------------------------------------- +# Value Distribution & Exploration +# ------------------------------------------------------------------------------- +v_min: 0 +v_max: 600.0 +policy_noise: 0.001 +std_min: 0.001 +std_max: 0.4 +noise_clip: 0.5 + +# ------------------------------------------------------------------------------- +# Algorithm Flags +# ------------------------------------------------------------------------------- +use_cdq: true +compile: true +obs_normalization: true +max_grad_norm: 0.0 +amp: true +amp_dtype: "fp16" +disable_bootstrap: false +measure_burnin: 3 + +# ------------------------------------------------------------------------------- +# Logging & Checkpointing +# ------------------------------------------------------------------------------- +wandb_project: "get_started_fttd3" +exp_name: "get_started_fttd3" +use_wandb: false +# checkpoint_path: '/usr1/home/s125mdg56_03/RoboVerse/models_water/pour_water.approach_grasp_simple_130000.pt' +# checkpoint_path: '/usr1/home/s125mdg56_03/RoboVerse/models_pick_hu2/pick_place.approach_grasp_hu_1350000.pt' +eval_interval: 5000 +# save_interval: 5000 +video_width: 1024 +video_height: 1024 +model_dir: "models_pick_knife" # Directory to save checkpoints (default: "models") diff --git a/roboverse_learn/rl/fast_td3/configs/pick_place_teapot.yaml b/roboverse_learn/rl/fast_td3/configs/pick_place_teapot.yaml new file mode 100644 index 000000000..90e9334a2 --- /dev/null +++ b/roboverse_learn/rl/fast_td3/configs/pick_place_teapot.yaml @@ -0,0 +1,100 @@ +# Base Configuration for FastTD3 Training +# Default configuration with IsaacGym simulator and H1 humanoid robot + +# ------------------------------------------------------------------------------- +# Environment +# ------------------------------------------------------------------------------- + + +# isaac gym setting +# asset_options.vhacd_enabled = True +# asset_options.vhacd_params.resolution = 500000 # +# asset_options.vhacd_params.max_convex_hulls = 64 # +# asset_options.vhacd_params.max_num_vertices_per_ch = 64 +# asset_options.thickness = 0.001 + + +sim: "isaacgym" +robots: ["franka"] +# task: "pick_place.approach_grasp_knife" +task: "pick_place.approach_grasp_hu" +decimation: 4 +train_or_eval: "train" +headless: True + +# ------------------------------------------------------------------------------- +# Seeds & Device +# ------------------------------------------------------------------------------- +seed: 1 +cuda: true +torch_deterministic: true +device_rank: 0 + +# ------------------------------------------------------------------------------- +# Rollout & Timesteps +# ------------------------------------------------------------------------------- +num_envs: 400 +num_eval_envs: 400 +total_timesteps: 2000000 +learning_starts: 10 +num_steps: 1 + +# ------------------------------------------------------------------------------- +# Replay, Batching, Discounting +# ------------------------------------------------------------------------------- +buffer_size: 20480 +batch_size: 32768 +gamma: 0.99 +tau: 0.1 + +# ------------------------------------------------------------------------------- +# Update Schedule +# ------------------------------------------------------------------------------- +policy_frequency: 2 +num_updates: 5 +# ------------------------------------------------------------------------------- +# Optimizer & Network +# ------------------------------------------------------------------------------- +critic_learning_rate: 0.0003 +actor_learning_rate: 0.0003 +weight_decay: 0.1 +critic_hidden_dim: 512 +actor_hidden_dim: 256 +init_scale: 0.01 +num_atoms: 101 + +# ------------------------------------------------------------------------------- +# Value Distribution & Exploration +# ------------------------------------------------------------------------------- +v_min: 0 +v_max: 600.0 +policy_noise: 0.001 +std_min: 0.001 +std_max: 0.4 +noise_clip: 0.5 + +# ------------------------------------------------------------------------------- +# Algorithm Flags +# ------------------------------------------------------------------------------- +use_cdq: true +compile: true +obs_normalization: true +max_grad_norm: 0.0 +amp: true +amp_dtype: "fp16" +disable_bootstrap: false +measure_burnin: 3 + +# ------------------------------------------------------------------------------- +# Logging & Checkpointing +# ------------------------------------------------------------------------------- +wandb_project: "get_started_fttd3" +exp_name: "get_started_fttd3" +use_wandb: false +# checkpoint_path: '/usr1/home/s125mdg56_03/RoboVerse/models_water/pour_water.approach_grasp_simple_130000.pt' +checkpoint_path: '/usr1/home/s125mdg56_03/RoboVerse/models_pick_hu2/pick_place.approach_grasp_hu_1350000.pt' +eval_interval: 5000 +# save_interval: 5000 +video_width: 1024 +video_height: 1024 +model_dir: "models_pick_hu2" # Directory to save checkpoints (default: "models") diff --git a/roboverse_learn/rl/fast_td3/configs/track_knife.yaml b/roboverse_learn/rl/fast_td3/configs/track_knife.yaml new file mode 100644 index 000000000..d063336cf --- /dev/null +++ b/roboverse_learn/rl/fast_td3/configs/track_knife.yaml @@ -0,0 +1,94 @@ +# Configuration for FastTD3 Training - Track Task +# Stage 3: Track task - for training trajectory tracking +# Starts from saved grasp states, only trains trajectory tracking + +# ------------------------------------------------------------------------------- +# Environment +# ------------------------------------------------------------------------------- +sim: "isaacgym" +robots: ["franka"] +task: "pick_place.track_hu" +decimation: 4 +train_or_eval: "train" +headless: True + +# State file path for track task (pkl file path for grasp states) +# If null, uses default path or env var PICK_PLACE_TRACK_STATE_FILE +state_file_path: "/usr1/home/s125mdg56_03/RoboVerse/eval_states/pick_place.approach_grasp_knife_franka_linka_lift_states_198states_20251216_215959.pkl" + +# ------------------------------------------------------------------------------- +# Seeds & Device +# ------------------------------------------------------------------------------- +seed: 1 +cuda: true +torch_deterministic: true +device_rank: 0 + +# ------------------------------------------------------------------------------- +# Rollout & Timesteps +# ------------------------------------------------------------------------------- +num_envs: 400 +num_eval_envs: 400 +total_timesteps: 2000000 +learning_starts: 10 +num_steps: 1 + +# ------------------------------------------------------------------------------- +# Replay, Batching, Discounting +# ------------------------------------------------------------------------------- +buffer_size: 20480 +batch_size: 32768 +gamma: 0.99 +tau: 0.1 + +# ------------------------------------------------------------------------------- +# Update Schedule +# ------------------------------------------------------------------------------- +policy_frequency: 2 +num_updates: 5 + +# ------------------------------------------------------------------------------- +# Optimizer & Network +# ------------------------------------------------------------------------------- +critic_learning_rate: 0.0003 +actor_learning_rate: 0.0003 +weight_decay: 0.1 +critic_hidden_dim: 512 +actor_hidden_dim: 256 +init_scale: 0.01 +num_atoms: 101 + +# ------------------------------------------------------------------------------- +# Value Distribution & Exploration +# ------------------------------------------------------------------------------- +v_min: 0 +v_max: 2400.0 +policy_noise: 0.001 +std_min: 0.001 +std_max: 0.4 +noise_clip: 0.5 + +# ------------------------------------------------------------------------------- +# Algorithm Flags +# ------------------------------------------------------------------------------- +use_cdq: true +compile: true +obs_normalization: true +max_grad_norm: 0.0 +amp: true +amp_dtype: "fp16" +disable_bootstrap: false +measure_burnin: 3 + +# ------------------------------------------------------------------------------- +# Logging & Checkpointing +# ------------------------------------------------------------------------------- +wandb_project: "pick_place_track_knife" +exp_name: "pick_place_track_knife" +use_wandb: false +checkpoint_path: null +eval_interval: 5000 +save_interval: 15000 +video_width: 1024 +video_height: 1024 +model_dir: "models_track_knife" # Directory to save checkpoints (default: "models") diff --git a/roboverse_learn/rl/fast_td3/configs/track_teapot.yaml b/roboverse_learn/rl/fast_td3/configs/track_teapot.yaml new file mode 100644 index 000000000..c4d8ad401 --- /dev/null +++ b/roboverse_learn/rl/fast_td3/configs/track_teapot.yaml @@ -0,0 +1,94 @@ +# Configuration for FastTD3 Training - Track Task +# Stage 3: Track task - for training trajectory tracking +# Starts from saved grasp states, only trains trajectory tracking + +# ------------------------------------------------------------------------------- +# Environment +# ------------------------------------------------------------------------------- +sim: "isaacgym" +robots: ["franka"] +task: "pick_place.track_hu" +decimation: 4 +train_or_eval: "train" +headless: True + +# State file path for track task (pkl file path for grasp states) +# If null, uses default path or env var PICK_PLACE_TRACK_STATE_FILE +state_file_path: "/usr1/home/s125mdg56_03/RoboVerse/eval_states/pick_place.approach_grasp_hu_franka_lift_states_122states_20251214_150655.pkl" + +# ------------------------------------------------------------------------------- +# Seeds & Device +# ------------------------------------------------------------------------------- +seed: 1 +cuda: true +torch_deterministic: true +device_rank: 0 + +# ------------------------------------------------------------------------------- +# Rollout & Timesteps +# ------------------------------------------------------------------------------- +num_envs: 400 +num_eval_envs: 400 +total_timesteps: 2000000 +learning_starts: 10 +num_steps: 1 + +# ------------------------------------------------------------------------------- +# Replay, Batching, Discounting +# ------------------------------------------------------------------------------- +buffer_size: 20480 +batch_size: 32768 +gamma: 0.99 +tau: 0.1 + +# ------------------------------------------------------------------------------- +# Update Schedule +# ------------------------------------------------------------------------------- +policy_frequency: 2 +num_updates: 5 + +# ------------------------------------------------------------------------------- +# Optimizer & Network +# ------------------------------------------------------------------------------- +critic_learning_rate: 0.0003 +actor_learning_rate: 0.0003 +weight_decay: 0.1 +critic_hidden_dim: 512 +actor_hidden_dim: 256 +init_scale: 0.01 +num_atoms: 101 + +# ------------------------------------------------------------------------------- +# Value Distribution & Exploration +# ------------------------------------------------------------------------------- +v_min: 0 +v_max: 2400.0 +policy_noise: 0.001 +std_min: 0.001 +std_max: 0.4 +noise_clip: 0.5 + +# ------------------------------------------------------------------------------- +# Algorithm Flags +# ------------------------------------------------------------------------------- +use_cdq: true +compile: true +obs_normalization: true +max_grad_norm: 0.0 +amp: true +amp_dtype: "fp16" +disable_bootstrap: false +measure_burnin: 3 + +# ------------------------------------------------------------------------------- +# Logging & Checkpointing +# ------------------------------------------------------------------------------- +wandb_project: "pick_place_track_hu" +exp_name: "pick_place_track_hu" +use_wandb: false +checkpoint_path: null +eval_interval: 5000 +save_interval: 15000 +video_width: 1024 +video_height: 1024 +model_dir: "models_track_teapot" # Directory to save checkpoints (default: "models") diff --git a/roboverse_pack/tasks/pick_place/approach_grasp_ceramic_teapot.py b/roboverse_pack/tasks/pick_place/approach_grasp_ceramic_teapot.py new file mode 100644 index 000000000..6de9fbe09 --- /dev/null +++ b/roboverse_pack/tasks/pick_place/approach_grasp_ceramic_teapot.py @@ -0,0 +1,514 @@ +"""Stage 1: Simple Approach and Grasp task with gripper control. + +This task focuses on learning to approach the object, grasp it with gripper, and lift it. +Simple gripper control: close when near the object. +""" + +from __future__ import annotations + +from copy import deepcopy + +import torch +from loguru import logger as log + +from metasim.constants import PhysicStateType +from metasim.scenario.objects import RigidObjCfg +from metasim.scenario.scenario import ScenarioCfg, SimParamCfg +from metasim.task.registry import register_task +from roboverse_pack.tasks.pick_place.base import DEFAULT_CONFIG, PickPlaceBase + +from .functions import * + + +@register_task("pick_place.approach_grasp_hu", "pick_place_approach_grasp_hu") +class PickPlaceApproachGraspHu(PickPlaceBase): + """Simple Approach and Grasp task with gripper control. + + This task focuses on: + - Approaching the object + - Grasping the object with simple gripper control (close when near) + + Success condition: Object is grasped (reward given when entering grasp state). + Episode terminates if object is released. + """ + + GRASP_DISTANCE_THRESHOLD = 0.02 # Distance threshold for both grasp check and gripper closing + GRASP_HISTORY_WINDOW = 5 # Number of frames to check for stable grasp + + # Joint2 lift parameters (for franka: panda_joint2) + JOINT2_LIFT_OFFSET = 0.5 # Amount to lift joint2 when grasped (positive = lift up) + JOINT2_LIFT_KP = 0.2 # Proportional gain for joint2 lift control + JOINT2_LIFT_MAX_DELTA = 0.3 # Maximum change per step + + DEFAULT_CONFIG_SIMPLE = deepcopy(DEFAULT_CONFIG) + DEFAULT_CONFIG_SIMPLE["reward_config"]["scales"].update({ + "gripper_approach": 0.5, + "grasp_reward": 4.0, + "gripper_orientation": 0.5, + }) + DEFAULT_CONFIG_SIMPLE["grasp_config"] = { + "grasp_check_distance": GRASP_DISTANCE_THRESHOLD, + "gripper_close_distance": GRASP_DISTANCE_THRESHOLD, + } + + scenario = ScenarioCfg( + objects=[ + # path https://huggingface.co/datasets/HorizonRobotics/EmbodiedGenData/tree/main/example_layouts/task_0001/asset3d + RigidObjCfg( + name="table", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/EmbodiedGenData/all_asset/table/usd/table.usd", + urdf_path="roboverse_data/EmbodiedGenData/all_asset/table/table.urdf", + mjcf_path="roboverse_data/EmbodiedGenData/all_asset/table/mjcf/table.xml", + fix_base_link=True, + ), + # path https://huggingface.co/datasets/HorizonRobotics/EmbodiedGenData/tree/main/example_layouts/task_0001/asset3d + RigidObjCfg( + name="bowl", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/EmbodiedGenData/all_asset/bowl/usd/bowl.usd", + urdf_path="roboverse_data/EmbodiedGenData/all_asset/bowl/bowl.urdf", + mjcf_path="roboverse_data/EmbodiedGenData/all_asset/bowl/mjcf/bowl.xml", + ), + # https://huggingface.co/datasets/HorizonRobotics/EmbodiedGenData/tree/main/example_layouts/task_0002/asset3d + RigidObjCfg( + name="object", + scale=(1, 1, 1), + # enabled_gravity=False, + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/EmbodiedGenData/all_asset/ceramic_teapot/usd/ceramic_teapot.usd", + urdf_path="roboverse_data/EmbodiedGenData/all_asset/ceramic_teapot/ceramic_teapot.urdf", + mjcf_path="roboverse_data/EmbodiedGenData/all_asset/ceramic_teapot/ceramic_teapot.xml", + ), + RigidObjCfg( + name="plate", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/EmbodiedGenData/all_asset/plate/usd/plate.usd", + urdf_path="roboverse_data/EmbodiedGenData/all_asset/plate/plate.urdf", + mjcf_path="roboverse_data/EmbodiedGenData/all_asset/plate/mjcf/plate.xml", + ), + # RigidObjCfg( + # name="object0", + # urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + # mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + # usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + # scale=0.2, + # physics=PhysicStateType.XFORM, + # enabled_gravity=False, + # collision_enabled=False, + # fix_base_link=True, + # ), + RigidObjCfg( + name="traj_marker_0", + urdf_path="roboverse_pack/tasks/pick_place/marker/axis_marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/axis_marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/axis_marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_1", + urdf_path="roboverse_pack/tasks/pick_place/marker/axis_marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/axis_marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/axis_marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_2", + urdf_path="roboverse_pack/tasks/pick_place/marker/axis_marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/axis_marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/axis_marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_3", + urdf_path="roboverse_pack/tasks/pick_place/marker/axis_marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/axis_marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/axis_marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_4", + urdf_path="roboverse_pack/tasks/pick_place/marker/axis_marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/axis_marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/axis_marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + ], + robots=["franka"], + sim_params=SimParamCfg( + dt=0.005, + ), + decimation=4, + ) + max_episode_steps = 200 + + def __init__(self, scenario, device=None): + # Placeholders needed during super().__init__ (reset may be called there) + self.object_grasped = None + self.gripper_joint_indices = [0, 1] # panda_finger_joint1, panda_finger_joint2 + self._grasp_notified = None + self._distance_history = None # Historyobufferrfor etabl p che ckfcknt2" + self.joint2_name = "panda_joint2" + self.joint2_index = None + self.initial_joint_pos = None + self.local_offset = torch.tensor([-0.00233746, -0.10298071, 0.03644049]) + + super().__init__(scenario, device) + + # Override reward functions for this task + self.reward_functions = [ + self._reward_gripper_approach, + self._reward_grasp, + self._reward_gripper_orientation, + ] + self.reward_weights = [ + self.DEFAULT_CONFIG_SIMPLE["reward_config"]["scales"]["gripper_approach"], + self.DEFAULT_CONFIG_SIMPLE["reward_config"]["scales"]["grasp_reward"], + self.DEFAULT_CONFIG_SIMPLE["reward_config"]["scales"]["gripper_orientation"], + ] + + # Get config values + grasp_config = self.DEFAULT_CONFIG_SIMPLE["grasp_config"] + self.grasp_check_distance = grasp_config["grasp_check_distance"] + self.gripper_close_distance = grasp_config["gripper_close_distance"] + + # Initialize tracking buffers + self.object_grasped = torch.zeros(self.num_envs, dtype=torch.bool, device=self.device) + self._grasp_notified = torch.zeros(self.num_envs, dtype=torch.bool, device=self.device) + self._distance_history = torch.full( + (self.num_envs, self.GRASP_HISTORY_WINDOW), + float("inf"), + device=self.device, + ) + + # Find joint2 index + joint_names = self.handler.get_joint_names(self.robot_name, sort=True) + if self.joint2_name in joint_names: + self.joint2_index = joint_names.index(self.joint2_name) + else: + log.warning(f"Joint {self.joint2_name} not found, joint2 lift disabled") + + def get_geometric_center(self, current_states): + """Calculate the geometric center of the object in world coordinates.""" + # w, x, y, z + + root_pos = current_states.objects["object"].root_state[:, 0:3] + root_rot = current_states.objects["object"].root_state[:, 3:7] + local_offset = self.local_offset.to(self.device) + + w, x, y, z = root_rot[:, 0], root_rot[:, 1], root_rot[:, 2], root_rot[:, 3] + + v = local_offset.unsqueeze(0).expand(root_pos.shape[0], -1) + + q_vec = torch.stack([x, y, z], dim=1) # [N, 3] + + # cross(q_xyz, v) + t = torch.cross(q_vec, v, dim=1) + + # cross(q_xyz, t) + w * t + + final_vec = v + 2.0 * torch.cross(q_vec, t, dim=1) + 2.0 * w.unsqueeze(1) * t + + center_pos = root_pos + final_vec + + return center_pos + + def reset(self, env_ids=None): + """Reset environment and tracking variables.""" + obs, info = super().reset(env_ids=env_ids) + + if env_ids is None: + env_ids_tensor = torch.arange(self.num_envs, device=self.device) + else: + env_ids_tensor = ( + torch.tensor(env_ids, device=self.device) if not isinstance(env_ids, torch.Tensor) else env_ids + ) + + # Reset grasp tracking + self.object_grasped[env_ids_tensor] = False + if self._grasp_notified is None or self._grasp_notified.shape[0] != self.num_envs: + self._grasp_notified = torch.zeros(self.num_envs, dtype=torch.bool, device=self.device) + self._distance_history = torch.full( + (self.num_envs, self.GRASP_HISTORY_WINDOW), + float("inf"), + device=self.device, + ) + else: + self._grasp_notified[env_ids_tensor] = False + self._distance_history[env_ids_tensor] = float("inf") + + # Store initial joint positions if not already stored + if self.initial_joint_pos is None: + states = self.handler.get_states(mode="tensor") + self.initial_joint_pos = states.robots[self.robot_name].joint_pos.clone() + + return obs, info + + def step(self, actions): + """Step with delta control and simple gripper control.""" + current_states = self.handler.get_states(mode="tensor") + # box_pos = current_states.objects["object"].root_state[:, 0:3] + box_pos = self.get_geometric_center(current_states) + gripper_pos, _ = self._get_ee_state(current_states) + gripper_box_dist = torch.norm(gripper_pos - box_pos, dim=-1) + + # Apply delta control + delta_actions = actions * self._action_scale + new_actions = self._last_action + delta_actions + real_actions = torch.clamp(new_actions, self._action_low, self._action_high) + + # Simple gripper control: close when near object + real_actions = self._apply_simple_gripper_control(real_actions, gripper_box_dist) + + # Apply joint2 lift control if grasped + if self.object_grasped is not None and self.object_grasped.any() and self.joint2_index is not None: + real_actions = self._apply_joint2_lift_control(real_actions, current_states) + + # Bypass PickPlaceBase.step to avoid its gripper control logic + # Call RLTaskEnv.step directly + # Note: reward functions will be called inside super().step() + # and they will compute newly_grasped by comparing current state with self.object_grasped + obs, reward, terminated, time_out, info = super(PickPlaceBase, self).step(real_actions) + self._last_action = real_actions + + # Update grasp state after step (for next step's comparison) + updated_states = self.handler.get_states(mode="tensor") + old_grasped = self.object_grasped.clone() + self.object_grasped = self._compute_grasp_state(updated_states) + + newly_grasped = self.object_grasped & (~old_grasped) + newly_released = (~self.object_grasped) & old_grasped + + if newly_grasped.any() and newly_grasped[0]: + log.info(f"[Env 0] Object grasped! Distance: {gripper_box_dist[0].item():.4f}m") + self._grasp_notified[newly_grasped] = True + + if newly_released.any() and newly_released[0]: + log.info(f"[Env 0] Object released! Distance: {gripper_box_dist[0].item():.4f}m") + self._grasp_notified[newly_released] = False + + # Terminate episode if object is released + terminated = terminated | newly_released + + # Track lift state: check if joint2 has been lifted significantly + lift_active = torch.zeros(self.num_envs, dtype=torch.bool, device=self.device) + if self.joint2_index is not None and self.initial_joint_pos is not None: + current_joint2 = updated_states.robots[self.robot_name].joint_pos[:, self.joint2_index] + initial_joint2 = self.initial_joint_pos[:, self.joint2_index] + # Lift is active if joint2 has moved up significantly (more than 0.1 radians) + lift_active = (current_joint2 - initial_joint2) > 0.1 + + info["grasp_success"] = self.object_grasped + info["lift_active"] = lift_active + info["stage"] = torch.full((self.num_envs,), 1, dtype=torch.long, device=self.device) + + return obs, reward, terminated, time_out, info + + def _apply_simple_gripper_control(self, actions, gripper_box_dist): + """Simple gripper control: close when near object.""" + # Close gripper when close to object + gripper_close = gripper_box_dist < self.gripper_close_distance + gripper_value_close = torch.tensor(0.0, device=self.device, dtype=actions.dtype) # Closed + gripper_value_open = torch.tensor(0.04, device=self.device, dtype=actions.dtype) # Open + + # Set gripper joints + for gripper_idx in self.gripper_joint_indices: + actions[:, gripper_idx] = torch.where( + gripper_close, + gripper_value_close, + gripper_value_open, + ) + + return actions + + def _apply_joint2_lift_control(self, actions, current_states): + """Apply joint2 lift control when object is grasped.""" + if self.initial_joint_pos is None: + self.initial_joint_pos = current_states.robots[self.robot_name].joint_pos.clone() + + joint_pos = current_states.robots[self.robot_name].joint_pos + joint2_idx = self.joint2_index + + # Target position: initial position + lift offset (positive offset lifts up) + target_lift = self.initial_joint_pos[:, joint2_idx] + self.JOINT2_LIFT_OFFSET + joint_error = target_lift - joint_pos[:, joint2_idx] + + # Apply proportional control with max delta limit + desired = joint_pos[:, joint2_idx] + self.JOINT2_LIFT_KP * joint_error + delta = torch.clamp( + desired - joint_pos[:, joint2_idx], + -self.JOINT2_LIFT_MAX_DELTA, + self.JOINT2_LIFT_MAX_DELTA, + ) + joint2_value = torch.clamp( + joint_pos[:, joint2_idx] + delta, + self._action_low[joint2_idx], + self._action_high[joint2_idx], + ) + + # Apply lift control only to environments where object is grasped + actions[self.object_grasped, joint2_idx] = joint2_value[self.object_grasped] + + return actions + + def _compute_grasp_state(self, states): + """Compute if object is grasped (requires 5 stable frames based on distance only).""" + # box_pos = states.objects["object"].root_state[:, 0:3] + box_pos = self.get_geometric_center(states) + + gripper_pos, _ = self._get_ee_state(states) + gripper_box_dist = torch.norm(gripper_pos - box_pos, dim=-1) + + # Update rolling distance history + if self._distance_history is None or self._distance_history.shape[0] != self.num_envs: + self._distance_history = torch.full( + (self.num_envs, self.GRASP_HISTORY_WINDOW), + float("inf"), + device=self.device, + ) + self._distance_history = torch.roll(self._distance_history, shifts=-1, dims=1) + self._distance_history[:, -1] = gripper_box_dist + + # Object is grasped if distance has been stable (close) for 5 frames + stable_grasp = (self._distance_history < self.grasp_check_distance).all(dim=1) + is_grasping = stable_grasp + + return is_grasping + + def _reward_gripper_approach(self, env_states) -> torch.Tensor: + """Reward for gripper approaching the box.""" + # box_pos = env_states.objects["object"].root_state[:, 0:3] + box_pos = self.get_geometric_center(env_states) + + gripper_pos, _ = self._get_ee_state(env_states) + gripper_box_dist = torch.norm(box_pos - gripper_pos, dim=-1) + + approach_reward_far = 1 - torch.tanh(gripper_box_dist) + approach_reward_near = 1 - torch.tanh(gripper_box_dist * 10) + return approach_reward_far + approach_reward_near + + def _reward_grasp(self, env_states) -> torch.Tensor: + """Reward for maintaining grasp state (continuous reward while grasped).""" + # Use cached grasp state (computed in step method) + return self.object_grasped.float() + + def _reward_gripper_orientation(self, env_states) -> torch.Tensor: + """Calculate gripper orientation reward.""" + _, gripper_quat = self._get_ee_state(env_states) + box_quat = env_states.objects["object"].root_state[:, 3:7] + + w, x, y, z = gripper_quat[:, 0], gripper_quat[:, 1], gripper_quat[:, 2], gripper_quat[:, 3] + + bw, bx, by, bz = box_quat[:, 0], box_quat[:, 1], box_quat[:, 2], box_quat[:, 3] + + gripper_z_axis_z_component = 1.0 - 2.0 * (torch.square(x) + torch.square(y)) + + reward_z_down = (-gripper_z_axis_z_component + 1.0) / 2.0 + + reward_z_down = torch.square(reward_z_down) + + box_x_axis = torch.stack([1 - 2 * (by**2 + bz**2), 2 * (bx * by + bw * bz), 2 * (bx * bz - bw * by)], dim=-1) + + # gripper_axis_to_align = torch.stack([ + # 1 - 2 * (y**2 + z**2), + # 2 * (x*y + w*z), + # 2 * (x*z - w*y) + # ], dim=-1) + + gripper_axis_to_align = torch.stack([2 * (x * y - w * z), 1 - 2 * (x**2 + z**2), 2 * (y * z + w * x)], dim=-1) + + dot_prod = torch.sum(gripper_axis_to_align * box_x_axis, dim=-1) + + reward_align = torch.abs(dot_prod) + + total_reward = reward_z_down * reward_align + + return total_reward + + def _get_initial_states(self) -> list[dict] | None: + """Get initial states for all environments.""" + init = [ + { + "objects": { + "table": { + "pos": torch.tensor([-0.000000, 0.000000, 0.376990]), + "rot": torch.tensor([1.000000, -0.000000, 0.000000, 0.000000]), + }, + "bowl": { + "pos": torch.tensor([-0.491991, 0.194712, 0.828524]), + "rot": torch.tensor([-0.774328, -0.006966, 0.006029, 0.632717]), + }, + "object": { + "pos": torch.tensor([-0.000850, -0.357659, 0.873023]), + "rot": torch.tensor([-0.835106, -0.002912, -0.008612, 0.550015]), + }, + "plate": { + "pos": torch.tensor([0.000060, 0.000040, 0.774218]), + "rot": torch.tensor([-0.980610, -0.002716, -0.002327, 0.195939]), + }, + "traj_marker_0": { + "pos": torch.tensor([-0.025781, -0.526361, 0.873023]), + "rot": torch.tensor([-0.835106, -0.002912, -0.008612, 0.550015]), + }, + "traj_marker_1": { + "pos": torch.tensor([-0.045492, -0.285306, 0.941898]), + "rot": torch.tensor([-0.317816, -0.002321, 0.001691, 0.948148]), + }, + "traj_marker_2": { + "pos": torch.tensor([-0.030328, -0.190204, 0.992140]), + "rot": torch.tensor([-0.489972, -0.004560, 0.003323, 0.871720]), + }, + "traj_marker_3": { + "pos": torch.tensor([-0.015164, -0.095102, 0.942381]), + "rot": torch.tensor([-0.644740, -0.006638, 0.004836, 0.764358]), + }, + "traj_marker_4": { + "pos": torch.tensor([0.000000, 0.000000, 0.792622]), + "rot": torch.tensor([-0.776629, -0.008479, 0.006178, 0.629871]), + }, + }, + "robots": { + "franka": { + "pos": torch.tensor([-0.6733999252319336, 2.3283064365386963e-10, 0.7760999798774719]), + "rot": torch.tensor([-1.0, 1.489094958451176e-10, 8.78133399329073e-10, 8.47253794900027e-11]), + "dof_pos": { + "panda_joint1": 0.0, + "panda_joint2": -0.785398, + "panda_joint3": 0.0, + "panda_joint4": -2.356194, + "panda_joint5": 0.0, + "panda_joint6": 1.570796, + "panda_joint7": 0.785398, + "panda_finger_joint1": 0.04, + "panda_finger_joint2": 0.04, + }, + }, + }, + } + for _ in range(self.num_envs) + ] + + return init diff --git a/roboverse_pack/tasks/pick_place/approach_grasp_knife.py b/roboverse_pack/tasks/pick_place/approach_grasp_knife.py new file mode 100644 index 000000000..6b4283e06 --- /dev/null +++ b/roboverse_pack/tasks/pick_place/approach_grasp_knife.py @@ -0,0 +1,613 @@ +"""Stage 1: Simple Approach and Grasp task with gripper control. + +This task focuses on learning to approach the object, grasp it with gripper, and lift it. +Simple gripper control: close when near the object. +""" + +from __future__ import annotations + +from copy import deepcopy + +import torch +from loguru import logger as log + +from metasim.constants import PhysicStateType +from metasim.scenario.objects import RigidObjCfg +from metasim.scenario.scenario import ScenarioCfg, SimParamCfg +from metasim.task.registry import register_task +from roboverse_pack.tasks.pick_place.base import DEFAULT_CONFIG, PickPlaceBase + +from .functions import * + + +@register_task("pick_place.approach_grasp_knife", "pick_place_approach_grasp_knife") +class PickPlaceApproachGraspKnife(PickPlaceBase): + """Simple Approach and Grasp task with gripper control. + + This task focuses on: + - Approaching the object + - Grasping the object with simple gripper control (close when near) + + Success condition: Object is grasped (reward given when entering grasp state). + Episode terminates if object is released. + """ + + GRASP_DISTANCE_THRESHOLD = 0.02 # Distance threshold for both grasp check and gripper closing + GRASP_HISTORY_WINDOW = 5 # Number of frames to check for stable grasp + + # Joint2 lift parameters (for franka: panda_joint2) + JOINT2_LIFT_OFFSET = 0.5 # Amount to lift joint2 when grasped (positive = lift up) + JOINT2_LIFT_KP = 0.2 # Proportional gain for joint2 lift control + JOINT2_LIFT_MAX_DELTA = 0.3 # Maximum change per step + + DEFAULT_CONFIG_SIMPLE = deepcopy(DEFAULT_CONFIG) + DEFAULT_CONFIG_SIMPLE["reward_config"]["scales"].update({ + "gripper_approach": 0.5, + "grasp_reward": 4.0, + "gripper_orientation": 0.5, + }) + DEFAULT_CONFIG_SIMPLE["grasp_config"] = { + "grasp_check_distance": GRASP_DISTANCE_THRESHOLD, + "gripper_close_distance": GRASP_DISTANCE_THRESHOLD, + } + + scenario = ScenarioCfg( + objects=[ + # path https://huggingface.co/datasets/HorizonRobotics/EmbodiedGenData/tree/main/example_layouts/task_0001/asset3d + RigidObjCfg( + name="table", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/EmbodiedGenData/all_asset/table/usd/table.usd", + urdf_path="roboverse_data/EmbodiedGenData/all_asset/table/table.urdf", + mjcf_path="roboverse_data/EmbodiedGenData/all_asset/table/mjcf/table.xml", + fix_base_link=True, + ), + # path https://huggingface.co/datasets/HorizonRobotics/EmbodiedGenData/tree/main/example_layouts/task_0001/asset3d + RigidObjCfg( + name="bowl", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/EmbodiedGenData/all_asset/bowl/usd/bowl.usd", + urdf_path="roboverse_data/EmbodiedGenData/all_asset/bowl/bowl.urdf", + mjcf_path="roboverse_data/EmbodiedGenData/all_asset/bowl/mjcf/bowl.xml", + ), + # path https://huggingface.co/datasets/HorizonRobotics/EmbodiedGenData/tree/main/example_layouts/task_0001/asset3d + RigidObjCfg( + name="object", + scale=(1, 1, 1), + # enabled_gravity=False, + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/EmbodiedGenData/all_asset/knife/usd/knife.usd", + urdf_path="roboverse_data/EmbodiedGenData/all_asset/knife/knife.urdf", + mjcf_path="roboverse_data/EmbodiedGenData/all_asset/knife/knife.xml", + ), + # path https://huggingface.co/datasets/HorizonRobotics/EmbodiedGenData/tree/main/example_layouts/task_0001/asset3d + RigidObjCfg( + name="plate", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/EmbodiedGenData/all_asset/plate/usd/plate.usd", + urdf_path="roboverse_data/EmbodiedGenData/all_asset/plate/plate.urdf", + mjcf_path="roboverse_data/EmbodiedGenData/all_asset/plate/mjcf/plate.xml", + ), + # RigidObjCfg( + # name="knife", + # scale=(1, 1, 1), + # physics=PhysicStateType.RIGIDBODY, + # usd_path="/home/dyz/RoboVerse/roboverse_data/assets/EmbodiedGenData/example_layouts/task_0001/asset3d/knife/usd/knife.usd", + # urdf_path="/home/dyz/RoboVerse/roboverse_data/assets/EmbodiedGenData/example_layouts/task_0001/asset3d/knife/result/knife.urdf", + # mjcf_path="/home/dyz/RoboVerse/roboverse_data/assets/EmbodiedGenData/example_layouts/task_0001/asset3d/knife/mjcf/knife.xml", + # ), + RigidObjCfg( + name="object0", + urdf_path="roboverse_pack/tasks/pick_place/marker/axis_marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/axis_marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/axis_marker.usd", + scale=1.0, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + # fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_0", + urdf_path="roboverse_pack/tasks/pick_place/marker/axis_marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/axis_marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/axis_marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_1", + urdf_path="roboverse_pack/tasks/pick_place/marker/axis_marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/axis_marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/axis_marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_2", + urdf_path="roboverse_pack/tasks/pick_place/marker/axis_marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/axis_marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/axis_marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_3", + urdf_path="roboverse_pack/tasks/pick_place/marker/axis_marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/axis_marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/axis_marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_4", + urdf_path="roboverse_pack/tasks/pick_place/marker/axis_marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/axis_marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/axis_marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + ], + robots=["franka"], + sim_params=SimParamCfg( + dt=0.005, + ), + decimation=4, + ) + max_episode_steps = 200 + + def __init__(self, scenario, device=None): + # Placeholders needed during super().__init__ (reset may be called there) + self.object_grasped = None + self.gripper_joint_indices = [0, 1] # panda_finger_joint1, panda_finger_joint2 + self._grasp_notified = None + self._distance_history = None # Historyobufferrfor etabl p che ckfcknt2" + self.joint2_name = "panda_joint2" + self.joint2_index = None + self.initial_joint_pos = None + # self.local_offset = torch.tensor([-0.1091152, -0.02266935, -0.00410238]) + # self.local_offset = torch.tensor([0,0,0]) + # self.object_pos = torch.tensor([0.201373, -0.330642, 0.779824]).unsqueeze(0) + # self.object_rot = torch.tensor([-0.398238, 0.035423, -0.027580, -0.916183]).unsqueeze(0) + # self.target_pos = torch.tensor([0.118386, -0.429724, 0.780205]).unsqueeze(0) + self.local_offset = torch.tensor([-0.01538026, 0.1282216, -0.00245847]).to(device) + + super().__init__(scenario, device) + + # Override reward functions for this task + self.reward_functions = [ + self._reward_gripper_approach, + self._reward_grasp, + self._reward_gripper_orientation, + ] + self.reward_weights = [ + self.DEFAULT_CONFIG_SIMPLE["reward_config"]["scales"]["gripper_approach"], + self.DEFAULT_CONFIG_SIMPLE["reward_config"]["scales"]["grasp_reward"], + self.DEFAULT_CONFIG_SIMPLE["reward_config"]["scales"]["gripper_orientation"], + ] + + # Get config values + grasp_config = self.DEFAULT_CONFIG_SIMPLE["grasp_config"] + self.grasp_check_distance = grasp_config["grasp_check_distance"] + self.gripper_close_distance = grasp_config["gripper_close_distance"] + + # Initialize tracking buffers + self.object_grasped = torch.zeros(self.num_envs, dtype=torch.bool, device=self.device) + self._grasp_notified = torch.zeros(self.num_envs, dtype=torch.bool, device=self.device) + self._distance_history = torch.full( + (self.num_envs, self.GRASP_HISTORY_WINDOW), + float("inf"), + device=self.device, + ) + + # Find joint2 index + joint_names = self.handler.get_joint_names(self.robot_name, sort=True) + if self.joint2_name in joint_names: + self.joint2_index = joint_names.index(self.joint2_name) + else: + log.warning(f"Joint {self.joint2_name} not found, joint2 lift disabled") + + def reset(self, env_ids=None): + """Reset environment and tracking variables.""" + obs, info = super().reset(env_ids=env_ids) + + if env_ids is None: + env_ids_tensor = torch.arange(self.num_envs, device=self.device) + else: + env_ids_tensor = ( + torch.tensor(env_ids, device=self.device) if not isinstance(env_ids, torch.Tensor) else env_ids + ) + + # Reset grasp tracking + self.object_grasped[env_ids_tensor] = False + if self._grasp_notified is None or self._grasp_notified.shape[0] != self.num_envs: + self._grasp_notified = torch.zeros(self.num_envs, dtype=torch.bool, device=self.device) + self._distance_history = torch.full( + (self.num_envs, self.GRASP_HISTORY_WINDOW), + float("inf"), + device=self.device, + ) + else: + self._grasp_notified[env_ids_tensor] = False + self._distance_history[env_ids_tensor] = float("inf") + + # Store initial joint positions if not already stored + if self.initial_joint_pos is None: + states = self.handler.get_states(mode="tensor") + self.initial_joint_pos = states.robots[self.robot_name].joint_pos.clone() + + return obs, info + + def step(self, actions): + """Step with delta control and simple gripper control.""" + current_states = self.handler.get_states(mode="tensor") + # box_pos = current_states.objects["object"].root_state[:, 0:3] + # current_rot = current_states.objects["object"].root_state[:, 3:7].clone() + box_pos = self.get_geometric_center(current_states) + gripper_pos, _ = self._get_ee_state(current_states) + gripper_box_dist = torch.norm(gripper_pos - box_pos, dim=-1) + + # Apply delta control + delta_actions = actions * self._action_scale + new_actions = self._last_action + delta_actions + real_actions = torch.clamp(new_actions, self._action_low, self._action_high) + + # Simple gripper control: close when near object + real_actions = self._apply_simple_gripper_control(real_actions, gripper_box_dist) + + # Apply joint2 lift control if grasped + if self.object_grasped is not None and self.object_grasped.any() and self.joint2_index is not None: + real_actions = self._apply_joint2_lift_control(real_actions, current_states) + + # Bypass PickPlaceBase.step to avoid its gripper control logic + # Call RLTaskEnv.step directly + # Note: reward functions will be called inside super().step() + # and they will compute newly_grasped by comparing current state with self.object_grasped + obs, reward, terminated, time_out, info = super(PickPlaceBase, self).step(real_actions) + self._last_action = real_actions + + # Update grasp state after step (for next step's comparison) + updated_states = self.handler.get_states(mode="tensor") + old_grasped = self.object_grasped.clone() + self.object_grasped = self._compute_grasp_state(updated_states) + + newly_grasped = self.object_grasped & (~old_grasped) + newly_released = (~self.object_grasped) & old_grasped + + if newly_grasped.any() and newly_grasped[0]: + log.info(f"[Env 0] Object grasped! Distance: {gripper_box_dist[0].item():.4f}m") + self._grasp_notified[newly_grasped] = True + + if newly_released.any() and newly_released[0]: + log.info(f"[Env 0] Object released! Distance: {gripper_box_dist[0].item():.4f}m") + self._grasp_notified[newly_released] = False + + # Terminate episode if object is released + terminated = terminated | newly_released + + # Track lift state: check if joint2 has been lifted significantly + lift_active = torch.zeros(self.num_envs, dtype=torch.bool, device=self.device) + if self.joint2_index is not None and self.initial_joint_pos is not None: + current_joint2 = updated_states.robots[self.robot_name].joint_pos[:, self.joint2_index] + initial_joint2 = self.initial_joint_pos[:, self.joint2_index] + # Lift is active if joint2 has moved up significantly (more than 0.1 radians) + lift_active = (current_joint2 - initial_joint2) > 0.1 + + info["grasp_success"] = self.object_grasped + info["lift_active"] = lift_active + info["stage"] = torch.full((self.num_envs,), 1, dtype=torch.long, device=self.device) + + return obs, reward, terminated, time_out, info + + def _apply_simple_gripper_control(self, actions, gripper_box_dist): + """Simple gripper control: close when near object.""" + # Close gripper when close to object + gripper_close = gripper_box_dist < self.gripper_close_distance + gripper_value_close = torch.tensor(0.0, device=self.device, dtype=actions.dtype) # Closed + gripper_value_open = torch.tensor(0.04, device=self.device, dtype=actions.dtype) # Open + + # Set gripper joints + for gripper_idx in self.gripper_joint_indices: + actions[:, gripper_idx] = torch.where( + gripper_close, + gripper_value_close, + gripper_value_open, + ) + + return actions + + def _apply_joint2_lift_control(self, actions, current_states): + """Apply joint2 lift control when object is grasped.""" + if self.initial_joint_pos is None: + self.initial_joint_pos = current_states.robots[self.robot_name].joint_pos.clone() + + joint_pos = current_states.robots[self.robot_name].joint_pos + joint2_idx = self.joint2_index + + # Target position: initial position + lift offset (positive offset lifts up) + target_lift = self.initial_joint_pos[:, joint2_idx] + self.JOINT2_LIFT_OFFSET + joint_error = target_lift - joint_pos[:, joint2_idx] + + # Apply proportional control with max delta limit + desired = joint_pos[:, joint2_idx] + self.JOINT2_LIFT_KP * joint_error + delta = torch.clamp( + desired - joint_pos[:, joint2_idx], + -self.JOINT2_LIFT_MAX_DELTA, + self.JOINT2_LIFT_MAX_DELTA, + ) + joint2_value = torch.clamp( + joint_pos[:, joint2_idx] + delta, + self._action_low[joint2_idx], + self._action_high[joint2_idx], + ) + + # Apply lift control only to environments where object is grasped + actions[self.object_grasped, joint2_idx] = joint2_value[self.object_grasped] + + return actions + + def _compute_grasp_state(self, states): + """Compute if object is grasped (requires 5 stable frames based on distance only).""" + # box_pos = states.objects["object"].root_state[:, 0:3] + box_pos = self.get_geometric_center(states) + + gripper_pos, _ = self._get_ee_state(states) + gripper_box_dist = torch.norm(gripper_pos - box_pos, dim=-1) + + # Update rolling distance history + if self._distance_history is None or self._distance_history.shape[0] != self.num_envs: + self._distance_history = torch.full( + (self.num_envs, self.GRASP_HISTORY_WINDOW), + float("inf"), + device=self.device, + ) + self._distance_history = torch.roll(self._distance_history, shifts=-1, dims=1) + self._distance_history[:, -1] = gripper_box_dist + + # Object is grasped if distance has been stable (close) for 5 frames + stable_grasp = (self._distance_history < self.grasp_check_distance).all(dim=1) + is_grasping = stable_grasp + + return is_grasping + + def _reward_gripper_approach(self, env_states) -> torch.Tensor: + """Reward for gripper approaching the box.""" + # box_pos = env_states.objects["object"].root_state[:, 0:3] + box_pos = self.get_geometric_center(env_states) + + gripper_pos, _ = self._get_ee_state(env_states) + gripper_box_dist = torch.norm(box_pos - gripper_pos, dim=-1) + + approach_reward_far = 1 - torch.tanh(gripper_box_dist) + approach_reward_near = 1 - torch.tanh(gripper_box_dist * 10) + return approach_reward_far + approach_reward_near + + def _reward_grasp(self, env_states) -> torch.Tensor: + """Reward for maintaining grasp state (continuous reward while grasped).""" + # Use cached grasp state (computed in step method) + return self.object_grasped.float() + + def _reward_gripper_orientation(self, env_states) -> torch.Tensor: + """Reward for maintaining gripper orientation.""" + _, gripper_quat = self._get_ee_state(env_states) + box_quat = env_states.objects["object"].root_state[:, 3:7] + + w, x, y, z = gripper_quat[:, 0], gripper_quat[:, 1], gripper_quat[:, 2], gripper_quat[:, 3] + + bw, bx, by, bz = box_quat[:, 0], box_quat[:, 1], box_quat[:, 2], box_quat[:, 3] + + gripper_z_axis_z_component = 1.0 - 2.0 * (torch.square(x) + torch.square(y)) + + reward_z_down = (-gripper_z_axis_z_component + 1.0) / 2.0 + + reward_z_down = torch.square(reward_z_down) + + box_x_axis = torch.stack([1 - 2 * (by**2 + bz**2), 2 * (bx * by + bw * bz), 2 * (bx * bz - bw * by)], dim=-1) + + # gripper_axis_to_align = torch.stack([ + # 1 - 2 * (y**2 + z**2), + # 2 * (x*y + w*z), + # 2 * (x*z - w*y) + # ], dim=-1) + + gripper_axis_to_align = torch.stack([2 * (x * y - w * z), 1 - 2 * (x**2 + z**2), 2 * (y * z + w * x)], dim=-1) + + dot_prod = torch.sum(gripper_axis_to_align * box_x_axis, dim=-1) + + reward_align = torch.abs(dot_prod) + + total_reward = reward_z_down * reward_align + + return total_reward + + def calculate_local_offset(self): + """Calculate local offset from object to target position.""" + diff_world = self.target_pos - self.object_pos + + w, x, y, z = self.object_rot[:, 0], self.object_rot[:, 1], self.object_rot[:, 2], self.object_rot[:, 3] + + q_vec_inv = torch.stack([-x, -y, -z], dim=1) + w = w.unsqueeze(1) + + t = 2.0 * torch.cross(q_vec_inv, diff_world, dim=1) + + # result = v + w*t + cross(q_vec, t) + local_offset = diff_world + w * t + torch.cross(q_vec_inv, t, dim=1) + + return local_offset + + def get_geometric_center(self, current_states): + """Get geometric center of the object.""" + root_pos = current_states.objects["object"].root_state[:, 0:3] + root_rot = current_states.objects["object"].root_state[:, 3:7] + # local_offset = self.local_offset.to(self.device) + w, x, y, z = root_rot[:, 0], root_rot[:, 1], root_rot[:, 2], root_rot[:, 3] + + q_vec = torch.stack([x, y, z], dim=1) + w = w.unsqueeze(1) + + v = self.local_offset.unsqueeze(0) + + t = 2.0 * torch.cross(q_vec, v, dim=1) + final_vec = v + w * t + torch.cross(q_vec, t, dim=1) + + return root_pos + final_vec + + def _prepare_states(self, states, env_ids): + """Preprocess initial states, randomizing positions within specified ranges. + + Only handles generic objects (object, markers) and robot state. + Specific objects (wall, window, cup, table) should be handled by subclasses if needed. + """ + from copy import deepcopy + + states = deepcopy(states) + + rand_config = DEFAULT_CONFIG["randomization"] + + initial_states_list = self._get_initial_states() + box_center = initial_states_list[0]["objects"]["object"]["pos"] + if not isinstance(box_center, torch.Tensor): + box_center = torch.tensor(box_center, device=self.device) + else: + box_center = box_center.to(self.device) + + box_pos_range_val = rand_config["box_pos_range"] + box_pos_range = torch.tensor( + [ + [box_center[0] - box_pos_range_val, box_center[1] - box_pos_range_val, box_center[2]], + [box_center[0] + box_pos_range_val, box_center[1] + box_pos_range_val, box_center[2]], + ], + device=self.device, + ) + + box_pos = ( + torch.rand(self.num_envs, 3, device=self.device) * (box_pos_range[1] - box_pos_range[0]) + box_pos_range[0] + ) + box_quat = states.objects["object"].root_state[:, 3:7].clone() + zero_vel = torch.zeros(self.num_envs, 3, device=self.device) + zero_ang_vel = torch.zeros(self.num_envs, 3, device=self.device) + states.objects["object"].root_state = torch.cat([box_pos, box_quat, zero_vel, zero_ang_vel], dim=-1) + + # import ipdb; ipdb.set_trace() + + obj_box_pos = self.get_geometric_center(states) + # # import ipdb; ipdb.set_trace() + states.objects["object0"].root_state = torch.cat([obj_box_pos, box_quat, zero_vel, zero_ang_vel], dim=-1) + + # Handle trajectory markers + for i in range(self.num_waypoints): + marker_name = f"traj_marker_{i}" + if marker_name in states.objects: + marker_pos = self.waypoint_positions[i].unsqueeze(0).expand(self.num_envs, -1) + marker_quat = self.waypoint_rotations[i].unsqueeze(0).expand(self.num_envs, -1) + states.objects[marker_name].root_state = torch.cat( + [marker_pos, marker_quat, zero_vel, zero_ang_vel], dim=-1 + ) + + # Handle robot state + robot_pos = states.robots[self.robot_name].root_state[:, 0:3].clone() + robot_pos_noise_val = rand_config["robot_pos_noise"] + robot_pos_noise = (torch.rand(self.num_envs, 3, device=self.device) - 0.5) * robot_pos_noise_val + robot_pos_new = robot_pos + robot_pos_noise + robot_quat = states.robots[self.robot_name].root_state[:, 3:7].clone() + robot_vel = states.robots[self.robot_name].root_state[:, 7:].clone() + states.robots[self.robot_name].root_state = torch.cat([robot_pos_new, robot_quat, robot_vel], dim=-1) + + robot_joint_pos = states.robots[self.robot_name].joint_pos.clone() + joint_noise_range = rand_config["joint_noise_range"] + joint_noise = (torch.rand_like(robot_joint_pos, device=self.device) - 0.5) * 2 * joint_noise_range + robot_joint_pos_new = robot_joint_pos + joint_noise + robot_joint_pos_new[:, 0] = torch.clamp(robot_joint_pos_new[:, 0], 0.0, 0.04) + robot_joint_pos_new[:, 1] = torch.clamp(robot_joint_pos_new[:, 1], 0.0, 0.04) + robot_joint_pos_new[:, 2:] = torch.clamp(robot_joint_pos_new[:, 2:], -2.8973, 2.8973) + states.robots[self.robot_name].joint_pos = robot_joint_pos_new + + return states + + def _get_initial_states(self) -> list[dict] | None: + """Get initial states for all environments.""" + init = [ + { + "objects": { + "table": { + "pos": torch.tensor([-0.000000, 0.000000, 0.376990]), + "rot": torch.tensor([1.000000, -0.000000, 0.000000, 0.000000]), + }, + "bowl": { + "pos": torch.tensor([-0.491991, 0.194712, 0.828524]), + "rot": torch.tensor([-0.774328, -0.006966, 0.006029, 0.632717]), + }, + "object": { + "pos": torch.tensor([0.027866, -0.379829, 0.768413]), + "rot": torch.tensor([-0.818808, -0.009771, -0.020791, -0.573607]), + }, + "object0": { + "pos": torch.tensor([0.027866, -0.379829, 0.768413]), + "rot": torch.tensor([-0.818808, -0.009771, -0.020791, -0.573607]), + }, + "plate": { + "pos": torch.tensor([0.000060, 0.000040, 0.774218]), + "rot": torch.tensor([-0.980610, -0.002716, -0.002327, 0.195939]), + }, + "traj_marker_0": { + "pos": torch.tensor([0.027866, -0.379829, 0.768413]), + "rot": torch.tensor([-0.818808, -0.009771, -0.020791, -0.573607]), + }, + "traj_marker_1": { + "pos": torch.tensor([-0.045492, -0.285306, 0.941898]), + "rot": torch.tensor([-0.317816, -0.002321, 0.001691, 0.948148]), + }, + "traj_marker_2": { + "pos": torch.tensor([-0.030328, -0.190204, 0.992140]), + "rot": torch.tensor([-0.489972, -0.004560, 0.003323, 0.871720]), + }, + "traj_marker_3": { + "pos": torch.tensor([-0.015164, -0.095102, 0.942381]), + "rot": torch.tensor([-0.644740, -0.006638, 0.004836, 0.764358]), + }, + "traj_marker_4": { + "pos": torch.tensor([0.000000, 0.000000, 0.792622]), + "rot": torch.tensor([-0.776629, -0.008479, 0.006178, 0.629871]), + }, + }, + "robots": { + "franka": { + "pos": torch.tensor([-0.6733999252319336, 2.3283064365386963e-10, 0.7760999798774719]), + "rot": torch.tensor([-1.0, 1.489094958451176e-10, 8.78133399329073e-10, 8.47253794900027e-11]), + "dof_pos": { + "panda_joint1": 0.0, + "panda_joint2": -0.785398, + "panda_joint3": 0.0, + "panda_joint4": -2.356194, + "panda_joint5": 0.0, + "panda_joint6": 1.570796, + "panda_joint7": 0.785398, + "panda_finger_joint1": 0.04, + "panda_finger_joint2": 0.04, + }, + }, + }, + } + for _ in range(self.num_envs) + ] + + return init diff --git a/roboverse_pack/tasks/pick_place/track_ceramic_teapot.py b/roboverse_pack/tasks/pick_place/track_ceramic_teapot.py new file mode 100644 index 000000000..044528d35 --- /dev/null +++ b/roboverse_pack/tasks/pick_place/track_ceramic_teapot.py @@ -0,0 +1,472 @@ +"""Stage 3: Track task for trajectory tracking. + +Trains trajectory tracking from saved grasp states. +Object is already grasped, only needs to learn trajectory following. +""" + +from __future__ import annotations + +import os +import pickle +from copy import deepcopy + +import numpy as np +import torch +from loguru import logger as log + +from metasim.constants import PhysicStateType +from metasim.scenario.objects import RigidObjCfg +from metasim.scenario.scenario import ScenarioCfg, SimParamCfg +from metasim.task.registry import register_task +from metasim.utils.math import matrix_from_quat +from roboverse_pack.tasks.pick_place.base import DEFAULT_CONFIG, PickPlaceBase + + +def load_states_from_pkl(pkl_path: str): + """Load state list from pkl file.""" + if not os.path.exists(pkl_path): + raise FileNotFoundError(f"State file not found: {pkl_path}") + + with open(pkl_path, "rb") as f: + states_list = pickle.load(f) + + log.info(f"Loaded {len(states_list)} states from {pkl_path}") + return states_list + + +def convert_state_dict_to_initial_state(state_dict: dict, device: torch.device, robot_name: str = "franka") -> dict: + """Convert state dict to initial state format.""" + initial_state = { + "objects": {}, + "robots": {}, + } + + if "objects" in state_dict and "robots" in state_dict: + for obj_name, obj_state in state_dict["objects"].items(): + pos = obj_state.get("pos") + rot = obj_state.get("rot") + + if isinstance(pos, (list, tuple, np.ndarray)): + pos = torch.tensor(pos, device=device, dtype=torch.float32) + elif isinstance(pos, torch.Tensor): + pos = pos.to(device).float() + + if isinstance(rot, (list, tuple, np.ndarray)): + rot = torch.tensor(rot, device=device, dtype=torch.float32) + elif isinstance(rot, torch.Tensor): + rot = rot.to(device).float() + + initial_state["objects"][obj_name] = { + "pos": pos, + "rot": rot, + } + + if "dof_pos" in obj_state: + initial_state["objects"][obj_name]["dof_pos"] = obj_state["dof_pos"] + + for robot_name_key, robot_state in state_dict["robots"].items(): + pos = robot_state.get("pos") + rot = robot_state.get("rot") + + if isinstance(pos, (list, tuple, np.ndarray)): + pos = torch.tensor(pos, device=device, dtype=torch.float32) + elif isinstance(pos, torch.Tensor): + pos = pos.to(device).float() + + if isinstance(rot, (list, tuple, np.ndarray)): + rot = torch.tensor(rot, device=device, dtype=torch.float32) + elif isinstance(rot, torch.Tensor): + rot = rot.to(device).float() + + initial_state["robots"][robot_name_key] = { + "pos": pos, + "rot": rot, + } + + if "dof_pos" in robot_state: + initial_state["robots"][robot_name_key]["dof_pos"] = robot_state["dof_pos"] + else: + # Flat format: convert to nested + for name, entity_state in state_dict.items(): + if name in ["objects", "robots"]: + continue + + pos = entity_state.get("pos") + rot = entity_state.get("rot") + + if isinstance(pos, (list, tuple, np.ndarray)): + pos = torch.tensor(pos, device=device, dtype=torch.float32) + elif isinstance(pos, torch.Tensor): + pos = pos.to(device).float() + elif isinstance(pos, np.ndarray): + pos = torch.from_numpy(pos).to(device).float() + + if isinstance(rot, (list, tuple, np.ndarray)): + rot = torch.tensor(rot, device=device, dtype=torch.float32) + elif isinstance(rot, torch.Tensor): + rot = rot.to(device).float() + elif isinstance(rot, np.ndarray): + rot = torch.from_numpy(rot).to(device).float() + + entity_entry = { + "pos": pos, + "rot": rot, + } + + if "dof_pos" in entity_state: + entity_entry["dof_pos"] = entity_state["dof_pos"] + + if name == robot_name: + initial_state["robots"][name] = entity_entry + else: + initial_state["objects"][name] = entity_entry + + return initial_state + + +DEFAULT_CONFIG_TRACK = deepcopy(DEFAULT_CONFIG) +DEFAULT_CONFIG_TRACK["reward_config"]["scales"].update({ + "tracking_approach": 4.0, + "tracking_progress": 150.0, + "rotation_tracking": 0.0, +}) +# 移除不需要的奖励 +DEFAULT_CONFIG_TRACK["reward_config"]["scales"].pop("gripper_approach", None) +DEFAULT_CONFIG_TRACK["reward_config"]["scales"].pop("gripper_close", None) +# Disable randomization for exact state reproduction +DEFAULT_CONFIG_TRACK["randomization"]["box_pos_range"] = 0.0 +DEFAULT_CONFIG_TRACK["randomization"]["robot_pos_noise"] = 0.0 +DEFAULT_CONFIG_TRACK["randomization"]["joint_noise_range"] = 0.0 +DEFAULT_CONFIG_TRACK["trajectory_tracking"]["num_waypoints"] = 3 + + +@register_task("pick_place.track_hu", "pick_place_track_hu") +class PickPlaceTrackHu(PickPlaceBase): + """Trajectory tracking task from grasp states. + + Assumes object is already grasped, only learns trajectory following. + Initial states loaded from pkl file. + """ + + scenario = ScenarioCfg( + objects=[ + RigidObjCfg( + name="table", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/EmbodiedGenData/all_asset/table/usd/table.usd", + urdf_path="roboverse_data/EmbodiedGenData/all_asset/table/table.urdf", + mjcf_path="roboverse_data/EmbodiedGenData/all_asset/table/mjcf/table.xml", + fix_base_link=True, + ), + RigidObjCfg( + name="bowl", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/EmbodiedGenData/all_asset/bowl/usd/bowl.usd", + urdf_path="roboverse_data/EmbodiedGenData/all_asset/bowl/bowl.urdf", + mjcf_path="roboverse_data/EmbodiedGenData/all_asset/bowl/mjcf/bowl.xml", + ), + RigidObjCfg( + name="object", + scale=(1, 1, 1), + enabled_gravity=False, + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/EmbodiedGenData/all_asset/ceramic_teapot/usd/ceramic_teapot.usd", + urdf_path="roboverse_data/EmbodiedGenData/all_asset/ceramic_teapot/ceramic_teapot.urdf", + mjcf_path="roboverse_data/EmbodiedGenData/all_asset/ceramic_teapot/mjcf/ceramic_teapot.xml", + ), + RigidObjCfg( + name="plate", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/EmbodiedGenData/all_asset/plate/usd/plate.usd", + urdf_path="roboverse_data/EmbodiedGenData/all_asset/plate/plate.urdf", + mjcf_path="roboverse_data/EmbodiedGenData/all_asset/plate/mjcf/plate.xml", + ), + RigidObjCfg( + name="traj_marker_0", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_1", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_2", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_3", + urdf_path="roboverse_pack/tasks/pick_place/marker/axis_marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/axis_marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/axis_marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_4", + urdf_path="roboverse_pack/tasks/pick_place/marker/axis_marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/axis_marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/axis_marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + ], + robots=["franka"], + sim_params=SimParamCfg( + dt=0.005, + ), + decimation=4, + ) + max_episode_steps = 200 + + def __init__(self, scenario, device=None): + self.state_file_path = "/usr1/home/s125mdg56_03/RoboVerse/eval_states/pick_place.approach_grasp_hu_franka_lift_states_122states_20251214_150655.pkl" + self._loaded_states = None + self._action_scale = 0.04 + + if device is None: + device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + self._device = device + + self.object_grasped = None + + super().__init__(scenario, device) + self.local_offset = torch.tensor([-0.00233746, -0.10298071, 0.03644049], device=device) + + self.object_grasped = torch.ones(self.num_envs, dtype=torch.bool, device=self.device) + self.reward_functions = [ + self._reward_trajectory_tracking, + self._reward_rotation_tracking, + ] + self.reward_weights = [ + 1.0, + 1.0, # rotation_tracking weight is already applied inside the function + ] + + def _prepare_states(self, states, env_ids): + """Override to disable randomization for track task.""" + return states + + def _get_initial_states(self) -> list[dict] | None: + """Load initial states from pkl file.""" + # import ipdb; ipdb.set_trace() + if self._loaded_states is not None: + return self._loaded_states + + states_list = load_states_from_pkl(self.state_file_path) + + device = getattr(self, "_device", None) or getattr(self, "device", None) + if device is None: + device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + + initial_states = [] + robot_name = "franka" + for state_dict in states_list: + initial_state = convert_state_dict_to_initial_state(state_dict, device, robot_name=robot_name) + initial_states.append(initial_state) + + if len(initial_states) < self.num_envs: + k = self.num_envs // len(initial_states) + remainder = self.num_envs % len(initial_states) + initial_states = initial_states * k + initial_states[:remainder] + + initial_states = initial_states[: self.num_envs] + + # Default waypoint positions + default_positions = [ + torch.tensor([-0.025798, -0.496286, 1.093025], device=device), + torch.tensor([-0.045492, -0.285306, 1.081898], device=device), + torch.tensor([-0.030328, -0.130204, 1.082140], device=device), + torch.tensor([-0.015164, 0.064898, 1.052381], device=device), + torch.tensor([0.130000, 0.160000, 1.082622], device=device), + ] + default_rotations = [ + torch.tensor([-0.835365, -0.002910, -0.008611, 0.549620], device=device), + torch.tensor([-0.317816, -0.002321, 0.001691, 0.948148], device=device), + torch.tensor([-0.489972, -0.004560, 0.003323, 0.871720], device=device), + torch.tensor([-0.644740, -0.006638, 0.004836, 0.764358], device=device), + torch.tensor([-0.776629, -0.008479, 0.006178, 0.629871], device=device), + ] + + for env_idx, initial_state in enumerate(initial_states): + if "objects" not in initial_state: + initial_state["objects"] = {} + # import ipdb; ipdb.set_trace() + for i in range(self.num_waypoints): + # import ipdb; ipdb.set_trace() + marker_name = f"traj_marker_{i}" + # if marker_name not in initial_state["objects"]: + # import ipdb; ipdb.set_trace() + if i < len(default_positions): + initial_state["objects"][marker_name] = { + "pos": default_positions[i].clone(), + "rot": default_rotations[i].clone(), + } + + self._loaded_states = initial_states + # import ipdb; ipdb.set_trace() + log.info(f"Loaded {len(initial_states)} initial states from {self.state_file_path}") + return initial_states + + def step(self, actions): + """Step with delta control, keeping gripper closed.""" + # print(actions[0]) + delta_actions = actions * self._action_scale + current_actions = self.handler.get_states().robots[self.robot_name].joint_pos + new_actions = current_actions + delta_actions + # print(self._action_low, self._action_high) + # print(self._action_scale) + real_actions = torch.clamp(new_actions, self._action_low, self._action_high) + + gripper_value_closed = torch.tensor(0.0, device=self.device, dtype=real_actions.dtype) + real_actions[:, 0] = gripper_value_closed + real_actions[:, 1] = gripper_value_closed + + obs, reward, terminated, time_out, info = super(PickPlaceBase, self).step(real_actions) + # obs, reward, terminated, time_out, info = super().step(real_actions) + self._last_action = real_actions.clone() + + updated_states = self.handler.get_states(mode="tensor") + + if self.local_offset is not None: + box_pos = self.get_geometric_center(updated_states) + else: + box_pos = updated_states.objects["object"].root_state[:, 0:3] + gripper_pos, _ = self._get_ee_state(updated_states) + + gripper_box_dist = torch.norm(gripper_pos - box_pos, dim=-1) + # print(gripper_box_dist) + # print(self.local_offset) + is_grasping = gripper_box_dist < self.grasp_check_distance + + self.object_grasped = is_grasping + newly_released = ~is_grasping + + if newly_released.any() and newly_released[0]: + log.warning(f"[Env 0] Object released during tracking! Distance: {gripper_box_dist[0].item():.4f}m") + + terminated = terminated | newly_released + + info["grasp_success"] = self.object_grasped + info["stage"] = torch.full((self.num_envs,), 3, dtype=torch.long, device=self.device) + + return obs, reward, terminated, time_out, info + + def _reward_gripper_close(self, env_states) -> torch.Tensor: + """Reward for closing gripper when close to box.""" + if self.local_offset is not None: + box_pos = self.get_geometric_center(env_states) + else: + box_pos = env_states.objects["object"].root_state[:, 0:3] + + gripper_pos, _ = self._get_ee_state(env_states) + gripper_box_dist = torch.norm(box_pos - gripper_pos, dim=-1) + + close_bonus = (gripper_box_dist < 0.02).float() + return close_bonus + + def _reward_gripper_approach(self, env_states) -> torch.Tensor: + """Reward for gripper approaching the box.""" + if self.local_offset is not None: + box_pos = self.get_geometric_center(env_states) + else: + box_pos = env_states.objects["object"].root_state[:, 0:3] + gripper_pos, _ = self._get_ee_state(env_states) + gripper_box_dist = torch.norm(box_pos - gripper_pos, dim=-1) + + approach_reward_far = 1 - torch.tanh(gripper_box_dist) + approach_reward_near = 1 - torch.tanh(gripper_box_dist * 10) + return approach_reward_far + approach_reward_near + + def get_geometric_center(self, current_states): + """Get the geometric center of the object in the world frame.""" + root_pos = current_states.objects["object"].root_state[:, 0:3] + root_rot = current_states.objects["object"].root_state[:, 3:7] + # local_offset = self.local_offset.to(self.device) + w, x, y, z = root_rot[:, 0], root_rot[:, 1], root_rot[:, 2], root_rot[:, 3] + + q_vec = torch.stack([x, y, z], dim=1) + + v = self.local_offset.unsqueeze(0) + + t = 2.0 * torch.cross(q_vec, v, dim=1) + + final_vec = v + w.view(-1, 1) * t + torch.cross(q_vec, t, dim=-1) + + return root_pos + final_vec + + def _observation(self, env_states) -> torch.Tensor: + """Get observation using RoboVerse tensor state.""" + # if self.local_offset is not None: + # box_pos = self.get_geometric_center(env_states) # [num_envs, 3] + # else: + box_pos = env_states.objects["object"].root_state[:, 0:3] # [num_envs, 3] + box_quat = env_states.objects["object"].root_state[:, 3:7] # [num_envs, 4] + + gripper_pos, gripper_quat = self._get_ee_state(env_states) # (B, 3), (B, 4) + gripper_mat = matrix_from_quat(gripper_quat).view(self.num_envs, -1) # (B, 9) + robot_joint_pos = env_states.robots[self.robot_name].joint_pos # [num_envs, num_joints] + robot_joint_vel = env_states.robots[self.robot_name].joint_vel # [num_envs, num_joints] + + # Convert quaternion to rotation matrix for box + box_mat = matrix_from_quat(box_quat) # [num_envs, 3, 3] + box_mat_flat = box_mat.view(self.num_envs, -1) # [num_envs, 9] + + # Ensure gripper_mat has correct shape [num_envs, 9] + if gripper_mat.dim() == 3: + gripper_mat = gripper_mat.view(self.num_envs, -1) # Reshape to [num_envs, 9] + + box_to_gripper = box_pos - gripper_pos # [num_envs, 3] + + target_pos = self.waypoint_positions[self.current_waypoint_idx] + target_to_gripper = target_pos - gripper_pos + num_reached = self.waypoints_reached.sum(dim=1, keepdim=True).float() / self.num_waypoints + + # Convert target quaternion to rotation matrix + target_quat = self.waypoint_rotations[self.current_waypoint_idx] + target_mat = matrix_from_quat(target_quat).reshape(self.num_envs, 9) # [num_envs, 9] + + obs_list = [ + robot_joint_pos, + robot_joint_vel, + gripper_pos, + gripper_mat[:, 3:], + box_mat_flat[:, 3:], + target_mat[:, 3:], + box_to_gripper, + target_to_gripper, + num_reached, + ] + + obs = torch.cat(obs_list, dim=-1) # [num_envs, obs_dim] + + return obs diff --git a/roboverse_pack/tasks/pick_place/track_knife.py b/roboverse_pack/tasks/pick_place/track_knife.py new file mode 100644 index 000000000..5d53f8c82 --- /dev/null +++ b/roboverse_pack/tasks/pick_place/track_knife.py @@ -0,0 +1,473 @@ +"""Stage 3: Track task for trajectory tracking. + +Trains trajectory tracking from saved grasp states. +Object is already grasped, only needs to learn trajectory following. +""" + +from __future__ import annotations + +import os +import pickle +from copy import deepcopy + +import numpy as np +import torch +from loguru import logger as log + +from metasim.constants import PhysicStateType +from metasim.scenario.objects import RigidObjCfg +from metasim.scenario.scenario import ScenarioCfg, SimParamCfg +from metasim.task.registry import register_task +from metasim.utils.math import matrix_from_quat +from roboverse_pack.tasks.pick_place.base import DEFAULT_CONFIG, PickPlaceBase + + +def load_states_from_pkl(pkl_path: str): + """Load state list from pkl file.""" + if not os.path.exists(pkl_path): + raise FileNotFoundError(f"State file not found: {pkl_path}") + + with open(pkl_path, "rb") as f: + states_list = pickle.load(f) + + log.info(f"Loaded {len(states_list)} states from {pkl_path}") + return states_list + + +def convert_state_dict_to_initial_state(state_dict: dict, device: torch.device, robot_name: str = "franka") -> dict: + """Convert state dict to initial state format.""" + initial_state = { + "objects": {}, + "robots": {}, + } + + if "objects" in state_dict and "robots" in state_dict: + for obj_name, obj_state in state_dict["objects"].items(): + pos = obj_state.get("pos") + rot = obj_state.get("rot") + + if isinstance(pos, (list, tuple, np.ndarray)): + pos = torch.tensor(pos, device=device, dtype=torch.float32) + elif isinstance(pos, torch.Tensor): + pos = pos.to(device).float() + + if isinstance(rot, (list, tuple, np.ndarray)): + rot = torch.tensor(rot, device=device, dtype=torch.float32) + elif isinstance(rot, torch.Tensor): + rot = rot.to(device).float() + + initial_state["objects"][obj_name] = { + "pos": pos, + "rot": rot, + } + + if "dof_pos" in obj_state: + initial_state["objects"][obj_name]["dof_pos"] = obj_state["dof_pos"] + + for robot_name_key, robot_state in state_dict["robots"].items(): + pos = robot_state.get("pos") + rot = robot_state.get("rot") + + if isinstance(pos, (list, tuple, np.ndarray)): + pos = torch.tensor(pos, device=device, dtype=torch.float32) + elif isinstance(pos, torch.Tensor): + pos = pos.to(device).float() + + if isinstance(rot, (list, tuple, np.ndarray)): + rot = torch.tensor(rot, device=device, dtype=torch.float32) + elif isinstance(rot, torch.Tensor): + rot = rot.to(device).float() + + initial_state["robots"][robot_name_key] = { + "pos": pos, + "rot": rot, + } + + if "dof_pos" in robot_state: + initial_state["robots"][robot_name_key]["dof_pos"] = robot_state["dof_pos"] + else: + # Flat format: convert to nested + for name, entity_state in state_dict.items(): + if name in ["objects", "robots"]: + continue + + pos = entity_state.get("pos") + rot = entity_state.get("rot") + + if isinstance(pos, (list, tuple, np.ndarray)): + pos = torch.tensor(pos, device=device, dtype=torch.float32) + elif isinstance(pos, torch.Tensor): + pos = pos.to(device).float() + elif isinstance(pos, np.ndarray): + pos = torch.from_numpy(pos).to(device).float() + + if isinstance(rot, (list, tuple, np.ndarray)): + rot = torch.tensor(rot, device=device, dtype=torch.float32) + elif isinstance(rot, torch.Tensor): + rot = rot.to(device).float() + elif isinstance(rot, np.ndarray): + rot = torch.from_numpy(rot).to(device).float() + + entity_entry = { + "pos": pos, + "rot": rot, + } + + if "dof_pos" in entity_state: + entity_entry["dof_pos"] = entity_state["dof_pos"] + + if name == robot_name: + initial_state["robots"][name] = entity_entry + else: + initial_state["objects"][name] = entity_entry + + return initial_state + + +DEFAULT_CONFIG_TRACK = deepcopy(DEFAULT_CONFIG) +DEFAULT_CONFIG_TRACK["reward_config"]["scales"].update({ + "tracking_approach": 4.0, + "tracking_progress": 150.0, + "rotation_tracking": 0.0, +}) +# 移除不需要的奖励 +DEFAULT_CONFIG_TRACK["reward_config"]["scales"].pop("gripper_approach", None) +DEFAULT_CONFIG_TRACK["reward_config"]["scales"].pop("gripper_close", None) +# Disable randomization for exact state reproduction +DEFAULT_CONFIG_TRACK["randomization"]["box_pos_range"] = 0.0 +DEFAULT_CONFIG_TRACK["randomization"]["robot_pos_noise"] = 0.0 +DEFAULT_CONFIG_TRACK["randomization"]["joint_noise_range"] = 0.0 +DEFAULT_CONFIG_TRACK["trajectory_tracking"]["num_waypoints"] = 3 + + +@register_task("pick_place.track_knife", "pick_place_track_knife") +class PickPlaceTrackKnife(PickPlaceBase): + """Trajectory tracking task from grasp states. + + Assumes object is already grasped, only learns trajectory following. + Initial states loaded from pkl file. + """ + + scenario = ScenarioCfg( + objects=[ + RigidObjCfg( + name="table", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/EmbodiedGenData/all_asset/table/usd/table.usd", + urdf_path="roboverse_data/EmbodiedGenData/all_asset/table/table.urdf", + mjcf_path="roboverse_data/EmbodiedGenData/all_asset/table/mjcf/table.xml", + fix_base_link=True, + ), + RigidObjCfg( + name="bowl", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/EmbodiedGenData/all_asset/bowl/usd/bowl.usd", + urdf_path="roboverse_data/EmbodiedGenData/all_asset/bowl/bowl.urdf", + mjcf_path="roboverse_data/EmbodiedGenData/all_asset/bowl/mjcf/bowl.xml", + ), + RigidObjCfg( + name="object", + scale=(1, 1, 1), + enabled_gravity=False, + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/EmbodiedGenData/all_asset/ceramic_teapot/usd/ceramic_teapot.usd", + urdf_path="roboverse_data/EmbodiedGenData/all_asset/ceramic_teapot/ceramic_teapot.urdf", + mjcf_path="roboverse_data/EmbodiedGenData/all_asset/ceramic_teapot/mjcf/ceramic_teapot.xml", + ), + RigidObjCfg( + name="plate", + scale=(1, 1, 1), + physics=PhysicStateType.RIGIDBODY, + usd_path="roboverse_data/EmbodiedGenData/all_asset/plate/usd/plate.usd", + urdf_path="roboverse_data/EmbodiedGenData/all_asset/plate/plate.urdf", + mjcf_path="roboverse_data/EmbodiedGenData/all_asset/plate/mjcf/plate.xml", + ), + RigidObjCfg( + name="traj_marker_0", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_1", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_2", + urdf_path="roboverse_pack/tasks/pick_place/marker/marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_3", + urdf_path="roboverse_pack/tasks/pick_place/marker/axis_marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/axis_marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/axis_marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + RigidObjCfg( + name="traj_marker_4", + urdf_path="roboverse_pack/tasks/pick_place/marker/axis_marker.urdf", + mjcf_path="roboverse_pack/tasks/pick_place/marker/axis_marker.xml", + usd_path="roboverse_pack/tasks/pick_place/marker/axis_marker.usd", + scale=0.2, + physics=PhysicStateType.XFORM, + enabled_gravity=False, + collision_enabled=False, + fix_base_link=True, + ), + ], + robots=["franka"], + sim_params=SimParamCfg( + dt=0.005, + ), + decimation=4, + ) + max_episode_steps = 200 + + def __init__(self, scenario, device=None): + self.state_file_path = "/usr1/home/s125mdg56_03/RoboVerse/eval_states/pick_place.approach_grasp_knife_franka_lift_states_198states_20251216_215959.pkl" + self._loaded_states = None + self._action_scale = 0.04 + + if device is None: + device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + self._device = device + + self.object_grasped = None + + super().__init__(scenario, device) + self.local_offset = torch.tensor([-0.01538026, 0.1282216, -0.00245847]).to(device) + + self.object_grasped = torch.ones(self.num_envs, dtype=torch.bool, device=self.device) + self.reward_functions = [ + self._reward_trajectory_tracking, + self._reward_rotation_tracking, + ] + self.reward_weights = [ + 1.0, + 1.0, # rotation_tracking weight is already applied inside the function + ] + + def _prepare_states(self, states, env_ids): + """Override to disable randomization for track task.""" + return states + + def _get_initial_states(self) -> list[dict] | None: + """Load initial states from pkl file.""" + # import ipdb; ipdb.set_trace() + if self._loaded_states is not None: + return self._loaded_states + + states_list = load_states_from_pkl(self.state_file_path) + + device = getattr(self, "_device", None) or getattr(self, "device", None) + if device is None: + device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + + initial_states = [] + robot_name = "franka" + for state_dict in states_list: + initial_state = convert_state_dict_to_initial_state(state_dict, device, robot_name=robot_name) + initial_states.append(initial_state) + + if len(initial_states) < self.num_envs: + k = self.num_envs // len(initial_states) + remainder = self.num_envs % len(initial_states) + initial_states = initial_states * k + initial_states[:remainder] + + initial_states = initial_states[: self.num_envs] + + # Default waypoint positions + default_positions = [ + torch.tensor([-0.025798, -0.496286, 1.093025], device=device), + torch.tensor([-0.045492, -0.285306, 1.081898], device=device), + torch.tensor([-0.030328, -0.130204, 1.082140], device=device), + torch.tensor([-0.015164, 0.064898, 1.052381], device=device), + torch.tensor([0.130000, 0.160000, 1.082622], device=device), + ] + default_rotations = [ + torch.tensor([-0.835365, -0.002910, -0.008611, 0.549620], device=device), + torch.tensor([-0.317816, -0.002321, 0.001691, 0.948148], device=device), + torch.tensor([-0.489972, -0.004560, 0.003323, 0.871720], device=device), + torch.tensor([-0.644740, -0.006638, 0.004836, 0.764358], device=device), + torch.tensor([-0.776629, -0.008479, 0.006178, 0.629871], device=device), + ] + + for env_idx, initial_state in enumerate(initial_states): + if "objects" not in initial_state: + initial_state["objects"] = {} + # import ipdb; ipdb.set_trace() + for i in range(self.num_waypoints): + # import ipdb; ipdb.set_trace() + marker_name = f"traj_marker_{i}" + # if marker_name not in initial_state["objects"]: + # import ipdb; ipdb.set_trace() + if i < len(default_positions): + initial_state["objects"][marker_name] = { + "pos": default_positions[i].clone(), + "rot": default_rotations[i].clone(), + } + + self._loaded_states = initial_states + # import ipdb; ipdb.set_trace() + log.info(f"Loaded {len(initial_states)} initial states from {self.state_file_path}") + return initial_states + + def step(self, actions): + """Step with delta control, keeping gripper closed.""" + # print(actions[0]) + delta_actions = actions * self._action_scale + current_actions = self.handler.get_states().robots[self.robot_name].joint_pos + new_actions = current_actions + delta_actions + # print(self._action_low, self._action_high) + # print(self._action_scale) + real_actions = torch.clamp(new_actions, self._action_low, self._action_high) + + gripper_value_closed = torch.tensor(0.0, device=self.device, dtype=real_actions.dtype) + real_actions[:, 0] = gripper_value_closed + real_actions[:, 1] = gripper_value_closed + + obs, reward, terminated, time_out, info = super(PickPlaceBase, self).step(real_actions) + # obs, reward, terminated, time_out, info = super().step(real_actions) + self._last_action = real_actions.clone() + + updated_states = self.handler.get_states(mode="tensor") + + if self.local_offset is not None: + box_pos = self.get_geometric_center(updated_states) + else: + box_pos = updated_states.objects["object"].root_state[:, 0:3] + gripper_pos, _ = self._get_ee_state(updated_states) + + gripper_box_dist = torch.norm(gripper_pos - box_pos, dim=-1) + # print(gripper_box_dist) + # print(self.local_offset) + is_grasping = gripper_box_dist < self.grasp_check_distance + + self.object_grasped = is_grasping + newly_released = ~is_grasping + + if newly_released.any() and newly_released[0]: + log.warning(f"[Env 0] Object released during tracking! Distance: {gripper_box_dist[0].item():.4f}m") + + terminated = terminated | newly_released + + info["grasp_success"] = self.object_grasped + info["stage"] = torch.full((self.num_envs,), 3, dtype=torch.long, device=self.device) + + return obs, reward, terminated, time_out, info + + def _reward_gripper_close(self, env_states) -> torch.Tensor: + """Reward for closing gripper when close to box.""" + if self.local_offset is not None: + box_pos = self.get_geometric_center(env_states) + else: + box_pos = env_states.objects["object"].root_state[:, 0:3] + + gripper_pos, _ = self._get_ee_state(env_states) + gripper_box_dist = torch.norm(box_pos - gripper_pos, dim=-1) + + close_bonus = (gripper_box_dist < 0.02).float() + return close_bonus + + def _reward_gripper_approach(self, env_states) -> torch.Tensor: + """Reward for gripper approaching the box.""" + if self.local_offset is not None: + box_pos = self.get_geometric_center(env_states) + else: + box_pos = env_states.objects["object"].root_state[:, 0:3] + gripper_pos, _ = self._get_ee_state(env_states) + gripper_box_dist = torch.norm(box_pos - gripper_pos, dim=-1) + + approach_reward_far = 1 - torch.tanh(gripper_box_dist) + approach_reward_near = 1 - torch.tanh(gripper_box_dist * 10) + return approach_reward_far + approach_reward_near + + def get_geometric_center(self, current_states): + """Get the geometric center of the object in the world frame.""" + root_pos = current_states.objects["object"].root_state[:, 0:3] + root_rot = current_states.objects["object"].root_state[:, 3:7] + # local_offset = self.local_offset.to(self.device) + w, x, y, z = root_rot[:, 0], root_rot[:, 1], root_rot[:, 2], root_rot[:, 3] + + q_vec = torch.stack([x, y, z], dim=1) + # w = w.unsqueeze(1) + + v = self.local_offset.unsqueeze(0) + + t = 2.0 * torch.cross(q_vec, v, dim=1) + + final_vec = v + w.view(-1, 1) * t + torch.cross(q_vec, t, dim=-1) + + return root_pos + final_vec + + def _observation(self, env_states) -> torch.Tensor: + """Get observation using RoboVerse tensor state.""" + # if self.local_offset is not None: + # box_pos = self.get_geometric_center(env_states) # [num_envs, 3] + # else: + box_pos = env_states.objects["object"].root_state[:, 0:3] # [num_envs, 3] + box_quat = env_states.objects["object"].root_state[:, 3:7] # [num_envs, 4] + + gripper_pos, gripper_quat = self._get_ee_state(env_states) # (B, 3), (B, 4) + gripper_mat = matrix_from_quat(gripper_quat).view(self.num_envs, -1) # (B, 9) + robot_joint_pos = env_states.robots[self.robot_name].joint_pos # [num_envs, num_joints] + robot_joint_vel = env_states.robots[self.robot_name].joint_vel # [num_envs, num_joints] + + # Convert quaternion to rotation matrix for box + box_mat = matrix_from_quat(box_quat) # [num_envs, 3, 3] + box_mat_flat = box_mat.view(self.num_envs, -1) # [num_envs, 9] + + # Ensure gripper_mat has correct shape [num_envs, 9] + if gripper_mat.dim() == 3: + gripper_mat = gripper_mat.view(self.num_envs, -1) # Reshape to [num_envs, 9] + + box_to_gripper = box_pos - gripper_pos # [num_envs, 3] + + target_pos = self.waypoint_positions[self.current_waypoint_idx] + target_to_gripper = target_pos - gripper_pos + num_reached = self.waypoints_reached.sum(dim=1, keepdim=True).float() / self.num_waypoints + + # Convert target quaternion to rotation matrix + target_quat = self.waypoint_rotations[self.current_waypoint_idx] + target_mat = matrix_from_quat(target_quat).reshape(self.num_envs, 9) # [num_envs, 9] + + obs_list = [ + robot_joint_pos, + robot_joint_vel, + gripper_pos, + gripper_mat[:, 3:], + box_mat_flat[:, 3:], + target_mat[:, 3:], + box_to_gripper, + target_to_gripper, + num_reached, + ] + + obs = torch.cat(obs_list, dim=-1) # [num_envs, obs_dim] + + return obs From 7b435c5b37ab9dc3780d4033a7fffefe62bd84c5 Mon Sep 17 00:00:00 2001 From: Mingyuan Sun <64095400+myuansun@users.noreply.github.com> Date: Fri, 26 Dec 2025 02:28:51 +0800 Subject: [PATCH 37/50] [fix] camera pos with env origin (#718) * [fix] camera pos with env origin * [fix] replace self.sim.reset() with more accurate sensor init --- get_started/3_parallel_envs.py | 1 + metasim/sim/isaacsim/isaacsim.py | 14 +++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/get_started/3_parallel_envs.py b/get_started/3_parallel_envs.py index 334cff3cb..202a83962 100644 --- a/get_started/3_parallel_envs.py +++ b/get_started/3_parallel_envs.py @@ -59,6 +59,7 @@ def __post_init__(self): simulator=args.sim, headless=args.headless, num_envs=args.num_envs, + env_spacing=5.0, ) # add cameras diff --git a/metasim/sim/isaacsim/isaacsim.py b/metasim/sim/isaacsim/isaacsim.py index 7207e4314..ac3ac4d64 100644 --- a/metasim/sim/isaacsim/isaacsim.py +++ b/metasim/sim/isaacsim/isaacsim.py @@ -155,11 +155,21 @@ def _init_keyboard(self) -> None: ) def _update_camera_pose(self) -> None: + env_origins = getattr(self.scene, "env_origins", None) + if env_origins is None: + env_origins = torch.zeros((self.num_envs, 3), device=self.device) + else: + env_origins = env_origins.to(self.device) + for camera in self.cameras: if isinstance(camera, PinholeCameraCfg): # set look at position using isaaclab's api if camera.mount_to is None: camera_inst = self.scene.sensors[camera.name] + position_tensor = torch.as_tensor(camera.pos, device=self.device).expand(self.num_envs, -1) + camera_lookat_tensor = torch.as_tensor(camera.look_at, device=self.device).expand(self.num_envs, -1) + position_tensor = position_tensor + env_origins + camera_lookat_tensor = camera_lookat_tensor + env_origins position_tensor = torch.tensor(camera.pos, device=self.device, dtype=torch.float32).unsqueeze(0) position_tensor = position_tensor.repeat(self.num_envs, 1) camera_lookat_tensor = torch.tensor( @@ -205,7 +215,9 @@ def launch(self, simulation_app=None, simulation_args=None) -> None: # Initialize GS background if enabled self._build_gs_background() super().launch() - self.sim.reset() # crucial for calling _initialize_callbacks in binded sensors + for sensor in self.scene.sensors.values(): + if hasattr(sensor, "_initialize_callback"): + sensor._initialize_callback(None) def close(self) -> None: log.info("close Isaacsim Handler") From 2072425320c13f603ed9b2c5d278717ff470ee9d Mon Sep 17 00:00:00 2001 From: KUNAGJI ZUO Date: Wed, 31 Dec 2025 18:16:17 +0800 Subject: [PATCH 38/50] Add support for 6 dexterous hands with demo script and documentation (#738) * [fix] mjcf scale * support rerun visualizer * update rerun readme, doc and index * update rerun code format to fit pre-commit hook * unified viz wrapper * [update] autotest trigger for forked repo (#713) * [update] autotest trigger for forked repo * [fix] drop declared core * Dev/fork autotest (#715) * [update] autotest trigger for forked repo * [fix] drop declared core * [fix] drop cli action * [fix] use pat token for triggering autotest (#716) * [fix] use pat token for triggering autotest * [update] merge priv-ci into merge queue * [fix] secrets grammar error * Add support for 6 dexterous hands with demo script and documentation This commit introduces support for three types of dexterous hands (6 variants total): **Inspire Hand (Left/Right)** - 12 DoFs total: 6 actuated joints + 6 mimic/passive (coupled) joints - Thumb: 2 actuated (yaw + pitch) + 2 passive (intermediate, distal) - Four fingers: 1 actuated (proximal) + 1 passive (intermediate) each - Passive joints mechanically follow actuated joints via coupling **BrainCo Hand (Left/Right)** - 11 DoFs total: 6 actuated joints + 5 mimic/passive joints - Prosthetic hand design with coupled joint mechanisms - 6 independent control inputs drive all finger movements **PSIHand (Left/Right)** - 21 DoFs total: All 21 joints fully actuated - 5 fingers with 4 joints each (thumb has 5 joints) - Note: Known compatibility issues with IsaacGym (NaN joint positions in URDF mode, DOF force tensor failures in USD mode). Use MuJoCo or Genesis instead. **Components Added:** - 6 robot configuration files in roboverse_pack/robots/ - Control demo script: get_started/dexhands/1_diff_dex_hand.py - Updated documentation: docs/source/dataset_benchmark/dataset/robots.md - Registered new hands in roboverse_pack/robots/__init__.py The demo script supports all RoboVerse simulators and includes camera configuration for video recording. For IsaacGym, initial pose is set to properly orient hands. --------- Co-authored-by: MurphyZhao04 Co-authored-by: geng-haoran Co-authored-by: Mingyuan Sun <64095400+myuansun@users.noreply.github.com> Co-authored-by: 23369 <2336988354@qq.com> Co-authored-by: Murphy <167211955+MurphyZhao04@users.noreply.github.com> --- .github/workflows/premerge-ci.yml | 966 ++++++++++++------ .../dataset_benchmark/dataset/robots.md | 15 + get_started/dexhands/1_diff_dex_hand.py | 216 ++++ roboverse_pack/robots/__init__.py | 6 + .../robots/brainco_hand_left_cfg.py | 128 +++ .../robots/brainco_hand_right_cfg.py | 128 +++ .../robots/inspire_hand_left_cfg.py | 119 +++ .../robots/inspire_hand_right_cfg.py | 119 +++ roboverse_pack/robots/psihand_left_cfg.py | 138 +++ roboverse_pack/robots/psihand_right_cfg.py | 138 +++ 10 files changed, 1646 insertions(+), 327 deletions(-) create mode 100644 get_started/dexhands/1_diff_dex_hand.py create mode 100644 roboverse_pack/robots/brainco_hand_left_cfg.py create mode 100644 roboverse_pack/robots/brainco_hand_right_cfg.py create mode 100644 roboverse_pack/robots/inspire_hand_left_cfg.py create mode 100644 roboverse_pack/robots/inspire_hand_right_cfg.py create mode 100644 roboverse_pack/robots/psihand_left_cfg.py create mode 100644 roboverse_pack/robots/psihand_right_cfg.py diff --git a/.github/workflows/premerge-ci.yml b/.github/workflows/premerge-ci.yml index b2951e4cf..0ed19c7ad 100644 --- a/.github/workflows/premerge-ci.yml +++ b/.github/workflows/premerge-ci.yml @@ -23,349 +23,661 @@ env: ECR_REPOSITORY: "roboverse-dev" jobs: + prepare-or-promote: + name: prepare-or-promote + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + issues: write + env: + PRIV_CI_PUSH_TOKEN: ${{ secrets.PRIV_CI_PUSH_TOKEN }} + outputs: + run_direct: ${{ steps.decide.outputs.run_direct }} + pr_number: ${{ steps.decide.outputs.pr_number }} + reason: ${{ steps.decide.outputs.reason }} + steps: + - name: Route CI flow + id: decide + env: + GITHUB_EVENT_NAME: ${{ github.event_name }} + GITHUB_REF: ${{ github.ref }} + PR_NUMBER: ${{ github.event.pull_request.number || '' }} + HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name || '' }} + BASE_REPO: ${{ github.repository }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_EVENT_PATH: ${{ github.event_path }} + run: | + RUN_DIRECT="false" + PR_NUM="${PR_NUMBER}" + REASON="" + + case "$GITHUB_EVENT_NAME" in + merge_group) + BASE_REPO_NAME="${BASE_REPO}" + HAS_FORK="false" + PRS=$(jq -r '.merge_group.pull_requests[].number // empty' "$GITHUB_EVENT_PATH" 2>/dev/null || true) + + if [ -z "$PRS" ]; then + echo "No pull requests listed in merge_group payload; defaulting to same-repo." + fi + + for PR in $PRS; do + HEAD_REPO_NAME=$(gh api "repos/${BASE_REPO_NAME}/pulls/${PR}" --jq '.head.repo.full_name' 2>/dev/null || echo "") + BASE_REPO_FROM_PR=$(gh api "repos/${BASE_REPO_NAME}/pulls/${PR}" --jq '.base.repo.full_name' 2>/dev/null || echo "$BASE_REPO_NAME") + + if [ -z "$HEAD_REPO_NAME" ]; then + echo "Warning: could not determine head repo for PR #$PR; assuming same-repo." + continue + fi + + if [ "$HEAD_REPO_NAME" != "$BASE_REPO_FROM_PR" ]; then + HAS_FORK="true" + echo "Detected fork PR #$PR (head repo: $HEAD_REPO_NAME, base repo: $BASE_REPO_FROM_PR)" + else + echo "PR #$PR is same-repo (head repo: $HEAD_REPO_NAME)" + fi + done + + if [ "$HAS_FORK" = "true" ]; then + RUN_DIRECT="false" + REASON="merge_group_contains_fork" + else + RUN_DIRECT="true" + REASON="merge_group_same_repo_only" + fi + ;; + push) + if [[ "$GITHUB_REF" =~ ^refs/heads/ci/pr- ]]; then + RUN_DIRECT="true" + REASON="ci/pr-* push" + PR_NUM="${GITHUB_REF#refs/heads/ci/pr-}" + else + REASON="unsupported push ref" + fi + ;; + workflow_dispatch) + RUN_DIRECT="true" + REASON="manual dispatch" + ;; + pull_request_target) + if [ "$HEAD_REPO" = "$BASE_REPO" ]; then + REASON="same-repo PR, no promotion needed" + else + REASON="fork PR requires promotion" + fi + ;; + *) + REASON="unsupported event" + ;; + esac + + echo "run_direct=${RUN_DIRECT}" >> "$GITHUB_OUTPUT" + echo "pr_number=${PR_NUM}" >> "$GITHUB_OUTPUT" + echo "reason=${REASON}" >> "$GITHUB_OUTPUT" + + - name: Log routing decision + run: | + echo "run_direct: ${{ steps.decide.outputs.run_direct }}" + echo "pr_number: ${{ steps.decide.outputs.pr_number }}" + echo "reason: ${{ steps.decide.outputs.reason }}" + + - name: Ensure promotion token exists + if: > + github.event_name == 'pull_request_target' && + steps.decide.outputs.reason == 'fork PR requires promotion' && + (env.PRIV_CI_PUSH_TOKEN == '' || env.PRIV_CI_PUSH_TOKEN == null) + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ steps.decide.outputs.pr_number }} + run: | + gh pr comment "$PR_NUMBER" --body "🔒 Privileged CI promotion skipped: secret \`PRIV_CI_PUSH_TOKEN\` is not configured." + echo "Missing PRIV_CI_PUSH_TOKEN; exiting router after notifying PR." + exit 0 + + - name: Check actor permission + id: perm + if: > + github.event_name == 'pull_request_target' && + steps.decide.outputs.reason == 'fork PR requires promotion' && + env.PRIV_CI_PUSH_TOKEN != '' && + env.PRIV_CI_PUSH_TOKEN != null + env: + ACTOR: ${{ github.actor }} + REPO: ${{ github.repository }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + perm=$(gh api "repos/$REPO/collaborators/$ACTOR/permission" --jq .permission 2>/dev/null || echo "none") + echo "permission=$perm" >> "$GITHUB_OUTPUT" + echo "Actor: $ACTOR, permission: $perm" + + - name: Skip promotion without push access + if: > + github.event_name == 'pull_request_target' && + steps.decide.outputs.reason == 'fork PR requires promotion' && + steps.perm.outputs.permission != '' && + (steps.perm.outputs.permission == 'read' || steps.perm.outputs.permission == 'triage' || steps.perm.outputs.permission == 'none') + env: + ACTOR: ${{ github.actor }} + REPO: ${{ github.repository }} + PR_NUMBER: ${{ steps.decide.outputs.pr_number }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh pr comment "$PR_NUMBER" --body "🔒 Privileged CI promotion skipped: @${ACTOR} does not have push permission on ${REPO}." + echo "Insufficient permission; exiting router after notifying PR." + exit 0 + + - name: Checkout repository for promotion + if: > + github.event_name == 'pull_request_target' && + steps.decide.outputs.reason == 'fork PR requires promotion' && + (steps.perm.outputs.permission == 'write' || steps.perm.outputs.permission == 'maintain' || steps.perm.outputs.permission == 'admin') + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.PRIV_CI_PUSH_TOKEN }} + + - name: Promote fork PR to ci/pr-* branch + if: > + github.event_name == 'pull_request_target' && + steps.decide.outputs.reason == 'fork PR requires promotion' && + (steps.perm.outputs.permission == 'write' || steps.perm.outputs.permission == 'maintain' || steps.perm.outputs.permission == 'admin') + env: + PR_NUMBER: ${{ steps.decide.outputs.pr_number }} + REPO: ${{ github.repository }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + BRANCH="ci/pr-${PR_NUMBER}" + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + gh pr checkout "$PR_NUMBER" + git push origin HEAD:"$BRANCH" + + - name: Comment promotion result + if: > + github.event_name == 'pull_request_target' && + steps.decide.outputs.reason == 'fork PR requires promotion' && + (steps.perm.outputs.permission == 'write' || steps.perm.outputs.permission == 'maintain' || steps.perm.outputs.permission == 'admin') + env: + PR_NUMBER: ${{ steps.decide.outputs.pr_number }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + BRANCH="ci/pr-${PR_NUMBER}" + gh pr comment "$PR_NUMBER" --body "🔐 Privileged CI has been promoted to branch \`${BRANCH}\`. Tests will run from that branch." + pre-merge-tests: + needs: prepare-or-promote + if: needs.prepare-or-promote.outputs.run_direct == 'true' + permissions: + contents: write # Need write access to delete ci/pr-* branches + pull-requests: write + issues: write # Post status comment back to PR runs-on: codebuild-EC2_Launcher2-${{ github.run_id }}-${{ github.run_attempt }} timeout-minutes: 720 steps: - # change to the source code directory - - name: Checkout code - uses: actions/checkout@v4 - - run: aws --version - ############# Prebuild ############ - - name: pre_build - env: - SSH_KEY: ${{ secrets.EC2_SSH_KEY }} - run: | - # Get AWS account ID - AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) - echo "AWS_ACCOUNT_ID=$AWS_ACCOUNT_ID" >> $GITHUB_ENV - if [ -z "$AWS_ACCOUNT_ID" ]; then - echo "Error: Failed to get AWS account ID" - exit 1 - fi - - echo "Preparing S3 bucket..." - CACHE_BUCKET="${CACHE_BUCKET_PREFIX}-${AWS_ACCOUNT_ID}" - aws s3api head-bucket --bucket $CACHE_BUCKET || \ - aws s3 mb s3://$CACHE_BUCKET --region $REGION - - # Configure S3 bucket lifecycle rule for cache expiration - aws s3api put-bucket-lifecycle-configuration \ - --bucket $CACHE_BUCKET \ - --lifecycle-configuration '{ - "Rules": [ - { - "ID": "ExpireBuildKitCache", - "Status": "Enabled", - "Filter": { - "Prefix": "" - }, - "Expiration": { - "Days": 14 - } - } - ] - }' - echo "CACHE_BUCKET=$CACHE_BUCKET" >> $GITHUB_ENV - - - echo "Launching EC2 instance to run tests..." - INSTANCE_ID=$(aws ec2 run-instances \ - --image-id ami-0b7f5f52689b2c0d0 \ - --instance-type $INSTANCE_TYPE \ - --region $REGION \ - --key-name $KEY_NAME \ - --security-group-ids sg-03f9110d8d39282ad \ - --subnet-id subnet-0c56793ce29caa78b \ - --iam-instance-profile Name="RoboverseCi" \ - --block-device-mappings '[{"DeviceName":"/dev/sda1","Ebs":{"VolumeSize":500}}]' \ - --output text \ - --query 'Instances[0].InstanceId') - echo "INSTANCE_ID=$INSTANCE_ID" >> $GITHUB_ENV - - - # Create ECR repository if it doesn't exist - aws ecr describe-repositories --repository-names $ECR_REPOSITORY || \ - aws ecr create-repository --repository-name $ECR_REPOSITORY - echo "Waiting for instance $INSTANCE_ID to be running..." - aws ec2 wait instance-running \ - --instance-ids $INSTANCE_ID \ - --region $REGION - - echo "Getting instance IP address..." - EC2_INSTANCE_IP=$(aws ec2 describe-instances \ - --region $REGION \ - --filters "Name=instance-state-name,Values=running" "Name=instance-id,Values=$INSTANCE_ID" \ - --query 'Reservations[*].Instances[*].[PrivateIpAddress]' \ - --output text) - echo "EC2_INSTANCE_IP=$EC2_INSTANCE_IP" >> $GITHUB_ENV - - echo "Setting up SSH configuration..." - mkdir -p ~/.ssh - aws ec2 describe-key-pairs \ - --include-public-key \ - --key-name $KEY_NAME \ - --query 'KeyPairs[0].PublicKey' \ - --output text > ~/.ssh/id_rsa.pub - echo "$SSH_KEY" > ~/.ssh/id_rsa - chmod 400 ~/.ssh/id_* - echo "Host $EC2_INSTANCE_IP\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile=/dev/null\n" >> ~/.ssh/config - - echo "Sending SSH public key to instance..." - aws ec2-instance-connect send-ssh-public-key \ - --instance-id $INSTANCE_ID \ - --availability-zone $AZ \ - --ssh-public-key file://~/.ssh/id_rsa.pub \ - --instance-os-user $EC2_USER_NAME - - ############# Build ############# - - name: build - run: | - echo "====Copying source code...====" - wait_time=$RETRY_WAIT_TIME - SRC_DIR=$(basename $GITHUB_WORKSPACE) - echo ""====Check environment variables..."====" - echo "GITHUB_WORKSPACE=$GITHUB_WORKSPACE" - echo "CODEBUILD_SRC_DIR=$CODEBUILD_SRC_DIR" - echo "EC2_USER_NAME=$EC2_USER_NAME" - echo "SRC_DIR=$SRC_DIR" - echo "RETRY_WAIT_TIME=$RETRY_WAIT_TIME" - echo "MAX_RETRIES=$MAX_RETRIES" - echo "====Repo file check...====" - ls ./ - - # ==== before buildx build ==== - DOCKERFILE_HASH=$(sha256sum Dockerfile | cut -c1-16) - IMAGE_URI="$AWS_ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/$ECR_REPOSITORY:df-$DOCKERFILE_HASH" - echo "IMAGE_URI=$IMAGE_URI" - - retry_count=0 - - # change to parent directory t - cd .. - - while [ $retry_count -lt $MAX_RETRIES ]; do - if [ $retry_count -gt 0 ]; then - wait_time=$((wait_time * 2)) - echo "Retry attempt $((retry_count + 1))/$MAX_RETRIES. Waiting $wait_time seconds..." - sleep $wait_time + # change to the source code directory + - name: Checkout code + uses: actions/checkout@v4 + - run: aws --version + ############# Prebuild ############ + - name: pre_build + env: + SSH_KEY: ${{ secrets.EC2_SSH_KEY }} + run: | + # Get AWS account ID + AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) + echo "AWS_ACCOUNT_ID=$AWS_ACCOUNT_ID" >> $GITHUB_ENV + if [ -z "$AWS_ACCOUNT_ID" ]; then + echo "Error: Failed to get AWS account ID" + exit 1 fi - if scp -o ConnectTimeout=10 -o StrictHostKeyChecking=no -r $SRC_DIR $EC2_USER_NAME@$EC2_INSTANCE_IP:~; then - echo "SCP command succeeded" - break + echo "Preparing S3 bucket..." + CACHE_BUCKET="${CACHE_BUCKET_PREFIX}-${AWS_ACCOUNT_ID}" + aws s3api head-bucket --bucket $CACHE_BUCKET || \ + aws s3 mb s3://$CACHE_BUCKET --region $REGION + + # Configure S3 bucket lifecycle rule for cache expiration + aws s3api put-bucket-lifecycle-configuration \ + --bucket $CACHE_BUCKET \ + --lifecycle-configuration '{ + "Rules": [ + { + "ID": "ExpireBuildKitCache", + "Status": "Enabled", + "Filter": { + "Prefix": "" + }, + "Expiration": { + "Days": 14 + } + } + ] + }' + echo "CACHE_BUCKET=$CACHE_BUCKET" >> $GITHUB_ENV + + + echo "Launching EC2 instance to run tests..." + INSTANCE_ID=$(aws ec2 run-instances \ + --image-id ami-0b7f5f52689b2c0d0 \ + --instance-type $INSTANCE_TYPE \ + --region $REGION \ + --key-name $KEY_NAME \ + --security-group-ids sg-03f9110d8d39282ad \ + --subnet-id subnet-0c56793ce29caa78b \ + --iam-instance-profile Name="RoboverseCi" \ + --block-device-mappings '[{"DeviceName":"/dev/sda1","Ebs":{"VolumeSize":500}}]' \ + --output text \ + --query 'Instances[0].InstanceId') + echo "INSTANCE_ID=$INSTANCE_ID" >> $GITHUB_ENV + + + # Create ECR repository if it doesn't exist + aws ecr describe-repositories --repository-names $ECR_REPOSITORY || \ + aws ecr create-repository --repository-name $ECR_REPOSITORY + echo "Waiting for instance $INSTANCE_ID to be running..." + aws ec2 wait instance-running \ + --instance-ids $INSTANCE_ID \ + --region $REGION + + echo "Getting instance IP address..." + EC2_INSTANCE_IP=$(aws ec2 describe-instances \ + --region $REGION \ + --filters "Name=instance-state-name,Values=running" "Name=instance-id,Values=$INSTANCE_ID" \ + --query 'Reservations[*].Instances[*].[PrivateIpAddress]' \ + --output text) + echo "EC2_INSTANCE_IP=$EC2_INSTANCE_IP" >> $GITHUB_ENV + + echo "Setting up SSH configuration..." + mkdir -p ~/.ssh + aws ec2 describe-key-pairs \ + --include-public-key \ + --key-name $KEY_NAME \ + --query 'KeyPairs[0].PublicKey' \ + --output text > ~/.ssh/id_rsa.pub + echo "$SSH_KEY" > ~/.ssh/id_rsa + chmod 400 ~/.ssh/id_* + echo "Host $EC2_INSTANCE_IP\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile=/dev/null\n" >> ~/.ssh/config + + echo "Sending SSH public key to instance..." + aws ec2-instance-connect send-ssh-public-key \ + --instance-id $INSTANCE_ID \ + --availability-zone $AZ \ + --ssh-public-key file://~/.ssh/id_rsa.pub \ + --instance-os-user $EC2_USER_NAME + + ############# Build ############# + - name: build + run: | + echo "====Copying source code...====" + wait_time=$RETRY_WAIT_TIME + SRC_DIR=$(basename $GITHUB_WORKSPACE) + echo ""====Check environment variables..."====" + echo "GITHUB_WORKSPACE=$GITHUB_WORKSPACE" + echo "CODEBUILD_SRC_DIR=$CODEBUILD_SRC_DIR" + echo "EC2_USER_NAME=$EC2_USER_NAME" + echo "SRC_DIR=$SRC_DIR" + echo "RETRY_WAIT_TIME=$RETRY_WAIT_TIME" + echo "MAX_RETRIES=$MAX_RETRIES" + echo "====Repo file check...====" + ls ./ + + # ==== before buildx build ==== + DOCKERFILE_HASH=$(sha256sum Dockerfile | cut -c1-16) + IMAGE_URI="$AWS_ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/$ECR_REPOSITORY:df-$DOCKERFILE_HASH" + echo "IMAGE_URI=$IMAGE_URI" + + retry_count=0 + + # change to parent directory t + cd .. + + while [ $retry_count -lt $MAX_RETRIES ]; do + if [ $retry_count -gt 0 ]; then + wait_time=$((wait_time * 2)) + echo "Retry attempt $((retry_count + 1))/$MAX_RETRIES. Waiting $wait_time seconds..." + sleep $wait_time + fi + + if scp -o ConnectTimeout=10 -o StrictHostKeyChecking=no -r $SRC_DIR $EC2_USER_NAME@$EC2_INSTANCE_IP:~; then + echo "SCP command succeeded" + break + fi + + retry_count=$((retry_count + 1)) + done + + if [ $retry_count -eq $MAX_RETRIES ]; then + echo "SCP command failed after $MAX_RETRIES attempts" + exit 1 fi - retry_count=$((retry_count + 1)) - done - - if [ $retry_count -eq $MAX_RETRIES ]; then - echo "SCP command failed after $MAX_RETRIES attempts" - exit 1 - fi + # login + ECR_LOGIN_TOKEN=$(aws ecr get-login-password --region $REGION) + + echo "====Running tests on EC2 instance...====" + ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no $EC2_USER_NAME@$EC2_INSTANCE_IP " + + set -euo pipefail + + # Login to ECR using token from CodeBuild + echo \"$ECR_LOGIN_TOKEN\" | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com + + # Configure BuildKit environment + export DOCKER_BUILDKIT=1 + export BUILDKIT_INLINE_CACHE=1 + + docker buildx create --name metasim-builder --driver docker-container \ + --driver-opt env.AWS_REGION=$REGION \ + --driver-opt env.AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \ + --driver-opt env.AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \ + --bootstrap + docker buildx use metasim-builder + + cd \"\$HOME/${SRC_DIR}\" + + # docker build + if docker pull "$IMAGE_URI" 2>/dev/null ; then + echo "Image $IMAGE_URI already exists. Skipping build." + else + echo "===Starting docker build.===" + docker buildx build --progress=plain --platform linux/amd64 \ + -t "$IMAGE_URI" \ + --cache-from type=registry,ref=$AWS_ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/$ECR_REPOSITORY:cache,mode=max \ + --cache-to type=registry,ref=$AWS_ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/$ECR_REPOSITORY:cache,mode=max \ + --build-arg DOCKER_UID=1000 \ + --build-arg DOCKER_GID=1000 \ + --build-arg DOCKER_USER=$EC2_USER_NAME \ + -f Dockerfile \ + --load . + + docker push "$IMAGE_URI" + fi + + # begin run test + GENERAL_TEST_EXIT_CODE=0 + MUJOCO_TEST_EXIT_CODE=0 + SAPIEN3_TEST_EXIT_CODE=0 + ISAACSIM_TEST_EXIT_CODE=0 + ISAACGYM_TEST_EXIT_CODE=0 + # run all test + # Run general tests (no simulator required) + docker run --rm --entrypoint bash --runtime=nvidia --network=host \ + --name metasim-autotest \ + --user 1000:1000 --privileged \ + -e LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu \ + -e ACCEPT_EULA=Y -e PRIVACY_CONSENT=Y -e OMNI_KIT_ACCEPT_EULA=YES \ + -v /usr/local/cuda:/usr/local/cuda \ + -v "$(pwd)":/home/$EC2_USER_NAME/RoboVerse \ + "$IMAGE_URI" \ + -c "bash -lc 'set -o pipefail; \ + /home/$EC2_USER_NAME/conda/envs/metasim/bin/python3 -m pytest -k general -vv \ + | tee /home/$EC2_USER_NAME/${SRC_DIR}/pytest-general.log'" \ + || GENERAL_TEST_EXIT_CODE=$? + + docker run --rm --entrypoint bash --runtime=nvidia --network=host \ + --name metasim-autotest \ + --user 1000:1000 --privileged \ + -e LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu \ + -e ACCEPT_EULA=Y -e PRIVACY_CONSENT=Y -e OMNI_KIT_ACCEPT_EULA=YES \ + -v /usr/local/cuda:/usr/local/cuda \ + -v "$(pwd)":/home/$EC2_USER_NAME/RoboVerse \ + "$IMAGE_URI" \ + -c "bash -lc 'set -o pipefail; \ + /home/$EC2_USER_NAME/conda/envs/metasim/bin/python3 -m pytest -k mujoco -vv \ + | tee /home/$EC2_USER_NAME/${SRC_DIR}/pytest-mujoco.log'" \ + || MUJOCO_TEST_EXIT_CODE=$? + + docker run --rm --entrypoint bash --runtime=nvidia --network=host \ + --name metasim-autotest \ + --user 1000:1000 --privileged \ + -e LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu \ + -e ACCEPT_EULA=Y -e PRIVACY_CONSENT=Y -e OMNI_KIT_ACCEPT_EULA=YES \ + -v /usr/local/cuda:/usr/local/cuda \ + -v "$(pwd)":/home/$EC2_USER_NAME/RoboVerse \ + "$IMAGE_URI" \ + -c "bash -lc 'set -o pipefail; \ + /home/$EC2_USER_NAME/conda/envs/metasim/bin/python3 -m pytest -k sapien3 -vv \ + | tee /home/$EC2_USER_NAME/${SRC_DIR}/pytest-sapien3.log'" \ + || SAPIEN3_TEST_EXIT_CODE=$? + + docker run --rm --entrypoint bash --runtime=nvidia --network=host \ + --name metasim-autotest \ + --user 1000:1000 --privileged \ + -e LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu \ + -e ACCEPT_EULA=Y -e PRIVACY_CONSENT=Y -e OMNI_KIT_ACCEPT_EULA=YES \ + -v /usr/local/cuda:/usr/local/cuda \ + -v "$(pwd)":/home/$EC2_USER_NAME/RoboVerse \ + "$IMAGE_URI" \ + -c "bash -lc 'set -o pipefail; \ + /home/$EC2_USER_NAME/conda/envs/metasim/bin/python3 -m pytest -k isaacsim -vv \ + | tee /home/$EC2_USER_NAME/${SRC_DIR}/pytest-isaacsim.log'" \ + || ISAACSIM_TEST_EXIT_CODE=$? + + docker run --rm --entrypoint bash --runtime=nvidia --network=host \ + --name metasim-autotest \ + --user 1000:1000 --privileged \ + -e LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu \ + -e ACCEPT_EULA=Y -e PRIVACY_CONSENT=Y -e OMNI_KIT_ACCEPT_EULA=YES \ + -v /usr/local/cuda:/usr/local/cuda \ + -v "$(pwd)":/home/$EC2_USER_NAME/RoboVerse \ + "$IMAGE_URI" \ + -c "bash -lc 'set -o pipefail; \ + /home/$EC2_USER_NAME/conda/envs/metasim_isaacgym/bin/python3 /home/$EC2_USER_NAME/RoboVerse/metasim/test/isaacgym_entry.py -k isaacgym -vv \ + | tee /home/$EC2_USER_NAME/${SRC_DIR}/pytest-isaacgym.log'" \ + || ISAACGYM_TEST_EXIT_CODE=$? + + # TODO check if test_exit_code necessary + touch ~/$SRC_DIR/test_exit_codes.txt + { + echo \"GENERAL_TEST_EXIT_CODE=\$GENERAL_TEST_EXIT_CODE\" + echo \"MUJOCO_TEST_EXIT_CODE=\$MUJOCO_TEST_EXIT_CODE\" + echo \"SAPIEN3_TEST_EXIT_CODE=\$SAPIEN3_TEST_EXIT_CODE\" + echo \"ISAACSIM_TEST_EXIT_CODE=\$ISAACSIM_TEST_EXIT_CODE\" + echo \"ISAACGYM_TEST_EXIT_CODE=\$ISAACGYM_TEST_EXIT_CODE\" + } > ~/${SRC_DIR}/test_exit_codes.txt + " || { echo "Test execution failed"; exit 1; } + + echo "===Copying test reports...===" + scp -o ConnectTimeout=10 -o StrictHostKeyChecking=no $EC2_USER_NAME@$EC2_INSTANCE_IP:~/$SRC_DIR/test_exit_codes.txt $CODEBUILD_SRC_DIR/ + + source $CODEBUILD_SRC_DIR/test_exit_codes.txt + + echo "General test exit code: ${GENERAL_TEST_EXIT_CODE}" + echo "Mujoco test exit code: ${MUJOCO_TEST_EXIT_CODE}" + echo "Sapien3 test exit code: ${SAPIEN3_TEST_EXIT_CODE}" + echo "IsaacSim test exit code: ${ISAACSIM_TEST_EXIT_CODE}" + echo "IsaacGym test exit code: ${ISAACGYM_TEST_EXIT_CODE}" + + EXIT_CODE=0 + + if [ "${GENERAL_TEST_EXIT_CODE:-0}" -ne 0 ]; then + echo "=== General tests failed. Fetching logs... ===" + scp -o ConnectTimeout=10 -o StrictHostKeyChecking=no \ + $EC2_USER_NAME@$EC2_INSTANCE_IP:~/$SRC_DIR/pytest-general.log \ + $CODEBUILD_SRC_DIR/ || true + echo "===== General pytest log =====" + cat $CODEBUILD_SRC_DIR/pytest-general.log || true + EXIT_CODE=1 + fi - # login - ECR_LOGIN_TOKEN=$(aws ecr get-login-password --region $REGION) + if [ "${MUJOCO_TEST_EXIT_CODE:-0}" -ne 0 ]; then + echo "=== Mujoco tests failed. Fetching logs... ===" + scp -o ConnectTimeout=10 -o StrictHostKeyChecking=no \ + $EC2_USER_NAME@$EC2_INSTANCE_IP:~/$SRC_DIR/pytest-mujoco.log \ + $CODEBUILD_SRC_DIR/ || true + echo "===== Mujoco pytest log =====" + cat $CODEBUILD_SRC_DIR/pytest-mujoco.log || true + EXIT_CODE=1 + fi - echo "====Running tests on EC2 instance...====" - ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no $EC2_USER_NAME@$EC2_INSTANCE_IP " + if [ "${SAPIEN3_TEST_EXIT_CODE:-0}" -ne 0 ]; then + echo "=== Sapien3 tests failed. Fetching logs... ===" + scp -o ConnectTimeout=10 -o StrictHostKeyChecking=no \ + $EC2_USER_NAME@$EC2_INSTANCE_IP:~/$SRC_DIR/pytest-sapien3.log \ + $CODEBUILD_SRC_DIR/ || true + echo "===== Sapien3 pytest log =====" + cat $CODEBUILD_SRC_DIR/pytest-sapien3.log || true + EXIT_CODE=1 + fi - set -euo pipefail + if [ "${ISAACSIM_TEST_EXIT_CODE:-0}" -ne 0 ]; then + echo "=== IsaacSim tests failed. Fetching logs... ===" + scp -o ConnectTimeout=10 -o StrictHostKeyChecking=no \ + $EC2_USER_NAME@$EC2_INSTANCE_IP:~/$SRC_DIR/pytest-isaacsim.log \ + $CODEBUILD_SRC_DIR/ || true + echo "===== IsaacSim pytest log =====" + cat $CODEBUILD_SRC_DIR/pytest-isaacsim.log || true + EXIT_CODE=1 + fi - # Login to ECR using token from CodeBuild - echo \"$ECR_LOGIN_TOKEN\" | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com + if [ "${ISAACGYM_TEST_EXIT_CODE:-0}" -ne 0 ]; then + echo "=== IsaacGym tests failed. Fetching logs... ===" + scp -o ConnectTimeout=10 -o StrictHostKeyChecking=no \ + $EC2_USER_NAME@$EC2_INSTANCE_IP:~/$SRC_DIR/pytest-isaacgym.log \ + $CODEBUILD_SRC_DIR/ || true + echo "===== IsaacGym pytest log =====" + cat $CODEBUILD_SRC_DIR/pytest-isaacgym.log || true + EXIT_CODE=1 + fi - # Configure BuildKit environment - export DOCKER_BUILDKIT=1 - export BUILDKIT_INLINE_CACHE=1 + if [ "$EXIT_CODE" -ne 0 ]; then + echo "Tests failed with exit code $EXIT_CODE" + exit 1 + else + echo "===All tests passed!===" + fi - docker buildx create --name metasim-builder --driver docker-container \ - --driver-opt env.AWS_REGION=$REGION \ - --driver-opt env.AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \ - --driver-opt env.AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \ - --bootstrap - docker buildx use metasim-builder + ########### Postbuild ######### + - name: post_build + if: always() # always try to terminate the instance + run: | + echo "Cleaning up resources..." + if [ ! -z "$INSTANCE_ID" ]; then + echo "Terminating EC2 instance $INSTANCE_ID..." + aws ec2 terminate-instances --instance-ids $INSTANCE_ID --region $REGION || true + fi - cd \"\$HOME/${SRC_DIR}\" + - name: Prepare test logs for upload + if: always() + run: | + # Copy test logs from CODEBUILD_SRC_DIR to workspace root for artifact upload + if [ -d "$CODEBUILD_SRC_DIR" ]; then + cp -v $CODEBUILD_SRC_DIR/pytest-*.log . 2>/dev/null || echo "No pytest logs found" + cp -v $CODEBUILD_SRC_DIR/test_exit_codes.txt . 2>/dev/null || echo "No exit codes file found" + else + echo "CODEBUILD_SRC_DIR not set, files should already be in workspace" + fi - # docker build - if docker pull "$IMAGE_URI" 2>/dev/null ; then - echo "Image $IMAGE_URI already exists. Skipping build." + - name: Upload test logs as artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-logs + path: | + pytest-*.log + test_exit_codes.txt + if-no-files-found: warn + retention-days: 7 + + - name: Comment result to PR + if: always() + uses: actions/github-script@v7 + env: + JOB_STATUS: ${{ job.status }} + with: + script: | + const fs = require('fs'); + + const status = process.env.JOB_STATUS; // success / failure / cancelled + const ref = context.ref; // refs/heads/ci/pr-123 or merge_group scenario + + // only reply comments to branches ci/pr-* branches, skip commenting in merge_group runs + const match = ref.match(/^refs\/heads\/ci\/pr-(\d+)$/); + if (!match) { + core.info(`Ref ${ref} is not a ci/pr-* branch, skip PR comment.`); + return; + } + + const prNumber = Number(match[1]); + + let emoji, msg; + if (status === 'success') { + emoji = '✅'; + msg = `Privileged CI passed on branch \`${ref.split('/').pop()}\`.`; + } else if (status === 'cancelled') { + emoji = '⚪️'; + msg = `Privileged CI was cancelled on branch \`${ref.split('/').pop()}\`.`; + } else { + emoji = '❌'; + msg = `Privileged CI failed on branch \`${ref.split('/').pop()}\`.`; + } + + const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const artifactUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}#artifacts`; + + // Read test exit codes if available (copied to workspace by previous step) + let testSummary = ''; + try { + const exitCodesPath = 'test_exit_codes.txt'; + if (fs.existsSync(exitCodesPath)) { + const exitCodesContent = fs.readFileSync(exitCodesPath, 'utf8'); + const exitCodes = {}; + exitCodesContent.split('\n').forEach(line => { + const [key, value] = line.split('='); + if (key && value) exitCodes[key.trim()] = parseInt(value.trim()); + }); + + const testSuites = [ + { name: 'General', code: exitCodes['GENERAL_TEST_EXIT_CODE'] }, + { name: 'MuJoCo', code: exitCodes['MUJOCO_TEST_EXIT_CODE'] }, + { name: 'Sapien3', code: exitCodes['SAPIEN3_TEST_EXIT_CODE'] }, + { name: 'IsaacSim', code: exitCodes['ISAACSIM_TEST_EXIT_CODE'] }, + { name: 'IsaacGym', code: exitCodes['ISAACGYM_TEST_EXIT_CODE'] } + ]; + + testSummary = '\n\n**Test Suite Results:**\n'; + testSuites.forEach(suite => { + const icon = (suite.code === 0 || suite.code === undefined) ? '✅' : '❌'; + testSummary += `- ${icon} ${suite.name} tests\n`; + }); + + testSummary += `\n📦 [Download test logs](${artifactUrl})`; + } + } catch (error) { + core.warning(`Failed to read test exit codes: ${error.message}`); + } + + const body = `${emoji} ${msg}${testSummary}\n\n🔗 [View full details](${runUrl})`; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body + }); + - name: Clean up ci/pr-* branch + if: always() + run: | + REF="${GITHUB_REF}" + # Only delete if we're running on a ci/pr-* branch + if [[ "$REF" =~ ^refs/heads/ci/pr-[0-9]+$ ]]; then + BRANCH="${REF#refs/heads/}" + echo "Cleaning up temporary branch: $BRANCH" + # Delete the branch from remote (ignore errors if already deleted) + git push origin --delete "$BRANCH" 2>/dev/null || echo "Branch $BRANCH already deleted or not found" + echo "✓ Cleanup complete" else - echo "===Starting docker build.===" - docker buildx build --progress=plain --platform linux/amd64 \ - -t "$IMAGE_URI" \ - --cache-from type=registry,ref=$AWS_ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/$ECR_REPOSITORY:cache,mode=max \ - --cache-to type=registry,ref=$AWS_ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/$ECR_REPOSITORY:cache,mode=max \ - --build-arg DOCKER_UID=1000 \ - --build-arg DOCKER_GID=1000 \ - --build-arg DOCKER_USER=$EC2_USER_NAME \ - -f Dockerfile \ - --load . - - docker push "$IMAGE_URI" + echo "Not a ci/pr-* branch (ref: $REF), skipping cleanup" fi - # begin run test - GENERAL_TEST_EXIT_CODE=0 - MUJOCO_TEST_EXIT_CODE=0 - SAPIEN3_TEST_EXIT_CODE=0 - ISAACSIM_TEST_EXIT_CODE=0 - ISAACGYM_TEST_EXIT_CODE=0 - # run all test - # Run general tests (no simulator required) - docker run --rm --entrypoint bash --runtime=nvidia --network=host \ - --name metasim-autotest \ - --user 1000:1000 --privileged \ - -e LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu \ - -e ACCEPT_EULA=Y -e PRIVACY_CONSENT=Y -e OMNI_KIT_ACCEPT_EULA=YES \ - -v /usr/local/cuda:/usr/local/cuda \ - -v "$(pwd)":/home/$EC2_USER_NAME/RoboVerse \ - "$IMAGE_URI" \ - -c "bash -lc 'set -o pipefail; \ - /home/$EC2_USER_NAME/conda/envs/metasim/bin/python3 -m pytest -k general -vv \ - | tee /home/$EC2_USER_NAME/${SRC_DIR}/pytest-general.log'" \ - || GENERAL_TEST_EXIT_CODE=$? - - docker run --rm --entrypoint bash --runtime=nvidia --network=host \ - --name metasim-autotest \ - --user 1000:1000 --privileged \ - -e LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu \ - -e ACCEPT_EULA=Y -e PRIVACY_CONSENT=Y -e OMNI_KIT_ACCEPT_EULA=YES \ - -v /usr/local/cuda:/usr/local/cuda \ - -v "$(pwd)":/home/$EC2_USER_NAME/RoboVerse \ - "$IMAGE_URI" \ - -c "bash -lc 'set -o pipefail; \ - /home/$EC2_USER_NAME/conda/envs/metasim/bin/python3 -m pytest -k mujoco -vv \ - | tee /home/$EC2_USER_NAME/${SRC_DIR}/pytest-mujoco.log'" \ - || MUJOCO_TEST_EXIT_CODE=$? - - docker run --rm --entrypoint bash --runtime=nvidia --network=host \ - --name metasim-autotest \ - --user 1000:1000 --privileged \ - -e LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu \ - -e ACCEPT_EULA=Y -e PRIVACY_CONSENT=Y -e OMNI_KIT_ACCEPT_EULA=YES \ - -v /usr/local/cuda:/usr/local/cuda \ - -v "$(pwd)":/home/$EC2_USER_NAME/RoboVerse \ - "$IMAGE_URI" \ - -c "bash -lc 'set -o pipefail; \ - /home/$EC2_USER_NAME/conda/envs/metasim/bin/python3 -m pytest -k sapien3 -vv \ - | tee /home/$EC2_USER_NAME/${SRC_DIR}/pytest-sapien3.log'" \ - || SAPIEN3_TEST_EXIT_CODE=$? - - docker run --rm --entrypoint bash --runtime=nvidia --network=host \ - --name metasim-autotest \ - --user 1000:1000 --privileged \ - -e LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu \ - -e ACCEPT_EULA=Y -e PRIVACY_CONSENT=Y -e OMNI_KIT_ACCEPT_EULA=YES \ - -v /usr/local/cuda:/usr/local/cuda \ - -v "$(pwd)":/home/$EC2_USER_NAME/RoboVerse \ - "$IMAGE_URI" \ - -c "bash -lc 'set -o pipefail; \ - /home/$EC2_USER_NAME/conda/envs/metasim/bin/python3 -m pytest -k isaacsim -vv \ - | tee /home/$EC2_USER_NAME/${SRC_DIR}/pytest-isaacsim.log'" \ - || ISAACSIM_TEST_EXIT_CODE=$? - - docker run --rm --entrypoint bash --runtime=nvidia --network=host \ - --name metasim-autotest \ - --user 1000:1000 --privileged \ - -e LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu \ - -e ACCEPT_EULA=Y -e PRIVACY_CONSENT=Y -e OMNI_KIT_ACCEPT_EULA=YES \ - -v /usr/local/cuda:/usr/local/cuda \ - -v "$(pwd)":/home/$EC2_USER_NAME/RoboVerse \ - "$IMAGE_URI" \ - -c "bash -lc 'set -o pipefail; \ - /home/$EC2_USER_NAME/conda/envs/metasim_isaacgym/bin/python3 /home/$EC2_USER_NAME/RoboVerse/metasim/test/isaacgym_entry.py -k isaacgym -vv \ - | tee /home/$EC2_USER_NAME/${SRC_DIR}/pytest-isaacgym.log'" \ - || ISAACGYM_TEST_EXIT_CODE=$? - - # TODO check if test_exit_code necessary - touch ~/$SRC_DIR/test_exit_codes.txt - { - echo \"GENERAL_TEST_EXIT_CODE=\$GENERAL_TEST_EXIT_CODE\" - echo \"MUJOCO_TEST_EXIT_CODE=\$MUJOCO_TEST_EXIT_CODE\" - echo \"SAPIEN3_TEST_EXIT_CODE=\$SAPIEN3_TEST_EXIT_CODE\" - echo \"ISAACSIM_TEST_EXIT_CODE=\$ISAACSIM_TEST_EXIT_CODE\" - echo \"ISAACGYM_TEST_EXIT_CODE=\$ISAACGYM_TEST_EXIT_CODE\" - } > ~/${SRC_DIR}/test_exit_codes.txt - " || { echo "Test execution failed"; exit 1; } - - echo "===Copying test reports...===" - scp -o ConnectTimeout=10 -o StrictHostKeyChecking=no $EC2_USER_NAME@$EC2_INSTANCE_IP:~/$SRC_DIR/test_exit_codes.txt $CODEBUILD_SRC_DIR/ - - source $CODEBUILD_SRC_DIR/test_exit_codes.txt - - echo "General test exit code: ${GENERAL_TEST_EXIT_CODE}" - echo "Mujoco test exit code: ${MUJOCO_TEST_EXIT_CODE}" - echo "Sapien3 test exit code: ${SAPIEN3_TEST_EXIT_CODE}" - echo "IsaacSim test exit code: ${ISAACSIM_TEST_EXIT_CODE}" - echo "IsaacGym test exit code: ${ISAACGYM_TEST_EXIT_CODE}" - - EXIT_CODE=0 - - if [ "${GENERAL_TEST_EXIT_CODE:-0}" -ne 0 ]; then - echo "=== General tests failed. Fetching logs... ===" - scp -o ConnectTimeout=10 -o StrictHostKeyChecking=no \ - $EC2_USER_NAME@$EC2_INSTANCE_IP:~/$SRC_DIR/pytest-general.log \ - $CODEBUILD_SRC_DIR/ || true - echo "===== General pytest log =====" - cat $CODEBUILD_SRC_DIR/pytest-general.log || true - EXIT_CODE=1 - fi - - if [ "${MUJOCO_TEST_EXIT_CODE:-0}" -ne 0 ]; then - echo "=== Mujoco tests failed. Fetching logs... ===" - scp -o ConnectTimeout=10 -o StrictHostKeyChecking=no \ - $EC2_USER_NAME@$EC2_INSTANCE_IP:~/$SRC_DIR/pytest-mujoco.log \ - $CODEBUILD_SRC_DIR/ || true - echo "===== Mujoco pytest log =====" - cat $CODEBUILD_SRC_DIR/pytest-mujoco.log || true - EXIT_CODE=1 - fi - - if [ "${SAPIEN3_TEST_EXIT_CODE:-0}" -ne 0 ]; then - echo "=== Sapien3 tests failed. Fetching logs... ===" - scp -o ConnectTimeout=10 -o StrictHostKeyChecking=no \ - $EC2_USER_NAME@$EC2_INSTANCE_IP:~/$SRC_DIR/pytest-sapien3.log \ - $CODEBUILD_SRC_DIR/ || true - echo "===== Sapien3 pytest log =====" - cat $CODEBUILD_SRC_DIR/pytest-sapien3.log || true - EXIT_CODE=1 - fi - - if [ "${ISAACSIM_TEST_EXIT_CODE:-0}" -ne 0 ]; then - echo "=== IsaacSim tests failed. Fetching logs... ===" - scp -o ConnectTimeout=10 -o StrictHostKeyChecking=no \ - $EC2_USER_NAME@$EC2_INSTANCE_IP:~/$SRC_DIR/pytest-isaacsim.log \ - $CODEBUILD_SRC_DIR/ || true - echo "===== IsaacSim pytest log =====" - cat $CODEBUILD_SRC_DIR/pytest-isaacsim.log || true - EXIT_CODE=1 - fi - - if [ "${ISAACGYM_TEST_EXIT_CODE:-0}" -ne 0 ]; then - echo "=== IsaacGym tests failed. Fetching logs... ===" - scp -o ConnectTimeout=10 -o StrictHostKeyChecking=no \ - $EC2_USER_NAME@$EC2_INSTANCE_IP:~/$SRC_DIR/pytest-isaacgym.log \ - $CODEBUILD_SRC_DIR/ || true - echo "===== IsaacGym pytest log =====" - cat $CODEBUILD_SRC_DIR/pytest-isaacgym.log || true - EXIT_CODE=1 - fi - - if [ "$EXIT_CODE" -ne 0 ]; then - echo "Tests failed with exit code $EXIT_CODE" - exit 1 - else - echo "===All tests passed!===" - fi - - ########### Postbuild ######### - - name: post_build - if: always() # always try to terminate the instance - run: | - echo "Cleaning up resources..." - if [ ! -z "$INSTANCE_ID" ]; then - echo "Terminating EC2 instance $INSTANCE_ID..." - aws ec2 terminate-instances --instance-ids $INSTANCE_ID --region $REGION || true - fi + fork-merge-group-placeholder: + needs: prepare-or-promote + if: needs.prepare-or-promote.outputs.reason == 'merge_group_contains_fork' + runs-on: ubuntu-latest + steps: + - name: Skip privileged tests for fork merge group + run: | + echo "merge_group contains fork PR(s)." + echo "Skipping MetaSim EC2 tests on merge_group and relying on ci/pr-* runs for privileged CI." diff --git a/docs/source/dataset_benchmark/dataset/robots.md b/docs/source/dataset_benchmark/dataset/robots.md index 90063e9df..0a4ef323d 100644 --- a/docs/source/dataset_benchmark/dataset/robots.md +++ b/docs/source/dataset_benchmark/dataset/robots.md @@ -19,3 +19,18 @@ RoboVerse currently includes some robots. | UR5e | Arm | 6 | ur5e_2f85 | | Walker | Bipedal | 6 | walker | | Ant | Quadreuped | 12 | ant | + +## Dexterous Hands + +RoboVerse includes support for various dexterous hands for manipulation tasks. + +| Robot Name | Number of DoFs | Config Name | Notes | +| ------ | ---------------- | ------------ | ----- | +| Allegro Hand | 16 | allegrohand | 4-finger anthropomorphic hand | +| BrainCo Hand (Left) | 11 | brainco_hand_left | 6 actuated + 5 mimic/passive joints, prosthetic hand | +| BrainCo Hand (Right) | 11 | brainco_hand_right | 6 actuated + 5 mimic/passive joints, prosthetic hand | +| Inspire Hand (Left) | 12 | inspire_hand_left | 6 actuated + 6 mimic/passive (coupled) joints | +| Inspire Hand (Right) | 12 | inspire_hand_right | 6 actuated + 6 mimic/passive (coupled) joints | +| PSIHand (Left) | 21 | psihand_left | All 21 joints actuated. Known issues with IsaacGym, use MuJoCo/Genesis | +| PSIHand (Right) | 21 | psihand_right | All 21 joints actuated. Known issues with IsaacGym, use MuJoCo/Genesis | +| XHand | 12 | xhand | Compact dexterous hand | diff --git a/get_started/dexhands/1_diff_dex_hand.py b/get_started/dexhands/1_diff_dex_hand.py new file mode 100644 index 000000000..95cbefd6a --- /dev/null +++ b/get_started/dexhands/1_diff_dex_hand.py @@ -0,0 +1,216 @@ +"""Direct control of Inspire Hand - Stable version based on random_action_notik.py""" + +from __future__ import annotations + +import os +from dataclasses import dataclass +from typing import Literal + +import tyro +from loguru import logger as log +from rich.logging import RichHandler + +log.configure(handlers=[{"sink": RichHandler(), "format": "{message}"}]) + +try: + import isaacgym # noqa: F401 +except ImportError: + pass + +import torch + +from metasim.scenario.cameras import PinholeCameraCfg +from metasim.scenario.scenario import ScenarioCfg +from metasim.utils.obs_utils import ObsSaver +from metasim.utils.setup_util import get_handler, get_robot + + +@dataclass +class Args: + """Arguments. + + NOTE: PSIHand robots (psihand_left/psihand_right) have known compatibility issues with IsaacGym. + Use MuJoCo, Genesis, or other simulators for PSIHand instead. + """ + + robot: str = "inspire_hand_left" # or "inspire_hand_right, brainco_hand_right/left, psihand_left/right" + sim: Literal["isaaclab", "isaacgym", "genesis", "pybullet", "mujoco", "sapien2", "sapien3"] = "mujoco" + num_envs: int = 1 + num_steps: int = 10 # Number of different random poses + + +def main(): + args = tyro.cli(Args) + log.info(f"Args: {args}") + + # Get robot configuration + robot = get_robot(args.robot) + + # Create scenario configuration with custom dt + scenario = ScenarioCfg( + robots=[robot], + cameras=[ + PinholeCameraCfg( + width=640, + height=480, + pos=(0.2, -0.3, 0.2), # Front-facing camera position + look_at=(0.0, 0.0, 0.1), # Looking at hand center + ) + ], + simulator=args.sim, + num_envs=args.num_envs, + ) + + # Set simulation timestep to 0.002 seconds (2ms per step) - MuJoCo default + # Smaller timestep = more stable physics for complex models with constraints + scenario.sim_params.dt = 0.002 + + # Create handler + handler = get_handler(scenario) + + # Set initial pose for IsaacGym to lift hand above ground + # Only set position, not rotation, to avoid NaN issues with some robots + # MuJoCo uses MJCF-defined pose to avoid constraint violations + if args.sim == "isaacgym": + init_states = [ + { + "robots": { + robot.name: { + "pos": torch.tensor([0.0, 0.0, 0.05]), + "rot": torch.tensor([-0.7071, 0.7071, 0.0, 0.0]), + } + }, + "objects": {}, # Empty objects dict required by IsaacGym handler + } + ] + handler.set_states(init_states) + + # Get initial states + states = handler.get_states(mode="tensor") + + # Create output directory and initialize ObsSaver + os.makedirs("get_started/output/dexhands", exist_ok=True) + obs_saver = ObsSaver(video_path=f"get_started/output/dexhands/1_diff_dex_hand_{args.sim}_{args.robot}.mp4") + obs_saver.add(states) + + log.info(f"Robot: {robot.name}") + log.info(f"Number of joints: {len(robot.actuators)}") + log.info(f"Joint names: {list(robot.actuators.keys())}") + + # Get all joint names from the handler (actual order in simulation) + all_joint_names_from_handler = handler.get_joint_names(robot.name, sort=True) + log.info(f"\nJoint names from handler (actual sim order): {all_joint_names_from_handler}") + log.info(f"Number of joints in handler: {len(all_joint_names_from_handler)}") + + # Identify which joints are actuated (not passive) + # CRITICAL: Only select joints where fully_actuated is explicitly True + # This excludes both fully_actuated=False (passive joints) and any None values + actuated_joints = [name for name in all_joint_names_from_handler if robot.actuators[name].fully_actuated is True] + log.info(f"\nActuated joints (controllable): {len(actuated_joints)}") + log.info(f"Actuated joint names: {actuated_joints}") + + # Identify passive joints + passive_joints = [name for name in all_joint_names_from_handler if robot.actuators[name].fully_actuated is False] + log.info(f"\nPassive joints (coupled/mimic): {len(passive_joints)}") + log.info(f"Passive joint names: {passive_joints}") + + # Get joint limits for ONLY actuated joints + j_names_actuated = actuated_joints + j_limits = torch.tensor( + [[robot.joint_limits[name][0], robot.joint_limits[name][1]] for name in j_names_actuated], device="cuda:0" + ) + j_ranges = j_limits[:, 1] - j_limits[:, 0] + j_centers = (j_limits[:, 0] + j_limits[:, 1]) / 2 + + num_actuators = len(j_names_actuated) # Only actuated joints + num_envs = args.num_envs + + log.info("\nStarting random control demo...") + log.info(f"Will generate {args.num_steps} different random poses") + log.info("Each pose will be held for 60 simulation steps\n") + + # Run simulation + for step_i in range(args.num_steps): + log.info("=" * 60) + log.info(f"Step {step_i + 1}/{args.num_steps}: Generating new random pose...") + + # Generate random joint positions (only for actuated joints) + random_offset = (torch.rand((num_envs, num_actuators), device="cuda:0") - 0.5) * 0.6 # 60% of range + q_actuated = j_centers.unsqueeze(0) + random_offset * j_ranges.unsqueeze(0) + q_actuated = torch.clamp(q_actuated, j_limits[:, 0].unsqueeze(0), j_limits[:, 1].unsqueeze(0)) + + # For MuJoCo with equality constraints, we need to set passive joint targets + # to match the expected positions from the mimic relationships. + # This prevents conflicts between actuator control and equality constraints. + # The key is: set passive joints to their COUPLED values, not arbitrary values. + + actions = [] + for i_env in range(num_envs): + # Start with actuated joints + dof_targets = dict(zip(j_names_actuated, q_actuated[i_env].tolist())) + + # Calculate passive joint targets based on mimic relationships + # This is needed for ALL simulators to ensure consistency + # Thumb: intermediate = pitch * 1.6, distal = pitch * 2.4 + # Fingers: intermediate = proximal * 1.0 + for passive_joint in passive_joints: + if "thumb_intermediate" in passive_joint: + # Find the pitch joint for this hand + pitch_joint = passive_joint.replace("intermediate", "proximal_pitch") + dof_targets[passive_joint] = dof_targets.get(pitch_joint, 0.0) * 1.6 + elif "thumb_distal" in passive_joint: + pitch_joint = passive_joint.replace("distal", "proximal_pitch") + dof_targets[passive_joint] = dof_targets.get(pitch_joint, 0.0) * 2.4 + elif "intermediate" in passive_joint: + # For index, middle, ring, pinky fingers + proximal_joint = passive_joint.replace("intermediate", "proximal") + dof_targets[passive_joint] = dof_targets.get(proximal_joint, 0.0) * 1.0 + else: + # Fallback to default position + dof_targets[passive_joint] = robot.default_joint_positions[passive_joint] + + actions.append({robot.name: {"dof_pos_target": dof_targets}}) + + # Log joint positions (only actuated joints) + log.info("Target joint positions (actuated only):") + for i, (name, value) in enumerate(zip(j_names_actuated[:3], q_actuated[0][:3].tolist())): + log.info(f" {name}: {value:.3f}") + log.info(" ... (showing first 3 actuated joints)") + + # Hold this pose for 60 steps (~0.24 seconds at dt=0.004) + # Testing shows joints converge within 40 steps, so 60 is sufficient + for hold_step in range(60): + handler.set_dof_targets(actions) + handler.simulate() + + # Debug: check actual joint positions at key steps + if hold_step in [0, 30, 59]: # Check at start, middle, and end + log.info(f" Holding pose... step {hold_step}/60") + # Get current state to see actual joint positions + current_states = handler.get_states(mode="tensor") + # Access through robots dict -> RobotState -> joint_pos + actual_q_all = current_states.robots[robot.name].joint_pos[0] # All joints + + # Show only actuated joints for comparison + actuated_indices = [all_joint_names_from_handler.index(name) for name in j_names_actuated] + actual_q_actuated = actual_q_all[actuated_indices] + + log.info(f" First 3 actuated joints - Actual: {actual_q_actuated[:3].tolist()}") + log.info(f" First 3 actuated joints - Target: {q_actuated[0][:3].tolist()}") + + # Save observation only at the final step of each pose for concise video + if hold_step == 59: + obs_saver.add(current_states) + + log.info("\n" + "=" * 60) + log.info("Demo completed successfully!") + log.info("Saving video...") + obs_saver.save() + log.info(f"Video saved to: get_started/output/dexhands/1_diff_dex_hand_{args.sim}_{args.robot}.mp4") + log.info("Closing simulation...") + handler.close() + log.info("Done!") + + +if __name__ == "__main__": + main() diff --git a/roboverse_pack/robots/__init__.py b/roboverse_pack/robots/__init__.py index 7654564a5..b892ee7d6 100644 --- a/roboverse_pack/robots/__init__.py +++ b/roboverse_pack/robots/__init__.py @@ -6,6 +6,8 @@ from .aloha import AlohaCfg from .ant_cfg import AntCfg from .anymal_cfg import AnymalCfg +from .brainco_hand_left_cfg import BraincoHandLeftCfg +from .brainco_hand_right_cfg import BraincoHandRightCfg from .cartpole_cfg import CartpoleCfg from .fetch_cfg import FetchCfg from .franka_cfg import FrankaCfg @@ -26,10 +28,14 @@ from .h1_wrist_cfg import H1WristCfg from .iiwa14_cfg import Iiwa14Cfg from .iiwa_cfg import IiwaCfg +from .inspire_hand_left_cfg import InspireHandLeftCfg +from .inspire_hand_right_cfg import InspireHandRightCfg from .kinova_gen3_cfg import KinovaGen3Cfg from .kinova_gen3_robotiq_2f85 import KinovaGen3Robotiq2f85Cfg from .koch_mjcf_cfg import KochCfg from .lite6_mjcf_cfg import Lite6Cfg +from .psihand_left_cfg import PsihandLeftCfg +from .psihand_right_cfg import PsihandRightCfg from .sawyer_cfg import SawyerCfg from .sawyer_mujoco_cfg import SawyerMujocoCfg from .shadow_hand_cfg import ShadowHandCfg diff --git a/roboverse_pack/robots/brainco_hand_left_cfg.py b/roboverse_pack/robots/brainco_hand_left_cfg.py new file mode 100644 index 000000000..eac23f695 --- /dev/null +++ b/roboverse_pack/robots/brainco_hand_left_cfg.py @@ -0,0 +1,128 @@ +"""Configuration for BrainCo Hand Left.""" + +from __future__ import annotations + +from typing import Literal + +from metasim.scenario.robot import BaseActuatorCfg, RobotCfg +from metasim.utils import configclass + + +@configclass +class BraincoHandLeftCfg(RobotCfg): + """Configuration for BrainCo Hand Left. + + Joint structure: + - 11 revolute joints total + - 6 independent joints (actively controlled) + - 5 mimic joints (passively follow independent joints) + + Independent joints: + - left_thumb_metacarpal_joint: Thumb base rotation + - left_thumb_proximal_joint: Thumb proximal flexion + - left_index_proximal_joint: Index finger flexion + - left_middle_proximal_joint: Middle finger flexion + - left_ring_proximal_joint: Ring finger flexion + - left_pinky_proximal_joint: Pinky finger flexion + + Mimic joints (passive): + - left_thumb_distal_joint: Mimics left_thumb_proximal_joint (multiplier=1.0) + - left_index_distal_joint: Mimics left_index_proximal_joint (multiplier=1.155) + - left_middle_distal_joint: Mimics left_middle_proximal_joint (multiplier=1.155) + - left_ring_distal_joint: Mimics left_ring_proximal_joint (multiplier=1.155) + - left_pinky_distal_joint: Mimics left_pinky_proximal_joint (multiplier=1.155) + """ + + name: str = "brainco_hand_left" + num_joints: int = 11 # Total revolute joints + fix_base_link: bool = True + urdf_path: str = "roboverse_data/robots/brainco_hand/urdf/brainco_left.urdf" + mjcf_path: str = "roboverse_data/robots/brainco_hand/mjcf/brainco_left.xml" + enabled_gravity: bool = False + enabled_self_collisions: bool = False + + # Isaac Gym specific settings + isaacgym_flip_visual_attachments: bool = False + collapse_fixed_joints: bool = False + + # Set initial pose for the hand (vertical orientation) + default_position: tuple[float, float, float] = (0.0, 0.0, 0.15) # 15cm above ground + default_orientation: tuple[float, float, float, float] = ( + 0.7071, + -0.7071, + 0.0, + 0.0, + ) # (w, x, y, z) - rotated -90° around X axis + + actuators: dict[str, BaseActuatorCfg] = { + # Thumb (2 actuated + 1 passive = 3 joints) + "left_thumb_metacarpal_joint": BaseActuatorCfg(stiffness=10.0, damping=1.0), + "left_thumb_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=1.0), + "left_thumb_distal_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + # Index finger (1 actuated + 1 passive = 2 joints) + "left_index_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=1.0), + "left_index_distal_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + # Middle finger (1 actuated + 1 passive = 2 joints) + "left_middle_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=1.0), + "left_middle_distal_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + # Ring finger (1 actuated + 1 passive = 2 joints) + "left_ring_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=1.0), + "left_ring_distal_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + # Pinky finger (1 actuated + 1 passive = 2 joints) + "left_pinky_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=1.0), + "left_pinky_distal_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + } + + # Joint limits from MJCF (in radians) + joint_limits: dict[str, tuple[float, float]] = { + # Thumb + "left_thumb_metacarpal_joint": (0.0, 1.5184), + "left_thumb_proximal_joint": (0.0, 1.0472), + "left_thumb_distal_joint": (0.0, 1.0472), # Passive, same range as proximal + # Index finger + "left_index_proximal_joint": (0.0, 1.4661), + "left_index_distal_joint": (0.0, 1.693), # Passive, x 1.155 + # Middle finger + "left_middle_proximal_joint": (0.0, 1.4661), + "left_middle_distal_joint": (0.0, 1.693), # Passive, x 1.155 + # Ring finger + "left_ring_proximal_joint": (0.0, 1.4661), + "left_ring_distal_joint": (0.0, 1.693), # Passive, x 1.155 + # Pinky finger + "left_pinky_proximal_joint": (0.0, 1.4661), + "left_pinky_distal_joint": (0.0, 1.693), # Passive, x 1.155 + } + + default_joint_positions: dict[str, float] = { + # Thumb - slightly flexed + "left_thumb_metacarpal_joint": 0.2, + "left_thumb_proximal_joint": 0.1, + "left_thumb_distal_joint": 0.1, # mimic x 1.0 + # Index finger - slightly flexed + "left_index_proximal_joint": 0.2, + "left_index_distal_joint": 0.231, # mimic x 1.155 + # Middle finger - slightly flexed + "left_middle_proximal_joint": 0.2, + "left_middle_distal_joint": 0.231, # mimic x 1.155 + # Ring finger - slightly flexed + "left_ring_proximal_joint": 0.2, + "left_ring_distal_joint": 0.231, # mimic x 1.155 + # Pinky finger - slightly flexed + "left_pinky_proximal_joint": 0.2, + "left_pinky_distal_joint": 0.231, # mimic x 1.155 + } + + control_type: dict[str, Literal["position", "effort"]] = { + # All joints use position control + "left_thumb_metacarpal_joint": "position", + "left_thumb_proximal_joint": "position", + "left_thumb_distal_joint": "position", + "left_index_proximal_joint": "position", + "left_index_distal_joint": "position", + "left_middle_proximal_joint": "position", + "left_middle_distal_joint": "position", + "left_ring_proximal_joint": "position", + "left_ring_distal_joint": "position", + "left_pinky_proximal_joint": "position", + "left_pinky_distal_joint": "position", + } diff --git a/roboverse_pack/robots/brainco_hand_right_cfg.py b/roboverse_pack/robots/brainco_hand_right_cfg.py new file mode 100644 index 000000000..73673ec80 --- /dev/null +++ b/roboverse_pack/robots/brainco_hand_right_cfg.py @@ -0,0 +1,128 @@ +"""Configuration for BrainCo Hand Right.""" + +from __future__ import annotations + +from typing import Literal + +from metasim.scenario.robot import BaseActuatorCfg, RobotCfg +from metasim.utils import configclass + + +@configclass +class BraincoHandRightCfg(RobotCfg): + """Configuration for BrainCo Hand Right. + + Joint structure: + - 11 revolute joints total + - 6 independent joints (actively controlled) + - 5 mimic joints (passively follow independent joints) + + Independent joints: + - right_thumb_metacarpal_joint: Thumb base rotation + - right_thumb_proximal_joint: Thumb proximal flexion + - right_index_proximal_joint: Index finger flexion + - right_middle_proximal_joint: Middle finger flexion + - right_ring_proximal_joint: Ring finger flexion + - right_pinky_proximal_joint: Pinky finger flexion + + Mimic joints (passive): + - right_thumb_distal_joint: Mimics right_thumb_proximal_joint (multiplier=1.0) + - right_index_distal_joint: Mimics right_index_proximal_joint (multiplier=1.155) + - right_middle_distal_joint: Mimics right_middle_proximal_joint (multiplier=1.155) + - right_ring_distal_joint: Mimics right_ring_proximal_joint (multiplier=1.155) + - right_pinky_distal_joint: Mimics right_pinky_proximal_joint (multiplier=1.155) + """ + + name: str = "brainco_hand_right" + num_joints: int = 11 # Total revolute joints + fix_base_link: bool = True + urdf_path: str = "roboverse_data/robots/brainco_hand/urdf/brainco_right.urdf" + mjcf_path: str = "roboverse_data/robots/brainco_hand/mjcf/brainco_right.xml" + enabled_gravity: bool = False + enabled_self_collisions: bool = False + + # Isaac Gym specific settings + isaacgym_flip_visual_attachments: bool = False + collapse_fixed_joints: bool = False + + # Set initial pose for the hand (vertical orientation) + default_position: tuple[float, float, float] = (0.0, 0.0, 0.15) # 15cm above ground + default_orientation: tuple[float, float, float, float] = ( + 0.7071, + -0.7071, + 0.0, + 0.0, + ) # (w, x, y, z) - rotated -90° around X axis + + actuators: dict[str, BaseActuatorCfg] = { + # Thumb (2 actuated + 1 passive = 3 joints) + "right_thumb_metacarpal_joint": BaseActuatorCfg(stiffness=10.0, damping=1.0), + "right_thumb_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=1.0), + "right_thumb_distal_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + # Index finger (1 actuated + 1 passive = 2 joints) + "right_index_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=1.0), + "right_index_distal_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + # Middle finger (1 actuated + 1 passive = 2 joints) + "right_middle_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=1.0), + "right_middle_distal_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + # Ring finger (1 actuated + 1 passive = 2 joints) + "right_ring_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=1.0), + "right_ring_distal_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + # Pinky finger (1 actuated + 1 passive = 2 joints) + "right_pinky_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=1.0), + "right_pinky_distal_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + } + + # Joint limits from MJCF (in radians) + joint_limits: dict[str, tuple[float, float]] = { + # Thumb + "right_thumb_metacarpal_joint": (0.0, 1.5184), + "right_thumb_proximal_joint": (0.0, 1.0472), + "right_thumb_distal_joint": (0.0, 1.0472), # Passive, same range as proximal + # Index finger + "right_index_proximal_joint": (0.0, 1.4661), + "right_index_distal_joint": (0.0, 1.693), # Passive, x 1.155 + # Middle finger + "right_middle_proximal_joint": (0.0, 1.4661), + "right_middle_distal_joint": (0.0, 1.693), # Passive, x 1.155 + # Ring finger + "right_ring_proximal_joint": (0.0, 1.4661), + "right_ring_distal_joint": (0.0, 1.693), # Passive, x 1.155 + # Pinky finger + "right_pinky_proximal_joint": (0.0, 1.4661), + "right_pinky_distal_joint": (0.0, 1.693), # Passive, x 1.155 + } + + default_joint_positions: dict[str, float] = { + # Thumb - slightly flexed + "right_thumb_metacarpal_joint": 0.2, + "right_thumb_proximal_joint": 0.1, + "right_thumb_distal_joint": 0.1, # mimic x 1.0 + # Index finger - slightly flexed + "right_index_proximal_joint": 0.2, + "right_index_distal_joint": 0.231, # mimic x 1.155 + # Middle finger - slightly flexed + "right_middle_proximal_joint": 0.2, + "right_middle_distal_joint": 0.231, # mimic x 1.155 + # Ring finger - slightly flexed + "right_ring_proximal_joint": 0.2, + "right_ring_distal_joint": 0.231, # mimic x 1.155 + # Pinky finger - slightly flexed + "right_pinky_proximal_joint": 0.2, + "right_pinky_distal_joint": 0.231, # mimic x 1.155 + } + + control_type: dict[str, Literal["position", "effort"]] = { + # All joints use position control + "right_thumb_metacarpal_joint": "position", + "right_thumb_proximal_joint": "position", + "right_thumb_distal_joint": "position", + "right_index_proximal_joint": "position", + "right_index_distal_joint": "position", + "right_middle_proximal_joint": "position", + "right_middle_distal_joint": "position", + "right_ring_proximal_joint": "position", + "right_ring_distal_joint": "position", + "right_pinky_proximal_joint": "position", + "right_pinky_distal_joint": "position", + } diff --git a/roboverse_pack/robots/inspire_hand_left_cfg.py b/roboverse_pack/robots/inspire_hand_left_cfg.py new file mode 100644 index 000000000..fe5c31748 --- /dev/null +++ b/roboverse_pack/robots/inspire_hand_left_cfg.py @@ -0,0 +1,119 @@ +from __future__ import annotations + +from typing import Literal + +from metasim.scenario.robot import BaseActuatorCfg, RobotCfg +from metasim.utils import configclass + + +@configclass +class InspireHandLeftCfg(RobotCfg): + """Configuration for Inspire Hand Left. + + The Inspire Hand has 6 DOF (degrees of freedom) with 12 joints: + - 6 actuated joints (independent control) + - 6 passive joints (mechanically coupled to actuated joints) + + DOF breakdown: + - Thumb: 2 DOF (yaw + pitch), 2 passive (intermediate, distal) + - Index: 1 DOF (proximal), 1 passive (intermediate) + - Middle: 1 DOF (proximal), 1 passive (intermediate) + - Ring: 1 DOF (proximal), 1 passive (intermediate) + - Pinky: 1 DOF (proximal), 1 passive (intermediate) + """ + + name: str = "inspire_hand_left" + num_joints: int = 12 + fix_base_link: bool = True + urdf_path: str = "roboverse_data/robots/inspire_hand/urdf/inspire_hand_left.urdf" + mjcf_path: str = "roboverse_data/robots/inspire_hand/mjcf/inspire_hand_left.xml" + enabled_gravity: bool = False + enabled_self_collisions: bool = False + + # Isaac Gym specific settings + isaacgym_flip_visual_attachments: bool = False + collapse_fixed_joints: bool = False + + # NOTE: init_state commented out to use MJCF-defined initial pose + # MuJoCo's equality constraints (mimic joints) require initial state to satisfy constraints + # Using MJCF-defined pose avoids constraint violation and BADQACC errors + # init_state: dict = { + # "pos": (0.0, 0.0, 0.15), + # "rot": (0.7071, 0.7071, 0.0, 0.0), + # } + + actuators: dict[str, BaseActuatorCfg] = { + # Thumb (2 actuated + 2 passive = 4 joints) + "L_thumb_proximal_yaw_joint": BaseActuatorCfg(stiffness=10.0, damping=2.0), + "L_thumb_proximal_pitch_joint": BaseActuatorCfg(stiffness=10.0, damping=2.0), + "L_thumb_intermediate_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + "L_thumb_distal_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + # Index finger (1 actuated + 1 passive = 2 joints) + "L_index_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=2.0), + "L_index_intermediate_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + # Middle finger (1 actuated + 1 passive = 2 joints) + "L_middle_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=2.0), + "L_middle_intermediate_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + # Ring finger (1 actuated + 1 passive = 2 joints) + "L_ring_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=2.0), + "L_ring_intermediate_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + # Pinky finger (1 actuated + 1 passive = 2 joints) + "L_pinky_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=2.0), + "L_pinky_intermediate_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + } + + joint_limits: dict[str, tuple[float, float]] = { + # Thumb + "L_thumb_proximal_yaw_joint": (-0.1, 1.3), + "L_thumb_proximal_pitch_joint": (0.0, 0.5), + "L_thumb_intermediate_joint": (0.0, 0.8), + "L_thumb_distal_joint": (0.0, 1.2), + # Index finger + "L_index_proximal_joint": (0.0, 1.7), + "L_index_intermediate_joint": (0.0, 1.7), + # Middle finger + "L_middle_proximal_joint": (0.0, 1.7), + "L_middle_intermediate_joint": (0.0, 1.7), + # Ring finger + "L_ring_proximal_joint": (0.0, 1.7), + "L_ring_intermediate_joint": (0.0, 1.7), + # Pinky finger + "L_pinky_proximal_joint": (0.0, 1.7), + "L_pinky_intermediate_joint": (0.0, 1.7), + } + + default_joint_positions: dict[str, float] = { + # Thumb - slightly abducted and flexed + "L_thumb_proximal_yaw_joint": 0.3, + "L_thumb_proximal_pitch_joint": 0.1, + "L_thumb_intermediate_joint": 0.16, # mimic * 1.6 + "L_thumb_distal_joint": 0.24, # mimic * 2.4 + # Index finger - slightly flexed + "L_index_proximal_joint": 0.2, + "L_index_intermediate_joint": 0.2, # mimic * 1.0 + # Middle finger - slightly flexed + "L_middle_proximal_joint": 0.2, + "L_middle_intermediate_joint": 0.2, # mimic * 1.0 + # Ring finger - slightly flexed + "L_ring_proximal_joint": 0.2, + "L_ring_intermediate_joint": 0.2, # mimic * 1.0 + # Pinky finger - slightly flexed + "L_pinky_proximal_joint": 0.2, + "L_pinky_intermediate_joint": 0.2, # mimic * 1.0 + } + + control_type: dict[str, Literal["position", "effort"]] = { + # All joints use position control + "L_thumb_proximal_yaw_joint": "position", + "L_thumb_proximal_pitch_joint": "position", + "L_thumb_intermediate_joint": "position", + "L_thumb_distal_joint": "position", + "L_index_proximal_joint": "position", + "L_index_intermediate_joint": "position", + "L_middle_proximal_joint": "position", + "L_middle_intermediate_joint": "position", + "L_ring_proximal_joint": "position", + "L_ring_intermediate_joint": "position", + "L_pinky_proximal_joint": "position", + "L_pinky_intermediate_joint": "position", + } diff --git a/roboverse_pack/robots/inspire_hand_right_cfg.py b/roboverse_pack/robots/inspire_hand_right_cfg.py new file mode 100644 index 000000000..663429ddb --- /dev/null +++ b/roboverse_pack/robots/inspire_hand_right_cfg.py @@ -0,0 +1,119 @@ +from __future__ import annotations + +from typing import Literal + +from metasim.scenario.robot import BaseActuatorCfg, RobotCfg +from metasim.utils import configclass + + +@configclass +class InspireHandRightCfg(RobotCfg): + """Configuration for Inspire Hand Right. + + The Inspire Hand has 6 DOF (degrees of freedom) with 12 joints: + - 6 actuated joints (independent control) + - 6 passive joints (mechanically coupled to actuated joints) + + DOF breakdown: + - Thumb: 2 DOF (yaw + pitch), 2 passive (intermediate, distal) + - Index: 1 DOF (proximal), 1 passive (intermediate) + - Middle: 1 DOF (proximal), 1 passive (intermediate) + - Ring: 1 DOF (proximal), 1 passive (intermediate) + - Pinky: 1 DOF (proximal), 1 passive (intermediate) + """ + + name: str = "inspire_hand_right" + num_joints: int = 12 + fix_base_link: bool = True + urdf_path: str = "roboverse_data/robots/inspire_hand/urdf/inspire_hand_right.urdf" + mjcf_path: str = "roboverse_data/robots/inspire_hand/mjcf/inspire_hand_right.xml" + enabled_gravity: bool = False + enabled_self_collisions: bool = False + + # Isaac Gym specific settings + isaacgym_flip_visual_attachments: bool = False + collapse_fixed_joints: bool = False + + # NOTE: init_state commented out to use MJCF-defined initial pose + # MuJoCo's equality constraints (mimic joints) require initial state to satisfy constraints + # Using MJCF-defined pose avoids constraint violation and BADQACC errors + # init_state: dict = { + # "pos": (0.0, 0.0, 0.15), + # "rot": (0.7071, 0.7071, 0.0, 0.0), + # } + + actuators: dict[str, BaseActuatorCfg] = { + # Thumb (2 actuated + 2 passive = 4 joints) + "R_thumb_proximal_yaw_joint": BaseActuatorCfg(stiffness=10.0, damping=2.0), + "R_thumb_proximal_pitch_joint": BaseActuatorCfg(stiffness=10.0, damping=2.0), + "R_thumb_intermediate_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + "R_thumb_distal_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + # Index finger (1 actuated + 1 passive = 2 joints) + "R_index_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=2.0), + "R_index_intermediate_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + # Middle finger (1 actuated + 1 passive = 2 joints) + "R_middle_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=2.0), + "R_middle_intermediate_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + # Ring finger (1 actuated + 1 passive = 2 joints) + "R_ring_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=2.0), + "R_ring_intermediate_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + # Pinky finger (1 actuated + 1 passive = 2 joints) + "R_pinky_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=2.0), + "R_pinky_intermediate_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + } + + joint_limits: dict[str, tuple[float, float]] = { + # Thumb + "R_thumb_proximal_yaw_joint": (-0.1, 1.3), + "R_thumb_proximal_pitch_joint": (0.0, 0.5), + "R_thumb_intermediate_joint": (0.0, 0.8), + "R_thumb_distal_joint": (0.0, 1.2), + # Index finger + "R_index_proximal_joint": (0.0, 1.7), + "R_index_intermediate_joint": (0.0, 1.7), + # Middle finger + "R_middle_proximal_joint": (0.0, 1.7), + "R_middle_intermediate_joint": (0.0, 1.7), + # Ring finger + "R_ring_proximal_joint": (0.0, 1.7), + "R_ring_intermediate_joint": (0.0, 1.7), + # Pinky finger + "R_pinky_proximal_joint": (0.0, 1.7), + "R_pinky_intermediate_joint": (0.0, 1.7), + } + + default_joint_positions: dict[str, float] = { + # Thumb - slightly abducted and flexed + "R_thumb_proximal_yaw_joint": 0.3, + "R_thumb_proximal_pitch_joint": 0.1, + "R_thumb_intermediate_joint": 0.16, # mimic * 1.6 + "R_thumb_distal_joint": 0.24, # mimic * 2.4 + # Index finger - slightly flexed + "R_index_proximal_joint": 0.2, + "R_index_intermediate_joint": 0.2, # mimic * 1.0 + # Middle finger - slightly flexed + "R_middle_proximal_joint": 0.2, + "R_middle_intermediate_joint": 0.2, # mimic * 1.0 + # Ring finger - slightly flexed + "R_ring_proximal_joint": 0.2, + "R_ring_intermediate_joint": 0.2, # mimic * 1.0 + # Pinky finger - slightly flexed + "R_pinky_proximal_joint": 0.2, + "R_pinky_intermediate_joint": 0.2, # mimic * 1.0 + } + + control_type: dict[str, Literal["position", "effort"]] = { + # All joints use position control + "R_thumb_proximal_yaw_joint": "position", + "R_thumb_proximal_pitch_joint": "position", + "R_thumb_intermediate_joint": "position", + "R_thumb_distal_joint": "position", + "R_index_proximal_joint": "position", + "R_index_intermediate_joint": "position", + "R_middle_proximal_joint": "position", + "R_middle_intermediate_joint": "position", + "R_ring_proximal_joint": "position", + "R_ring_intermediate_joint": "position", + "R_pinky_proximal_joint": "position", + "R_pinky_intermediate_joint": "position", + } diff --git a/roboverse_pack/robots/psihand_left_cfg.py b/roboverse_pack/robots/psihand_left_cfg.py new file mode 100644 index 000000000..ca34c4abb --- /dev/null +++ b/roboverse_pack/robots/psihand_left_cfg.py @@ -0,0 +1,138 @@ +from __future__ import annotations + +from typing import Literal + +from metasim.scenario.robot import BaseActuatorCfg, RobotCfg +from metasim.utils import configclass + + +@configclass +class PsihandLeftCfg(RobotCfg): + """Configuration for PsiHand Left (Psi-SynHand). + + NOTE: This robot has known compatibility issues with IsaacGym: + - URDF mode: Joint positions become NaN, fingers not visible + - USD mode: DOF force tensor initialization fails + Recommended to use with MuJoCo, Genesis, or other simulators instead. + """ + + name: str = "psihand_left" + num_joints: int = 21 + fix_base_link: bool = True + urdf_path: str = "roboverse_data/robots/psihand/urdf/psihand_left.urdf" + mjcf_path: str = "roboverse_data/robots/psihand/mjcf/psihand_left.xml" + usd_path: str = "roboverse_data/robots/psihand/usd/psihand_left.usd" + enabled_gravity: bool = False + enabled_self_collisions: bool = False + + actuators: dict[str, BaseActuatorCfg] = { + # Thumb (5 joints) + "joint_1_1": BaseActuatorCfg(stiffness=1000, damping=100), + "joint_1_2": BaseActuatorCfg(stiffness=800, damping=80), + "joint_1_3": BaseActuatorCfg(stiffness=600, damping=60), + "joint_1_4": BaseActuatorCfg(stiffness=500, damping=50), + "joint_1_5": BaseActuatorCfg(stiffness=500, damping=50), + # Index finger (4 joints) + "joint_2_1": BaseActuatorCfg(stiffness=800, damping=80), + "joint_2_2": BaseActuatorCfg(stiffness=700, damping=70), + "joint_2_3": BaseActuatorCfg(stiffness=600, damping=60), + "joint_2_4": BaseActuatorCfg(stiffness=500, damping=50), + # Middle finger (4 joints) + "joint_3_1": BaseActuatorCfg(stiffness=800, damping=80), + "joint_3_2": BaseActuatorCfg(stiffness=700, damping=70), + "joint_3_3": BaseActuatorCfg(stiffness=600, damping=60), + "joint_3_4": BaseActuatorCfg(stiffness=500, damping=50), + # Ring finger (4 joints) + "joint_4_1": BaseActuatorCfg(stiffness=800, damping=80), + "joint_4_2": BaseActuatorCfg(stiffness=700, damping=70), + "joint_4_3": BaseActuatorCfg(stiffness=600, damping=60), + "joint_4_4": BaseActuatorCfg(stiffness=500, damping=50), + # Pinky finger (4 joints) + "joint_5_1": BaseActuatorCfg(stiffness=800, damping=80), + "joint_5_2": BaseActuatorCfg(stiffness=700, damping=70), + "joint_5_3": BaseActuatorCfg(stiffness=600, damping=60), + "joint_5_4": BaseActuatorCfg(stiffness=500, damping=50), + } + + joint_limits: dict[str, tuple[float, float]] = { + # Thumb + "joint_1_1": (-0.205, 0.75), + "joint_1_2": (0.0, 1.57), + "joint_1_3": (0.0, 0.628), + "joint_1_4": (0.0, 1.05), + "joint_1_5": (0.0, 1.1), + # Index finger + "joint_2_1": (-0.349, 0.349), + "joint_2_2": (0.0, 1.57), + "joint_2_3": (0.0, 1.27), + "joint_2_4": (0.0, 1.29), + # Middle finger + "joint_3_1": (-0.349, 0.349), + "joint_3_2": (0.0, 1.57), + "joint_3_3": (0.0, 1.27), + "joint_3_4": (0.0, 1.29), + # Ring finger + "joint_4_1": (-0.349, 0.349), + "joint_4_2": (0.0, 1.57), + "joint_4_3": (0.0, 1.27), + "joint_4_4": (0.0, 1.29), + # Pinky finger + "joint_5_1": (-0.349, 0.349), + "joint_5_2": (0.0, 1.57), + "joint_5_3": (0.0, 1.27), + "joint_5_4": (0.0, 1.29), + } + + default_joint_positions: dict[str, float] = { + # Thumb - slightly opened + "joint_1_1": 0.0, + "joint_1_2": 0.2, + "joint_1_3": 0.1, + "joint_1_4": 0.1, + "joint_1_5": 0.1, + # Index finger - slightly opened + "joint_2_1": 0.0, + "joint_2_2": 0.2, + "joint_2_3": 0.2, + "joint_2_4": 0.2, + # Middle finger - slightly opened + "joint_3_1": 0.0, + "joint_3_2": 0.2, + "joint_3_3": 0.2, + "joint_3_4": 0.2, + # Ring finger - slightly opened + "joint_4_1": 0.0, + "joint_4_2": 0.2, + "joint_4_3": 0.2, + "joint_4_4": 0.2, + # Pinky finger - slightly opened + "joint_5_1": 0.0, + "joint_5_2": 0.2, + "joint_5_3": 0.2, + "joint_5_4": 0.2, + } + + control_type: dict[str, Literal["position", "effort"]] = { + # All joints use position control + "joint_1_1": "position", + "joint_1_2": "position", + "joint_1_3": "position", + "joint_1_4": "position", + "joint_1_5": "position", + "joint_2_1": "position", + "joint_2_2": "position", + "joint_2_3": "position", + "joint_2_4": "position", + "joint_3_1": "position", + "joint_3_2": "position", + "joint_3_3": "position", + "joint_3_4": "position", + "joint_4_1": "position", + "joint_4_2": "position", + "joint_4_3": "position", + "joint_4_4": "position", + "joint_5_1": "position", + "joint_5_2": "position", + "joint_5_3": "position", + "joint_5_4": "position", + } diff --git a/roboverse_pack/robots/psihand_right_cfg.py b/roboverse_pack/robots/psihand_right_cfg.py new file mode 100644 index 000000000..528c89b81 --- /dev/null +++ b/roboverse_pack/robots/psihand_right_cfg.py @@ -0,0 +1,138 @@ +from __future__ import annotations + +from typing import Literal + +from metasim.scenario.robot import BaseActuatorCfg, RobotCfg +from metasim.utils import configclass + + +@configclass +class PsihandRightCfg(RobotCfg): + """Configuration for PsiHand Right (Psi-SynHand). + + NOTE: This robot has known compatibility issues with IsaacGym: + - URDF mode: Joint positions become NaN, fingers not visible + - USD mode: DOF force tensor initialization fails + Recommended to use with MuJoCo, Genesis, or other simulators instead. + """ + + name: str = "psihand_right" + num_joints: int = 21 + fix_base_link: bool = True + urdf_path: str = "roboverse_data/robots/psihand/urdf/psihand_right.urdf" + mjcf_path: str = "roboverse_data/robots/psihand/mjcf/psihand_right.xml" + usd_path: str = "roboverse_data/robots/psihand/usd/psihand_right.usd" + enabled_gravity: bool = False + enabled_self_collisions: bool = False + + actuators: dict[str, BaseActuatorCfg] = { + # Thumb (5 joints) + "joint_1_1": BaseActuatorCfg(stiffness=1000, damping=100), + "joint_1_2": BaseActuatorCfg(stiffness=800, damping=80), + "joint_1_3": BaseActuatorCfg(stiffness=600, damping=60), + "joint_1_4": BaseActuatorCfg(stiffness=500, damping=50), + "joint_1_5": BaseActuatorCfg(stiffness=500, damping=50), + # Index finger (4 joints) + "joint_2_1": BaseActuatorCfg(stiffness=800, damping=80), + "joint_2_2": BaseActuatorCfg(stiffness=700, damping=70), + "joint_2_3": BaseActuatorCfg(stiffness=600, damping=60), + "joint_2_4": BaseActuatorCfg(stiffness=500, damping=50), + # Middle finger (4 joints) + "joint_3_1": BaseActuatorCfg(stiffness=800, damping=80), + "joint_3_2": BaseActuatorCfg(stiffness=700, damping=70), + "joint_3_3": BaseActuatorCfg(stiffness=600, damping=60), + "joint_3_4": BaseActuatorCfg(stiffness=500, damping=50), + # Ring finger (4 joints) + "joint_4_1": BaseActuatorCfg(stiffness=800, damping=80), + "joint_4_2": BaseActuatorCfg(stiffness=700, damping=70), + "joint_4_3": BaseActuatorCfg(stiffness=600, damping=60), + "joint_4_4": BaseActuatorCfg(stiffness=500, damping=50), + # Pinky finger (4 joints) + "joint_5_1": BaseActuatorCfg(stiffness=800, damping=80), + "joint_5_2": BaseActuatorCfg(stiffness=700, damping=70), + "joint_5_3": BaseActuatorCfg(stiffness=600, damping=60), + "joint_5_4": BaseActuatorCfg(stiffness=500, damping=50), + } + + joint_limits: dict[str, tuple[float, float]] = { + # Thumb + "joint_1_1": (-0.205, 0.75), + "joint_1_2": (0.0, 1.57), + "joint_1_3": (0.0, 0.628), + "joint_1_4": (0.0, 1.05), + "joint_1_5": (0.0, 1.1), + # Index finger + "joint_2_1": (-0.349, 0.349), + "joint_2_2": (0.0, 1.57), + "joint_2_3": (0.0, 1.27), + "joint_2_4": (0.0, 1.29), + # Middle finger + "joint_3_1": (-0.349, 0.349), + "joint_3_2": (0.0, 1.57), + "joint_3_3": (0.0, 1.27), + "joint_3_4": (0.0, 1.29), + # Ring finger + "joint_4_1": (-0.349, 0.349), + "joint_4_2": (0.0, 1.57), + "joint_4_3": (0.0, 1.27), + "joint_4_4": (0.0, 1.29), + # Pinky finger + "joint_5_1": (-0.349, 0.349), + "joint_5_2": (0.0, 1.57), + "joint_5_3": (0.0, 1.27), + "joint_5_4": (0.0, 1.29), + } + + default_joint_positions: dict[str, float] = { + # Thumb - slightly opened + "joint_1_1": 0.0, + "joint_1_2": 0.2, + "joint_1_3": 0.1, + "joint_1_4": 0.1, + "joint_1_5": 0.1, + # Index finger - slightly opened + "joint_2_1": 0.0, + "joint_2_2": 0.2, + "joint_2_3": 0.2, + "joint_2_4": 0.2, + # Middle finger - slightly opened + "joint_3_1": 0.0, + "joint_3_2": 0.2, + "joint_3_3": 0.2, + "joint_3_4": 0.2, + # Ring finger - slightly opened + "joint_4_1": 0.0, + "joint_4_2": 0.2, + "joint_4_3": 0.2, + "joint_4_4": 0.2, + # Pinky finger - slightly opened + "joint_5_1": 0.0, + "joint_5_2": 0.2, + "joint_5_3": 0.2, + "joint_5_4": 0.2, + } + + control_type: dict[str, Literal["position", "effort"]] = { + # All joints use position control + "joint_1_1": "position", + "joint_1_2": "position", + "joint_1_3": "position", + "joint_1_4": "position", + "joint_1_5": "position", + "joint_2_1": "position", + "joint_2_2": "position", + "joint_2_3": "position", + "joint_2_4": "position", + "joint_3_1": "position", + "joint_3_2": "position", + "joint_3_3": "position", + "joint_3_4": "position", + "joint_4_1": "position", + "joint_4_2": "position", + "joint_4_3": "position", + "joint_4_4": "position", + "joint_5_1": "position", + "joint_5_2": "position", + "joint_5_3": "position", + "joint_5_4": "position", + } From fd14433f561887ea247683d25dfdf1379d0e1930 Mon Sep 17 00:00:00 2001 From: Ayanami-Yu <113404548+Ayanami-Yu@users.noreply.github.com> Date: Thu, 1 Jan 2026 13:13:58 +0800 Subject: [PATCH 39/50] [Feature] Add BeyondMimic motion tracking (#737) * [fix] usd_path of g1_cfg.G1Dof29Cfg * [fix] usd_path of g1_cfg.G1Dof29Cfg * Start migrating BeyondMimic * Slightly adapt unitree_rl main.py to BeyondMimic play.py (not finished) * Correct import paths * Save before adapting BeyondMimic evaluation * Start adapting BeyondMimic to unitree_rl infra * Add rewards, observations, terminations, randomization, motion commands, etc. * Ready to adapt main entry * Almost finished implementation * Debug and update * Able to run but metrics are not right * Fix a bug of mean reward * Able to run evaluation script but motions are probably not loaded correctly * Fix bugs about joint and body orders * Fix a bug of joint order of applied actions * Add G1 robot config from BeyondMimic * Debug soft_joint_pos_limit_factor * Adding Isaac Lab version BeyondMimic * Able to run Isaac Lab-specific BeyondMimic training * Implement training wrappers for both RSL v2.3.0 and v3.1.0; fix issues about entropy_coef * Save before debugging motion tracking evaluation pipeline * Refactor BeyondMimic MetaSim version * Refactor and debug task class; about to adapt evaluation script * Finish unified evaluation script for tracking; save before toggling __init__.py of submodules * Able to evaluate but previous issues are not fixed * Fix a bug in * Fix issues about updating commands after first reset and resampling commands; evaluation should be alright now * Fix a small issue of dimension mismatch in command resampling * Fix a bug of RoboVerse env wrapper; add a task class for training tracker for deployment * Clean up roboverse_pack * Clean up roboverse_learn, add README under docs/source/roboverse_learn/reinforcement_learning, and add scripts for converting motions * Modify BeyondMimic README * Clean up tracking_g1 * Try to re-merge * Clean up comments * Add URDF to USD conversion script; remove robot instantiation using URDF config in Isaac Sim handler * [fix] isaacgym gs * [fix] env origin * [update] move math functions into metasim.utils.math * [update] premerge-ci --------- Co-authored-by: myuansun <1273414643@qq.com> --- .github/workflows/premerge-ci.yml | 5 +- .gitignore | 4 + .../reinforcement_learning/beyondmimic.md | 96 +++ metasim/scenario/robot.py | 11 +- metasim/sim/isaacgym/isaacgym.py | 2 +- metasim/sim/isaacsim/isaacsim.py | 61 +- metasim/sim/pybullet/pybullet.py | 2 +- metasim/utils/math.py | 27 + .../rl/configs/rsl_rl/algorithm.py | 30 +- roboverse_learn/rl/configs/rsl_rl/ppo.py | 12 +- .../rl/configs/rsl_rl/ppo_tracking.py | 119 ++++ roboverse_learn/rl/rsl_rl/env_wrapper.py | 8 +- roboverse_learn/rl/rsl_rl/eval_tracking.py | 143 +++++ roboverse_learn/rl/rsl_rl/ppo_tracking.py | 114 ++++ .../callback_funcs/humanoid/reward_funcs.py | 1 + .../humanoid/termination_funcs.py | 4 +- roboverse_pack/robots/__init__.py | 1 + roboverse_pack/robots/allegrohand_cfg.py | 32 +- roboverse_pack/robots/ant_cfg.py | 16 +- roboverse_pack/robots/anymal_cfg.py | 24 +- roboverse_pack/robots/cartpole_cfg.py | 4 +- roboverse_pack/robots/g1_cfg.py | 91 +-- roboverse_pack/robots/g1_tracking.py | 267 ++++++++ roboverse_pack/robots/gen3_cfg.py | 20 +- roboverse_pack/robots/go2_cfg.py | 24 +- roboverse_pack/robots/vega_cfg.py | 90 +-- roboverse_pack/robots/xhand_cfg.py | 38 +- roboverse_pack/robots/z1_cfg.py | 12 +- roboverse_pack/tasks/beyondmimic/__init__.py | 1 + .../tasks/beyondmimic/isaaclab/__init__.py | 1 + .../isaaclab/configs/flat_env_cfg.py | 43 ++ .../isaaclab/configs/tracking_env_cfg.py | 330 ++++++++++ .../beyondmimic/isaaclab/envs/__init__.py | 1 + .../isaaclab/envs/tracking_base_env.py | 506 +++++++++++++++ .../isaaclab/envs/tracking_rl_env.py | 495 +++++++++++++++ .../beyondmimic/isaaclab/mdp/commands.py | 430 +++++++++++++ .../tasks/beyondmimic/isaaclab/mdp/events.py | 93 +++ .../beyondmimic/isaaclab/mdp/observations.py | 92 +++ .../tasks/beyondmimic/isaaclab/mdp/rewards.py | 88 +++ .../beyondmimic/isaaclab/mdp/terminations.py | 65 ++ .../beyondmimic/isaaclab/robots/actuator.py | 83 +++ .../tasks/beyondmimic/isaaclab/robots/g1.py | 196 ++++++ .../beyondmimic/isaaclab/robots/g1_delayed.py | 196 ++++++ .../tasks/beyondmimic/metasim/__init__.py | 1 + .../beyondmimic/metasim/configs/__init__.py | 1 + .../beyondmimic/metasim/configs/cfg_base.py | 69 ++ .../metasim/configs/cfg_queries.py | 117 ++++ .../metasim/configs/cfg_randomizers.py | 592 ++++++++++++++++++ .../metasim/configs/tracking_g1.py | 223 +++++++ .../beyondmimic/metasim/envs/__init__.py | 1 + .../metasim/envs/base_legged_robot.py | 426 +++++++++++++ .../beyondmimic/metasim/envs/tracking_g1.py | 93 +++ .../tasks/beyondmimic/metasim/mdp/commands.py | 404 ++++++++++++ .../tasks/beyondmimic/metasim/mdp/events.py | 82 +++ .../beyondmimic/metasim/mdp/observations.py | 117 ++++ .../tasks/beyondmimic/metasim/mdp/rewards.py | 121 ++++ .../beyondmimic/metasim/mdp/terminations.py | 49 ++ .../tasks/beyondmimic/metasim/utils/misc.py | 213 +++++++ .../tasks/beyondmimic/metasim/utils/string.py | 287 +++++++++ .../tasks/beyondmimic/scripts/convert_urdf.py | 163 +++++ .../tasks/beyondmimic/scripts/csv_to_npz.py | 384 ++++++++++++ .../tasks/beyondmimic/scripts/replay_npz.py | 129 ++++ roboverse_pack/tasks/calvin/base_table.py | 32 +- .../tasks/humanoid/base/base_legged_robot.py | 2 +- roboverse_pack/utils/curriculum_utils.py | 4 +- roboverse_pack/utils/humanoid_utils.py | 130 ++++ 66 files changed, 7289 insertions(+), 229 deletions(-) create mode 100644 docs/source/roboverse_learn/reinforcement_learning/beyondmimic.md create mode 100644 roboverse_learn/rl/configs/rsl_rl/ppo_tracking.py create mode 100644 roboverse_learn/rl/rsl_rl/eval_tracking.py create mode 100644 roboverse_learn/rl/rsl_rl/ppo_tracking.py create mode 100644 roboverse_pack/robots/g1_tracking.py create mode 100644 roboverse_pack/tasks/beyondmimic/__init__.py create mode 100644 roboverse_pack/tasks/beyondmimic/isaaclab/__init__.py create mode 100644 roboverse_pack/tasks/beyondmimic/isaaclab/configs/flat_env_cfg.py create mode 100644 roboverse_pack/tasks/beyondmimic/isaaclab/configs/tracking_env_cfg.py create mode 100644 roboverse_pack/tasks/beyondmimic/isaaclab/envs/__init__.py create mode 100644 roboverse_pack/tasks/beyondmimic/isaaclab/envs/tracking_base_env.py create mode 100644 roboverse_pack/tasks/beyondmimic/isaaclab/envs/tracking_rl_env.py create mode 100644 roboverse_pack/tasks/beyondmimic/isaaclab/mdp/commands.py create mode 100644 roboverse_pack/tasks/beyondmimic/isaaclab/mdp/events.py create mode 100644 roboverse_pack/tasks/beyondmimic/isaaclab/mdp/observations.py create mode 100644 roboverse_pack/tasks/beyondmimic/isaaclab/mdp/rewards.py create mode 100644 roboverse_pack/tasks/beyondmimic/isaaclab/mdp/terminations.py create mode 100644 roboverse_pack/tasks/beyondmimic/isaaclab/robots/actuator.py create mode 100644 roboverse_pack/tasks/beyondmimic/isaaclab/robots/g1.py create mode 100644 roboverse_pack/tasks/beyondmimic/isaaclab/robots/g1_delayed.py create mode 100644 roboverse_pack/tasks/beyondmimic/metasim/__init__.py create mode 100644 roboverse_pack/tasks/beyondmimic/metasim/configs/__init__.py create mode 100644 roboverse_pack/tasks/beyondmimic/metasim/configs/cfg_base.py create mode 100644 roboverse_pack/tasks/beyondmimic/metasim/configs/cfg_queries.py create mode 100644 roboverse_pack/tasks/beyondmimic/metasim/configs/cfg_randomizers.py create mode 100644 roboverse_pack/tasks/beyondmimic/metasim/configs/tracking_g1.py create mode 100644 roboverse_pack/tasks/beyondmimic/metasim/envs/__init__.py create mode 100644 roboverse_pack/tasks/beyondmimic/metasim/envs/base_legged_robot.py create mode 100644 roboverse_pack/tasks/beyondmimic/metasim/envs/tracking_g1.py create mode 100644 roboverse_pack/tasks/beyondmimic/metasim/mdp/commands.py create mode 100644 roboverse_pack/tasks/beyondmimic/metasim/mdp/events.py create mode 100644 roboverse_pack/tasks/beyondmimic/metasim/mdp/observations.py create mode 100644 roboverse_pack/tasks/beyondmimic/metasim/mdp/rewards.py create mode 100644 roboverse_pack/tasks/beyondmimic/metasim/mdp/terminations.py create mode 100644 roboverse_pack/tasks/beyondmimic/metasim/utils/misc.py create mode 100644 roboverse_pack/tasks/beyondmimic/metasim/utils/string.py create mode 100644 roboverse_pack/tasks/beyondmimic/scripts/convert_urdf.py create mode 100644 roboverse_pack/tasks/beyondmimic/scripts/csv_to_npz.py create mode 100644 roboverse_pack/tasks/beyondmimic/scripts/replay_npz.py diff --git a/.github/workflows/premerge-ci.yml b/.github/workflows/premerge-ci.yml index 0ed19c7ad..ed991710b 100644 --- a/.github/workflows/premerge-ci.yml +++ b/.github/workflows/premerge-ci.yml @@ -4,12 +4,15 @@ on: workflow_dispatch: merge_group: types: [checks_requested] - pull_request: + pull_request_target: types: - auto_merge_enabled branches: - main - develop + push: + branches: + - ci/pr-** env: REGION: us-west-2 diff --git a/.gitignore b/.gitignore index 73ef50423..fc9a76a6c 100644 --- a/.gitignore +++ b/.gitignore @@ -111,3 +111,7 @@ id_rsa* *.pkl *.pt output + +.cursor/ +artifacts/ +nohup/ diff --git a/docs/source/roboverse_learn/reinforcement_learning/beyondmimic.md b/docs/source/roboverse_learn/reinforcement_learning/beyondmimic.md new file mode 100644 index 000000000..975e98425 --- /dev/null +++ b/docs/source/roboverse_learn/reinforcement_learning/beyondmimic.md @@ -0,0 +1,96 @@ +# BeyondMimic Motion Tracking + +## Overview + +BeyondMimic motion tracking code adapted for RoboVerse. Package structure: + +- `roboverse_pack/tasks/beyondmimic/isaaclab`: Isaac Lab-native training. +- `roboverse_pack/tasks/beyondmimic/metasim`: simulator-agnostic (implemented using MetaSim handler) training (beta) and evaluation. +- `roboverse_pack/tasks/beyondmimic/scripts`: for converting and playing reference motions. + +## Environment Setup + +Tested with python 3.10, RSL-RL v3.1.0, and both Isaac Lab v2.1.0 and v2.3.0. You can install Isaac Lab following their [official documentation](https://isaac-sim.github.io/IsaacLab/main/source/setup/installation/pip_installation.html#installation-using-isaac-sim-pip-package), and install RSL-RL by: + +```bash +git clone [https://github.com/leggedrobotics/rsl_rl](https://github.com/leggedrobotics/rsl_rl) +git checkout v3.1.0 +pip install -e . +``` + +## Motion Preparation + +### Download Description Files + +```bash +cd RoboVerse +curl -L -o unitree_description.tar.gz https://storage.googleapis.com/qiayuanl_robot_descriptions/unitree_description.tar.gz && \ +tar -xzf unitree_description.tar.gz -C roboverse_data/ && \ +rm unitree_description.tar.gz +``` + +Since RoboVerse uses USD format to instantiate robots, you may need to convert URDF files to USD. We provide a utility script `roboverse_pack/tasks/beyondmimic/scripts/convert_urdf.py` for conversion. You can run it using: + +```bash +python roboverse_pack/tasks/beyondmimic/scripts/convert_urdf.py {source-urdf-path} {target-usd-path} --merge-joints --joint-stiffness 0.0 --joint-damping 0.0 --joint-target-type none +``` + +Since currently the paths are hard-coded inside the robot config (`roboverse_pack/robots/g1_tracking.py`), you may replace `{source-urdf-path}` with `roboverse_data/unitree_description/urdf/g1/main.urdf` and `{target-usd-path}` with `roboverse_data/unitree_description/usd/g1/g1.usd`. + +### Motion Preprocessing & Registry Setup + +We leverage the WandB registry to store and load reference motions automatically. +Note: The reference motion should be retargeted and use generalized coordinates only. + +- Gather the reference motion datasets (please follow the original licenses), we use the same convention as .csv of Unitree's dataset. + - Unitree-retargeted LAFAN1 Dataset is available + on [HuggingFace](https://huggingface.co/datasets/lvhaidong/LAFAN1_Retargeting_Dataset) + - Sidekicks are from [KungfuBot](https://kungfu-bot.github.io/) + - Christiano Ronaldo celebration is from [ASAP](https://github.com/LeCAR-Lab/ASAP) + - Balance motions are from [HuB](https://hub-robot.github.io/) + + +- Log in to your WandB account; access Registry under Core on the left. Create a new registry collection with the name "Motions" and artifact type "All Types". + + +- Convert retargeted motions to include the maximum coordinates information (body pose, body velocity, and body acceleration) via forward kinematics: + +```bash +python roboverse_pack/tasks/beyondmimic/scripts/csv_to_npz.py --input_file {motion_name}.csv --input_fps 30 --output_name {motion_name} --headless +``` + +This will automatically upload the processed motion file to the WandB registry with output name {motion_name}. + +- Test if the WandB registry works properly by replaying the motion in Isaac Sim: + +```bash +python roboverse_pack/tasks/beyondmimic/scripts/replay_npz.py --registry_name={your-organization}-org/wandb-registry-motions/{motion_name} +``` + +- Debugging + - Make sure to export WANDB_ENTITY to your organization name, not your personal username. + - If /tmp folder is not accessible, modify csv_to_npz.py to use a temporary folder of your choice. + +## Policy Training + +- Isaac Lab-Native + +```bash +python roboverse_learn/rl/rsl_rl/ppo_tracking.py --task motion-tracking-isaaclab --sim isaacsim --num_envs 4096 --use-wandb --registry_name {your-organization}-org/wandb-registry-motions/{motion_name} --headless --logger wandb +``` + +For training a tracker with delayed actuator and reduced observations (`base_lin_vel` and `motion_anchor_pos_b`), you can replace the command with `--task motion-tracking-isaaclab-deploy`. + +- MetaSim Handler + +Replace the above command with `--task motion-tracking`. Note that MetaSim version training is still in beta and is prone to issues. Feel free to pull a PR if you've improved the code or fixed any issues regarding this pipeline. + +## Policy Evaluation + +```bash +python roboverse_learn/rl/rsl_rl/eval_tracking.py --task motion-tracking --sim isaacsim --num-envs 2 --robot g1_tracking --wandb-path {wandb-run-path} +``` + +The WandB run path can be located in the run overview. It follows the format {your_organization}/{project_name}/ along with a unique 8-character identifier. Note that run_name is different from run_path. + +The evaluation pipeline is written using MetaSim handler and thus simulator-agnostic. However, currently simulators other than Isaac Sim may have missing functionality required by this pipeline, therefore may not behave as expected. \ No newline at end of file diff --git a/metasim/scenario/robot.py b/metasim/scenario/robot.py index 8ce28bd19..b97249d97 100644 --- a/metasim/scenario/robot.py +++ b/metasim/scenario/robot.py @@ -6,15 +6,22 @@ from metasim.utils import configclass +# FIXME current design does not support specifying a group of actuators based on regex, which results in `Articulation._apply_actuator_model()` (in Isaac Lab) computing the joint torques one at a time in a loop (sequentially), which may be inefficient @configclass class BaseActuatorCfg: """Base configuration class for actuators.""" + effort_limit_sim: float | None = None + """Torque (effort) limit of the actuator. If not specified, use the value specified in the asset file and interpreted by the simulator. Note that this corresponds to `effort_limit_sim` in Isaac Lab.""" + velocity_limit: float | None = None """Velocity limit of the actuator. If not specified, use the value specified in the asset file and interpreted by the simulator.""" - torque_limit: float | None = None - """Torque limit of the actuator. If not specified, use the value specified in the asset file and interpreted by the simulator.""" + velocity_limit_sim: float | None = None + """Velocity limit of the actuator in the simulator. Note that `velocity_limit` does not take effect when passed to Isaac Sim. Please use this instead.""" + + armature: float | None = None + """Armature of the actuator. If not specified, use the default value specified for the whole robot instead.""" damping: float | None = None """Damping of the actuator. If not specified, use the value specified in the asset file and interpreted by the simulator.""" diff --git a/metasim/sim/isaacgym/isaacgym.py b/metasim/sim/isaacgym/isaacgym.py index cf0a38050..f75218d0b 100644 --- a/metasim/sim/isaacgym/isaacgym.py +++ b/metasim/sim/isaacgym/isaacgym.py @@ -774,7 +774,7 @@ def _get_states(self, env_ids: list[int] | None = None) -> list[DictEnvState]: # Apply GS background rendering if enabled # TODO: Render with batch parallelization for efficiency - if self.scenario.gs_scene.with_gs_background and self.gs_background is not None: + if self.gs_background is not None and self.scenario.gs_scene.with_gs_background: assert ROBO_SPLATTER_AVAILABLE, "RoboSplatter is not available. GS background rendering will be disabled." camera_states = self._apply_gs_background_rendering(camera_states, env_ids) diff --git a/metasim/sim/isaacsim/isaacsim.py b/metasim/sim/isaacsim/isaacsim.py index ac3ac4d64..5c95c7977 100644 --- a/metasim/sim/isaacsim/isaacsim.py +++ b/metasim/sim/isaacsim/isaacsim.py @@ -103,7 +103,7 @@ def _init_scene(self, simulation_app=None, args=None) -> None: sim_config: SimulationCfg = SimulationCfg( device="cuda:0", - render_interval=self.scenario.decimation, # TTODO divide into render interval and control decimation + render_interval=self.scenario.decimation, # TODO divide into render interval and control decimation physx=PhysxCfg( bounce_threshold_velocity=self.scenario.sim_params.bounce_threshold_velocity, solver_type=self.scenario.sim_params.solver_type, @@ -170,12 +170,6 @@ def _update_camera_pose(self) -> None: camera_lookat_tensor = torch.as_tensor(camera.look_at, device=self.device).expand(self.num_envs, -1) position_tensor = position_tensor + env_origins camera_lookat_tensor = camera_lookat_tensor + env_origins - position_tensor = torch.tensor(camera.pos, device=self.device, dtype=torch.float32).unsqueeze(0) - position_tensor = position_tensor.repeat(self.num_envs, 1) - camera_lookat_tensor = torch.tensor( - camera.look_at, device=self.device, dtype=torch.float32 - ).unsqueeze(0) - camera_lookat_tensor = camera_lookat_tensor.repeat(self.num_envs, 1) camera_inst.set_world_poses_from_view(position_tensor, camera_lookat_tensor) # log.debug(f"Updated camera {camera.name} pose: pos={camera.pos}, look_at={camera.look_at}") else: @@ -653,50 +647,66 @@ def _simulate(self): if self._physics_step_counter < 5: self._update_camera_pose() - self._physics_step_counter += 1 - def _add_robot(self, robot: ArticulationObjCfg) -> None: import isaaclab.sim as sim_utils from isaaclab.actuators import ImplicitActuatorCfg from isaaclab.assets import Articulation, ArticulationCfg - manual_pd = any(mode == "effort" for mode in robot.control_type.values()) + control_type = getattr(robot, "control_type", None) + manual_pd = any(mode == "effort" for mode in control_type.values()) if control_type else False self._manual_pd_on.append(manual_pd) - cfg = ArticulationCfg( - spawn=sim_utils.UsdFileCfg( - usd_path=robot.usd_path, - activate_contact_sensors=True, - rigid_props=sim_utils.RigidBodyPropertiesCfg( - max_depenetration_velocity=getattr( - robot, "max_depenetration_velocity", self.scenario.sim_params.max_depenetration_velocity - ) + + spawn_cfg = sim_utils.UsdFileCfg( + usd_path=robot.usd_path, + activate_contact_sensors=True, + rigid_props=sim_utils.RigidBodyPropertiesCfg( + disable_gravity=not robot.enabled_gravity, + retain_accelerations=False, + linear_damping=0.0, + angular_damping=0.0, + max_linear_velocity=1000.0, + max_angular_velocity=1000.0, + max_depenetration_velocity=getattr( + robot, "max_depenetration_velocity", self.scenario.sim_params.max_depenetration_velocity ), - articulation_props=sim_utils.ArticulationRootPropertiesCfg(fix_root_link=robot.fix_base_link), - collision_props=sim_utils.CollisionPropertiesCfg( + ), + articulation_props=sim_utils.ArticulationRootPropertiesCfg( + enabled_self_collisions=robot.enabled_self_collisions, + fix_root_link=robot.fix_base_link, + solver_position_iteration_count=8, + solver_velocity_iteration_count=4, + ), + collision_props=getattr( + robot, + "collision_props", + sim_utils.CollisionPropertiesCfg( contact_offset=getattr(robot, "contact_offset", self.scenario.sim_params.contact_offset), rest_offset=getattr(robot, "rest_offset", self.scenario.sim_params.rest_offset), ), ), + ) + cfg = ArticulationCfg( + spawn=spawn_cfg, actuators={ jn: ImplicitActuatorCfg( joint_names_expr=[jn], + effort_limit_sim=actuator.effort_limit_sim, + velocity_limit_sim=actuator.velocity_limit_sim, stiffness=actuator.stiffness if not manual_pd else 0.0, damping=actuator.damping if not manual_pd else 0.0, - armature=getattr(robot, "armature", 0.01), + armature=actuator.armature if actuator.armature is not None else getattr(robot, "armature", 0.01), ) for jn, actuator in robot.actuators.items() }, ) cfg.prim_path = f"/World/envs/env_.*/{robot.name}" - cfg.spawn.usd_path = os.path.abspath(robot.usd_path) - cfg.spawn.rigid_props.disable_gravity = not robot.enabled_gravity - cfg.spawn.articulation_props.enabled_self_collisions = robot.enabled_self_collisions init_state = ArticulationCfg.InitialStateCfg( - pos=[0.0, 0.0, 0.0], + pos=getattr(robot, "default_pos", [0.0, 0.0, 0.0]), joint_pos=robot.default_joint_positions, joint_vel={".*": 0.0}, ) cfg.init_state = init_state + # NOTE `velocity_limit` here won't take effect for joint_name, actuator in robot.actuators.items(): cfg.actuators[joint_name].velocity_limit = actuator.velocity_limit robot_inst = Articulation(cfg) @@ -1175,6 +1185,7 @@ def _load_sensors(self) -> None: prim_path=f"/World/envs/env_.*/{self.robots[0].name}/.*", history_length=3, update_period=0.005, + force_threshold=10.0, track_air_time=True, ) self.contact_sensor = ContactSensor(contact_sensor_config) diff --git a/metasim/sim/pybullet/pybullet.py b/metasim/sim/pybullet/pybullet.py index 5ee67dfe4..5cf221dc2 100644 --- a/metasim/sim/pybullet/pybullet.py +++ b/metasim/sim/pybullet/pybullet.py @@ -192,7 +192,7 @@ def _build_pybullet(self): positionGain=joint_config.stiffness or PYBULLET_DEFAULT_POSITION_GAIN, velocityGain=joint_config.damping or PYBULLET_DEFAULT_VELOCITY_GAIN, maxVelocity=joint_config.velocity_limit or PYBULLET_DEFAULT_JOINT_MAX_VEL, - force=joint_config.torque_limit or PYBULLET_DEFAULT_JOINT_MAX_TORQUE, + force=joint_config.effort_limit_sim or PYBULLET_DEFAULT_JOINT_MAX_TORQUE, ) if hasattr(object, "default_joint_positions"): diff --git a/metasim/utils/math.py b/metasim/utils/math.py index dcfcfd579..59e5ede56 100644 --- a/metasim/utils/math.py +++ b/metasim/utils/math.py @@ -80,6 +80,7 @@ def saturate(x: torch.Tensor, lower: torch.Tensor, upper: torch.Tensor) -> torch return torch.max(torch.min(x, upper), lower) +# same as isaaclab.utils.math.normalize @torch.jit.script def normalize(x: torch.Tensor, eps: float = 1e-9) -> torch.Tensor: """Normalizes a given input tensor to unit length. @@ -142,6 +143,7 @@ def copysign(mag: float, other: torch.Tensor) -> torch.Tensor: """ +# same as isaaclab.utils.math.matrix_from_quat @torch.jit.script def matrix_from_quat(quaternions: torch.Tensor) -> torch.Tensor: """Convert rotations given as quaternions to rotation matrices. @@ -222,6 +224,7 @@ def convert_quat(quat: torch.Tensor | np.ndarray, to: Literal["xyzw", "wxyz"] = return quat.roll(1, dims=-1) +# same as isaaclab.utils.math.quat_from_euler_xyz @torch.jit.script def quat_from_euler_xyz(roll: torch.Tensor, pitch: torch.Tensor, yaw: torch.Tensor) -> torch.Tensor: """Convert rotations given as Euler angles in radians to Quaternions. @@ -461,6 +464,21 @@ def quat_unique(q: torch.Tensor) -> torch.Tensor: return torch.where(q[..., 0:1] < 0, -q, q) +@torch.jit.script +def quat_conjugate(q: torch.Tensor) -> torch.Tensor: + """Computes the conjugate of a quaternion. + + Args: + q: The quaternion orientation in (w, x, y, z). Shape is (..., 4). + + Returns: + The conjugate quaternion in (w, x, y, z). Shape is (..., 4). + """ + shape = q.shape + q = q.reshape(-1, 4) + return torch.cat((q[:, 0:1], -q[:, 1:]), dim=-1).view(shape) + + @torch.jit.script def quat_inv(q: torch.Tensor) -> torch.Tensor: """Compute the inverse of a quaternion. @@ -475,6 +493,7 @@ def quat_inv(q: torch.Tensor) -> torch.Tensor: return q * scaling +# same as isaaclab.utils.math.quat_mul @torch.jit.script def quat_mul(q1: torch.Tensor, q2: torch.Tensor) -> torch.Tensor: """Multiply two quaternions together. @@ -533,6 +552,7 @@ def quat_box_minus(q1: torch.Tensor, q2: torch.Tensor) -> torch.Tensor: return scale.unsqueeze(-1) * im +# same as isaaclab.utils.math.yaw_quat @torch.jit.script def yaw_quat(quat: torch.Tensor) -> torch.Tensor: """Extract the yaw component of a quaternion. @@ -557,6 +577,7 @@ def yaw_quat(quat: torch.Tensor) -> torch.Tensor: return quat_yaw.view(shape) +# same as isaaclab.utils.math.quat_apply @torch.jit.script def quat_apply(quat: torch.Tensor, vec: torch.Tensor) -> torch.Tensor: """Apply a quaternion rotation to a vector. @@ -617,6 +638,7 @@ def quat_rotate(q: torch.Tensor, v: torch.Tensor) -> torch.Tensor: return a + b + c +# same as isaaclab.utils.math.quat_rotate_inverse @torch.jit.script def quat_rotate_inverse(q: torch.Tensor, v: torch.Tensor) -> torch.Tensor: """Rotate a vector by the inverse of a quaternion along the last dimension of q and v. @@ -657,6 +679,7 @@ def quat_from_angle_axis(angle: torch.Tensor, axis: torch.Tensor) -> torch.Tenso return normalize(torch.cat([w, xyz], dim=-1)) +# same as isaaclab.utils.math.axis_angle_from_quat @torch.jit.script def axis_angle_from_quat(quat: torch.Tensor, eps: float = 1.0e-6) -> torch.Tensor: """Convert rotations given as quaternions to axis/angle. @@ -797,6 +820,7 @@ def combine_frame_transforms( # @torch.jit.script +# same as isaaclab.utils.math.subtract_frame_transforms def subtract_frame_transforms( t01: torch.Tensor, q01: torch.Tensor, t02: torch.Tensor | None = None, q02: torch.Tensor | None = None ) -> tuple[torch.Tensor, torch.Tensor]: @@ -1325,6 +1349,7 @@ def sample_triangle(lower: float, upper: float, size: int | tuple[int, ...], dev return (upper - lower) * r + lower +# same as isaaclab.utils.math.sample_uniform def sample_uniform( lower: torch.Tensor | float, upper: torch.Tensor | float, size: int | tuple[int, ...], device: str ) -> torch.Tensor: @@ -1346,6 +1371,7 @@ def sample_uniform( return torch.rand(*size, device=device) * (upper - lower) + lower +# same as isaaclab.utils.math.sample_log_uniform def sample_log_uniform( lower: torch.Tensor | float, upper: torch.Tensor | float, size: int | tuple[int, ...], device: str ) -> torch.Tensor: @@ -1377,6 +1403,7 @@ def sample_log_uniform( return torch.exp(sample_uniform(torch.log(lower), torch.log(upper), size, device)) +# same as isaaclab.utils.math.sample_gaussian def sample_gaussian( mean: torch.Tensor | float, std: torch.Tensor | float, size: int | tuple[int, ...], device: str ) -> torch.Tensor: diff --git a/roboverse_learn/rl/configs/rsl_rl/algorithm.py b/roboverse_learn/rl/configs/rsl_rl/algorithm.py index b116606ab..02c9ef764 100644 --- a/roboverse_learn/rl/configs/rsl_rl/algorithm.py +++ b/roboverse_learn/rl/configs/rsl_rl/algorithm.py @@ -74,6 +74,18 @@ class RslRlPpoAlgorithmCfg: class_name: str = "PPO" """The algorithm class name. Default is PPO.""" + value_loss_coef: float = MISSING + """The coefficient for the value loss.""" + + use_clipped_value_loss: bool = MISSING + """Whether to use clipped value loss.""" + + clip_param: float = MISSING + """The clipping parameter for the policy.""" + + entropy_coef: float = MISSING + """The coefficient for the entropy loss.""" + num_learning_epochs: int = MISSING """The number of learning epochs per update.""" @@ -92,24 +104,12 @@ class RslRlPpoAlgorithmCfg: lam: float = MISSING """The lambda parameter for Generalized Advantage Estimation (GAE).""" - entropy_coef: float = MISSING - """The coefficient for the entropy loss.""" - desired_kl: float = MISSING """The desired KL divergence.""" max_grad_norm: float = MISSING """The maximum gradient norm.""" - value_loss_coef: float = MISSING - """The coefficient for the value loss.""" - - use_clipped_value_loss: bool = MISSING - """Whether to use clipped value loss.""" - - clip_param: float = MISSING - """The clipping parameter for the policy.""" - normalize_advantage_per_mini_batch: bool = False """Whether to normalize the advantage per mini-batch. Default is False. @@ -117,12 +117,12 @@ class RslRlPpoAlgorithmCfg: Otherwise, the advantage is normalized over the entire collected trajectories. """ - rnd_cfg: None = None - """The RND configuration. Default is None, in which case RND is not used.""" - symmetry_cfg: None = None """The symmetry configuration. Default is None, in which case symmetry is not used.""" + rnd_cfg: None = None + """The RND configuration. Default is None, in which case RND is not used.""" + ######################### # Runner configurations # diff --git a/roboverse_learn/rl/configs/rsl_rl/ppo.py b/roboverse_learn/rl/configs/rsl_rl/ppo.py index 15d8b24b5..dc9e2913b 100644 --- a/roboverse_learn/rl/configs/rsl_rl/ppo.py +++ b/roboverse_learn/rl/configs/rsl_rl/ppo.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import Dict, List, Literal, Optional +from typing import Literal, Optional +from datetime import datetime from metasim.utils import configclass @@ -28,12 +29,14 @@ class RslRlPPOConfig(RslRlOnPolicyRunnerCfg): exp_name: str = "rsl_rl_ppo" experiment_name: str = "" # defaults to task name if left empty run_name: str = "" - seed: int = 1 + seed: int = 42 num_steps_per_env: int = 24 max_iterations: int = 50000 save_interval: int = 100 empirical_normalization: bool = False - obs_groups: Optional[Dict[str, List[str]]] = None + + # NOTE when `obs_groups` is None, it'll be resolved as {"policy": ["policy"], "critic": ["policy", "critic"]}, which makes the critic's obs the concatenated result of both policy and privileged obs, which means these two should not overlap under this setup + obs_groups: Optional[dict[str, list[str]]] = None clip_actions: Optional[float] = None logger: Literal["tensorboard", "neptune", "wandb"] = "tensorboard" neptune_project: str = "isaaclab" @@ -93,7 +96,8 @@ def __post_init__(self) -> None: if self.model_dir is None: name = self.exp_name or self.experiment_name - self.model_dir = os.path.join("outputs", name, self.task) + log_dir = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + self.model_dir = os.path.join("outputs", name, self.task, log_dir) if self.obs_groups is None: self.obs_groups = {"policy": ["policy"], "critic": ["policy", "critic"]} diff --git a/roboverse_learn/rl/configs/rsl_rl/ppo_tracking.py b/roboverse_learn/rl/configs/rsl_rl/ppo_tracking.py new file mode 100644 index 000000000..7ed16e1bb --- /dev/null +++ b/roboverse_learn/rl/configs/rsl_rl/ppo_tracking.py @@ -0,0 +1,119 @@ +import os +import wandb +import pathlib +from typing import Literal, Optional +from loguru import logger as log + +from metasim.utils import configclass +from roboverse_learn.rl.configs.rsl_rl.ppo import RslRlPPOConfig +from roboverse_learn.rl.configs.rsl_rl.algorithm import RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg + + +SimBackend = Literal[ + "isaacgym", + "isaacsim", + "isaaclab", + "mujoco", + "genesis", + "mjx", +] + + +@configclass +class RslRlPPOTrackingConfig(RslRlPPOConfig): + """RSL-RL PPO configs for motion tracking task.""" + # Experiment / runner settings + exp_name: str = "rsl_rl_ppo_tracking" + max_iterations = 30000 + save_interval = 500 + empirical_normalization = True # deprecated + obs_groups = {"policy": ["policy"], "critic": ["critic"]} + wandb_project: str = "rsl_rl_ppo_tracking" + + # Environment / device + task = "motion-tracking-isaaclab" + robot = "g1_tracking" # unused + sim: SimBackend = "isaacsim" + + # Logging + use_wandb: bool = True + + # WandB registry for loading motions (training) + registry_name: Optional[str] = None + + # WandB run path for loading model and motions (evaluation) + wandb_path: Optional[str] = None + + # Motion file path (gets overridden except when loading from local file) + motion_file: Optional[str] = None + + # Policy configuration + policy = RslRlPpoActorCriticCfg( + init_noise_std=1.0, + actor_obs_normalization=True, + critic_obs_normalization=True, + actor_hidden_dims=[512, 256, 128], + critic_hidden_dims=[512, 256, 128], + activation="elu", + ) + + # Algorithm configuration + algorithm = RslRlPpoAlgorithmCfg( + value_loss_coef=1.0, + use_clipped_value_loss=True, + clip_param=0.2, + # entropy_coef=0.05, # FIXME was a typo; high `entropy_coef` leads to high entropy loss, low converged mean reward, and short episode length + entropy_coef=0.005, # NOTE the only difference + num_learning_epochs=5, + num_mini_batches=4, + learning_rate=1.0e-3, + schedule="adaptive", + gamma=0.99, + lam=0.95, + desired_kl=0.01, + max_grad_norm=1.0, + ) + + def __post_init__(self) -> None: + """`motion_file` will point to an existing motion file path after `__post_init__()`.""" + super().__post_init__() + + if self.registry_name: + if ":" not in self.registry_name: # Check if the registry name includes alias, if not, append ":latest" + self.registry_name += ":latest" + + api = wandb.Api() + artifact = api.artifact(self.registry_name) + self.motion_file = str(pathlib.Path(artifact.download()) / "motion.npz") + + elif self.wandb_path: + if "model" in self.wandb_path: + run_path = "/".join(self.wandb_path.split("/")[:-1]) + # e.g., "org/project/run_id/model_1000.pt" yields "org/project/run_id" + else: + run_path = self.wandb_path + + wandb_run = wandb.Api().run(run_path) + if "model" in self.wandb_path: + # use specified model file + model_file = self.wandb_path.split("/")[-1] + else: + # files are formatted as model_xxx.pt, find the largest filename (max iter) + files = [file.name for file in wandb_run.files() if "model" in file.name] + model_file = max(files, key=lambda x: int(x.split("_")[1].split(".")[0])) + + # prepare log dir to store temporary files + log_dir = f"./outputs/{self.robot}/{self.task}/temp" + os.makedirs(log_dir, exist_ok=True) + + wandb_file = wandb_run.file(str(model_file)) + wandb_file.download(log_dir, replace=True) + log.info(f"Loading checkpoint from {run_path}/{model_file}") + self.checkpoint_path = f"{log_dir}/{model_file}" + + art = next((a for a in wandb_run.used_artifacts() if a.type == "motions"), None) + assert art, "No motion artifact found in WandB run" + self.motion_file = str(pathlib.Path(art.download()) / "motion.npz") + + else: + assert self.motion_file, "Provide local motion file path if not loading from WandB" diff --git a/roboverse_learn/rl/rsl_rl/env_wrapper.py b/roboverse_learn/rl/rsl_rl/env_wrapper.py index 548d9bb72..f0c7f00d3 100644 --- a/roboverse_learn/rl/rsl_rl/env_wrapper.py +++ b/roboverse_learn/rl/rsl_rl/env_wrapper.py @@ -36,6 +36,12 @@ def step(self, actions: torch.Tensor) -> tuple[TensorDict, torch.Tensor, torch.T # Merge info into extras extras = {**getattr(self.env, 'extras', {}), **info} + # move time out information to the extras dict + # this is only needed for infinite horizon tasks + # NOTE this is required by PPO to compute rewards for optimizing critic + if not getattr(self.env.cfg, "is_finite_horizon", False): + extras["time_outs"] = truncated + # Return RSL-RL format with TensorDict observations return self.obs_buf, rewards, dones, extras @@ -69,7 +75,7 @@ def device(self) -> torch.device: @property def cfg(self) -> dict | object: - return self.train_cfg + return self.env.cfg if hasattr(self.env, "cfg") else self.train_cfg @property def obs_buf(self) -> TensorDict: diff --git a/roboverse_learn/rl/rsl_rl/eval_tracking.py b/roboverse_learn/rl/rsl_rl/eval_tracking.py new file mode 100644 index 000000000..f22a58e87 --- /dev/null +++ b/roboverse_learn/rl/rsl_rl/eval_tracking.py @@ -0,0 +1,143 @@ +from __future__ import annotations + +import os +import random + +try: + import isaacgym # noqa: F401 +except ImportError: + pass + +import numpy as np +import rootutils +import torch +import tyro +import datetime +from loguru import logger as log +from rsl_rl.runners import OnPolicyRunner + +rootutils.setup_root(__file__, pythonpath=True) + +from roboverse_learn.rl.configs.rsl_rl.ppo_tracking import RslRlPPOTrackingConfig +from roboverse_learn.rl.rsl_rl.env_wrapper import RslRlEnvWrapper +from metasim.task.registry import get_task_class + + +torch.backends.cuda.matmul.allow_tf32 = True +torch.backends.cudnn.allow_tf32 = True +torch.backends.cudnn.benchmark = False + + +def get_log_dir(robot_name: str, task_name: str, now=None) -> str: + """Get the log directory.""" + if now is None: + now = datetime.datetime.now().strftime("%Y_%m%d_%H%M%S") + log_dir = f"./outputs/{robot_name}/{task_name}/{now}" + if not os.path.exists(log_dir): + os.makedirs(log_dir, exist_ok=True) + log.info("Log directory: {}", log_dir) + return log_dir + + +def get_load_path(load_root: str, checkpoint: int | str = None) -> str: + """Get the path to load the model from.""" + if isinstance(checkpoint, int): + if checkpoint == -1: + models = [file for file in os.listdir(load_root) if "model" in file and file.endswith(".pt")] + models.sort(key=lambda m: f"{m!s:0>15}") + model = models[-1] + load_path = f"{load_root}/{model}" + else: + load_path = f"{load_root}/model_{checkpoint}.pt" + else: + load_path = f"{load_root}/{checkpoint}.pt" + log.info(f"Loading checkpoint {checkpoint} from {load_root}") + return load_path + + +def make_roboverse_env(args: RslRlPPOTrackingConfig): + """Create RoboVerse task environment""" + task_cls = get_task_class(args.task) + + scenario = task_cls.scenario.update( + robots=[args.robot], + simulator=args.sim, + num_envs=args.num_envs, + headless=args.headless, + cameras=[] + ) + device = torch.device(args.device if torch.cuda.is_available() and args.cuda else "cpu") + + env = task_cls(scenario=scenario, args=args, device=device) + return env + + +def evaluate(args: RslRlPPOTrackingConfig): + """Evaluate a trained RSL-RL PPO policy""" + # Setup + random.seed(args.seed) + np.random.seed(args.seed) + torch.manual_seed(args.seed) + torch.backends.cudnn.deterministic = args.torch_deterministic + + device = torch.device(args.device if torch.cuda.is_available() and args.cuda else "cpu") + print(f"Using device: {device}") + + # Load checkpoint + if args.wandb_path: + checkpoint_path = args.checkpoint_path + + elif args.resume: + # Convert resume string to full log directory path + log_dir = ( + args.resume + if os.path.isdir(args.resume) + else get_log_dir(robot_name=args.robot, task_name=args.task, now=args.resume) + ) + + # Use get_load_path helper to handle checkpoint loading logic + # If checkpoint is None, default to -1 (latest checkpoint) + checkpoint_num = args.checkpoint if args.checkpoint is not None else -1 + checkpoint_path = get_load_path(load_root=log_dir, checkpoint=checkpoint_num) + print(f"Loading checkpoint from {checkpoint_path}") + + # checkpoint = torch.load(checkpoint_path, map_location=device) + else: + raise ValueError("Please provide either --wandb-path (WandB run path / model file path) or --resume (timestamp / log dir) for evaluation.") + + # Create environment + print(f"Creating environment: {args.task} with {args.num_envs} environments") + env = make_roboverse_env(args) + + # print(f"Loaded training config from task: {task_cls.__name__}") + + # Create environment wrapper + env_wrapper = RslRlEnvWrapper(env, train_cfg=args.train_cfg) + + runner = OnPolicyRunner( + env=env_wrapper, + train_cfg=args.train_cfg, + log_dir=None, + device=device + ) + runner.load(checkpoint_path) + policy = runner.get_inference_policy(device=device) + + # Reset environment + obs = env_wrapper.get_observations() + + print(f"Starting evaluation for 1000000 steps...") + for i in range(1000000): + actions = policy(obs) + obs, _, _, _ = env_wrapper.step(actions) + + if (i + 1) % 1000 == 0: + print(f"Step {i + 1}/1000000") + + env_wrapper.close() + print("Evaluation complete!") + + +if __name__ == "__main__": + args = tyro.cli(RslRlPPOTrackingConfig) + evaluate(args) diff --git a/roboverse_learn/rl/rsl_rl/ppo_tracking.py b/roboverse_learn/rl/rsl_rl/ppo_tracking.py new file mode 100644 index 000000000..81294d9c7 --- /dev/null +++ b/roboverse_learn/rl/rsl_rl/ppo_tracking.py @@ -0,0 +1,114 @@ +from __future__ import annotations + +import os +import random + +try: + import isaacgym # noqa: F401 +except ImportError: + pass + +import numpy as np +import rootutils +import torch +import tyro +from rsl_rl.runners import OnPolicyRunner + +rootutils.setup_root(__file__, pythonpath=True) + +from roboverse_learn.rl.configs.rsl_rl.ppo_tracking import RslRlPPOTrackingConfig +from roboverse_learn.rl.rsl_rl.env_wrapper import RslRlEnvWrapper +from metasim.task.registry import get_task_class + + +torch.backends.cuda.matmul.allow_tf32 = True +torch.backends.cudnn.allow_tf32 = True +torch.backends.cudnn.benchmark = False + + +def make_roboverse_env(args: RslRlPPOTrackingConfig): + """Create RoboVerse task environment""" + task_cls = get_task_class(args.task) + + # Load environment configuration from task + scenario = task_cls.scenario.update( + robots=[args.robot], + simulator=args.sim, + num_envs=args.num_envs, + headless=args.headless, + cameras=[] + ) + device = torch.device(args.device if torch.cuda.is_available() and args.cuda else "cpu") + + # Pass env_cfg to task constructor + env = task_cls(scenario=scenario, args=args, device=device) + return env + + +def train(args: RslRlPPOTrackingConfig): + """Train RSL-RL PPO""" + # Setup + random.seed(args.seed) + np.random.seed(args.seed) + torch.manual_seed(args.seed) + torch.backends.cudnn.deterministic = args.torch_deterministic + + device = torch.device(args.device if torch.cuda.is_available() and args.cuda else "cpu") + print(f"Using device: {device}") + os.makedirs(args.model_dir, exist_ok=True) + + # Initialize WandB + if args.use_wandb: + import wandb + wandb.init( + project=args.wandb_project, + entity=args.wandb_entity, + config=vars(args), + name=args.exp_name, + save_code=True + ) + # use artifact for training + if args.registry_name: + wandb.run.use_artifact(args.registry_name) + + # Create environment and wrapper + print(f"Creating environment: {args.task} with {args.num_envs} environments") + env = make_roboverse_env(args) + + # Use training config directly from args + train_cfg = args.train_cfg + + # Create environment wrapper + env_wrapper = RslRlEnvWrapper(env, train_cfg=train_cfg) + + runner = OnPolicyRunner( + env=env_wrapper, + train_cfg=train_cfg, + log_dir=args.model_dir, + device=device + ) + + # Train + print(f"Training RSL-RL PPO on {args.task} with {args.num_envs} environments") + print(f"Model directory: {args.model_dir}") + runner.learn( + num_learning_iterations=args.max_iterations, + init_at_random_ep_len=True + ) + + # Export policy + print("Exporting policy...") + policy = runner.get_inference_policy() + policy_path = os.path.join(args.model_dir, "policy.pt") + torch.jit.script(policy).save(policy_path) + print(f"Policy exported to {policy_path}") + + if args.use_wandb: + wandb.finish() + + print("Training complete!") + + +if __name__ == "__main__": + args = tyro.cli(RslRlPPOTrackingConfig) + train(args) diff --git a/roboverse_pack/callback_funcs/humanoid/reward_funcs.py b/roboverse_pack/callback_funcs/humanoid/reward_funcs.py index 4311fae98..6c3ad78ea 100644 --- a/roboverse_pack/callback_funcs/humanoid/reward_funcs.py +++ b/roboverse_pack/callback_funcs/humanoid/reward_funcs.py @@ -193,6 +193,7 @@ def feet_gait( is_stance = leg_phase[:, i] < threshold reward += ~(is_stance ^ is_contact[:, i]) + # only reward when the robot is commanded to move, so zero the reward when command speed is less than 0.1 if command_name == "base_velocity": cmd_norm = torch.norm(env.commands_manager.value[:, :2], dim=1) reward *= (cmd_norm > 0.1).float() diff --git a/roboverse_pack/callback_funcs/humanoid/termination_funcs.py b/roboverse_pack/callback_funcs/humanoid/termination_funcs.py index fdf2b80fb..01e340361 100644 --- a/roboverse_pack/callback_funcs/humanoid/termination_funcs.py +++ b/roboverse_pack/callback_funcs/humanoid/termination_funcs.py @@ -30,12 +30,12 @@ def bad_orientation(env: EnvTypes, env_states: TensorState, limit_angle: float) return torch.acos(-projected_gravity[:, 2]).abs() > limit_angle -def time_out(env: EnvTypes, env_states: TensorState) -> torch.Tensor: +def time_out(env: EnvTypes, env_states: TensorState) -> torch.Tensor: # used """Terminate the episode when the episode length exceeds the maximum episode length.""" return env._episode_steps >= env.max_episode_steps -def undesired_contact( +def undesired_contact( # unused env: EnvTypes, env_states: TensorState, contact_names: list[str], diff --git a/roboverse_pack/robots/__init__.py b/roboverse_pack/robots/__init__.py index b892ee7d6..c55ad98d2 100644 --- a/roboverse_pack/robots/__init__.py +++ b/roboverse_pack/robots/__init__.py @@ -17,6 +17,7 @@ from .franka_urdf_sapien_cfg import FrankaUrdfSapienCfg from .franka_with_gripper_extension_cfg import FrankaWithGripperExtensionCfg from .g1_cfg import G1Dof12Cfg, G1Dof23Cfg, G1Dof27Cfg, G1Dof29Cfg, G1Dof29Dex3Cfg +from .g1_tracking import G1TrackingCfg from .gen3_cfg import Gen3Cfg from .go2_cfg import Go2Cfg from .google_robot_static_cfg import GoogleRobotStaticCfg diff --git a/roboverse_pack/robots/allegrohand_cfg.py b/roboverse_pack/robots/allegrohand_cfg.py index 13aec516b..2c6db2a60 100644 --- a/roboverse_pack/robots/allegrohand_cfg.py +++ b/roboverse_pack/robots/allegrohand_cfg.py @@ -23,22 +23,22 @@ class AllegroHandCfg(RobotCfg): isaacgym_flip_visual_attachments: bool = False actuators: dict[str, BaseActuatorCfg] = { - "index_joint_0": BaseActuatorCfg(velocity_limit=30.0, torque_limit=0.5, stiffness=3.0, damping=0.1), - "index_joint_1": BaseActuatorCfg(velocity_limit=30.0, torque_limit=0.5, stiffness=3.0, damping=0.1), - "index_joint_2": BaseActuatorCfg(velocity_limit=30.0, torque_limit=0.5, stiffness=3.0, damping=0.1), - "index_joint_3": BaseActuatorCfg(velocity_limit=30.0, torque_limit=0.5, stiffness=3.0, damping=0.1), - "middle_joint_0": BaseActuatorCfg(velocity_limit=30.0, torque_limit=0.5, stiffness=3.0, damping=0.1), - "middle_joint_1": BaseActuatorCfg(velocity_limit=30.0, torque_limit=0.5, stiffness=3.0, damping=0.1), - "middle_joint_2": BaseActuatorCfg(velocity_limit=30.0, torque_limit=0.5, stiffness=3.0, damping=0.1), - "middle_joint_3": BaseActuatorCfg(velocity_limit=30.0, torque_limit=0.5, stiffness=3.0, damping=0.1), - "ring_joint_0": BaseActuatorCfg(velocity_limit=30.0, torque_limit=0.5, stiffness=3.0, damping=0.1), - "ring_joint_1": BaseActuatorCfg(velocity_limit=30.0, torque_limit=0.5, stiffness=3.0, damping=0.1), - "ring_joint_2": BaseActuatorCfg(velocity_limit=30.0, torque_limit=0.5, stiffness=3.0, damping=0.1), - "ring_joint_3": BaseActuatorCfg(velocity_limit=30.0, torque_limit=0.5, stiffness=3.0, damping=0.1), - "thumb_joint_0": BaseActuatorCfg(velocity_limit=30.0, torque_limit=0.5, stiffness=3.0, damping=0.1), - "thumb_joint_1": BaseActuatorCfg(velocity_limit=30.0, torque_limit=0.5, stiffness=3.0, damping=0.1), - "thumb_joint_2": BaseActuatorCfg(velocity_limit=30.0, torque_limit=0.5, stiffness=3.0, damping=0.1), - "thumb_joint_3": BaseActuatorCfg(velocity_limit=30.0, torque_limit=0.5, stiffness=3.0, damping=0.1), + "index_joint_0": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=0.5, stiffness=3.0, damping=0.1), + "index_joint_1": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=0.5, stiffness=3.0, damping=0.1), + "index_joint_2": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=0.5, stiffness=3.0, damping=0.1), + "index_joint_3": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=0.5, stiffness=3.0, damping=0.1), + "middle_joint_0": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=0.5, stiffness=3.0, damping=0.1), + "middle_joint_1": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=0.5, stiffness=3.0, damping=0.1), + "middle_joint_2": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=0.5, stiffness=3.0, damping=0.1), + "middle_joint_3": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=0.5, stiffness=3.0, damping=0.1), + "ring_joint_0": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=0.5, stiffness=3.0, damping=0.1), + "ring_joint_1": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=0.5, stiffness=3.0, damping=0.1), + "ring_joint_2": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=0.5, stiffness=3.0, damping=0.1), + "ring_joint_3": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=0.5, stiffness=3.0, damping=0.1), + "thumb_joint_0": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=0.5, stiffness=3.0, damping=0.1), + "thumb_joint_1": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=0.5, stiffness=3.0, damping=0.1), + "thumb_joint_2": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=0.5, stiffness=3.0, damping=0.1), + "thumb_joint_3": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=0.5, stiffness=3.0, damping=0.1), } joint_limits: dict[str, tuple[float, float]] = { diff --git a/roboverse_pack/robots/ant_cfg.py b/roboverse_pack/robots/ant_cfg.py index 041dc5a15..1b6190edb 100644 --- a/roboverse_pack/robots/ant_cfg.py +++ b/roboverse_pack/robots/ant_cfg.py @@ -66,14 +66,14 @@ def __post_init__(self): # For direct torque control, set stiffness=1.0, damping=0.0 # This makes the PD controller act as direct torque (torque = 1.0 * action) self.actuators = { - "hip_1": BaseActuatorCfg(velocity_limit=30.0, torque_limit=15.0, stiffness=1.0, damping=0.0), - "ankle_1": BaseActuatorCfg(velocity_limit=30.0, torque_limit=15.0, stiffness=1.0, damping=0.0), - "hip_2": BaseActuatorCfg(velocity_limit=30.0, torque_limit=15.0, stiffness=1.0, damping=0.0), - "ankle_2": BaseActuatorCfg(velocity_limit=30.0, torque_limit=15.0, stiffness=1.0, damping=0.0), - "hip_3": BaseActuatorCfg(velocity_limit=30.0, torque_limit=15.0, stiffness=1.0, damping=0.0), - "ankle_3": BaseActuatorCfg(velocity_limit=30.0, torque_limit=15.0, stiffness=1.0, damping=0.0), - "hip_4": BaseActuatorCfg(velocity_limit=30.0, torque_limit=15.0, stiffness=1.0, damping=0.0), - "ankle_4": BaseActuatorCfg(velocity_limit=30.0, torque_limit=15.0, stiffness=1.0, damping=0.0), + "hip_1": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=15.0, stiffness=1.0, damping=0.0), + "ankle_1": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=15.0, stiffness=1.0, damping=0.0), + "hip_2": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=15.0, stiffness=1.0, damping=0.0), + "ankle_2": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=15.0, stiffness=1.0, damping=0.0), + "hip_3": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=15.0, stiffness=1.0, damping=0.0), + "ankle_3": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=15.0, stiffness=1.0, damping=0.0), + "hip_4": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=15.0, stiffness=1.0, damping=0.0), + "ankle_4": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=15.0, stiffness=1.0, damping=0.0), } if self.control_type is None: diff --git a/roboverse_pack/robots/anymal_cfg.py b/roboverse_pack/robots/anymal_cfg.py index 2cb09b0bf..7669f07b3 100644 --- a/roboverse_pack/robots/anymal_cfg.py +++ b/roboverse_pack/robots/anymal_cfg.py @@ -23,20 +23,20 @@ class AnymalCfg(RobotCfg): # Define actuators for each joint actuators: dict[str, BaseActuatorCfg] = { # Hip Abduction/Adduction - "LF_HAA": BaseActuatorCfg(velocity_limit=30.0, torque_limit=40.0, stiffness=85.0, damping=2.0), - "LH_HAA": BaseActuatorCfg(velocity_limit=30.0, torque_limit=40.0, stiffness=85.0, damping=2.0), - "RF_HAA": BaseActuatorCfg(velocity_limit=30.0, torque_limit=40.0, stiffness=85.0, damping=2.0), - "RH_HAA": BaseActuatorCfg(velocity_limit=30.0, torque_limit=40.0, stiffness=85.0, damping=2.0), + "LF_HAA": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=40.0, stiffness=85.0, damping=2.0), + "LH_HAA": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=40.0, stiffness=85.0, damping=2.0), + "RF_HAA": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=40.0, stiffness=85.0, damping=2.0), + "RH_HAA": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=40.0, stiffness=85.0, damping=2.0), # Hip Flexion/Extension - "LF_HFE": BaseActuatorCfg(velocity_limit=30.0, torque_limit=40.0, stiffness=85.0, damping=2.0), - "LH_HFE": BaseActuatorCfg(velocity_limit=30.0, torque_limit=40.0, stiffness=85.0, damping=2.0), - "RF_HFE": BaseActuatorCfg(velocity_limit=30.0, torque_limit=40.0, stiffness=85.0, damping=2.0), - "RH_HFE": BaseActuatorCfg(velocity_limit=30.0, torque_limit=40.0, stiffness=85.0, damping=2.0), + "LF_HFE": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=40.0, stiffness=85.0, damping=2.0), + "LH_HFE": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=40.0, stiffness=85.0, damping=2.0), + "RF_HFE": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=40.0, stiffness=85.0, damping=2.0), + "RH_HFE": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=40.0, stiffness=85.0, damping=2.0), # Knee Flexion/Extension - "LF_KFE": BaseActuatorCfg(velocity_limit=30.0, torque_limit=40.0, stiffness=85.0, damping=2.0), - "LH_KFE": BaseActuatorCfg(velocity_limit=30.0, torque_limit=40.0, stiffness=85.0, damping=2.0), - "RF_KFE": BaseActuatorCfg(velocity_limit=30.0, torque_limit=40.0, stiffness=85.0, damping=2.0), - "RH_KFE": BaseActuatorCfg(velocity_limit=30.0, torque_limit=40.0, stiffness=85.0, damping=2.0), + "LF_KFE": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=40.0, stiffness=85.0, damping=2.0), + "LH_KFE": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=40.0, stiffness=85.0, damping=2.0), + "RF_KFE": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=40.0, stiffness=85.0, damping=2.0), + "RH_KFE": BaseActuatorCfg(velocity_limit=30.0, effort_limit_sim=40.0, stiffness=85.0, damping=2.0), } joint_limits: dict[str, tuple[float, float]] = { diff --git a/roboverse_pack/robots/cartpole_cfg.py b/roboverse_pack/robots/cartpole_cfg.py index 6de346b36..5e1f06483 100644 --- a/roboverse_pack/robots/cartpole_cfg.py +++ b/roboverse_pack/robots/cartpole_cfg.py @@ -44,13 +44,13 @@ def __post_init__(self): self.actuators = { "slider_to_cart": BaseActuatorCfg( velocity_limit=100.0, # From URDF - torque_limit=400.0, # Max force for cart (from IsaacGymEnvs) + effort_limit_sim=400.0, # Max force for cart (from IsaacGymEnvs) stiffness=0.0, # Direct force control damping=0.0, # No damping for direct control ), "cart_to_pole": BaseActuatorCfg( velocity_limit=8.0, # From URDF - torque_limit=0.0, # Passive joint - no actuation + effort_limit_sim=0.0, # Passive joint - no actuation stiffness=0.0, damping=0.0, fully_actuated=False, # This is a passive joint diff --git a/roboverse_pack/robots/g1_cfg.py b/roboverse_pack/robots/g1_cfg.py index 82171d132..31ef1a19b 100644 --- a/roboverse_pack/robots/g1_cfg.py +++ b/roboverse_pack/robots/g1_cfg.py @@ -21,24 +21,24 @@ class G1Dof12Cfg(RobotCfg): enabled_self_collisions: bool = True isaacgym_read_mjcf = False isaacgym_flip_visual_attachments: bool = False - collapse_fixed_joints: bool = False # True + collapse_fixed_joints: bool = False actuators: dict[str, BaseActuatorCfg] = { # N7520-14.3: hip_pitch, hip_yaw (stiffness 100, damping 2, torque 88, vel 32) - "left_hip_pitch_joint": BaseActuatorCfg(stiffness=100, damping=2, torque_limit=88, velocity_limit=32.0), - "left_hip_yaw_joint": BaseActuatorCfg(stiffness=100, damping=2, torque_limit=88, velocity_limit=32.0), - "right_hip_pitch_joint": BaseActuatorCfg(stiffness=100, damping=2, torque_limit=88, velocity_limit=32.0), - "right_hip_yaw_joint": BaseActuatorCfg(stiffness=100, damping=2, torque_limit=88, velocity_limit=32.0), + "left_hip_pitch_joint": BaseActuatorCfg(stiffness=100, damping=2, effort_limit_sim=88, velocity_limit=32.0), + "left_hip_yaw_joint": BaseActuatorCfg(stiffness=100, damping=2, effort_limit_sim=88, velocity_limit=32.0), + "right_hip_pitch_joint": BaseActuatorCfg(stiffness=100, damping=2, effort_limit_sim=88, velocity_limit=32.0), + "right_hip_yaw_joint": BaseActuatorCfg(stiffness=100, damping=2, effort_limit_sim=88, velocity_limit=32.0), # N7520-22.5: hip_roll, knee (hip_roll stiffness 100/damping 2; knee stiffness 150/damping 4; torque 139; vel 20) - "left_hip_roll_joint": BaseActuatorCfg(stiffness=100, damping=2, torque_limit=139, velocity_limit=20.0), - "right_hip_roll_joint": BaseActuatorCfg(stiffness=100, damping=2, torque_limit=139, velocity_limit=20.0), - "left_knee_joint": BaseActuatorCfg(stiffness=150, damping=4, torque_limit=139, velocity_limit=20.0), - "right_knee_joint": BaseActuatorCfg(stiffness=150, damping=4, torque_limit=139, velocity_limit=20.0), + "left_hip_roll_joint": BaseActuatorCfg(stiffness=100, damping=2, effort_limit_sim=139, velocity_limit=20.0), + "right_hip_roll_joint": BaseActuatorCfg(stiffness=100, damping=2, effort_limit_sim=139, velocity_limit=20.0), + "left_knee_joint": BaseActuatorCfg(stiffness=150, damping=4, effort_limit_sim=139, velocity_limit=20.0), + "right_knee_joint": BaseActuatorCfg(stiffness=150, damping=4, effort_limit_sim=139, velocity_limit=20.0), # N5020-16: ankles (stiffness 40, damping 2, torque 25, vel 37) - "left_ankle_pitch_joint": BaseActuatorCfg(stiffness=40, damping=2, torque_limit=25, velocity_limit=37.0), - "left_ankle_roll_joint": BaseActuatorCfg(stiffness=40, damping=2, torque_limit=25, velocity_limit=37.0), - "right_ankle_pitch_joint": BaseActuatorCfg(stiffness=40, damping=2, torque_limit=25, velocity_limit=37.0), - "right_ankle_roll_joint": BaseActuatorCfg(stiffness=40, damping=2, torque_limit=25, velocity_limit=37.0), + "left_ankle_pitch_joint": BaseActuatorCfg(stiffness=40, damping=2, effort_limit_sim=25, velocity_limit=37.0), + "left_ankle_roll_joint": BaseActuatorCfg(stiffness=40, damping=2, effort_limit_sim=25, velocity_limit=37.0), + "right_ankle_pitch_joint": BaseActuatorCfg(stiffness=40, damping=2, effort_limit_sim=25, velocity_limit=37.0), + "right_ankle_roll_joint": BaseActuatorCfg(stiffness=40, damping=2, effort_limit_sim=25, velocity_limit=37.0), } joint_limits: dict[str, tuple[float, float]] = { @@ -119,18 +119,20 @@ class G1Dof23Cfg(G1Dof12Cfg): actuators = { **G1Dof12Cfg().actuators, # N7520-14.3: waist_yaw (stiffness 200, damping 5, torque 88, vel 32) - "waist_yaw_joint": BaseActuatorCfg(stiffness=200, damping=5, torque_limit=88, velocity_limit=32.0), + "waist_yaw_joint": BaseActuatorCfg(stiffness=200, damping=5, effort_limit_sim=88, velocity_limit=32.0), # N5020-16: shoulders, elbows, wrist_roll (stiffness 40, damping 1, torque 25, vel 37) - "left_shoulder_pitch_joint": BaseActuatorCfg(stiffness=40, damping=1, torque_limit=25, velocity_limit=37.0), - "left_shoulder_roll_joint": BaseActuatorCfg(stiffness=40, damping=1, torque_limit=25, velocity_limit=37.0), - "left_shoulder_yaw_joint": BaseActuatorCfg(stiffness=40, damping=1, torque_limit=25, velocity_limit=37.0), - "left_elbow_joint": BaseActuatorCfg(stiffness=40, damping=1, torque_limit=25, velocity_limit=37.0), - "left_wrist_roll_joint": BaseActuatorCfg(stiffness=40, damping=1, torque_limit=25, velocity_limit=37.0), - "right_shoulder_pitch_joint": BaseActuatorCfg(stiffness=40, damping=1, torque_limit=25, velocity_limit=37.0), - "right_shoulder_roll_joint": BaseActuatorCfg(stiffness=40, damping=1, torque_limit=25, velocity_limit=37.0), - "right_shoulder_yaw_joint": BaseActuatorCfg(stiffness=40, damping=1, torque_limit=25, velocity_limit=37.0), - "right_elbow_joint": BaseActuatorCfg(stiffness=40, damping=1, torque_limit=25, velocity_limit=37.0), - "right_wrist_roll_joint": BaseActuatorCfg(stiffness=40, damping=1, torque_limit=25, velocity_limit=37.0), + "left_shoulder_pitch_joint": BaseActuatorCfg(stiffness=40, damping=1, effort_limit_sim=25, velocity_limit=37.0), + "left_shoulder_roll_joint": BaseActuatorCfg(stiffness=40, damping=1, effort_limit_sim=25, velocity_limit=37.0), + "left_shoulder_yaw_joint": BaseActuatorCfg(stiffness=40, damping=1, effort_limit_sim=25, velocity_limit=37.0), + "left_elbow_joint": BaseActuatorCfg(stiffness=40, damping=1, effort_limit_sim=25, velocity_limit=37.0), + "left_wrist_roll_joint": BaseActuatorCfg(stiffness=40, damping=1, effort_limit_sim=25, velocity_limit=37.0), + "right_shoulder_pitch_joint": BaseActuatorCfg( + stiffness=40, damping=1, effort_limit_sim=25, velocity_limit=37.0 + ), + "right_shoulder_roll_joint": BaseActuatorCfg(stiffness=40, damping=1, effort_limit_sim=25, velocity_limit=37.0), + "right_shoulder_yaw_joint": BaseActuatorCfg(stiffness=40, damping=1, effort_limit_sim=25, velocity_limit=37.0), + "right_elbow_joint": BaseActuatorCfg(stiffness=40, damping=1, effort_limit_sim=25, velocity_limit=37.0), + "right_wrist_roll_joint": BaseActuatorCfg(stiffness=40, damping=1, effort_limit_sim=25, velocity_limit=37.0), } joint_limits = { @@ -197,10 +199,10 @@ class G1Dof27Cfg(G1Dof23Cfg): actuators = { **G1Dof23Cfg().actuators, # W4010-25: wrist_pitch/yaw (stiffness 40, damping 1, torque 5, vel 22) - "left_wrist_pitch_joint": BaseActuatorCfg(stiffness=40, damping=1, torque_limit=5, velocity_limit=22.0), - "left_wrist_yaw_joint": BaseActuatorCfg(stiffness=40, damping=1, torque_limit=5, velocity_limit=22.0), - "right_wrist_pitch_joint": BaseActuatorCfg(stiffness=40, damping=1, torque_limit=5, velocity_limit=22.0), - "right_wrist_yaw_joint": BaseActuatorCfg(stiffness=40, damping=1, torque_limit=5, velocity_limit=22.0), + "left_wrist_pitch_joint": BaseActuatorCfg(stiffness=40, damping=1, effort_limit_sim=5, velocity_limit=22.0), + "left_wrist_yaw_joint": BaseActuatorCfg(stiffness=40, damping=1, effort_limit_sim=5, velocity_limit=22.0), + "right_wrist_pitch_joint": BaseActuatorCfg(stiffness=40, damping=1, effort_limit_sim=5, velocity_limit=22.0), + "right_wrist_yaw_joint": BaseActuatorCfg(stiffness=40, damping=1, effort_limit_sim=5, velocity_limit=22.0), } joint_limits = { @@ -218,7 +220,6 @@ class G1Dof27Cfg(G1Dof23Cfg): # "right_wrist_pitch_joint": 5, # "right_wrist_yaw_joint": 5, # } - default_joint_positions = { **G1Dof23Cfg().default_joint_positions, "left_wrist_pitch_joint": 0.0, @@ -248,8 +249,8 @@ class G1Dof29Cfg(G1Dof27Cfg): actuators = { **G1Dof27Cfg().actuators, # N5020-16: waist roll/pitch (stiffness 40, damping 5, torque 25, vel 37) - "waist_roll_joint": BaseActuatorCfg(stiffness=40, damping=5, torque_limit=25, velocity_limit=37.0), - "waist_pitch_joint": BaseActuatorCfg(stiffness=40, damping=5, torque_limit=25, velocity_limit=37.0), + "waist_roll_joint": BaseActuatorCfg(stiffness=40, damping=5, effort_limit_sim=25, velocity_limit=37.0), + "waist_pitch_joint": BaseActuatorCfg(stiffness=40, damping=5, effort_limit_sim=25, velocity_limit=37.0), } joint_limits = { @@ -282,20 +283,20 @@ class G1Dof29Dex3Cfg(G1Dof29Cfg): actuators = { **G1Dof29Cfg().actuators, - "left_hand_thumb_0_joint": BaseActuatorCfg(stiffness=5, damping=1, torque_limit=2.45), - "left_hand_thumb_1_joint": BaseActuatorCfg(stiffness=5, damping=1, torque_limit=1.4), - "left_hand_thumb_2_joint": BaseActuatorCfg(stiffness=5, damping=1, torque_limit=1.4), - "left_hand_middle_0_joint": BaseActuatorCfg(stiffness=5, damping=1, torque_limit=1.4), - "left_hand_middle_1_joint": BaseActuatorCfg(stiffness=5, damping=1, torque_limit=1.4), - "left_hand_index_0_joint": BaseActuatorCfg(stiffness=5, damping=1, torque_limit=1.4), - "left_hand_index_1_joint": BaseActuatorCfg(stiffness=5, damping=1, torque_limit=1.4), - "right_hand_thumb_0_joint": BaseActuatorCfg(stiffness=5, damping=1, torque_limit=2.45), - "right_hand_thumb_1_joint": BaseActuatorCfg(stiffness=5, damping=1, torque_limit=1.4), - "right_hand_thumb_2_joint": BaseActuatorCfg(stiffness=5, damping=1, torque_limit=1.4), - "right_hand_middle_0_joint": BaseActuatorCfg(stiffness=5, damping=1, torque_limit=1.4), - "right_hand_middle_1_joint": BaseActuatorCfg(stiffness=5, damping=1, torque_limit=1.4), - "right_hand_index_0_joint": BaseActuatorCfg(stiffness=5, damping=1, torque_limit=1.4), - "right_hand_index_1_joint": BaseActuatorCfg(stiffness=5, damping=1, torque_limit=1.4), + "left_hand_thumb_0_joint": BaseActuatorCfg(stiffness=5, damping=1, effort_limit_sim=2.45), + "left_hand_thumb_1_joint": BaseActuatorCfg(stiffness=5, damping=1, effort_limit_sim=1.4), + "left_hand_thumb_2_joint": BaseActuatorCfg(stiffness=5, damping=1, effort_limit_sim=1.4), + "left_hand_middle_0_joint": BaseActuatorCfg(stiffness=5, damping=1, effort_limit_sim=1.4), + "left_hand_middle_1_joint": BaseActuatorCfg(stiffness=5, damping=1, effort_limit_sim=1.4), + "left_hand_index_0_joint": BaseActuatorCfg(stiffness=5, damping=1, effort_limit_sim=1.4), + "left_hand_index_1_joint": BaseActuatorCfg(stiffness=5, damping=1, effort_limit_sim=1.4), + "right_hand_thumb_0_joint": BaseActuatorCfg(stiffness=5, damping=1, effort_limit_sim=2.45), + "right_hand_thumb_1_joint": BaseActuatorCfg(stiffness=5, damping=1, effort_limit_sim=1.4), + "right_hand_thumb_2_joint": BaseActuatorCfg(stiffness=5, damping=1, effort_limit_sim=1.4), + "right_hand_middle_0_joint": BaseActuatorCfg(stiffness=5, damping=1, effort_limit_sim=1.4), + "right_hand_middle_1_joint": BaseActuatorCfg(stiffness=5, damping=1, effort_limit_sim=1.4), + "right_hand_index_0_joint": BaseActuatorCfg(stiffness=5, damping=1, effort_limit_sim=1.4), + "right_hand_index_1_joint": BaseActuatorCfg(stiffness=5, damping=1, effort_limit_sim=1.4), } joint_limits = { diff --git a/roboverse_pack/robots/g1_tracking.py b/roboverse_pack/robots/g1_tracking.py new file mode 100644 index 000000000..77ef9f630 --- /dev/null +++ b/roboverse_pack/robots/g1_tracking.py @@ -0,0 +1,267 @@ +from __future__ import annotations + +import os +from dataclasses import MISSING +from typing import Any + +from metasim.scenario.robot import BaseActuatorCfg, RobotCfg +from metasim.utils import configclass +from roboverse_pack.tasks.beyondmimic.metasim.utils.string import resolve_matching_names_values + +ASSET_DIR = "roboverse_data" + +ARMATURE_5020 = 0.003609725 +ARMATURE_7520_14 = 0.010177520 +ARMATURE_7520_22 = 0.025101925 +ARMATURE_4010 = 0.00425 + +NATURAL_FREQ = 10 * 2.0 * 3.1415926535 # 10Hz +DAMPING_RATIO = 2.0 + +STIFFNESS_5020 = ARMATURE_5020 * NATURAL_FREQ**2 +STIFFNESS_7520_14 = ARMATURE_7520_14 * NATURAL_FREQ**2 +STIFFNESS_7520_22 = ARMATURE_7520_22 * NATURAL_FREQ**2 +STIFFNESS_4010 = ARMATURE_4010 * NATURAL_FREQ**2 + +DAMPING_5020 = 2.0 * DAMPING_RATIO * ARMATURE_5020 * NATURAL_FREQ +DAMPING_7520_14 = 2.0 * DAMPING_RATIO * ARMATURE_7520_14 * NATURAL_FREQ +DAMPING_7520_22 = 2.0 * DAMPING_RATIO * ARMATURE_7520_22 * NATURAL_FREQ +DAMPING_4010 = 2.0 * DAMPING_RATIO * ARMATURE_4010 * NATURAL_FREQ + + +@configclass +class ActuatorCfg: + joint_names_expr: list[str] = MISSING + effort_limit_sim: dict[str, float] | float = MISSING + velocity_limit_sim: dict[str, float] | float = MISSING + stiffness: dict[str, float] | float = MISSING + damping: dict[str, float] | float = MISSING + armature: dict[str, float] | float = MISSING + + +@configclass +class G1TrackingCfg(RobotCfg): + name: str = "g1_tracking" + num_joints: int = 29 + + # NOTE this path should be absolute because the converted USD file may contain references to other files (e.g., @configuration/g1_sensor.usd@) which will be incorrectly resolved if the USD path is relative + usd_path: str = os.path.abspath(f"{ASSET_DIR}/unitree_description/usd/g1/g1.usd") + xml_path: str = f"{ASSET_DIR}/unitree_description/mjcf/g1.xml" + urdf_path: str = f"{ASSET_DIR}/unitree_description/urdf/g1/main.urdf" + mjcf_path = xml_path + enabled_gravity: bool = True + enabled_self_collisions: bool = True + + max_depenetration_velocity: float = 1.0 + fix_base_link: bool | None = None + + # to override the default collision properties of USD file config in Isaac Sim handler + collision_props: Any | None = None + + # NOTE initial state is defined in `BaseEnvCfg.InitialStates` + + default_pos = (0.0, 0.0, 0.76) + default_joint_positions = { + ".*_hip_pitch_joint": -0.312, + ".*_knee_joint": 0.669, + ".*_ankle_pitch_joint": -0.363, + ".*_elbow_joint": 0.6, + "left_shoulder_roll_joint": 0.2, + "left_shoulder_pitch_joint": 0.2, + "right_shoulder_roll_joint": -0.2, + "right_shoulder_pitch_joint": 0.2, + } + default_joint_velocities = {".*": 0.0} + default_rot = (1.0, 0.0, 0.0, 0.0) + soft_joint_pos_limit_factor = 0.9 + + # NOTE joint position limits obtained through `Articulation.root_physx_view.get_dof_limits()` in Isaac Lab + joint_limits: dict[str, tuple[float, float]] = { + "left_hip_pitch_joint": (-2.5306997299194336, 2.8797998428344727), + "right_hip_pitch_joint": (-2.5306997299194336, 2.8797998428344727), + "waist_yaw_joint": (-2.618000030517578, 2.618000030517578), + "left_hip_roll_joint": (-0.5235999226570129, 2.967099666595459), + "right_hip_roll_joint": (-2.967099666595459, 0.5235999226570129), + "waist_roll_joint": (-0.5199999213218689, 0.5199999213218689), + "left_hip_yaw_joint": (-2.7576000690460205, 2.7576000690460205), + "right_hip_yaw_joint": (-2.7576000690460205, 2.7576000690460205), + "waist_pitch_joint": (-0.5199999213218689, 0.5199999213218689), + "left_knee_joint": (-0.08726699650287628, 2.8797998428344727), + "right_knee_joint": (-0.08726699650287628, 2.8797998428344727), + "left_shoulder_pitch_joint": (-3.0891997814178467, 2.6703999042510986), + "right_shoulder_pitch_joint": (-3.0891997814178467, 2.6703999042510986), + "left_ankle_pitch_joint": (-0.8726699352264404, 0.5235999226570129), + "right_ankle_pitch_joint": (-0.8726699352264404, 0.5235999226570129), + "left_shoulder_roll_joint": (-1.5881999731063843, 2.251499652862549), + "right_shoulder_roll_joint": (-2.251499652862549, 1.5881999731063843), + "left_ankle_roll_joint": (-0.26179996132850647, 0.26179996132850647), + "right_ankle_roll_joint": (-0.26179996132850647, 0.26179996132850647), + "left_shoulder_yaw_joint": (-2.618000030517578, 2.618000030517578), + "right_shoulder_yaw_joint": (-2.618000030517578, 2.618000030517578), + "left_elbow_joint": (-1.0471998453140259, 2.0943996906280518), + "right_elbow_joint": (-1.0471998453140259, 2.0943996906280518), + "left_wrist_roll_joint": (-1.972221851348877, 1.972221851348877), + "right_wrist_roll_joint": (-1.972221851348877, 1.972221851348877), + "left_wrist_pitch_joint": (-1.6144295930862427, 1.6144295930862427), + "right_wrist_pitch_joint": (-1.6144295930862427, 1.6144295930862427), + "left_wrist_yaw_joint": (-1.6144295930862427, 1.6144295930862427), + "right_wrist_yaw_joint": (-1.6144295930862427, 1.6144295930862427), + } + + actuators_cfg = { + "legs": ActuatorCfg( + joint_names_expr=[ + ".*_hip_yaw_joint", + ".*_hip_roll_joint", + ".*_hip_pitch_joint", + ".*_knee_joint", + ], + effort_limit_sim={ + ".*_hip_yaw_joint": 88.0, + ".*_hip_roll_joint": 139.0, + ".*_hip_pitch_joint": 88.0, + ".*_knee_joint": 139.0, + }, + velocity_limit_sim={ + ".*_hip_yaw_joint": 32.0, + ".*_hip_roll_joint": 20.0, + ".*_hip_pitch_joint": 32.0, + ".*_knee_joint": 20.0, + }, + stiffness={ + ".*_hip_pitch_joint": STIFFNESS_7520_14, + ".*_hip_roll_joint": STIFFNESS_7520_22, + ".*_hip_yaw_joint": STIFFNESS_7520_14, + ".*_knee_joint": STIFFNESS_7520_22, + }, + damping={ + ".*_hip_pitch_joint": DAMPING_7520_14, + ".*_hip_roll_joint": DAMPING_7520_22, + ".*_hip_yaw_joint": DAMPING_7520_14, + ".*_knee_joint": DAMPING_7520_22, + }, + armature={ + ".*_hip_pitch_joint": ARMATURE_7520_14, + ".*_hip_roll_joint": ARMATURE_7520_22, + ".*_hip_yaw_joint": ARMATURE_7520_14, + ".*_knee_joint": ARMATURE_7520_22, + }, + ), + "feet": ActuatorCfg( + effort_limit_sim=50.0, + velocity_limit_sim=37.0, + joint_names_expr=[".*_ankle_pitch_joint", ".*_ankle_roll_joint"], + stiffness=2.0 * STIFFNESS_5020, + damping=2.0 * DAMPING_5020, + armature=2.0 * ARMATURE_5020, + ), + "waist": ActuatorCfg( + effort_limit_sim=50, + velocity_limit_sim=37.0, + joint_names_expr=["waist_roll_joint", "waist_pitch_joint"], + stiffness=2.0 * STIFFNESS_5020, + damping=2.0 * DAMPING_5020, + armature=2.0 * ARMATURE_5020, + ), + "waist_yaw": ActuatorCfg( + effort_limit_sim=88, + velocity_limit_sim=32.0, + joint_names_expr=["waist_yaw_joint"], + stiffness=STIFFNESS_7520_14, + damping=DAMPING_7520_14, + armature=ARMATURE_7520_14, + ), + "arms": ActuatorCfg( + joint_names_expr=[ + ".*_shoulder_pitch_joint", + ".*_shoulder_roll_joint", + ".*_shoulder_yaw_joint", + ".*_elbow_joint", + ".*_wrist_roll_joint", + ".*_wrist_pitch_joint", + ".*_wrist_yaw_joint", + ], + effort_limit_sim={ + ".*_shoulder_pitch_joint": 25.0, + ".*_shoulder_roll_joint": 25.0, + ".*_shoulder_yaw_joint": 25.0, + ".*_elbow_joint": 25.0, + ".*_wrist_roll_joint": 25.0, + ".*_wrist_pitch_joint": 5.0, + ".*_wrist_yaw_joint": 5.0, + }, + velocity_limit_sim={ + ".*_shoulder_pitch_joint": 37.0, + ".*_shoulder_roll_joint": 37.0, + ".*_shoulder_yaw_joint": 37.0, + ".*_elbow_joint": 37.0, + ".*_wrist_roll_joint": 37.0, + ".*_wrist_pitch_joint": 22.0, + ".*_wrist_yaw_joint": 22.0, + }, + stiffness={ + ".*_shoulder_pitch_joint": STIFFNESS_5020, + ".*_shoulder_roll_joint": STIFFNESS_5020, + ".*_shoulder_yaw_joint": STIFFNESS_5020, + ".*_elbow_joint": STIFFNESS_5020, + ".*_wrist_roll_joint": STIFFNESS_5020, + ".*_wrist_pitch_joint": STIFFNESS_4010, + ".*_wrist_yaw_joint": STIFFNESS_4010, + }, + damping={ + ".*_shoulder_pitch_joint": DAMPING_5020, + ".*_shoulder_roll_joint": DAMPING_5020, + ".*_shoulder_yaw_joint": DAMPING_5020, + ".*_elbow_joint": DAMPING_5020, + ".*_wrist_roll_joint": DAMPING_5020, + ".*_wrist_pitch_joint": DAMPING_4010, + ".*_wrist_yaw_joint": DAMPING_4010, + }, + armature={ + ".*_shoulder_pitch_joint": ARMATURE_5020, + ".*_shoulder_roll_joint": ARMATURE_5020, + ".*_shoulder_yaw_joint": ARMATURE_5020, + ".*_elbow_joint": ARMATURE_5020, + ".*_wrist_roll_joint": ARMATURE_5020, + ".*_wrist_pitch_joint": ARMATURE_4010, + ".*_wrist_yaw_joint": ARMATURE_4010, + }, + ), + } + actuators: dict[str, BaseActuatorCfg] = dict() + action_scale: dict[str, float] = dict() + action_clip: float | None = None + action_offset: bool = True # offset actions by `default_dof_pos_original` specified in the task class + + def __post_init__(self): + actuators = {} + action_scale = {} + for cfg in self.actuators_cfg.values(): + for name in cfg.joint_names_expr: + effort_limit = ( + cfg.effort_limit_sim[name] if isinstance(cfg.effort_limit_sim, dict) else cfg.effort_limit_sim + ) + vel_limit = ( + cfg.velocity_limit_sim[name] if isinstance(cfg.velocity_limit_sim, dict) else cfg.velocity_limit_sim + ) + stiffness = cfg.stiffness[name] if isinstance(cfg.stiffness, dict) else cfg.stiffness + damping = cfg.damping[name] if isinstance(cfg.damping, dict) else cfg.damping + armature = cfg.armature[name] if isinstance(cfg.armature, dict) else cfg.armature + + # align actuators with RoboVerse API + actuators[name] = BaseActuatorCfg( + effort_limit_sim=effort_limit, + velocity_limit_sim=vel_limit, + stiffness=stiffness, + damping=damping, + armature=armature, + ) + # compute action scales + action_scale[name] = 0.25 * effort_limit / stiffness + + # resolve regex to avoid compatibility issues with RoboVerse APIs + joint_names = list(self.joint_limits.keys()) + _, name_list, value_list = resolve_matching_names_values(actuators, joint_names) + self.actuators = {k: v for k, v in zip(name_list, value_list)} + _, name_list, value_list = resolve_matching_names_values(action_scale, joint_names) + self.action_scale = {k: v for k, v in zip(name_list, value_list)} diff --git a/roboverse_pack/robots/gen3_cfg.py b/roboverse_pack/robots/gen3_cfg.py index f0e607edf..d127706e1 100644 --- a/roboverse_pack/robots/gen3_cfg.py +++ b/roboverse_pack/robots/gen3_cfg.py @@ -31,13 +31,19 @@ class Gen3Cfg(RobotCfg): # Actuator configuration - Based on Gen3 joint specifications # Large actuators for main arm joints, small actuators for wrist joints actuators: dict[str, BaseActuatorCfg] = { - "joint_1": BaseActuatorCfg(stiffness=2000, damping=100, velocity_limit=1.3963, torque_limit=105), - "joint_2": BaseActuatorCfg(stiffness=2000, damping=100, velocity_limit=1.3963, torque_limit=105), - "joint_3": BaseActuatorCfg(stiffness=2000, damping=100, velocity_limit=1.3963, torque_limit=105), - "joint_4": BaseActuatorCfg(stiffness=2000, damping=100, velocity_limit=1.3963, torque_limit=105), - "joint_5": BaseActuatorCfg(stiffness=500, damping=50, velocity_limit=1.2218, torque_limit=52), # Small actuator - "joint_6": BaseActuatorCfg(stiffness=500, damping=50, velocity_limit=1.2218, torque_limit=52), # Small actuator - "joint_7": BaseActuatorCfg(stiffness=500, damping=50, velocity_limit=1.2218, torque_limit=52), # Small actuator + "joint_1": BaseActuatorCfg(stiffness=2000, damping=100, velocity_limit=1.3963, effort_limit_sim=105), + "joint_2": BaseActuatorCfg(stiffness=2000, damping=100, velocity_limit=1.3963, effort_limit_sim=105), + "joint_3": BaseActuatorCfg(stiffness=2000, damping=100, velocity_limit=1.3963, effort_limit_sim=105), + "joint_4": BaseActuatorCfg(stiffness=2000, damping=100, velocity_limit=1.3963, effort_limit_sim=105), + "joint_5": BaseActuatorCfg( + stiffness=500, damping=50, velocity_limit=1.2218, effort_limit_sim=52 + ), # Small actuator + "joint_6": BaseActuatorCfg( + stiffness=500, damping=50, velocity_limit=1.2218, effort_limit_sim=52 + ), # Small actuator + "joint_7": BaseActuatorCfg( + stiffness=500, damping=50, velocity_limit=1.2218, effort_limit_sim=52 + ), # Small actuator } # Joint limits - Based on Gen3 actual joint limits from MJCF/URDF (radians) diff --git a/roboverse_pack/robots/go2_cfg.py b/roboverse_pack/robots/go2_cfg.py index 13ff08dff..f8ac0ead3 100644 --- a/roboverse_pack/robots/go2_cfg.py +++ b/roboverse_pack/robots/go2_cfg.py @@ -21,18 +21,18 @@ class Go2Cfg(RobotCfg): collapse_fixed_joints: bool = True actuators: dict[str, BaseActuatorCfg] = { - "FL_hip_joint": BaseActuatorCfg(stiffness=20.0, damping=0.5, torque_limit=23.7), - "RL_hip_joint": BaseActuatorCfg(stiffness=20.0, damping=0.5, torque_limit=23.7), - "FR_hip_joint": BaseActuatorCfg(stiffness=20.0, damping=0.5, torque_limit=23.7), - "RR_hip_joint": BaseActuatorCfg(stiffness=20.0, damping=0.5, torque_limit=23.7), - "FL_thigh_joint": BaseActuatorCfg(stiffness=20.0, damping=0.5, torque_limit=23.7), - "RL_thigh_joint": BaseActuatorCfg(stiffness=20.0, damping=0.5, torque_limit=23.7), - "FR_thigh_joint": BaseActuatorCfg(stiffness=20.0, damping=0.5, torque_limit=23.7), - "RR_thigh_joint": BaseActuatorCfg(stiffness=20.0, damping=0.5, torque_limit=23.7), - "FL_calf_joint": BaseActuatorCfg(stiffness=20.0, damping=0.5, torque_limit=35.55), - "RL_calf_joint": BaseActuatorCfg(stiffness=20.0, damping=0.5, torque_limit=35.55), - "FR_calf_joint": BaseActuatorCfg(stiffness=20.0, damping=0.5, torque_limit=35.55), - "RR_calf_joint": BaseActuatorCfg(stiffness=20.0, damping=0.5, torque_limit=35.55), + "FL_hip_joint": BaseActuatorCfg(stiffness=20.0, damping=0.5, effort_limit_sim=23.7), + "RL_hip_joint": BaseActuatorCfg(stiffness=20.0, damping=0.5, effort_limit_sim=23.7), + "FR_hip_joint": BaseActuatorCfg(stiffness=20.0, damping=0.5, effort_limit_sim=23.7), + "RR_hip_joint": BaseActuatorCfg(stiffness=20.0, damping=0.5, effort_limit_sim=23.7), + "FL_thigh_joint": BaseActuatorCfg(stiffness=20.0, damping=0.5, effort_limit_sim=23.7), + "RL_thigh_joint": BaseActuatorCfg(stiffness=20.0, damping=0.5, effort_limit_sim=23.7), + "FR_thigh_joint": BaseActuatorCfg(stiffness=20.0, damping=0.5, effort_limit_sim=23.7), + "RR_thigh_joint": BaseActuatorCfg(stiffness=20.0, damping=0.5, effort_limit_sim=23.7), + "FL_calf_joint": BaseActuatorCfg(stiffness=20.0, damping=0.5, effort_limit_sim=35.55), + "RL_calf_joint": BaseActuatorCfg(stiffness=20.0, damping=0.5, effort_limit_sim=35.55), + "FR_calf_joint": BaseActuatorCfg(stiffness=20.0, damping=0.5, effort_limit_sim=35.55), + "RR_calf_joint": BaseActuatorCfg(stiffness=20.0, damping=0.5, effort_limit_sim=35.55), } joint_limits: dict[str, tuple[float, float]] = { diff --git a/roboverse_pack/robots/vega_cfg.py b/roboverse_pack/robots/vega_cfg.py index 6a719e2be..8a7f56761 100644 --- a/roboverse_pack/robots/vega_cfg.py +++ b/roboverse_pack/robots/vega_cfg.py @@ -33,72 +33,72 @@ class VegaCfg(RobotCfg): # ==================== Actuator Configuration ==================== actuators: dict[str, BaseActuatorCfg] = { # Base wheels - continuous joints need higher stiffness for smooth rotation - "B_wheel_j1": BaseActuatorCfg(velocity_limit=12.0, torque_limit=16.0, stiffness=1e4, damping=1e3), - "B_wheel_j2": BaseActuatorCfg(velocity_limit=12.0, torque_limit=16.0, stiffness=1e4, damping=1e3), - "R_wheel_j1": BaseActuatorCfg(velocity_limit=3.0, torque_limit=6.0, stiffness=5e3, damping=500), - "R_wheel_j2": BaseActuatorCfg(velocity_limit=12.0, torque_limit=16.0, stiffness=1e4, damping=1e3), - "L_wheel_j1": BaseActuatorCfg(velocity_limit=3.0, torque_limit=6.0, stiffness=5e3, damping=500), - "L_wheel_j2": BaseActuatorCfg(velocity_limit=12.0, torque_limit=16.0, stiffness=1e4, damping=1e3), + "B_wheel_j1": BaseActuatorCfg(velocity_limit=12.0, effort_limit_sim=16.0, stiffness=1e4, damping=1e3), + "B_wheel_j2": BaseActuatorCfg(velocity_limit=12.0, effort_limit_sim=16.0, stiffness=1e4, damping=1e3), + "R_wheel_j1": BaseActuatorCfg(velocity_limit=3.0, effort_limit_sim=6.0, stiffness=5e3, damping=500), + "R_wheel_j2": BaseActuatorCfg(velocity_limit=12.0, effort_limit_sim=16.0, stiffness=1e4, damping=1e3), + "L_wheel_j1": BaseActuatorCfg(velocity_limit=3.0, effort_limit_sim=6.0, stiffness=5e3, damping=500), + "L_wheel_j2": BaseActuatorCfg(velocity_limit=12.0, effort_limit_sim=16.0, stiffness=1e4, damping=1e3), # Torso - high torque joints need high stiffness "torso_j1": BaseActuatorCfg( - velocity_limit=0.9, torque_limit=700.0, stiffness=1e6, damping=1e5 + velocity_limit=0.9, effort_limit_sim=700.0, stiffness=1e6, damping=1e5 ), # Increased stiffness for stability "torso_j2": BaseActuatorCfg( - velocity_limit=0.9, torque_limit=380.0, stiffness=1e6, damping=1e5 + velocity_limit=0.9, effort_limit_sim=380.0, stiffness=1e6, damping=1e5 ), # Increased stiffness for stability "torso_j3": BaseActuatorCfg( - velocity_limit=0.9, torque_limit=380.0, stiffness=1e6, damping=1e5 + velocity_limit=0.9, effort_limit_sim=380.0, stiffness=1e6, damping=1e5 ), # Increased stiffness for stability # # Left arm - progressive stiffness from base to tip "L_arm_j1": BaseActuatorCfg( - velocity_limit=2.4, torque_limit=150.0, stiffness=5e4, damping=5e3 + velocity_limit=2.4, effort_limit_sim=150.0, stiffness=5e4, damping=5e3 ), # Increased for stability "L_arm_j2": BaseActuatorCfg( - velocity_limit=2.4, torque_limit=150.0, stiffness=5e4, damping=5e3 + velocity_limit=2.4, effort_limit_sim=150.0, stiffness=5e4, damping=5e3 ), # Increased for stability - "L_arm_j3": BaseActuatorCfg(velocity_limit=2.7, torque_limit=80.0, stiffness=2e4, damping=2e3), - "L_arm_j4": BaseActuatorCfg(velocity_limit=2.7, torque_limit=80.0, stiffness=1e4, damping=1e3), - "L_arm_j5": BaseActuatorCfg(velocity_limit=2.7, torque_limit=25.0, stiffness=5e3, damping=500), - "L_arm_j6": BaseActuatorCfg(velocity_limit=2.7, torque_limit=25.0, stiffness=5e3, damping=500), - "L_arm_j7": BaseActuatorCfg(velocity_limit=2.7, torque_limit=25.0, stiffness=5e3, damping=500), + "L_arm_j3": BaseActuatorCfg(velocity_limit=2.7, effort_limit_sim=80.0, stiffness=2e4, damping=2e3), + "L_arm_j4": BaseActuatorCfg(velocity_limit=2.7, effort_limit_sim=80.0, stiffness=1e4, damping=1e3), + "L_arm_j5": BaseActuatorCfg(velocity_limit=2.7, effort_limit_sim=25.0, stiffness=5e3, damping=500), + "L_arm_j6": BaseActuatorCfg(velocity_limit=2.7, effort_limit_sim=25.0, stiffness=5e3, damping=500), + "L_arm_j7": BaseActuatorCfg(velocity_limit=2.7, effort_limit_sim=25.0, stiffness=5e3, damping=500), # Right arm - progressive stiffness from base to tip "R_arm_j1": BaseActuatorCfg( - velocity_limit=2.4, torque_limit=150.0, stiffness=5e4, damping=5e3 + velocity_limit=2.4, effort_limit_sim=150.0, stiffness=5e4, damping=5e3 ), # Increased for stability "R_arm_j2": BaseActuatorCfg( - velocity_limit=2.4, torque_limit=150.0, stiffness=5e4, damping=5e3 + velocity_limit=2.4, effort_limit_sim=150.0, stiffness=5e4, damping=5e3 ), # Increased for stability - "R_arm_j3": BaseActuatorCfg(velocity_limit=2.7, torque_limit=80.0, stiffness=2e4, damping=2e3), - "R_arm_j4": BaseActuatorCfg(velocity_limit=2.7, torque_limit=80.0, stiffness=1e4, damping=1e3), - "R_arm_j5": BaseActuatorCfg(velocity_limit=2.7, torque_limit=25.0, stiffness=5e3, damping=500), - "R_arm_j6": BaseActuatorCfg(velocity_limit=2.7, torque_limit=25.0, stiffness=5e3, damping=500), - "R_arm_j7": BaseActuatorCfg(velocity_limit=2.7, torque_limit=25.0, stiffness=5e3, damping=500), + "R_arm_j3": BaseActuatorCfg(velocity_limit=2.7, effort_limit_sim=80.0, stiffness=2e4, damping=2e3), + "R_arm_j4": BaseActuatorCfg(velocity_limit=2.7, effort_limit_sim=80.0, stiffness=1e4, damping=1e3), + "R_arm_j5": BaseActuatorCfg(velocity_limit=2.7, effort_limit_sim=25.0, stiffness=5e3, damping=500), + "R_arm_j6": BaseActuatorCfg(velocity_limit=2.7, effort_limit_sim=25.0, stiffness=5e3, damping=500), + "R_arm_j7": BaseActuatorCfg(velocity_limit=2.7, effort_limit_sim=25.0, stiffness=5e3, damping=500), # Left hand - Thumb - "L_th_j0": BaseActuatorCfg(velocity_limit=6.28, torque_limit=1.4, stiffness=300, damping=22), - "L_th_j1": BaseActuatorCfg(velocity_limit=6.28, torque_limit=1.4, stiffness=300, damping=22), - "L_th_j2": BaseActuatorCfg(velocity_limit=6.28, torque_limit=1.1, stiffness=260, damping=20), + "L_th_j0": BaseActuatorCfg(velocity_limit=6.28, effort_limit_sim=1.4, stiffness=300, damping=22), + "L_th_j1": BaseActuatorCfg(velocity_limit=6.28, effort_limit_sim=1.4, stiffness=300, damping=22), + "L_th_j2": BaseActuatorCfg(velocity_limit=6.28, effort_limit_sim=1.1, stiffness=260, damping=20), # Left hand - Fingers - "L_ff_j1": BaseActuatorCfg(velocity_limit=6.28, torque_limit=0.9, stiffness=320, damping=22), - "L_ff_j2": BaseActuatorCfg(velocity_limit=6.28, torque_limit=0.9, stiffness=320, damping=22), - "L_mf_j1": BaseActuatorCfg(velocity_limit=6.28, torque_limit=0.9, stiffness=320, damping=22), - "L_mf_j2": BaseActuatorCfg(velocity_limit=6.28, torque_limit=0.9, stiffness=320, damping=22), - "L_rf_j1": BaseActuatorCfg(velocity_limit=6.28, torque_limit=0.9, stiffness=320, damping=22), - "L_rf_j2": BaseActuatorCfg(velocity_limit=6.28, torque_limit=0.9, stiffness=320, damping=22), - "L_lf_j1": BaseActuatorCfg(velocity_limit=6.28, torque_limit=0.9, stiffness=320, damping=22), - "L_lf_j2": BaseActuatorCfg(velocity_limit=6.28, torque_limit=0.9, stiffness=320, damping=22), + "L_ff_j1": BaseActuatorCfg(velocity_limit=6.28, effort_limit_sim=0.9, stiffness=320, damping=22), + "L_ff_j2": BaseActuatorCfg(velocity_limit=6.28, effort_limit_sim=0.9, stiffness=320, damping=22), + "L_mf_j1": BaseActuatorCfg(velocity_limit=6.28, effort_limit_sim=0.9, stiffness=320, damping=22), + "L_mf_j2": BaseActuatorCfg(velocity_limit=6.28, effort_limit_sim=0.9, stiffness=320, damping=22), + "L_rf_j1": BaseActuatorCfg(velocity_limit=6.28, effort_limit_sim=0.9, stiffness=320, damping=22), + "L_rf_j2": BaseActuatorCfg(velocity_limit=6.28, effort_limit_sim=0.9, stiffness=320, damping=22), + "L_lf_j1": BaseActuatorCfg(velocity_limit=6.28, effort_limit_sim=0.9, stiffness=320, damping=22), + "L_lf_j2": BaseActuatorCfg(velocity_limit=6.28, effort_limit_sim=0.9, stiffness=320, damping=22), # Right hand - Thumb - "R_th_j0": BaseActuatorCfg(velocity_limit=6.28, torque_limit=1.4, stiffness=300, damping=22), - "R_th_j1": BaseActuatorCfg(velocity_limit=6.28, torque_limit=1.4, stiffness=300, damping=22), - "R_th_j2": BaseActuatorCfg(velocity_limit=6.28, torque_limit=1.1, stiffness=260, damping=20), + "R_th_j0": BaseActuatorCfg(velocity_limit=6.28, effort_limit_sim=1.4, stiffness=300, damping=22), + "R_th_j1": BaseActuatorCfg(velocity_limit=6.28, effort_limit_sim=1.4, stiffness=300, damping=22), + "R_th_j2": BaseActuatorCfg(velocity_limit=6.28, effort_limit_sim=1.1, stiffness=260, damping=20), # Right hand - Fingers - "R_ff_j1": BaseActuatorCfg(velocity_limit=6.28, torque_limit=0.9, stiffness=320, damping=22), - "R_ff_j2": BaseActuatorCfg(velocity_limit=6.28, torque_limit=0.9, stiffness=320, damping=22), - "R_mf_j1": BaseActuatorCfg(velocity_limit=6.28, torque_limit=0.9, stiffness=320, damping=22), - "R_mf_j2": BaseActuatorCfg(velocity_limit=6.28, torque_limit=0.9, stiffness=320, damping=22), - "R_rf_j1": BaseActuatorCfg(velocity_limit=6.28, torque_limit=0.9, stiffness=320, damping=22), - "R_rf_j2": BaseActuatorCfg(velocity_limit=6.28, torque_limit=0.9, stiffness=320, damping=22), - "R_lf_j1": BaseActuatorCfg(velocity_limit=6.28, torque_limit=0.9, stiffness=320, damping=22), - "R_lf_j2": BaseActuatorCfg(velocity_limit=6.28, torque_limit=0.9, stiffness=320, damping=22), + "R_ff_j1": BaseActuatorCfg(velocity_limit=6.28, effort_limit_sim=0.9, stiffness=320, damping=22), + "R_ff_j2": BaseActuatorCfg(velocity_limit=6.28, effort_limit_sim=0.9, stiffness=320, damping=22), + "R_mf_j1": BaseActuatorCfg(velocity_limit=6.28, effort_limit_sim=0.9, stiffness=320, damping=22), + "R_mf_j2": BaseActuatorCfg(velocity_limit=6.28, effort_limit_sim=0.9, stiffness=320, damping=22), + "R_rf_j1": BaseActuatorCfg(velocity_limit=6.28, effort_limit_sim=0.9, stiffness=320, damping=22), + "R_rf_j2": BaseActuatorCfg(velocity_limit=6.28, effort_limit_sim=0.9, stiffness=320, damping=22), + "R_lf_j1": BaseActuatorCfg(velocity_limit=6.28, effort_limit_sim=0.9, stiffness=320, damping=22), + "R_lf_j2": BaseActuatorCfg(velocity_limit=6.28, effort_limit_sim=0.9, stiffness=320, damping=22), } # ==================== Joint Limits ==================== diff --git a/roboverse_pack/robots/xhand_cfg.py b/roboverse_pack/robots/xhand_cfg.py index 32c328bef..84a1d2195 100644 --- a/roboverse_pack/robots/xhand_cfg.py +++ b/roboverse_pack/robots/xhand_cfg.py @@ -73,29 +73,43 @@ class XhandRightCfg(RobotCfg): actuators: dict[str, BaseActuatorCfg] = { # Thumb "right_hand_thumb_bend_joint": BaseActuatorCfg( - velocity_limit=8.63, torque_limit=1.1, stiffness=3.0, damping=0.1 + velocity_limit=8.63, effort_limit_sim=1.1, stiffness=3.0, damping=0.1 ), "right_hand_thumb_rota_joint1": BaseActuatorCfg( - velocity_limit=8.63, torque_limit=1.1, stiffness=3.0, damping=0.1 + velocity_limit=8.63, effort_limit_sim=1.1, stiffness=3.0, damping=0.1 ), "right_hand_thumb_rota_joint2": BaseActuatorCfg( - velocity_limit=14.38, torque_limit=0.4, stiffness=3.0, damping=0.1 + velocity_limit=14.38, effort_limit_sim=0.4, stiffness=3.0, damping=0.1 ), # Index "right_hand_index_bend_joint": BaseActuatorCfg( - velocity_limit=14.38, torque_limit=0.4, stiffness=3.0, damping=0.1 + velocity_limit=14.38, effort_limit_sim=0.4, stiffness=3.0, damping=0.1 + ), + "right_hand_index_joint1": BaseActuatorCfg( + velocity_limit=8.63, effort_limit_sim=1.1, stiffness=3.0, damping=0.1 + ), + "right_hand_index_joint2": BaseActuatorCfg( + velocity_limit=14.38, effort_limit_sim=0.4, stiffness=3.0, damping=0.1 ), - "right_hand_index_joint1": BaseActuatorCfg(velocity_limit=8.63, torque_limit=1.1, stiffness=3.0, damping=0.1), - "right_hand_index_joint2": BaseActuatorCfg(velocity_limit=14.38, torque_limit=0.4, stiffness=3.0, damping=0.1), # Middle - "right_hand_mid_joint1": BaseActuatorCfg(velocity_limit=8.63, torque_limit=1.1, stiffness=3.0, damping=0.1), - "right_hand_mid_joint2": BaseActuatorCfg(velocity_limit=14.38, torque_limit=0.4, stiffness=3.0, damping=0.1), + "right_hand_mid_joint1": BaseActuatorCfg(velocity_limit=8.63, effort_limit_sim=1.1, stiffness=3.0, damping=0.1), + "right_hand_mid_joint2": BaseActuatorCfg( + velocity_limit=14.38, effort_limit_sim=0.4, stiffness=3.0, damping=0.1 + ), # Ring - "right_hand_ring_joint1": BaseActuatorCfg(velocity_limit=8.63, torque_limit=1.1, stiffness=3.0, damping=0.1), - "right_hand_ring_joint2": BaseActuatorCfg(velocity_limit=14.38, torque_limit=0.4, stiffness=3.0, damping=0.1), + "right_hand_ring_joint1": BaseActuatorCfg( + velocity_limit=8.63, effort_limit_sim=1.1, stiffness=3.0, damping=0.1 + ), + "right_hand_ring_joint2": BaseActuatorCfg( + velocity_limit=14.38, effort_limit_sim=0.4, stiffness=3.0, damping=0.1 + ), # Pinky - "right_hand_pinky_joint1": BaseActuatorCfg(velocity_limit=8.63, torque_limit=1.1, stiffness=3.0, damping=0.1), - "right_hand_pinky_joint2": BaseActuatorCfg(velocity_limit=14.38, torque_limit=0.4, stiffness=3.0, damping=0.1), + "right_hand_pinky_joint1": BaseActuatorCfg( + velocity_limit=8.63, effort_limit_sim=1.1, stiffness=3.0, damping=0.1 + ), + "right_hand_pinky_joint2": BaseActuatorCfg( + velocity_limit=14.38, effort_limit_sim=0.4, stiffness=3.0, damping=0.1 + ), } # -------- Defaults -------- diff --git a/roboverse_pack/robots/z1_cfg.py b/roboverse_pack/robots/z1_cfg.py index 6e55b3fd0..5f9059ef2 100644 --- a/roboverse_pack/robots/z1_cfg.py +++ b/roboverse_pack/robots/z1_cfg.py @@ -31,14 +31,14 @@ class Z1Cfg(RobotCfg): # Actuator configuration - Based on Z1 joint specifications # Using gainprm and force settings from MJCF actuators: dict[str, BaseActuatorCfg] = { - "joint1": BaseActuatorCfg(stiffness=1000, damping=100, velocity_limit=3.1415, torque_limit=30), + "joint1": BaseActuatorCfg(stiffness=1000, damping=100, velocity_limit=3.1415, effort_limit_sim=30), "joint2": BaseActuatorCfg( - stiffness=1500, damping=150, velocity_limit=3.1415, torque_limit=60 + stiffness=1500, damping=150, velocity_limit=3.1415, effort_limit_sim=60 ), # Higher torque for shoulder - "joint3": BaseActuatorCfg(stiffness=1000, damping=100, velocity_limit=3.1415, torque_limit=30), - "joint4": BaseActuatorCfg(stiffness=1000, damping=100, velocity_limit=3.1415, torque_limit=30), - "joint5": BaseActuatorCfg(stiffness=1000, damping=100, velocity_limit=3.1415, torque_limit=30), - "joint6": BaseActuatorCfg(stiffness=1000, damping=100, velocity_limit=3.1415, torque_limit=30), + "joint3": BaseActuatorCfg(stiffness=1000, damping=100, velocity_limit=3.1415, effort_limit_sim=30), + "joint4": BaseActuatorCfg(stiffness=1000, damping=100, velocity_limit=3.1415, effort_limit_sim=30), + "joint5": BaseActuatorCfg(stiffness=1000, damping=100, velocity_limit=3.1415, effort_limit_sim=30), + "joint6": BaseActuatorCfg(stiffness=1000, damping=100, velocity_limit=3.1415, effort_limit_sim=30), } # Joint limits - Based on Z1 actual joint limits from MJCF (radians) diff --git a/roboverse_pack/tasks/beyondmimic/__init__.py b/roboverse_pack/tasks/beyondmimic/__init__.py new file mode 100644 index 000000000..585dab3d5 --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/__init__.py @@ -0,0 +1 @@ +"""BeyondMimic motion tracking task package for RoboVerse.""" diff --git a/roboverse_pack/tasks/beyondmimic/isaaclab/__init__.py b/roboverse_pack/tasks/beyondmimic/isaaclab/__init__.py new file mode 100644 index 000000000..2580c71ce --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/isaaclab/__init__.py @@ -0,0 +1 @@ +"""BeyondMimic motion tracking written explicitly in Isaac Lab.""" diff --git a/roboverse_pack/tasks/beyondmimic/isaaclab/configs/flat_env_cfg.py b/roboverse_pack/tasks/beyondmimic/isaaclab/configs/flat_env_cfg.py new file mode 100644 index 000000000..10c1aa27c --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/isaaclab/configs/flat_env_cfg.py @@ -0,0 +1,43 @@ +from isaaclab.utils import configclass + +from roboverse_pack.tasks.beyondmimic.isaaclab.configs.tracking_env_cfg import TrackingEnvCfg +from roboverse_pack.tasks.beyondmimic.isaaclab.robots.g1 import G1_ACTION_SCALE, G1_CYLINDER_CFG +from roboverse_pack.tasks.beyondmimic.isaaclab.robots.g1_delayed import G1_DELAYED_CYLINDER_CFG + + +@configclass +class G1FlatEnvCfg(TrackingEnvCfg): + """Configuration for the G1 flat environment.""" + + def __post_init__(self): + super().__post_init__() + self.scene.robot = G1_CYLINDER_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + self.actions.joint_pos.scale = G1_ACTION_SCALE + self.commands.motion.anchor_body_name = "torso_link" + self.commands.motion.body_names = [ + "pelvis", + "left_hip_roll_link", + "left_knee_link", + "left_ankle_roll_link", + "right_hip_roll_link", + "right_knee_link", + "right_ankle_roll_link", + "torso_link", + "left_shoulder_roll_link", + "left_elbow_link", + "left_wrist_yaw_link", + "right_shoulder_roll_link", + "right_elbow_link", + "right_wrist_yaw_link", + ] + + +@configclass +class G1FlatEnvCfgDeploy(G1FlatEnvCfg): + """Configuration for the G1 flat environment for deployment.""" + + def __post_init__(self): + super().__post_init__() + self.scene.robot = G1_DELAYED_CYLINDER_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + delattr(self.observations.policy, "base_lin_vel") + delattr(self.observations.policy, "motion_anchor_pos_b") diff --git a/roboverse_pack/tasks/beyondmimic/isaaclab/configs/tracking_env_cfg.py b/roboverse_pack/tasks/beyondmimic/isaaclab/configs/tracking_env_cfg.py new file mode 100644 index 000000000..863989979 --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/isaaclab/configs/tracking_env_cfg.py @@ -0,0 +1,330 @@ +from __future__ import annotations + +from dataclasses import MISSING + +import isaaclab.envs.mdp as mdp +import isaaclab.sim as sim_utils +from isaaclab.assets import ArticulationCfg, AssetBaseCfg +from isaaclab.envs import ManagerBasedRLEnvCfg +from isaaclab.managers import EventTermCfg as EventTerm +from isaaclab.managers import ObservationGroupCfg as ObsGroup +from isaaclab.managers import ObservationTermCfg as ObsTerm +from isaaclab.managers import RewardTermCfg as RewTerm +from isaaclab.managers import SceneEntityCfg +from isaaclab.managers import TerminationTermCfg as DoneTerm +from isaaclab.scene import InteractiveSceneCfg +from isaaclab.sensors import ContactSensorCfg +from isaaclab.terrains import TerrainImporterCfg + +## +# Pre-defined configs +## +from isaaclab.utils import configclass +from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise + +from roboverse_pack.tasks.beyondmimic.isaaclab.mdp import commands, events +from roboverse_pack.tasks.beyondmimic.isaaclab.mdp import observations as obs +from roboverse_pack.tasks.beyondmimic.isaaclab.mdp import rewards as rew +from roboverse_pack.tasks.beyondmimic.isaaclab.mdp import terminations as done + +## +# Scene definition +## + +VELOCITY_RANGE = { + "x": (-0.5, 0.5), # lin vel + "y": (-0.5, 0.5), + "z": (-0.2, 0.2), + "roll": (-0.52, 0.52), # ang vel + "pitch": (-0.52, 0.52), + "yaw": (-0.78, 0.78), +} + + +@configclass +class MySceneCfg(InteractiveSceneCfg): + """Configuration for the terrain scene with a legged robot.""" + + # ground terrain + terrain = TerrainImporterCfg( + prim_path="/World/ground", + terrain_type="plane", + collision_group=-1, + physics_material=sim_utils.RigidBodyMaterialCfg( + friction_combine_mode="multiply", + restitution_combine_mode="multiply", + static_friction=1.0, + dynamic_friction=1.0, + ), + visual_material=sim_utils.MdlFileCfg( + mdl_path="{NVIDIA_NUCLEUS_DIR}/Materials/Base/Architecture/Shingles_01.mdl", + project_uvw=True, + ), + ) + # robots + robot: ArticulationCfg = MISSING + # lights + light = AssetBaseCfg( + prim_path="/World/light", + spawn=sim_utils.DistantLightCfg(color=(0.75, 0.75, 0.75), intensity=3000.0), + ) + sky_light = AssetBaseCfg( + prim_path="/World/skyLight", + spawn=sim_utils.DomeLightCfg(color=(0.13, 0.13, 0.13), intensity=1000.0), + ) + contact_forces = ContactSensorCfg( + prim_path="{ENV_REGEX_NS}/Robot/.*", history_length=3, track_air_time=True, force_threshold=10.0, debug_vis=True + ) + + +## +# MDP settings +## + + +@configclass +class CommandsCfg: + """Command specifications for the MDP.""" + + motion = commands.MotionCommandCfg( + asset_name="robot", + resampling_time_range=(1.0e9, 1.0e9), + debug_vis=True, + pose_range={ + "x": (-0.05, 0.05), + "y": (-0.05, 0.05), + "z": (-0.01, 0.01), + "roll": (-0.1, 0.1), + "pitch": (-0.1, 0.1), + "yaw": (-0.2, 0.2), + }, + velocity_range=VELOCITY_RANGE, + joint_position_range=(-0.1, 0.1), + ) + + +@configclass +class ActionsCfg: + """Action specifications for the MDP.""" + + joint_pos = mdp.JointPositionActionCfg(asset_name="robot", joint_names=[".*"], use_default_offset=True) + + +@configclass +class ObservationsCfg: + """Observation specifications for the MDP.""" + + @configclass + class PolicyCfg(ObsGroup): + """Observations for policy group.""" + + # observation terms (order preserved) + command = ObsTerm(func=mdp.generated_commands, params={"command_name": "motion"}) + motion_anchor_pos_b = ObsTerm( + func=obs.motion_anchor_pos_b, params={"command_name": "motion"}, noise=Unoise(n_min=-0.25, n_max=0.25) + ) + motion_anchor_ori_b = ObsTerm( + func=obs.motion_anchor_ori_b, params={"command_name": "motion"}, noise=Unoise(n_min=-0.05, n_max=0.05) + ) + base_lin_vel = ObsTerm(func=mdp.base_lin_vel, noise=Unoise(n_min=-0.5, n_max=0.5)) + base_ang_vel = ObsTerm(func=mdp.base_ang_vel, noise=Unoise(n_min=-0.2, n_max=0.2)) + joint_pos = ObsTerm(func=mdp.joint_pos_rel, noise=Unoise(n_min=-0.01, n_max=0.01)) + joint_vel = ObsTerm(func=mdp.joint_vel_rel, noise=Unoise(n_min=-0.5, n_max=0.5)) + actions = ObsTerm(func=mdp.last_action) + + def __post_init__(self): + self.enable_corruption = True + self.concatenate_terms = True + + @configclass + class PrivilegedCfg(ObsGroup): + """Observations for privileged group.""" + + command = ObsTerm(func=mdp.generated_commands, params={"command_name": "motion"}) + motion_anchor_pos_b = ObsTerm(func=obs.motion_anchor_pos_b, params={"command_name": "motion"}) + motion_anchor_ori_b = ObsTerm(func=obs.motion_anchor_ori_b, params={"command_name": "motion"}) + body_pos = ObsTerm(func=obs.robot_body_pos_b, params={"command_name": "motion"}) + body_ori = ObsTerm(func=obs.robot_body_ori_b, params={"command_name": "motion"}) + base_lin_vel = ObsTerm(func=mdp.base_lin_vel) + base_ang_vel = ObsTerm(func=mdp.base_ang_vel) + joint_pos = ObsTerm(func=mdp.joint_pos_rel) + joint_vel = ObsTerm(func=mdp.joint_vel_rel) + actions = ObsTerm(func=mdp.last_action) + + # observation groups + policy: PolicyCfg = PolicyCfg() + critic: PrivilegedCfg = PrivilegedCfg() + + +@configclass +class EventCfg: + """Configuration for events.""" + + # startup + physics_material = EventTerm( + func=mdp.randomize_rigid_body_material, + mode="startup", + params={ + "asset_cfg": SceneEntityCfg("robot", body_names=".*"), + "static_friction_range": (0.3, 1.6), + "dynamic_friction_range": (0.3, 1.2), + "restitution_range": (0.0, 0.5), + "num_buckets": 64, + }, + ) + + add_joint_default_pos = EventTerm( + func=events.randomize_joint_default_pos, + mode="startup", + params={ + "asset_cfg": SceneEntityCfg("robot", joint_names=[".*"]), + "pos_distribution_params": (-0.01, 0.01), + "operation": "add", + }, + ) + + base_com = EventTerm( + func=events.randomize_rigid_body_com, + mode="startup", + params={ + "asset_cfg": SceneEntityCfg("robot", body_names="torso_link"), + "com_range": {"x": (-0.025, 0.025), "y": (-0.05, 0.05), "z": (-0.05, 0.05)}, + }, + ) + + # interval + push_robot = EventTerm( + func=mdp.push_by_setting_velocity, + mode="interval", + interval_range_s=(1.0, 3.0), + params={"velocity_range": VELOCITY_RANGE}, + ) + + +@configclass +class RewardsCfg: + """Reward terms for the MDP.""" + + motion_global_anchor_pos = RewTerm( + func=rew.motion_global_anchor_position_error_exp, + weight=0.5, + params={"command_name": "motion", "std": 0.3}, + ) + motion_global_anchor_ori = RewTerm( + func=rew.motion_global_anchor_orientation_error_exp, + weight=0.5, + params={"command_name": "motion", "std": 0.4}, + ) + motion_body_pos = RewTerm( + func=rew.motion_relative_body_position_error_exp, + weight=1.0, + params={"command_name": "motion", "std": 0.3}, + ) + motion_body_ori = RewTerm( + func=rew.motion_relative_body_orientation_error_exp, + weight=1.0, + params={"command_name": "motion", "std": 0.4}, + ) + motion_body_lin_vel = RewTerm( + func=rew.motion_global_body_linear_velocity_error_exp, + weight=1.0, + params={"command_name": "motion", "std": 1.0}, + ) + motion_body_ang_vel = RewTerm( + func=rew.motion_global_body_angular_velocity_error_exp, + weight=1.0, + params={"command_name": "motion", "std": 3.14}, + ) + action_rate_l2 = RewTerm(func=mdp.action_rate_l2, weight=-1e-1) + joint_limit = RewTerm( + func=mdp.joint_pos_limits, + weight=-10.0, + params={"asset_cfg": SceneEntityCfg("robot", joint_names=[".*"])}, + ) + undesired_contacts = RewTerm( + func=mdp.undesired_contacts, + weight=-0.1, + params={ + "sensor_cfg": SceneEntityCfg( + "contact_forces", + body_names=[ + r"^(?!left_ankle_roll_link$)(?!right_ankle_roll_link$)(?!left_wrist_yaw_link$)(?!right_wrist_yaw_link$).+$" + ], + ), + "threshold": 1.0, + }, + ) + + +@configclass +class TerminationsCfg: + """Termination terms for the MDP.""" + + time_out = DoneTerm(func=mdp.time_out, time_out=True) + anchor_pos = DoneTerm( + func=done.bad_anchor_pos_z_only, + params={"command_name": "motion", "threshold": 0.25}, + ) + anchor_ori = DoneTerm( + func=done.bad_anchor_ori, + params={"asset_cfg": SceneEntityCfg("robot"), "command_name": "motion", "threshold": 0.8}, + ) + ee_body_pos = DoneTerm( + func=done.bad_motion_body_pos_z_only, + params={ + "command_name": "motion", + "threshold": 0.25, + "body_names": [ + "left_ankle_roll_link", + "right_ankle_roll_link", + "left_wrist_yaw_link", + "right_wrist_yaw_link", + ], + }, + ) + + +@configclass +class CurriculumCfg: + """Curriculum terms for the MDP.""" + + pass + + +## +# Environment configuration +## + + +@configclass +class TrackingEnvCfg(ManagerBasedRLEnvCfg): + """Configuration for the locomotion velocity-tracking environment.""" + + # Scene settings + scene: MySceneCfg = MySceneCfg( + num_envs=4096, env_spacing=2.5 + ) # NOTE will be changed later by accessing `env_cfg.scene.num_envs` in `main()` + # Basic settings + observations: ObservationsCfg = ObservationsCfg() + actions: ActionsCfg = ActionsCfg() + commands: CommandsCfg = CommandsCfg() + # MDP settings + rewards: RewardsCfg = RewardsCfg() + terminations: TerminationsCfg = TerminationsCfg() + events: EventCfg = EventCfg() + curriculum: CurriculumCfg = CurriculumCfg() + + def __post_init__(self): + """Post initialization.""" + # general settings + self.decimation = 4 + self.episode_length_s = 10.0 + # simulation settings + self.sim.dt = 0.005 + self.sim.render_interval = self.decimation + self.sim.physics_material = self.scene.terrain.physics_material + self.sim.physx.gpu_max_rigid_patch_count = 10 * 2**15 + # viewer settings + self.viewer.eye = (1.5, 1.5, 1.5) + self.viewer.origin_type = "asset_root" + self.viewer.asset_name = "robot" diff --git a/roboverse_pack/tasks/beyondmimic/isaaclab/envs/__init__.py b/roboverse_pack/tasks/beyondmimic/isaaclab/envs/__init__.py new file mode 100644 index 000000000..72739a848 --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/isaaclab/envs/__init__.py @@ -0,0 +1 @@ +"""Submodule containing environment classes for RSL-RL v2.3.0 (v1) and v3.1.0 (v2).""" diff --git a/roboverse_pack/tasks/beyondmimic/isaaclab/envs/tracking_base_env.py b/roboverse_pack/tasks/beyondmimic/isaaclab/envs/tracking_base_env.py new file mode 100644 index 000000000..c34cb40a2 --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/isaaclab/envs/tracking_base_env.py @@ -0,0 +1,506 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import builtins +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any + +import torch +from loguru import logger as log + +if TYPE_CHECKING: + from isaaclab.envs.common import VecEnvObs + from isaaclab.envs.manager_based_env_cfg import ManagerBasedEnvCfg + + +class TrackingBaseEnv: + """The base environment encapsulates the simulation scene and the environment managers for the manager-based workflow. + + While a simulation scene or world comprises of different components such as the robots, objects, + and sensors (cameras, lidars, etc.), the environment is a higher level abstraction + that provides an interface for interacting with the simulation. The environment is comprised of + the following components: + + * **Scene**: The scene manager that creates and manages the virtual world in which the robot operates. + This includes defining the robot, static and dynamic objects, sensors, etc. + * **Observation Manager**: The observation manager that generates observations from the current simulation + state and the data gathered from the sensors. These observations may include privileged information + that is not available to the robot in the real world. Additionally, user-defined terms can be added + to process the observations and generate custom observations. For example, using a network to embed + high-dimensional observations into a lower-dimensional space. + * **Action Manager**: The action manager that processes the raw actions sent to the environment and + converts them to low-level commands that are sent to the simulation. It can be configured to accept + raw actions at different levels of abstraction. For example, in case of a robotic arm, the raw actions + can be joint torques, joint positions, or end-effector poses. Similarly for a mobile base, it can be + the joint torques, or the desired velocity of the floating base. + * **Event Manager**: The event manager orchestrates operations triggered based on simulation events. + This includes resetting the scene to a default state, applying random pushes to the robot at different intervals + of time, or randomizing properties such as mass and friction coefficients. This is useful for training + and evaluating the robot in a variety of scenarios. + * **Recorder Manager**: The recorder manager that handles recording data produced during different steps + in the simulation. This includes recording in the beginning and end of a reset and a step. The recorded data + is distinguished per episode, per environment and can be exported through a dataset file handler to a file. + + The environment provides a unified interface for interacting with the simulation. However, it does not + include task-specific quantities such as the reward function, or the termination conditions. These + quantities are often specific to defining Markov Decision Processes (MDPs) while the base environment + is agnostic to the MDP definition. + + The environment steps forward in time at a fixed time-step. The physics simulation is decimated at a + lower time-step. This is to ensure that the simulation is stable. These two time-steps can be configured + independently using the :attr:`ManagerBasedEnvCfg.decimation` (number of simulation steps per environment step) + and the :attr:`ManagerBasedEnvCfg.sim.dt` (physics time-step) parameters. Based on these parameters, the + environment time-step is computed as the product of the two. The two time-steps can be obtained by + querying the :attr:`physics_dt` and the :attr:`step_dt` properties respectively. + """ + + def __init__(self, cfg: ManagerBasedEnvCfg): + """Initialize the environment. + + Args: + cfg: The configuration object for the environment. + + Raises: + RuntimeError: If a simulation context already exists. The environment must always create one + since it configures the simulation context and controls the simulation. + """ + import omni.log + from isaaclab.managers import EventManager + from isaaclab.scene import InteractiveScene + from isaaclab.sim import SimulationContext + from isaaclab.utils.timer import Timer + + # check that the config is valid + cfg.validate() + # store inputs to class + self.cfg = cfg + # initialize internal variables + self._is_closed = False + + # set the seed for the environment + if self.cfg.seed is not None: + self.cfg.seed = self.seed(self.cfg.seed) + else: + omni.log.warn("Seed not set for the environment. The environment creation may not be deterministic.") + + # create a simulation context to control the simulator + if SimulationContext.instance() is None: + # the type-annotation is required to avoid a type-checking error + # since it gets confused with Isaac Sim's SimulationContext class + self.sim: SimulationContext = SimulationContext(self.cfg.sim) + else: + # simulation context should only be created before the environment + # when in extension mode + if not builtins.ISAAC_LAUNCHED_FROM_TERMINAL: + raise RuntimeError("Simulation context already exists. Cannot create a new one.") + self.sim: SimulationContext = SimulationContext.instance() + + # make sure torch is running on the correct device + if "cuda" in self.device: + torch.cuda.set_device(self.device) + + # print useful information + log.info("[INFO]: Base environment:") + log.info(f"\tEnvironment device : {self.device}") + log.info(f"\tEnvironment seed : {self.cfg.seed}") + log.info(f"\tPhysics step-size : {self.physics_dt}") + log.info(f"\tRendering step-size : {self.physics_dt * self.cfg.sim.render_interval}") + log.info(f"\tEnvironment step-size : {self.step_dt}") + + if self.cfg.sim.render_interval < self.cfg.decimation: + msg = ( + f"The render interval ({self.cfg.sim.render_interval}) is smaller than the decimation " + f"({self.cfg.decimation}). Multiple render calls will happen for each environment step. " + "If this is not intended, set the render interval to be equal to the decimation." + ) + omni.log.warn(msg) + + # counter for simulation steps + self._sim_step_counter = 0 + + # allocate dictionary to store metrics + self.extras = {} + + # generate scene + with Timer("[INFO]: Time taken for scene creation", "scene_creation"): + self.scene = InteractiveScene(self.cfg.scene) + log.info("[INFO]: Scene manager: ", self.scene) + + # set up camera viewport controller + # viewport is not available in other rendering modes so the function will throw a warning + # FIXME: This needs to be fixed in the future when we unify the UI functionalities even for + # non-rendering modes. + if self.sim.render_mode >= self.sim.RenderMode.PARTIAL_RENDERING: + from isaaclab.envs.ui import ViewportCameraController + + self.viewport_camera_controller = ViewportCameraController(self, self.cfg.viewer) + else: + self.viewport_camera_controller = None + + # create event manager + # note: this is needed here (rather than after simulation play) to allow USD-related randomization events + # that must happen before the simulation starts. Example: randomizing mesh scale + self.event_manager = EventManager(self.cfg.events, self) + + # apply USD-related randomization events + if "prestartup" in self.event_manager.available_modes: + self.event_manager.apply(mode="prestartup") + + # play the simulator to activate physics handles + # note: this activates the physics simulation view that exposes TensorAPIs + # note: when started in extension mode, first call sim.reset_async() and then initialize the managers + if builtins.ISAAC_LAUNCHED_FROM_TERMINAL is False: + log.info("[INFO]: Starting the simulation. This may take a few seconds. Please wait...") + with Timer("[INFO]: Time taken for simulation start", "simulation_start"): + self.sim.reset() + # update scene to pre populate data buffers for assets and sensors. + # this is needed for the observation manager to get valid tensors for initialization. + # this shouldn't cause an issue since later on, users do a reset over all the environments so the lazy buffers would be reset. + self.scene.update(dt=self.physics_dt) + # add timeline event to load managers + self.load_managers() + + # extend UI elements + # we need to do this here after all the managers are initialized + # this is because they dictate the sensors and commands right now + if self.sim.has_gui() and self.cfg.ui_window_class_type is not None: + # setup live visualizers + self.setup_manager_visualizers() + self._window = self.cfg.ui_window_class_type(self, window_name="IsaacLab") + else: + # if no window, then we don't need to store the window + self._window = None + + # initialize observation buffers + self._obs_buf = {} + + def __del__(self): + """Cleanup for the environment.""" + self.close() + + """ + Properties. + """ + + @property + def num_envs(self) -> int: + """The number of instances of the environment that are running.""" + return self.scene.num_envs + + @property + def physics_dt(self) -> float: + """The physics time-step (in s). + + This is the lowest time-decimation at which the simulation is happening. + """ + return self.cfg.sim.dt + + @property + def step_dt(self) -> float: + """The environment stepping time-step (in s). + + This is the time-step at which the environment steps forward. + """ + return self.cfg.sim.dt * self.cfg.decimation + + @property + def device(self): + """The device on which the environment is running.""" + return self.sim.device + + """ + Operations - Setup. + """ + + def load_managers(self): + """Load the managers for the environment. + + This function is responsible for creating the various managers (action, observation, + events, etc.) for the environment. Since the managers require access to physics handles, + they can only be created after the simulator is reset (i.e. played for the first time). + + .. note:: + In case of standalone application (when running simulator from Python), the function is called + automatically when the class is initialized. + + However, in case of extension mode, the user must call this function manually after the simulator + is reset. This is because the simulator is only reset when the user calls + :meth:`SimulationContext.reset_async` and it isn't possible to call async functions in the constructor. + + """ + from isaaclab.managers import ActionManager, ObservationManager, RecorderManager + + # prepare the managers + # -- event manager (we print it here to make the logging consistent) + log.info("[INFO] Event Manager: ", self.event_manager) + # -- recorder manager + self.recorder_manager = RecorderManager(self.cfg.recorders, self) + log.info("[INFO] Recorder Manager: ", self.recorder_manager) + # -- action manager + self.action_manager = ActionManager(self.cfg.actions, self) + log.info("[INFO] Action Manager: ", self.action_manager) + # -- observation manager + self.observation_manager = ObservationManager(self.cfg.observations, self) + log.info("[INFO] Observation Manager:", self.observation_manager) + + # perform events at the start of the simulation + # in-case a child implementation creates other managers, the randomization should happen + # when all the other managers are created + if self.__class__ == TrackingBaseEnv and "startup" in self.event_manager.available_modes: + self.event_manager.apply(mode="startup") + + def setup_manager_visualizers(self): + """Creates live visualizers for manager terms.""" + from isaaclab.ui.widgets import ManagerLiveVisualizer + + self.manager_visualizers = { + "action_manager": ManagerLiveVisualizer(manager=self.action_manager), + "observation_manager": ManagerLiveVisualizer(manager=self.observation_manager), + } + + """ + Operations - MDP. + """ + + def reset( + self, seed: int | None = None, env_ids: Sequence[int] | None = None, options: dict[str, Any] | None = None + ) -> tuple[VecEnvObs, dict]: + """Resets the specified environments and returns observations. + + This function calls the :meth:`_reset_idx` function to reset the specified environments. + However, certain operations, such as procedural terrain generation, that happened during initialization + are not repeated. + + Args: + seed: The seed to use for randomization. Defaults to None, in which case the seed is not set. + env_ids: The environment ids to reset. Defaults to None, in which case all environments are reset. + options: Additional information to specify how the environment is reset. Defaults to None. + + Note: + This argument is used for compatibility with Gymnasium environment definition. + + Returns: + A tuple containing the observations and extras. + """ + from isaacsim.core.simulation_manager import SimulationManager + + if env_ids is None: + env_ids = torch.arange(self.num_envs, dtype=torch.int64, device=self.device) + + # trigger recorder terms for pre-reset calls + self.recorder_manager.record_pre_reset(env_ids) + + # set the seed + if seed is not None: + self.seed(seed) + + # reset state of scene + self._reset_idx(env_ids) + + # update articulation kinematics + self.scene.write_data_to_sim() + self.sim.forward() + # if sensors are added to the scene, make sure we render to reflect changes in reset + if self.sim.has_rtx_sensors() and self.cfg.rerender_on_reset: + self.sim.render() + + # trigger recorder terms for post-reset calls + self.recorder_manager.record_post_reset(env_ids) + + # compute observations + self._obs_buf = self.observation_manager.compute() + + if self.cfg.wait_for_textures and self.sim.has_rtx_sensors(): + while SimulationManager.assets_loading(): + self.sim.render() + + # return observations + return self._obs_buf, self.extras + + def reset_to( + self, + state: dict[str, dict[str, dict[str, torch.Tensor]]], + env_ids: Sequence[int] | None, + seed: int | None = None, + is_relative: bool = False, + ): + """Resets specified environments to provided states. + + This function resets the environments to the provided states. The state is a dictionary + containing the state of the scene entities. Please refer to :meth:`InteractiveScene.get_state` + for the format. + + The function is different from the :meth:`reset` function as it resets the environments to specific states, + instead of using the randomization events for resetting the environments. + + Args: + state: The state to reset the specified environments to. Please refer to + :meth:`InteractiveScene.get_state` for the format. + env_ids: The environment ids to reset. Defaults to None, in which case all environments are reset. + seed: The seed to use for randomization. Defaults to None, in which case the seed is not set. + is_relative: If set to True, the state is considered relative to the environment origins. + Defaults to False. + """ + # reset all envs in the scene if env_ids is None + if env_ids is None: + env_ids = torch.arange(self.num_envs, dtype=torch.int64, device=self.device) + + # trigger recorder terms for pre-reset calls + self.recorder_manager.record_pre_reset(env_ids) + + # set the seed + if seed is not None: + self.seed(seed) + + self._reset_idx(env_ids) + + # set the state + self.scene.reset_to(state, env_ids, is_relative=is_relative) + + # update articulation kinematics + self.sim.forward() + + # if sensors are added to the scene, make sure we render to reflect changes in reset + if self.sim.has_rtx_sensors() and self.cfg.rerender_on_reset: + self.sim.render() + + # trigger recorder terms for post-reset calls + self.recorder_manager.record_post_reset(env_ids) + + # compute observations + self._obs_buf = self.observation_manager.compute() + + # return observations + return self._obs_buf, self.extras + + def step(self, action: torch.Tensor) -> tuple[VecEnvObs, dict]: + """Execute one time-step of the environment's dynamics. + + The environment steps forward at a fixed time-step, while the physics simulation is + decimated at a lower time-step. This is to ensure that the simulation is stable. These two + time-steps can be configured independently using the :attr:`ManagerBasedEnvCfg.decimation` (number of + simulation steps per environment step) and the :attr:`ManagerBasedEnvCfg.sim.dt` (physics time-step). + Based on these parameters, the environment time-step is computed as the product of the two. + + Args: + action: The actions to apply on the environment. Shape is (num_envs, action_dim). + + Returns: + A tuple containing the observations and extras. + """ + # process actions + self.action_manager.process_action(action.to(self.device)) + + self.recorder_manager.record_pre_step() + + # check if we need to do rendering within the physics loop + # note: checked here once to avoid multiple checks within the loop + is_rendering = self.sim.has_gui() or self.sim.has_rtx_sensors() + + # perform physics stepping + for _ in range(self.cfg.decimation): + self._sim_step_counter += 1 + # set actions into buffers + self.action_manager.apply_action() + # set actions into simulator + self.scene.write_data_to_sim() + # simulate + self.sim.step(render=False) + # render between steps only if the GUI or an RTX sensor needs it + # note: we assume the render interval to be the shortest accepted rendering interval. + # If a camera needs rendering at a faster frequency, this will lead to unexpected behavior. + if self._sim_step_counter % self.cfg.sim.render_interval == 0 and is_rendering: + self.sim.render() + # update buffers at sim dt + self.scene.update(dt=self.physics_dt) + + # post-step: step interval event + if "interval" in self.event_manager.available_modes: + self.event_manager.apply(mode="interval", dt=self.step_dt) + + # -- compute observations + self._obs_buf = self.observation_manager.compute() + self.recorder_manager.record_post_step() + + # return observations and extras + return self._obs_buf, self.extras + + @staticmethod + def seed(seed: int = -1) -> int: + """Set the seed for the environment. + + Args: + seed: The seed for random generator. Defaults to -1. + + Returns: + The seed used for random generator. + """ + import isaacsim.core.utils.torch as torch_utils + + # set seed for replicator + try: + import omni.replicator.core as rep + + rep.set_global_seed(seed) + except ModuleNotFoundError: + pass + # set seed for torch and other libraries + return torch_utils.set_seed(seed) + + def close(self): + """Cleanup for the environment.""" + if not self._is_closed: + # destructor is order-sensitive + del self.viewport_camera_controller + del self.action_manager + del self.observation_manager + del self.event_manager + del self.recorder_manager + del self.scene + # clear callbacks and instance + self.sim.clear_all_callbacks() + self.sim.clear_instance() + # destroy the window + if self._window is not None: + self._window = None + # update closing status + self._is_closed = True + + """ + Helper functions. + """ + + def _reset_idx(self, env_ids: Sequence[int]): + """Reset environments based on specified indices. + + Args: + env_ids: List of environment ids which must be reset + """ + # reset the internal buffers of the scene elements + self.scene.reset(env_ids) + + # apply events such as randomization for environments that need a reset + if "reset" in self.event_manager.available_modes: + env_step_count = self._sim_step_counter // self.cfg.decimation + self.event_manager.apply(mode="reset", env_ids=env_ids, global_env_step_count=env_step_count) + + # iterate over all managers and reset them + # this returns a dictionary of information which is stored in the extras + # note: This is order-sensitive! Certain things need be reset before others. + self.extras["log"] = dict() + # -- observation manager + info = self.observation_manager.reset(env_ids) + self.extras["log"].update(info) + # -- action manager + info = self.action_manager.reset(env_ids) + self.extras["log"].update(info) + # -- event manager + info = self.event_manager.reset(env_ids) + self.extras["log"].update(info) + # -- recorder manager + info = self.recorder_manager.reset(env_ids) + self.extras["log"].update(info) diff --git a/roboverse_pack/tasks/beyondmimic/isaaclab/envs/tracking_rl_env.py b/roboverse_pack/tasks/beyondmimic/isaaclab/envs/tracking_rl_env.py new file mode 100644 index 000000000..4393a055e --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/isaaclab/envs/tracking_rl_env.py @@ -0,0 +1,495 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +import argparse +import math +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any, ClassVar + +import gymnasium as gym +import numpy as np +import torch +from loguru import logger as log + +from metasim.scenario.scenario import ScenarioCfg +from metasim.task.registry import register_task + +from .tracking_base_env import TrackingBaseEnv + +if TYPE_CHECKING: + from isaaclab.envs.common import VecEnvStepReturn + + from roboverse_learn.rl.configs.rsl_rl.ppo_tracking import RslRlPPOTrackingConfig + + +@register_task("motion-tracking-isaaclab") +class TrackingRLEnv(TrackingBaseEnv, gym.Env): + """The superclass for the manager-based workflow reinforcement learning-based environments. + + This class inherits from :class:`TrackingBaseEnv` and implements the core functionality for + reinforcement learning-based environments. It is designed to be used with any RL + library. The class is designed to be used with vectorized environments, i.e., the + environment is expected to be run in parallel with multiple sub-environments. The + number of sub-environments is specified using the ``num_envs``. + + Each observation from the environment is a batch of observations for each sub- + environments. The method :meth:`step` is also expected to receive a batch of actions + for each sub-environment. + + While the environment itself is implemented as a vectorized environment, we do not + inherit from :class:`gym.vector.VectorEnv`. This is mainly because the class adds + various methods (for wait and asynchronous updates) which are not required. + Additionally, each RL library typically has its own definition for a vectorized + environment. Thus, to reduce complexity, we directly use the :class:`gym.Env` over + here and leave it up to library-defined wrappers to take care of wrapping this + environment for their agents. + + Note: + For vectorized environments, it is recommended to **only** call the :meth:`reset` + method once before the first call to :meth:`step`, i.e. after the environment is created. + After that, the :meth:`step` function handles the reset of terminated sub-environments. + This is because the simulator does not support resetting individual sub-environments + in a vectorized environment. + """ + + scenario = ScenarioCfg() # to align with the unified training script + + # TODO support render mode "rgb_array" when recording video + def __init__( + self, + scenario: ScenarioCfg, + args: RslRlPPOTrackingConfig, + device: str | torch.device | None = None, + render_mode: str | None = None, + reset_in_env_wrapper: bool = False, + ): + """Initialize the environment. + + Args: + scenario: For compatibility with the unified training script. + args: Arguments including CLI args. Needed for specifying motion file path. + device: The used device. + render_mode: The render mode for the environment. Defaults to None, which + is similar to ``"human"``. + reset_in_env_wrapper: Whether the env wrapper calls `env.reset()` in its `__init__()`. + """ + assert scenario.simulator in ["isaaclab", "isaacsim"], ( + "Only Isaac Lab is supported for training of motion tracking task" + ) + + self._launch(scenario.headless) + + # re-import the modules after `AppLauncher` is instantiated + from isaacsim.core.version import get_version + + # import roboverse_pack.tasks.beyondmimic.isaaclab.configs.flat_env_cfg as flat_env_cfg + + # whether the environment is a vectorized environment + self.is_vector_env: ClassVar[bool] = True + + # metadata for the environment + self.metadata: ClassVar[dict[str, Any]] = { + "render_modes": [None, "human", "rgb_array"], + "isaac_sim_version": get_version(), + } + + # -- counter for curriculum + self.common_step_counter = 0 + + # instantiate environment config + # NOTE robot (`ArticulationCfg`) is included in `G1FlatEnvCfg` so the CLI arg `robot` will be ignored + cfg = self._init_cfg() + cfg.scene.num_envs = scenario.num_envs + cfg.seed = args.train_cfg["seed"] + cfg.sim.device = args.device if args.device else cfg.sim.device + cfg.commands.motion.motion_file = args.motion_file + + # initialize the base class to setup the scene. + super().__init__(cfg=cfg) + # store the render mode + self.render_mode = render_mode + + # initialize data and constants + # -- init buffers + self.episode_length_buf = torch.zeros(self.num_envs, device=self.device, dtype=torch.long) + # -- set the framerate of the gym video recorder wrapper so that the playback speed of the produced video matches the simulation + self.metadata["render_fps"] = 1 / self.step_dt + + # NOTE this is needed here because RoboVerse' `RslRlVecEnvWrapper` doesn't call `env.reset()` in its `__init__()` + if not reset_in_env_wrapper: + self.reset() + + log.info("[INFO]: Completed setting up the environment...") + + def _init_cfg(self): + import roboverse_pack.tasks.beyondmimic.isaaclab.configs.flat_env_cfg as flat_env_cfg + + return flat_env_cfg.G1FlatEnvCfg() + + def _launch(self, headless: bool = False): + from isaaclab.app import AppLauncher + + parser = argparse.ArgumentParser() + AppLauncher.add_app_launcher_args(parser) + args = parser.parse_args([]) + args.enable_cameras = False + args.headless = headless + app_launcher = AppLauncher(args) + + # NOTE will be closed in `self.close()` (called in `self.__del__()`) + self.simulation_app = app_launcher.app + + """ + Properties. + """ + + @property + def obs_buf(self) -> torch.Tensor: + """Policy (actor) observations in shape (num_envs, num_obs).""" + return self._obs_buf["policy"] + + @property + def priv_obs_buf(self) -> torch.Tensor: + """Critic observations in shape (num_envs, num_priv_obs).""" + return self._obs_buf["critic"] + + @property + def max_episode_length_s(self) -> float: + """Maximum episode length in seconds.""" + return self.cfg.episode_length_s + + @property + def max_episode_steps(self) -> int: + """Maximum episode steps.""" + return self.max_episode_length + + @property + def num_actions(self) -> int: + """Total dimension of actions.""" + return self.action_manager.total_action_dim + + # to keep backward compatibility with Isaac Lab MDP functions + + @property + def max_episode_length(self) -> int: + """Maximum episode length in environment steps.""" + return math.ceil(self.max_episode_length_s / self.step_dt) + + @property + def _episode_steps(self) -> torch.Tensor: + """Current episode lengths of each env. Used in time-out computation.""" + return self.episode_length_buf + + @_episode_steps.setter + def _episode_steps(self, value: torch.Tensor): + self.episode_length_buf = value + + """ + Operations - Setup. + """ + + def load_managers(self): + """Load the managers for the environment.""" + # note: this order is important since observation manager needs to know the command and action managers + # and the reward manager needs to know the termination manager + from isaaclab.managers import CommandManager, CurriculumManager, RewardManager, TerminationManager + + # -- command manager + self.command_manager: CommandManager = CommandManager(self.cfg.commands, self) + log.info("[INFO] Command Manager: ", self.command_manager) + + # call the parent class to load the managers for observations and actions. + super().load_managers() + + # prepare the managers + # -- termination manager + self.termination_manager = TerminationManager(self.cfg.terminations, self) + log.info("[INFO] Termination Manager: ", self.termination_manager) + # -- reward manager + self.reward_manager = RewardManager(self.cfg.rewards, self) + log.info("[INFO] Reward Manager: ", self.reward_manager) + # -- curriculum manager + self.curriculum_manager = CurriculumManager(self.cfg.curriculum, self) + log.info("[INFO] Curriculum Manager: ", self.curriculum_manager) + + # setup the action and observation spaces for Gym + self._configure_gym_env_spaces() + + # perform events at the start of the simulation + if "startup" in self.event_manager.available_modes: + self.event_manager.apply(mode="startup") + + def setup_manager_visualizers(self): + """Creates live visualizers for manager terms.""" + from isaaclab.ui.widgets import ManagerLiveVisualizer + + self.manager_visualizers = { + "action_manager": ManagerLiveVisualizer(manager=self.action_manager), + "observation_manager": ManagerLiveVisualizer(manager=self.observation_manager), + "command_manager": ManagerLiveVisualizer(manager=self.command_manager), + "termination_manager": ManagerLiveVisualizer(manager=self.termination_manager), + "reward_manager": ManagerLiveVisualizer(manager=self.reward_manager), + "curriculum_manager": ManagerLiveVisualizer(manager=self.curriculum_manager), + } + + """ + Operations - MDP + """ + + def step(self, action: torch.Tensor) -> VecEnvStepReturn: + """Execute one time-step of the environment's dynamics and reset terminated environments. + + Unlike the :class:`TrackingBaseEnv.step` class, the function performs the following operations: + + 1. Process the actions. + 2. Perform physics stepping. + 3. Perform rendering if gui is enabled. + 4. Update the environment counters and compute the rewards and terminations. + 5. Reset the environments that terminated. + 6. Compute the observations. + 7. Return the observations, rewards, resets and extras. + + Args: + action: The actions to apply on the environment. Shape is (num_envs, action_dim). + + Returns: + A tuple containing the observations, rewards, resets (terminated and truncated) and extras. + """ + # process actions + self.action_manager.process_action(action.to(self.device)) + + self.recorder_manager.record_pre_step() + + # check if we need to do rendering within the physics loop + # note: checked here once to avoid multiple checks within the loop + is_rendering = self.sim.has_gui() or self.sim.has_rtx_sensors() + + # perform physics stepping + for _ in range(self.cfg.decimation): + self._sim_step_counter += 1 + # set actions into buffers + self.action_manager.apply_action() # NOTE sets `_data.joint_pos_target` + # set actions into simulator + self.scene.write_data_to_sim() + # simulate + self.sim.step(render=False) + # render between steps only if the GUI or an RTX sensor needs it + # note: we assume the render interval to be the shortest accepted rendering interval. + # If a camera needs rendering at a faster frequency, this will lead to unexpected behavior. + if self._sim_step_counter % self.cfg.sim.render_interval == 0 and is_rendering: + self.sim.render() + # update buffers at sim dt + self.scene.update(dt=self.physics_dt) + + # post-step: + # -- update env counters (used for curriculum generation) + self.episode_length_buf += 1 # step in current episode (per env) + self.common_step_counter += 1 # total step (common for all envs) + # -- check terminations + self.reset_buf = self.termination_manager.compute() + self.reset_terminated = self.termination_manager.terminated + self.reset_time_outs = self.termination_manager.time_outs + # -- reward computation + self.reward_buf = self.reward_manager.compute(dt=self.step_dt) + + if len(self.recorder_manager.active_terms) > 0: + # update observations for recording if needed + self._obs_buf = self.observation_manager.compute() + self.recorder_manager.record_post_step() + + # -- reset envs that terminated/timed-out and log the episode information + reset_env_ids = self.reset_buf.nonzero(as_tuple=False).squeeze(-1) + if len(reset_env_ids) > 0: + # trigger recorder terms for pre-reset calls + self.recorder_manager.record_pre_reset(reset_env_ids) + + self._reset_idx(reset_env_ids) + # update articulation kinematics + self.scene.write_data_to_sim() + self.sim.forward() + + # if sensors are added to the scene, make sure we render to reflect changes in reset + if self.sim.has_rtx_sensors() and self.cfg.rerender_on_reset: + self.sim.render() + + # trigger recorder terms for post-reset calls + self.recorder_manager.record_post_reset(reset_env_ids) + + # -- update command + self.command_manager.compute(dt=self.step_dt) + # -- step interval events + if "interval" in self.event_manager.available_modes: + self.event_manager.apply(mode="interval", dt=self.step_dt) + # -- compute observations + # note: done after reset to get the correct observations for reset envs + self._obs_buf = self.observation_manager.compute() + + # return observations, rewards, resets and extras + return self._obs_buf, self.reward_buf, self.reset_terminated, self.reset_time_outs, self.extras + + def render(self, recompute: bool = False) -> np.ndarray | None: + """Run rendering without stepping through the physics. + + By convention, if mode is: + + - **human**: Render to the current display and return nothing. Usually for human consumption. + - **rgb_array**: Return an numpy.ndarray with shape (x, y, 3), representing RGB values for an + x-by-y pixel image, suitable for turning into a video. + + Args: + recompute: Whether to force a render even if the simulator has already rendered the scene. + Defaults to False. + + Returns: + The rendered image as a numpy array if mode is "rgb_array". Otherwise, returns None. + + Raises: + RuntimeError: If mode is set to "rgb_data" and simulation render mode does not support it. + In this case, the simulation render mode must be set to ``RenderMode.PARTIAL_RENDERING`` + or ``RenderMode.FULL_RENDERING``. + NotImplementedError: If an unsupported rendering mode is specified. + """ + # run a rendering step of the simulator + # if we have rtx sensors, we do not need to render again sin + if not self.sim.has_rtx_sensors() and not recompute: + self.sim.render() + # decide the rendering mode + if self.render_mode == "human" or self.render_mode is None: + return None + elif self.render_mode == "rgb_array": + # check that if any render could have happened + if self.sim.render_mode.value < self.sim.RenderMode.PARTIAL_RENDERING.value: + raise RuntimeError( + f"Cannot render '{self.render_mode}' when the simulation render mode is" + f" '{self.sim.render_mode.name}'. Please set the simulation render mode to:" + f"'{self.sim.RenderMode.PARTIAL_RENDERING.name}' or '{self.sim.RenderMode.FULL_RENDERING.name}'." + " If running headless, make sure --enable_cameras is set." + ) + # create the annotator if it does not exist + if not hasattr(self, "_rgb_annotator"): + import omni.replicator.core as rep + + # create render product + self._render_product = rep.create.render_product( + self.cfg.viewer.cam_prim_path, self.cfg.viewer.resolution + ) + # create rgb annotator -- used to read data from the render product + self._rgb_annotator = rep.AnnotatorRegistry.get_annotator("rgb", device="cpu") + self._rgb_annotator.attach([self._render_product]) + # obtain the rgb data + rgb_data = self._rgb_annotator.get_data() + # convert to numpy array + rgb_data = np.frombuffer(rgb_data, dtype=np.uint8).reshape(*rgb_data.shape) + # return the rgb data + # note: initially the renerer is warming up and returns empty data + if rgb_data.size == 0: + return np.zeros((self.cfg.viewer.resolution[1], self.cfg.viewer.resolution[0], 3), dtype=np.uint8) + else: + return rgb_data[:, :, :3] + else: + raise NotImplementedError( + f"Render mode '{self.render_mode}' is not supported. Please use: {self.metadata['render_modes']}." + ) + + def close(self): + """Close the environment and simulation app.""" + if not self._is_closed: + # destructor is order-sensitive + del self.command_manager + del self.reward_manager + del self.termination_manager + del self.curriculum_manager + self.simulation_app.close() + # call the parent class to close the environment + super().close() + del self.simulation_app + + """ + Helper functions. + """ + + def _configure_gym_env_spaces(self): + """Configure the action and observation spaces for the Gym environment.""" + # observation space (unbounded since we don't impose any limits) + self.single_observation_space = gym.spaces.Dict() + for group_name, group_term_names in self.observation_manager.active_terms.items(): + # extract quantities about the group + has_concatenated_obs = self.observation_manager.group_obs_concatenate[group_name] + group_dim = self.observation_manager.group_obs_dim[group_name] + # check if group is concatenated or not + # if not concatenated, then we need to add each term separately as a dictionary + if has_concatenated_obs: + self.single_observation_space[group_name] = gym.spaces.Box(low=-np.inf, high=np.inf, shape=group_dim) + else: + self.single_observation_space[group_name] = gym.spaces.Dict({ + term_name: gym.spaces.Box(low=-np.inf, high=np.inf, shape=term_dim) + for term_name, term_dim in zip(group_term_names, group_dim) + }) + # action space (unbounded since we don't impose any limits) + action_dim = sum(self.action_manager.action_term_dim) + self.single_action_space = gym.spaces.Box(low=-np.inf, high=np.inf, shape=(action_dim,)) + + # batch the spaces for vectorized environments + self.observation_space = gym.vector.utils.batch_space(self.single_observation_space, self.num_envs) + self.action_space = gym.vector.utils.batch_space(self.single_action_space, self.num_envs) + + def _reset_idx(self, env_ids: Sequence[int]): + """Reset environments based on specified indices. + + Args: + env_ids: List of environment ids which must be reset + """ + # update the curriculum for environments that need a reset + self.curriculum_manager.compute(env_ids=env_ids) + # reset the internal buffers of the scene elements + self.scene.reset(env_ids) + # apply events such as randomizations for environments that need a reset + if "reset" in self.event_manager.available_modes: + env_step_count = self._sim_step_counter // self.cfg.decimation + self.event_manager.apply(mode="reset", env_ids=env_ids, global_env_step_count=env_step_count) + + # iterate over all managers and reset them + # this returns a dictionary of information which is stored in the extras + # note: This is order-sensitive! Certain things need be reset before others. + self.extras["log"] = dict() + # -- observation manager + info = self.observation_manager.reset(env_ids) + self.extras["log"].update(info) + # -- action manager + info = self.action_manager.reset(env_ids) + self.extras["log"].update(info) + # -- rewards manager + info = self.reward_manager.reset(env_ids) + self.extras["log"].update(info) + # -- curriculum manager + info = self.curriculum_manager.reset(env_ids) + self.extras["log"].update(info) + # -- command manager + info = self.command_manager.reset(env_ids) + self.extras["log"].update(info) + # -- event manager + info = self.event_manager.reset(env_ids) + self.extras["log"].update(info) + # -- termination manager + info = self.termination_manager.reset(env_ids) + self.extras["log"].update(info) + # -- recorder manager + info = self.recorder_manager.reset(env_ids) + self.extras["log"].update(info) + + # reset the episode length buffer + self.episode_length_buf[env_ids] = 0 + + +@register_task("motion-tracking-isaaclab-deploy") +class TrackingRLEnvDeploy(TrackingRLEnv, gym.Env): + """Task class for training motion tracker for deployment. The actuators are delayed, and the policy's observations `motion_anchor_pos_b` and `base_lin_vel` are removed.""" + + def _init_cfg(self): + import roboverse_pack.tasks.beyondmimic.isaaclab.configs.flat_env_cfg as flat_env_cfg + + return flat_env_cfg.G1FlatEnvCfgDeploy() diff --git a/roboverse_pack/tasks/beyondmimic/isaaclab/mdp/commands.py b/roboverse_pack/tasks/beyondmimic/isaaclab/mdp/commands.py new file mode 100644 index 000000000..7902dfea4 --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/isaaclab/mdp/commands.py @@ -0,0 +1,430 @@ +from __future__ import annotations + +import math +import os +from collections.abc import Sequence +from dataclasses import MISSING +from typing import TYPE_CHECKING + +import numpy as np +import torch +from isaaclab.assets import Articulation +from isaaclab.managers import CommandTerm, CommandTermCfg +from isaaclab.markers import VisualizationMarkers, VisualizationMarkersCfg +from isaaclab.markers.config import FRAME_MARKER_CFG +from isaaclab.utils import configclass +from isaaclab.utils.math import ( + quat_apply, + quat_error_magnitude, + quat_from_euler_xyz, + quat_inv, + quat_mul, + sample_uniform, + yaw_quat, +) + +if TYPE_CHECKING: + from roboverse_pack.tasks.beyondmimic.isaaclab.envs.tracking_rl_env import TrackingRLEnv + + +class MotionLoader: + """Load motion data from a file and provide access to target joint states and body positions and orientations in world frame.""" + + def __init__(self, motion_file: str, body_indexes: Sequence[int], device: str = "cpu"): + assert os.path.isfile(motion_file), f"Invalid file path: {motion_file}" + data = np.load(motion_file) + self.fps = data["fps"] + # target joint states from motion file + self.joint_pos = torch.tensor(data["joint_pos"], dtype=torch.float32, device=device) # [6574, 29] + self.joint_vel = torch.tensor(data["joint_vel"], dtype=torch.float32, device=device) # [6574, 29] + # target body positions and orientations in world frame + self._body_pos_w = torch.tensor(data["body_pos_w"], dtype=torch.float32, device=device) # [6574, 30, 3] + self._body_quat_w = torch.tensor(data["body_quat_w"], dtype=torch.float32, device=device) # [6574, 30, 4] + # target body velocities + self._body_lin_vel_w = torch.tensor(data["body_lin_vel_w"], dtype=torch.float32, device=device) # [6574, 30, 3] + self._body_ang_vel_w = torch.tensor(data["body_ang_vel_w"], dtype=torch.float32, device=device) # [6574, 30, 3] + self._body_indexes = body_indexes # [14,] + self.time_step_total = self.joint_pos.shape[0] # 6574 + + @property + def body_pos_w(self) -> torch.Tensor: + """Body positions in world frame.""" + return self._body_pos_w[:, self._body_indexes] + + @property + def body_quat_w(self) -> torch.Tensor: + """Body quaternions in world frame.""" + return self._body_quat_w[:, self._body_indexes] + + @property + def body_lin_vel_w(self) -> torch.Tensor: + """Body linear velocities in world frame.""" + return self._body_lin_vel_w[:, self._body_indexes] # (n_frames, n_bodies, 3) + + @property + def body_ang_vel_w(self) -> torch.Tensor: + """Body angular velocities in world frame.""" + return self._body_ang_vel_w[:, self._body_indexes] + + +class MotionCommand(CommandTerm): + """Command term for the motion tracking task.""" + + cfg: MotionCommandCfg + + def __init__(self, cfg: MotionCommandCfg, env: TrackingRLEnv): + super().__init__(cfg, env) + + self.robot: Articulation = env.scene[cfg.asset_name] + self.robot_anchor_body_index = self.robot.body_names.index(self.cfg.anchor_body_name) + self.motion_anchor_body_index = self.cfg.body_names.index(self.cfg.anchor_body_name) + self.body_indexes = torch.tensor( + self.robot.find_bodies(self.cfg.body_names, preserve_order=True)[0], dtype=torch.long, device=self.device + ) + + self.motion = MotionLoader(self.cfg.motion_file, self.body_indexes, device=self.device) + self.time_steps = torch.zeros(self.num_envs, dtype=torch.long, device=self.device) + self.body_pos_relative_w = torch.zeros(self.num_envs, len(cfg.body_names), 3, device=self.device) + self.body_quat_relative_w = torch.zeros(self.num_envs, len(cfg.body_names), 4, device=self.device) + self.body_quat_relative_w[:, :, 0] = 1.0 + + self.bin_count = int(self.motion.time_step_total // (1 / (env.cfg.decimation * env.cfg.sim.dt))) + 1 + self.bin_failed_count = torch.zeros(self.bin_count, dtype=torch.float, device=self.device) + self._current_bin_failed = torch.zeros(self.bin_count, dtype=torch.float, device=self.device) + self.kernel = torch.tensor( + [self.cfg.adaptive_lambda**i for i in range(self.cfg.adaptive_kernel_size)], device=self.device + ) + self.kernel = self.kernel / self.kernel.sum() + + self.metrics["error_anchor_pos"] = torch.zeros(self.num_envs, device=self.device) + self.metrics["error_anchor_rot"] = torch.zeros(self.num_envs, device=self.device) + self.metrics["error_anchor_lin_vel"] = torch.zeros(self.num_envs, device=self.device) + self.metrics["error_anchor_ang_vel"] = torch.zeros(self.num_envs, device=self.device) + self.metrics["error_body_pos"] = torch.zeros(self.num_envs, device=self.device) + self.metrics["error_body_rot"] = torch.zeros(self.num_envs, device=self.device) + self.metrics["error_joint_pos"] = torch.zeros(self.num_envs, device=self.device) + self.metrics["error_joint_vel"] = torch.zeros(self.num_envs, device=self.device) + self.metrics["sampling_entropy"] = torch.zeros(self.num_envs, device=self.device) + self.metrics["sampling_top1_prob"] = torch.zeros(self.num_envs, device=self.device) + self.metrics["sampling_top1_bin"] = torch.zeros(self.num_envs, device=self.device) + + @property + def command(self) -> torch.Tensor: + """Command for the motion tracking task.""" + return torch.cat([self.joint_pos, self.joint_vel], dim=1) + + @property + def joint_pos(self) -> torch.Tensor: + """Joint positions.""" + return self.motion.joint_pos[self.time_steps] + + @property + def joint_vel(self) -> torch.Tensor: + """Joint velocities.""" + return self.motion.joint_vel[self.time_steps] + + @property + def body_pos_w(self) -> torch.Tensor: + """Body positions in world frame.""" + return self.motion.body_pos_w[self.time_steps] + self._env.scene.env_origins[:, None, :] + + @property + def body_quat_w(self) -> torch.Tensor: + """Body quaternions in world frame.""" + return self.motion.body_quat_w[self.time_steps] + + @property + def body_lin_vel_w(self) -> torch.Tensor: + """Body linear velocities in world frame.""" + return self.motion.body_lin_vel_w[self.time_steps] # (n_frames, n_bodies_subset, 3) + + @property + def body_ang_vel_w(self) -> torch.Tensor: + """Body angular velocities in world frame.""" + return self.motion.body_ang_vel_w[self.time_steps] + + # target root body states + # `env_origins` is the world-space origins for each env, computed internally by Isaac Lab (using `num_envs` and `env_spacing`) + @property + def anchor_pos_w(self) -> torch.Tensor: + """Anchor position in world frame.""" + return self.motion.body_pos_w[self.time_steps, self.motion_anchor_body_index] + self._env.scene.env_origins + + @property + def anchor_quat_w(self) -> torch.Tensor: + """Anchor quaternion in world frame.""" + return self.motion.body_quat_w[self.time_steps, self.motion_anchor_body_index] + + @property + def anchor_lin_vel_w(self) -> torch.Tensor: + """Anchor linear velocity in world frame.""" + return self.motion.body_lin_vel_w[self.time_steps, self.motion_anchor_body_index] + + @property + def anchor_ang_vel_w(self) -> torch.Tensor: + """Anchor angular velocity in world frame.""" + return self.motion.body_ang_vel_w[self.time_steps, self.motion_anchor_body_index] + + # actual robot joint states + @property + def robot_joint_pos(self) -> torch.Tensor: + """Robot joint positions.""" + return self.robot.data.joint_pos + + @property + def robot_joint_vel(self) -> torch.Tensor: + """Robot joint velocities.""" + return self.robot.data.joint_vel + + # actual robot body states + @property + def robot_body_pos_w(self) -> torch.Tensor: + """Robot body positions in world frame.""" + return self.robot.data.body_pos_w[:, self.body_indexes] + + @property + def robot_body_quat_w(self) -> torch.Tensor: + """Robot body quaternions in world frame.""" + return self.robot.data.body_quat_w[:, self.body_indexes] + + @property + def robot_body_lin_vel_w(self) -> torch.Tensor: + """Robot body linear velocities in world frame.""" + return self.robot.data.body_lin_vel_w[:, self.body_indexes] + + @property + def robot_body_ang_vel_w(self) -> torch.Tensor: + """Robot body angular velocities in world frame.""" + return self.robot.data.body_ang_vel_w[:, self.body_indexes] + + # actual robot root body states + @property + def robot_anchor_pos_w(self) -> torch.Tensor: + """Robot anchor position in world frame.""" + return self.robot.data.body_pos_w[:, self.robot_anchor_body_index] + + @property + def robot_anchor_quat_w(self) -> torch.Tensor: + """Robot anchor quaternion in world frame.""" + return self.robot.data.body_quat_w[:, self.robot_anchor_body_index] + + @property + def robot_anchor_lin_vel_w(self) -> torch.Tensor: + """Robot anchor linear velocity in world frame.""" + return self.robot.data.body_lin_vel_w[:, self.robot_anchor_body_index] + + @property + def robot_anchor_ang_vel_w(self) -> torch.Tensor: + """Robot anchor angular velocity in world frame.""" + return self.robot.data.body_ang_vel_w[:, self.robot_anchor_body_index] + + def _update_metrics(self): + self.metrics["error_anchor_pos"] = torch.norm(self.anchor_pos_w - self.robot_anchor_pos_w, dim=-1) + self.metrics["error_anchor_rot"] = quat_error_magnitude(self.anchor_quat_w, self.robot_anchor_quat_w) + self.metrics["error_anchor_lin_vel"] = torch.norm(self.anchor_lin_vel_w - self.robot_anchor_lin_vel_w, dim=-1) + self.metrics["error_anchor_ang_vel"] = torch.norm(self.anchor_ang_vel_w - self.robot_anchor_ang_vel_w, dim=-1) + + self.metrics["error_body_pos"] = torch.norm(self.body_pos_relative_w - self.robot_body_pos_w, dim=-1).mean( + dim=-1 + ) + self.metrics["error_body_rot"] = quat_error_magnitude(self.body_quat_relative_w, self.robot_body_quat_w).mean( + dim=-1 + ) + + self.metrics["error_body_lin_vel"] = torch.norm(self.body_lin_vel_w - self.robot_body_lin_vel_w, dim=-1).mean( + dim=-1 + ) + self.metrics["error_body_ang_vel"] = torch.norm(self.body_ang_vel_w - self.robot_body_ang_vel_w, dim=-1).mean( + dim=-1 + ) + + self.metrics["error_joint_pos"] = torch.norm(self.joint_pos - self.robot_joint_pos, dim=-1) + self.metrics["error_joint_vel"] = torch.norm(self.joint_vel - self.robot_joint_vel, dim=-1) + + def _adaptive_sampling(self, env_ids: Sequence[int]): + episode_failed = self._env.termination_manager.terminated[env_ids] # boolean tensor of shape [n_envs] + if torch.any(episode_failed): + current_bin_index = torch.clamp( + (self.time_steps * self.bin_count) // max(self.motion.time_step_total, 1), 0, self.bin_count - 1 + ) + fail_bins = current_bin_index[env_ids][episode_failed] + self._current_bin_failed[:] = torch.bincount(fail_bins, minlength=self.bin_count) + + # Sample + sampling_probabilities = self.bin_failed_count + self.cfg.adaptive_uniform_ratio / float(self.bin_count) + sampling_probabilities = torch.nn.functional.pad( + sampling_probabilities.unsqueeze(0).unsqueeze(0), + (0, self.cfg.adaptive_kernel_size - 1), # Non-causal kernel + mode="replicate", + ) + sampling_probabilities = torch.nn.functional.conv1d(sampling_probabilities, self.kernel.view(1, 1, -1)).view(-1) + + sampling_probabilities = sampling_probabilities / sampling_probabilities.sum() + + sampled_bins = torch.multinomial(sampling_probabilities, len(env_ids), replacement=True) + + self.time_steps[env_ids] = ( + (sampled_bins + sample_uniform(0.0, 1.0, (len(env_ids),), device=self.device)) + / self.bin_count + * (self.motion.time_step_total - 1) + ).long() + + # Metrics + H = -(sampling_probabilities * (sampling_probabilities + 1e-12).log()).sum() + H_norm = H / math.log(self.bin_count) + pmax, imax = sampling_probabilities.max(dim=0) + self.metrics["sampling_entropy"][:] = H_norm + self.metrics["sampling_top1_prob"][:] = pmax + self.metrics["sampling_top1_bin"][:] = imax.float() / self.bin_count + + def _resample_command(self, env_ids: Sequence[int]): + if len(env_ids) == 0: + return + self._adaptive_sampling(env_ids) + + # NOTE index 0 corresponds to root (pelvis) + root_pos = self.body_pos_w[:, 0].clone() # (n_envs, 3) + root_ori = self.body_quat_w[:, 0].clone() # (n_envs, 4) + root_lin_vel = self.body_lin_vel_w[:, 0].clone() # (n_envs, 3) + root_ang_vel = self.body_ang_vel_w[:, 0].clone() # (n_envs, 3) + + range_list = [self.cfg.pose_range.get(key, (0.0, 0.0)) for key in ["x", "y", "z", "roll", "pitch", "yaw"]] + ranges = torch.tensor(range_list, device=self.device) + rand_samples = sample_uniform(ranges[:, 0], ranges[:, 1], (len(env_ids), 6), device=self.device) + root_pos[env_ids] += rand_samples[:, 0:3] + orientations_delta = quat_from_euler_xyz(rand_samples[:, 3], rand_samples[:, 4], rand_samples[:, 5]) + root_ori[env_ids] = quat_mul(orientations_delta, root_ori[env_ids]) + range_list = [self.cfg.velocity_range.get(key, (0.0, 0.0)) for key in ["x", "y", "z", "roll", "pitch", "yaw"]] + ranges = torch.tensor(range_list, device=self.device) + rand_samples = sample_uniform(ranges[:, 0], ranges[:, 1], (len(env_ids), 6), device=self.device) + root_lin_vel[env_ids] += rand_samples[:, :3] + root_ang_vel[env_ids] += rand_samples[:, 3:] + + joint_pos = self.joint_pos.clone() + joint_vel = self.joint_vel.clone() + + joint_pos += sample_uniform(*self.cfg.joint_position_range, joint_pos.shape, joint_pos.device) + soft_joint_pos_limits = self.robot.data.soft_joint_pos_limits[ + env_ids + ] # [n_envs, n_dofs, 2] # NOTE position limits are the same for all envs + joint_pos[env_ids] = torch.clip( + joint_pos[env_ids], soft_joint_pos_limits[:, :, 0], soft_joint_pos_limits[:, :, 1] + ) + self.robot.write_joint_state_to_sim(joint_pos[env_ids], joint_vel[env_ids], env_ids=env_ids) + self.robot.write_root_state_to_sim( + torch.cat([root_pos[env_ids], root_ori[env_ids], root_lin_vel[env_ids], root_ang_vel[env_ids]], dim=-1), + env_ids=env_ids, + ) + + def _update_command(self): + self.time_steps += 1 + env_ids = torch.where(self.time_steps >= self.motion.time_step_total)[0] + # picks new time steps using adaptive sampling for the envs that have reached the end of the motion + self._resample_command(env_ids) # resamples when motion ends + + # NOTE intuition: puts reference motion at the robot's XY (rotates it so its heading matches the robot's heading) while keeping the motion's height (Z) + anchor_pos_w_repeat = self.anchor_pos_w[:, None, :].repeat( + 1, len(self.cfg.body_names), 1 + ) # [2, 14, 3] -> [n_envs, n_bodies, 3] + anchor_quat_w_repeat = self.anchor_quat_w[:, None, :].repeat( + 1, len(self.cfg.body_names), 1 + ) # [n_envs, n_bodies, 4] + robot_anchor_pos_w_repeat = self.robot_anchor_pos_w[:, None, :].repeat( + 1, len(self.cfg.body_names), 1 + ) # [n_envs, n_bodies, 3] + robot_anchor_quat_w_repeat = self.robot_anchor_quat_w[:, None, :].repeat( + 1, len(self.cfg.body_names), 1 + ) # [n_envs, n_bodies, 4] + + # lets XY comes from the robot anchor, and Z comes from the motion anchor + # avoids penalizing global XY drift while preserving vertical posture from the motion + delta_pos_w = robot_anchor_pos_w_repeat + delta_pos_w[..., 2] = anchor_pos_w_repeat[..., 2] # overwrites Z + # first computes relative rotation between robot and motion anchors, then keeps only the yaw component + delta_ori_w = yaw_quat(quat_mul(robot_anchor_quat_w_repeat, quat_inv(anchor_quat_w_repeat))) + + self.body_quat_relative_w = quat_mul(delta_ori_w, self.body_quat_w) + self.body_pos_relative_w = delta_pos_w + quat_apply(delta_ori_w, self.body_pos_w - anchor_pos_w_repeat) + + self.bin_failed_count = ( + self.cfg.adaptive_alpha * self._current_bin_failed + (1 - self.cfg.adaptive_alpha) * self.bin_failed_count + ) + self._current_bin_failed.zero_() + + def _set_debug_vis_impl(self, debug_vis: bool): + if debug_vis: + if not hasattr(self, "current_anchor_visualizer"): + self.current_anchor_visualizer = VisualizationMarkers( + self.cfg.anchor_visualizer_cfg.replace(prim_path="/Visuals/Command/current/anchor") + ) + self.goal_anchor_visualizer = VisualizationMarkers( + self.cfg.anchor_visualizer_cfg.replace(prim_path="/Visuals/Command/goal/anchor") + ) + + self.current_body_visualizers = [] + self.goal_body_visualizers = [] + for name in self.cfg.body_names: + self.current_body_visualizers.append( + VisualizationMarkers( + self.cfg.body_visualizer_cfg.replace(prim_path="/Visuals/Command/current/" + name) + ) + ) + self.goal_body_visualizers.append( + VisualizationMarkers( + self.cfg.body_visualizer_cfg.replace(prim_path="/Visuals/Command/goal/" + name) + ) + ) + + self.current_anchor_visualizer.set_visibility(True) + self.goal_anchor_visualizer.set_visibility(True) + for i in range(len(self.cfg.body_names)): + self.current_body_visualizers[i].set_visibility(True) + self.goal_body_visualizers[i].set_visibility(True) + + else: + if hasattr(self, "current_anchor_visualizer"): + self.current_anchor_visualizer.set_visibility(False) + self.goal_anchor_visualizer.set_visibility(False) + for i in range(len(self.cfg.body_names)): + self.current_body_visualizers[i].set_visibility(False) + self.goal_body_visualizers[i].set_visibility(False) + + def _debug_vis_callback(self, event): + if not self.robot.is_initialized: + return + + self.current_anchor_visualizer.visualize(self.robot_anchor_pos_w, self.robot_anchor_quat_w) + self.goal_anchor_visualizer.visualize(self.anchor_pos_w, self.anchor_quat_w) + + for i in range(len(self.cfg.body_names)): + self.current_body_visualizers[i].visualize(self.robot_body_pos_w[:, i], self.robot_body_quat_w[:, i]) + self.goal_body_visualizers[i].visualize(self.body_pos_relative_w[:, i], self.body_quat_relative_w[:, i]) + + +@configclass +class MotionCommandCfg(CommandTermCfg): + """Configuration for the motion command.""" + + class_type: type = MotionCommand + + asset_name: str = MISSING + + motion_file: str = MISSING + anchor_body_name: str = MISSING + body_names: list[str] = MISSING + + pose_range: dict[str, tuple[float, float]] = {} + velocity_range: dict[str, tuple[float, float]] = {} + + joint_position_range: tuple[float, float] = (-0.52, 0.52) + + adaptive_kernel_size: int = 1 + adaptive_lambda: float = 0.8 + adaptive_uniform_ratio: float = 0.1 + adaptive_alpha: float = 0.001 + + anchor_visualizer_cfg: VisualizationMarkersCfg = FRAME_MARKER_CFG.replace(prim_path="/Visuals/Command/pose") + anchor_visualizer_cfg.markers["frame"].scale = (0.2, 0.2, 0.2) + + body_visualizer_cfg: VisualizationMarkersCfg = FRAME_MARKER_CFG.replace(prim_path="/Visuals/Command/pose") + body_visualizer_cfg.markers["frame"].scale = (0.1, 0.1, 0.1) diff --git a/roboverse_pack/tasks/beyondmimic/isaaclab/mdp/events.py b/roboverse_pack/tasks/beyondmimic/isaaclab/mdp/events.py new file mode 100644 index 000000000..2cfef8d61 --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/isaaclab/mdp/events.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal + +import isaaclab.utils.math as math_utils +import torch +from isaaclab.assets import Articulation +from isaaclab.envs.mdp.events import _randomize_prop_by_op +from isaaclab.managers import SceneEntityCfg + +if TYPE_CHECKING: + from roboverse_pack.tasks.beyondmimic.isaaclab.envs.tracking_rl_env import TrackingRLEnv + + +def randomize_joint_default_pos( + env: TrackingRLEnv, + env_ids: torch.Tensor | None, + asset_cfg: SceneEntityCfg, + pos_distribution_params: tuple[float, float] | None = None, + operation: Literal["add", "scale", "abs"] = "abs", + distribution: Literal["uniform", "log_uniform", "gaussian"] = "uniform", +): + """Randomize the joint default positions which may be different from URDF due to calibration errors.""" + # extract the used quantities (to enable type-hinting) + asset: Articulation = env.scene[asset_cfg.name] + + # save nominal value for export + asset.data.default_joint_pos_nominal = torch.clone( + asset.data.default_joint_pos[0] + ) # [n_dofs,] # NOTE all envs' `default_joint_pos` are the same + + # resolve environment ids + if env_ids is None: # True + env_ids = torch.arange(env.scene.num_envs, device=asset.device) # [n_envs,] + + # resolve joint indices + if asset_cfg.joint_ids == slice(None): + joint_ids = slice(None) # for optimization purposes + else: + joint_ids = torch.tensor(asset_cfg.joint_ids, dtype=torch.int, device=asset.device) # range(n_dofs) + + if pos_distribution_params is not None: + pos = asset.data.default_joint_pos.to(asset.device).clone() + pos = _randomize_prop_by_op( + pos, pos_distribution_params, env_ids, joint_ids, operation=operation, distribution=distribution + )[env_ids][:, joint_ids] + + if env_ids != slice(None) and joint_ids != slice(None): + env_ids = env_ids[:, None] + asset.data.default_joint_pos[env_ids, joint_ids] = pos + # update the offset in action since it is not updated automatically # NOTE this is required because `JointPositionAction._offset` equals `default_joint_pos` when `JointPositionActionCfg.use_default_offset` is True + env.action_manager.get_term("joint_pos")._offset[env_ids, joint_ids] = pos + + +def randomize_rigid_body_com( + env: TrackingRLEnv, + env_ids: torch.Tensor | None, + com_range: dict[str, tuple[float, float]], + asset_cfg: SceneEntityCfg, +): + """Randomize the center of mass (CoM) of rigid bodies by adding a random value sampled from the given ranges. + + .. note:: + This function uses CPU tensors to assign the CoM. It is recommended to use this function + only during the initialization of the environment. + """ + # extract the used quantities (to enable type-hinting) + asset: Articulation = env.scene[asset_cfg.name] + # resolve environment ids + if env_ids is None: + env_ids = torch.arange(env.scene.num_envs, device="cpu") + else: + env_ids = env_ids.cpu() + + # resolve body indices + if asset_cfg.body_ids == slice(None): + body_ids = torch.arange(asset.num_bodies, dtype=torch.int, device="cpu") + else: + body_ids = torch.tensor(asset_cfg.body_ids, dtype=torch.int, device="cpu") + + # sample random CoM values + range_list = [com_range.get(key, (0.0, 0.0)) for key in ["x", "y", "z"]] + ranges = torch.tensor(range_list, device="cpu") + rand_samples = math_utils.sample_uniform(ranges[:, 0], ranges[:, 1], (len(env_ids), 3), device="cpu").unsqueeze(1) + + # get the current com of the bodies (num_assets, num_bodies) + coms = asset.root_physx_view.get_coms().clone() # [2, 30, 7] -> [n_envs, n_bodies, 3 (pos) + 4 (ori)] + + # Randomize the com in range + coms[:, body_ids, :3] += rand_samples + + # Set the new coms + asset.root_physx_view.set_coms(coms, env_ids) diff --git a/roboverse_pack/tasks/beyondmimic/isaaclab/mdp/observations.py b/roboverse_pack/tasks/beyondmimic/isaaclab/mdp/observations.py new file mode 100644 index 000000000..a6f3a3f6f --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/isaaclab/mdp/observations.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import torch +from isaaclab.utils.math import matrix_from_quat, subtract_frame_transforms + +if TYPE_CHECKING: + from roboverse_pack.tasks.beyondmimic.isaaclab.envs.tracking_rl_env import TrackingRLEnv + from roboverse_pack.tasks.beyondmimic.isaaclab.mdp.commands import MotionCommand + + +def robot_anchor_ori_w(env: TrackingRLEnv, command_name: str) -> torch.Tensor: + """Robot anchor orientation in world frame.""" + command: MotionCommand = env.command_manager.get_term(command_name) + mat = matrix_from_quat(command.robot_anchor_quat_w) + return mat[..., :2].reshape(mat.shape[0], -1) + + +def robot_anchor_lin_vel_w(env: TrackingRLEnv, command_name: str) -> torch.Tensor: + """Robot anchor linear velocity in world frame.""" + command: MotionCommand = env.command_manager.get_term(command_name) + + return command.robot_anchor_vel_w[:, :3].view(env.num_envs, -1) + + +def robot_anchor_ang_vel_w(env: TrackingRLEnv, command_name: str) -> torch.Tensor: + """Robot anchor angular velocity in world frame.""" + command: MotionCommand = env.command_manager.get_term(command_name) + + return command.robot_anchor_vel_w[:, 3:6].view(env.num_envs, -1) + + +# NOTE observation callback results will be concatenated into a single tensor +def robot_body_pos_b(env: TrackingRLEnv, command_name: str) -> torch.Tensor: + """Body positions relative to (robot) anchor frame.""" + command: MotionCommand = env.command_manager.get_term(command_name) + + num_bodies = len(command.cfg.body_names) + pos_b, _ = subtract_frame_transforms( + command.robot_anchor_pos_w[:, None, :].repeat(1, num_bodies, 1), + command.robot_anchor_quat_w[:, None, :].repeat(1, num_bodies, 1), + command.robot_body_pos_w, + command.robot_body_quat_w, + ) # [n_envs, n_bodies, 3] positions of each body relative to the anchor frame + + return pos_b.view(env.num_envs, -1) # [n_envs, n_bodies * 3] + + +def robot_body_ori_b(env: TrackingRLEnv, command_name: str) -> torch.Tensor: + """Body orientations relative to anchor frame.""" + command: MotionCommand = env.command_manager.get_term(command_name) + + num_bodies = len(command.cfg.body_names) + _, ori_b = subtract_frame_transforms( + command.robot_anchor_pos_w[:, None, :].repeat(1, num_bodies, 1), + command.robot_anchor_quat_w[:, None, :].repeat(1, num_bodies, 1), + command.robot_body_pos_w, + command.robot_body_quat_w, + ) + mat = matrix_from_quat(ori_b) + return mat[..., :2].reshape(mat.shape[0], -1) + + +def motion_anchor_pos_b(env: TrackingRLEnv, command_name: str) -> torch.Tensor: + """Target anchor position relative to anchor frame.""" + command: MotionCommand = env.command_manager.get_term(command_name) + + pos, _ = subtract_frame_transforms( + command.robot_anchor_pos_w, + command.robot_anchor_quat_w, + command.anchor_pos_w, + command.anchor_quat_w, + ) + + return pos.view(env.num_envs, -1) + + +def motion_anchor_ori_b(env: TrackingRLEnv, command_name: str) -> torch.Tensor: + """Target anchor orientation relative to anchor frame.""" + command: MotionCommand = env.command_manager.get_term(command_name) + + _, ori = subtract_frame_transforms( + command.robot_anchor_pos_w, # [n_envs, 3] + command.robot_anchor_quat_w, # [n_envs, 4] + command.anchor_pos_w, # [n_envs, 3] + command.anchor_quat_w, # [n_envs, 4] + ) # [n_envs, 4] quaternion representing the relative rotation between the two frames + mat = matrix_from_quat(ori) # [n_envs, 3, 3] convert to rotation matrix + return mat[..., :2].reshape( + mat.shape[0], -1 + ) # [n_envs, 6] extract the first two rows because the third row can be derived from orthogonality diff --git a/roboverse_pack/tasks/beyondmimic/isaaclab/mdp/rewards.py b/roboverse_pack/tasks/beyondmimic/isaaclab/mdp/rewards.py new file mode 100644 index 000000000..ac4f91abe --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/isaaclab/mdp/rewards.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import torch +from isaaclab.managers import SceneEntityCfg +from isaaclab.sensors import ContactSensor +from isaaclab.utils.math import quat_error_magnitude + +if TYPE_CHECKING: + from roboverse_pack.tasks.beyondmimic.isaaclab.envs.tracking_rl_env import TrackingRLEnv + from roboverse_pack.tasks.beyondmimic.isaaclab.mdp.commands import MotionCommand + + +def _get_body_indexes(command: MotionCommand, body_names: list[str] | None) -> list[int]: + return [i for i, name in enumerate(command.cfg.body_names) if (body_names is None) or (name in body_names)] + + +def motion_global_anchor_position_error_exp(env: TrackingRLEnv, command_name: str, std: float) -> torch.Tensor: + """Distance between target and actual anchor position.""" + command: MotionCommand = env.command_manager.get_term(command_name) + error = torch.sum(torch.square(command.anchor_pos_w - command.robot_anchor_pos_w), dim=-1) + return torch.exp(-error / std**2) + + +def motion_global_anchor_orientation_error_exp(env: TrackingRLEnv, command_name: str, std: float) -> torch.Tensor: + """Distance between target and actual anchor orientation.""" + command: MotionCommand = env.command_manager.get_term(command_name) + error = quat_error_magnitude(command.anchor_quat_w, command.robot_anchor_quat_w) ** 2 + return torch.exp(-error / std**2) + + +def motion_relative_body_position_error_exp( + env: TrackingRLEnv, command_name: str, std: float, body_names: list[str] | None = None +) -> torch.Tensor: + """Distance between target and actual body position.""" + command: MotionCommand = env.command_manager.get_term(command_name) + body_indexes = _get_body_indexes(command, body_names) + error = torch.sum( + torch.square(command.body_pos_relative_w[:, body_indexes] - command.robot_body_pos_w[:, body_indexes]), dim=-1 + ) + return torch.exp(-error.mean(-1) / std**2) + + +def motion_relative_body_orientation_error_exp( + env: TrackingRLEnv, command_name: str, std: float, body_names: list[str] | None = None +) -> torch.Tensor: + """Distance between target and actual body orientation.""" + command: MotionCommand = env.command_manager.get_term(command_name) + body_indexes = _get_body_indexes(command, body_names) + error = ( + quat_error_magnitude(command.body_quat_relative_w[:, body_indexes], command.robot_body_quat_w[:, body_indexes]) + ** 2 + ) + return torch.exp(-error.mean(-1) / std**2) + + +def motion_global_body_linear_velocity_error_exp( + env: TrackingRLEnv, command_name: str, std: float, body_names: list[str] | None = None +) -> torch.Tensor: + """Linear velocity tracking error.""" + command: MotionCommand = env.command_manager.get_term(command_name) + body_indexes = _get_body_indexes(command, body_names) + error = torch.sum( + torch.square(command.body_lin_vel_w[:, body_indexes] - command.robot_body_lin_vel_w[:, body_indexes]), dim=-1 + ) + return torch.exp(-error.mean(-1) / std**2) + + +def motion_global_body_angular_velocity_error_exp( + env: TrackingRLEnv, command_name: str, std: float, body_names: list[str] | None = None +) -> torch.Tensor: + """Distance between target and actual body linear velocity.""" + command: MotionCommand = env.command_manager.get_term(command_name) + body_indexes = _get_body_indexes(command, body_names) + error = torch.sum( + torch.square(command.body_ang_vel_w[:, body_indexes] - command.robot_body_ang_vel_w[:, body_indexes]), dim=-1 + ) + return torch.exp(-error.mean(-1) / std**2) + + +def feet_contact_time(env: TrackingRLEnv, sensor_cfg: SceneEntityCfg, threshold: float) -> torch.Tensor: + """Time spent in contact with the ground.""" + contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name] + first_air = contact_sensor.compute_first_air(env.step_dt, env.physics_dt)[:, sensor_cfg.body_ids] + last_contact_time = contact_sensor.data.last_contact_time[:, sensor_cfg.body_ids] + reward = torch.sum((last_contact_time < threshold) * first_air, dim=-1) + return reward diff --git a/roboverse_pack/tasks/beyondmimic/isaaclab/mdp/terminations.py b/roboverse_pack/tasks/beyondmimic/isaaclab/mdp/terminations.py new file mode 100644 index 000000000..93dc971d5 --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/isaaclab/mdp/terminations.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import isaaclab.utils.math as math_utils +import torch +from isaaclab.assets import Articulation, RigidObject +from isaaclab.managers import SceneEntityCfg + +from roboverse_pack.tasks.beyondmimic.isaaclab.mdp.rewards import _get_body_indexes + +if TYPE_CHECKING: + from roboverse_pack.tasks.beyondmimic.isaaclab.envs.tracking_rl_env import TrackingRLEnv + from roboverse_pack.tasks.beyondmimic.isaaclab.mdp.commands import MotionCommand + + +def bad_anchor_pos(env: TrackingRLEnv, command_name: str, threshold: float) -> torch.Tensor: + """Distance between target and actual anchor position.""" + command: MotionCommand = env.command_manager.get_term(command_name) + return torch.norm(command.anchor_pos_w - command.robot_anchor_pos_w, dim=1) > threshold + + +# `anchor_pos_w` is of shape [n_envs, 3] and -1 retrieves the Z coordinate +def bad_anchor_pos_z_only(env: TrackingRLEnv, command_name: str, threshold: float) -> torch.Tensor: + """Distance between target and actual anchor position in the Z direction.""" + command: MotionCommand = env.command_manager.get_term(command_name) + return torch.abs(command.anchor_pos_w[:, -1] - command.robot_anchor_pos_w[:, -1]) > threshold + + +def bad_anchor_ori(env: TrackingRLEnv, asset_cfg: SceneEntityCfg, command_name: str, threshold: float) -> torch.Tensor: + """Distance between target and actual anchor orientation.""" + asset: RigidObject | Articulation = env.scene[asset_cfg.name] + + command: MotionCommand = env.command_manager.get_term(command_name) + # converts world-frame gravity vector to anchor frame + motion_projected_gravity_b = math_utils.quat_rotate_inverse( + command.anchor_quat_w, asset.data.GRAVITY_VEC_W + ) # [n_envs, 3] + + robot_projected_gravity_b = math_utils.quat_rotate_inverse(command.robot_anchor_quat_w, asset.data.GRAVITY_VEC_W) + + # checks whether the robot’s tilt magnitude deviates too much (how relatively "upright"), and ignores which way it leans + return (motion_projected_gravity_b[:, 2] - robot_projected_gravity_b[:, 2]).abs() > threshold + + +def bad_motion_body_pos( + env: TrackingRLEnv, command_name: str, threshold: float, body_names: list[str] | None = None +) -> torch.Tensor: + """Distance between target and actual body position.""" + command: MotionCommand = env.command_manager.get_term(command_name) + + body_indexes = _get_body_indexes(command, body_names) + error = torch.norm(command.body_pos_relative_w[:, body_indexes] - command.robot_body_pos_w[:, body_indexes], dim=-1) + return torch.any(error > threshold, dim=-1) + + +def bad_motion_body_pos_z_only( + env: TrackingRLEnv, command_name: str, threshold: float, body_names: list[str] | None = None +) -> torch.Tensor: + """Distance between target and actual body position in the Z direction.""" + command: MotionCommand = env.command_manager.get_term(command_name) + + body_indexes = _get_body_indexes(command, body_names) + error = torch.abs(command.body_pos_relative_w[:, body_indexes, -1] - command.robot_body_pos_w[:, body_indexes, -1]) + return torch.any(error > threshold, dim=-1) diff --git a/roboverse_pack/tasks/beyondmimic/isaaclab/robots/actuator.py b/roboverse_pack/tasks/beyondmimic/isaaclab/robots/actuator.py new file mode 100644 index 000000000..11923849b --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/isaaclab/robots/actuator.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +from collections.abc import Sequence + +import torch +from isaaclab.actuators import ImplicitActuator, ImplicitActuatorCfg +from isaaclab.utils import DelayBuffer, configclass +from isaaclab.utils.types import ArticulationActions + + +class DelayedImplicitActuator(ImplicitActuator): + """Ideal PD actuator with delayed command application. + + This class extends the :class:`IdealPDActuator` class by adding a delay to the actuator commands. The delay + is implemented using a circular buffer that stores the actuator commands for a certain number of physics steps. + The most recent actuation value is pushed to the buffer at every physics step, but the final actuation value + applied to the simulation is lagged by a certain number of physics steps. + + The amount of time lag is configurable and can be set to a random value between the minimum and maximum time + lag bounds at every reset. The minimum and maximum time lag values are set in the configuration instance passed + to the class. + """ + + cfg: DelayedImplicitActuatorCfg + """The configuration for the actuator model.""" + + def __init__(self, cfg: DelayedImplicitActuatorCfg, *args, **kwargs): + super().__init__(cfg, *args, **kwargs) + # instantiate the delay buffers + self.positions_delay_buffer = DelayBuffer(cfg.max_delay, self._num_envs, device=self._device) + self.velocities_delay_buffer = DelayBuffer(cfg.max_delay, self._num_envs, device=self._device) + self.efforts_delay_buffer = DelayBuffer(cfg.max_delay, self._num_envs, device=self._device) + # all of the envs + self._ALL_INDICES = torch.arange(self._num_envs, dtype=torch.long, device=self._device) + + def reset(self, env_ids: Sequence[int]): + """Reset the actuator model.""" + super().reset(env_ids) + # number of environments (since env_ids can be a slice) + if env_ids is None or env_ids == slice(None): + num_envs = self._num_envs + else: + num_envs = len(env_ids) + # set a new random delay for environments in env_ids + time_lags = torch.randint( + low=self.cfg.min_delay, + high=self.cfg.max_delay + 1, + size=(num_envs,), + dtype=torch.int, + device=self._device, + ) + # set delays + self.positions_delay_buffer.set_time_lag(time_lags, env_ids) + self.velocities_delay_buffer.set_time_lag(time_lags, env_ids) + self.efforts_delay_buffer.set_time_lag(time_lags, env_ids) + # reset buffers + self.positions_delay_buffer.reset(env_ids) + self.velocities_delay_buffer.reset(env_ids) + self.efforts_delay_buffer.reset(env_ids) + + def compute( + self, control_action: ArticulationActions, joint_pos: torch.Tensor, joint_vel: torch.Tensor + ) -> ArticulationActions: + """Compute the actuator model.""" + # apply delay based on the delay the model for all the setpoints + control_action.joint_positions = self.positions_delay_buffer.compute(control_action.joint_positions) + control_action.joint_velocities = self.velocities_delay_buffer.compute(control_action.joint_velocities) + control_action.joint_efforts = self.efforts_delay_buffer.compute(control_action.joint_efforts) + # compte actuator model + return super().compute(control_action, joint_pos, joint_vel) + + +@configclass +class DelayedImplicitActuatorCfg(ImplicitActuatorCfg): + """Configuration for a delayed PD actuator.""" + + class_type: type = DelayedImplicitActuator + + min_delay: int = 0 + """Minimum number of physics time-steps with which the actuator command may be delayed. Defaults to 0.""" + + max_delay: int = 3 + """Maximum number of physics time-steps with which the actuator command may be delayed. Defaults to 0.""" diff --git a/roboverse_pack/tasks/beyondmimic/isaaclab/robots/g1.py b/roboverse_pack/tasks/beyondmimic/isaaclab/robots/g1.py new file mode 100644 index 000000000..85efc4b4d --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/isaaclab/robots/g1.py @@ -0,0 +1,196 @@ +import isaaclab.sim as sim_utils +from isaaclab.actuators import ImplicitActuatorCfg +from isaaclab.assets.articulation import ArticulationCfg + +ASSET_DIR = "roboverse_data" + +ARMATURE_5020 = 0.003609725 +ARMATURE_7520_14 = 0.010177520 +ARMATURE_7520_22 = 0.025101925 +ARMATURE_4010 = 0.00425 + +NATURAL_FREQ = 10 * 2.0 * 3.1415926535 # 10Hz +DAMPING_RATIO = 2.0 + +STIFFNESS_5020 = ARMATURE_5020 * NATURAL_FREQ**2 +STIFFNESS_7520_14 = ARMATURE_7520_14 * NATURAL_FREQ**2 +STIFFNESS_7520_22 = ARMATURE_7520_22 * NATURAL_FREQ**2 +STIFFNESS_4010 = ARMATURE_4010 * NATURAL_FREQ**2 + +DAMPING_5020 = 2.0 * DAMPING_RATIO * ARMATURE_5020 * NATURAL_FREQ +DAMPING_7520_14 = 2.0 * DAMPING_RATIO * ARMATURE_7520_14 * NATURAL_FREQ +DAMPING_7520_22 = 2.0 * DAMPING_RATIO * ARMATURE_7520_22 * NATURAL_FREQ +DAMPING_4010 = 2.0 * DAMPING_RATIO * ARMATURE_4010 * NATURAL_FREQ + +G1_CYLINDER_CFG = ArticulationCfg( + spawn=sim_utils.UrdfFileCfg( + fix_base=False, + replace_cylinders_with_capsules=True, + asset_path=f"{ASSET_DIR}/unitree_description/urdf/g1/main.urdf", + 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=True, solver_position_iteration_count=8, solver_velocity_iteration_count=4 + ), + joint_drive=sim_utils.UrdfConverterCfg.JointDriveCfg( + gains=sim_utils.UrdfConverterCfg.JointDriveCfg.PDGainsCfg(stiffness=0, damping=0) + ), + ), + # NOTE `init_state` corresponds to `ArticulationData.default_joint_pos` + init_state=ArticulationCfg.InitialStateCfg( + pos=(0.0, 0.0, 0.76), + joint_pos={ + ".*_hip_pitch_joint": -0.312, + ".*_knee_joint": 0.669, + ".*_ankle_pitch_joint": -0.363, + ".*_elbow_joint": 0.6, + "left_shoulder_roll_joint": 0.2, + "left_shoulder_pitch_joint": 0.2, + "right_shoulder_roll_joint": -0.2, + "right_shoulder_pitch_joint": 0.2, + }, + 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", + ], + effort_limit_sim={ + ".*_hip_yaw_joint": 88.0, + ".*_hip_roll_joint": 139.0, + ".*_hip_pitch_joint": 88.0, + ".*_knee_joint": 139.0, + }, + velocity_limit_sim={ + ".*_hip_yaw_joint": 32.0, + ".*_hip_roll_joint": 20.0, + ".*_hip_pitch_joint": 32.0, + ".*_knee_joint": 20.0, + }, + stiffness={ + ".*_hip_pitch_joint": STIFFNESS_7520_14, + ".*_hip_roll_joint": STIFFNESS_7520_22, + ".*_hip_yaw_joint": STIFFNESS_7520_14, + ".*_knee_joint": STIFFNESS_7520_22, + }, + damping={ + ".*_hip_pitch_joint": DAMPING_7520_14, + ".*_hip_roll_joint": DAMPING_7520_22, + ".*_hip_yaw_joint": DAMPING_7520_14, + ".*_knee_joint": DAMPING_7520_22, + }, + armature={ + ".*_hip_pitch_joint": ARMATURE_7520_14, + ".*_hip_roll_joint": ARMATURE_7520_22, + ".*_hip_yaw_joint": ARMATURE_7520_14, + ".*_knee_joint": ARMATURE_7520_22, + }, + ), + "feet": ImplicitActuatorCfg( + effort_limit_sim=50.0, + velocity_limit_sim=37.0, + joint_names_expr=[".*_ankle_pitch_joint", ".*_ankle_roll_joint"], + stiffness=2.0 * STIFFNESS_5020, + damping=2.0 * DAMPING_5020, + armature=2.0 * ARMATURE_5020, + ), + "waist": ImplicitActuatorCfg( + effort_limit_sim=50, + velocity_limit_sim=37.0, + joint_names_expr=["waist_roll_joint", "waist_pitch_joint"], + stiffness=2.0 * STIFFNESS_5020, + damping=2.0 * DAMPING_5020, + armature=2.0 * ARMATURE_5020, + ), + "waist_yaw": ImplicitActuatorCfg( + effort_limit_sim=88, + velocity_limit_sim=32.0, + joint_names_expr=["waist_yaw_joint"], + stiffness=STIFFNESS_7520_14, + damping=DAMPING_7520_14, + armature=ARMATURE_7520_14, + ), + "arms": ImplicitActuatorCfg( + joint_names_expr=[ + ".*_shoulder_pitch_joint", + ".*_shoulder_roll_joint", + ".*_shoulder_yaw_joint", + ".*_elbow_joint", + ".*_wrist_roll_joint", + ".*_wrist_pitch_joint", + ".*_wrist_yaw_joint", + ], + effort_limit_sim={ + ".*_shoulder_pitch_joint": 25.0, + ".*_shoulder_roll_joint": 25.0, + ".*_shoulder_yaw_joint": 25.0, + ".*_elbow_joint": 25.0, + ".*_wrist_roll_joint": 25.0, + ".*_wrist_pitch_joint": 5.0, + ".*_wrist_yaw_joint": 5.0, + }, + velocity_limit_sim={ + ".*_shoulder_pitch_joint": 37.0, + ".*_shoulder_roll_joint": 37.0, + ".*_shoulder_yaw_joint": 37.0, + ".*_elbow_joint": 37.0, + ".*_wrist_roll_joint": 37.0, + ".*_wrist_pitch_joint": 22.0, + ".*_wrist_yaw_joint": 22.0, + }, + stiffness={ + ".*_shoulder_pitch_joint": STIFFNESS_5020, + ".*_shoulder_roll_joint": STIFFNESS_5020, + ".*_shoulder_yaw_joint": STIFFNESS_5020, + ".*_elbow_joint": STIFFNESS_5020, + ".*_wrist_roll_joint": STIFFNESS_5020, + ".*_wrist_pitch_joint": STIFFNESS_4010, + ".*_wrist_yaw_joint": STIFFNESS_4010, + }, + damping={ + ".*_shoulder_pitch_joint": DAMPING_5020, + ".*_shoulder_roll_joint": DAMPING_5020, + ".*_shoulder_yaw_joint": DAMPING_5020, + ".*_elbow_joint": DAMPING_5020, + ".*_wrist_roll_joint": DAMPING_5020, + ".*_wrist_pitch_joint": DAMPING_4010, + ".*_wrist_yaw_joint": DAMPING_4010, + }, + armature={ + ".*_shoulder_pitch_joint": ARMATURE_5020, + ".*_shoulder_roll_joint": ARMATURE_5020, + ".*_shoulder_yaw_joint": ARMATURE_5020, + ".*_elbow_joint": ARMATURE_5020, + ".*_wrist_roll_joint": ARMATURE_5020, + ".*_wrist_pitch_joint": ARMATURE_4010, + ".*_wrist_yaw_joint": ARMATURE_4010, + }, + ), + }, +) + +G1_ACTION_SCALE = {} +for a in G1_CYLINDER_CFG.actuators.values(): + e = a.effort_limit_sim + s = a.stiffness + names = a.joint_names_expr + if not isinstance(e, dict): # if `e` is a scalar, all joints specified by `names` will have the same value + e = {n: e for n in names} + if not isinstance(s, dict): # if `s` is a scalar, all joints specified by `names` will have the same value + s = {n: s for n in names} + for n in names: + if n in e and n in s and s[n]: + G1_ACTION_SCALE[n] = 0.25 * e[n] / s[n] diff --git a/roboverse_pack/tasks/beyondmimic/isaaclab/robots/g1_delayed.py b/roboverse_pack/tasks/beyondmimic/isaaclab/robots/g1_delayed.py new file mode 100644 index 000000000..46fff41a5 --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/isaaclab/robots/g1_delayed.py @@ -0,0 +1,196 @@ +import isaaclab.sim as sim_utils +from isaaclab.assets.articulation import ArticulationCfg + +from roboverse_pack.tasks.beyondmimic.isaaclab.robots.actuator import DelayedImplicitActuatorCfg + +ASSET_DIR = "roboverse_data" + +ARMATURE_5020 = 0.003609725 +ARMATURE_7520_14 = 0.010177520 +ARMATURE_7520_22 = 0.025101925 +ARMATURE_4010 = 0.00425 + +NATURAL_FREQ = 10 * 2.0 * 3.1415926535 # 10Hz +DAMPING_RATIO = 2.0 + +STIFFNESS_5020 = ARMATURE_5020 * NATURAL_FREQ**2 +STIFFNESS_7520_14 = ARMATURE_7520_14 * NATURAL_FREQ**2 +STIFFNESS_7520_22 = ARMATURE_7520_22 * NATURAL_FREQ**2 +STIFFNESS_4010 = ARMATURE_4010 * NATURAL_FREQ**2 + +DAMPING_5020 = 2.0 * DAMPING_RATIO * ARMATURE_5020 * NATURAL_FREQ +DAMPING_7520_14 = 2.0 * DAMPING_RATIO * ARMATURE_7520_14 * NATURAL_FREQ +DAMPING_7520_22 = 2.0 * DAMPING_RATIO * ARMATURE_7520_22 * NATURAL_FREQ +DAMPING_4010 = 2.0 * DAMPING_RATIO * ARMATURE_4010 * NATURAL_FREQ + +G1_DELAYED_CYLINDER_CFG = ArticulationCfg( + spawn=sim_utils.UrdfFileCfg( + fix_base=False, + replace_cylinders_with_capsules=True, + asset_path=f"{ASSET_DIR}/unitree_description/urdf/g1/main.urdf", + 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=True, solver_position_iteration_count=8, solver_velocity_iteration_count=4 + ), + joint_drive=sim_utils.UrdfConverterCfg.JointDriveCfg( + gains=sim_utils.UrdfConverterCfg.JointDriveCfg.PDGainsCfg(stiffness=0, damping=0) + ), + ), + init_state=ArticulationCfg.InitialStateCfg( + pos=(0.0, 0.0, 0.76), + joint_pos={ + ".*_hip_pitch_joint": -0.312, + ".*_knee_joint": 0.669, + ".*_ankle_pitch_joint": -0.363, + ".*_elbow_joint": 0.6, + "left_shoulder_roll_joint": 0.2, + "left_shoulder_pitch_joint": 0.2, + "right_shoulder_roll_joint": -0.2, + "right_shoulder_pitch_joint": 0.2, + }, + joint_vel={".*": 0.0}, + ), + soft_joint_pos_limit_factor=0.9, + actuators={ + "legs": DelayedImplicitActuatorCfg( + joint_names_expr=[ + ".*_hip_yaw_joint", + ".*_hip_roll_joint", + ".*_hip_pitch_joint", + ".*_knee_joint", + ], + effort_limit_sim={ + ".*_hip_yaw_joint": 88.0, + ".*_hip_roll_joint": 139.0, + ".*_hip_pitch_joint": 88.0, + ".*_knee_joint": 139.0, + }, + velocity_limit_sim={ + ".*_hip_yaw_joint": 32.0, + ".*_hip_roll_joint": 20.0, + ".*_hip_pitch_joint": 32.0, + ".*_knee_joint": 20.0, + }, + stiffness={ + ".*_hip_pitch_joint": STIFFNESS_7520_14, + ".*_hip_roll_joint": STIFFNESS_7520_22, + ".*_hip_yaw_joint": STIFFNESS_7520_14, + ".*_knee_joint": STIFFNESS_7520_22, + }, + damping={ + ".*_hip_pitch_joint": DAMPING_7520_14, + ".*_hip_roll_joint": DAMPING_7520_22, + ".*_hip_yaw_joint": DAMPING_7520_14, + ".*_knee_joint": DAMPING_7520_22, + }, + armature={ + ".*_hip_pitch_joint": ARMATURE_7520_14, + ".*_hip_roll_joint": ARMATURE_7520_22, + ".*_hip_yaw_joint": ARMATURE_7520_14, + ".*_knee_joint": ARMATURE_7520_22, + }, + ), + "feet": DelayedImplicitActuatorCfg( + effort_limit_sim=50.0, + velocity_limit_sim=37.0, + joint_names_expr=[".*_ankle_pitch_joint", ".*_ankle_roll_joint"], + stiffness=2.0 * STIFFNESS_5020, + damping=2.0 * DAMPING_5020, + armature=2.0 * ARMATURE_5020, + ), + "waist": DelayedImplicitActuatorCfg( + effort_limit_sim=50, + velocity_limit_sim=37.0, + joint_names_expr=["waist_roll_joint", "waist_pitch_joint"], + stiffness=2.0 * STIFFNESS_5020, + damping=2.0 * DAMPING_5020, + armature=2.0 * ARMATURE_5020, + ), + "waist_yaw": DelayedImplicitActuatorCfg( + effort_limit_sim=88, + velocity_limit_sim=32.0, + joint_names_expr=["waist_yaw_joint"], + stiffness=STIFFNESS_7520_14, + damping=DAMPING_7520_14, + armature=ARMATURE_7520_14, + ), + "arms": DelayedImplicitActuatorCfg( + joint_names_expr=[ + ".*_shoulder_pitch_joint", + ".*_shoulder_roll_joint", + ".*_shoulder_yaw_joint", + ".*_elbow_joint", + ".*_wrist_roll_joint", + ".*_wrist_pitch_joint", + ".*_wrist_yaw_joint", + ], + effort_limit_sim={ + ".*_shoulder_pitch_joint": 25.0, + ".*_shoulder_roll_joint": 25.0, + ".*_shoulder_yaw_joint": 25.0, + ".*_elbow_joint": 25.0, + ".*_wrist_roll_joint": 25.0, + ".*_wrist_pitch_joint": 5.0, + ".*_wrist_yaw_joint": 5.0, + }, + velocity_limit_sim={ + ".*_shoulder_pitch_joint": 37.0, + ".*_shoulder_roll_joint": 37.0, + ".*_shoulder_yaw_joint": 37.0, + ".*_elbow_joint": 37.0, + ".*_wrist_roll_joint": 37.0, + ".*_wrist_pitch_joint": 22.0, + ".*_wrist_yaw_joint": 22.0, + }, + stiffness={ + ".*_shoulder_pitch_joint": STIFFNESS_5020, + ".*_shoulder_roll_joint": STIFFNESS_5020, + ".*_shoulder_yaw_joint": STIFFNESS_5020, + ".*_elbow_joint": STIFFNESS_5020, + ".*_wrist_roll_joint": STIFFNESS_5020, + ".*_wrist_pitch_joint": STIFFNESS_4010, + ".*_wrist_yaw_joint": STIFFNESS_4010, + }, + damping={ + ".*_shoulder_pitch_joint": DAMPING_5020, + ".*_shoulder_roll_joint": DAMPING_5020, + ".*_shoulder_yaw_joint": DAMPING_5020, + ".*_elbow_joint": DAMPING_5020, + ".*_wrist_roll_joint": DAMPING_5020, + ".*_wrist_pitch_joint": DAMPING_4010, + ".*_wrist_yaw_joint": DAMPING_4010, + }, + armature={ + ".*_shoulder_pitch_joint": ARMATURE_5020, + ".*_shoulder_roll_joint": ARMATURE_5020, + ".*_shoulder_yaw_joint": ARMATURE_5020, + ".*_elbow_joint": ARMATURE_5020, + ".*_wrist_roll_joint": ARMATURE_5020, + ".*_wrist_pitch_joint": ARMATURE_4010, + ".*_wrist_yaw_joint": ARMATURE_4010, + }, + ), + }, +) + +G1_ACTION_SCALE = {} +for a in G1_DELAYED_CYLINDER_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]: + G1_ACTION_SCALE[n] = 0.25 * e[n] / s[n] diff --git a/roboverse_pack/tasks/beyondmimic/metasim/__init__.py b/roboverse_pack/tasks/beyondmimic/metasim/__init__.py new file mode 100644 index 000000000..c30def297 --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/metasim/__init__.py @@ -0,0 +1 @@ +"""BeyondMimic motion tracking using MetaSim's handler.""" diff --git a/roboverse_pack/tasks/beyondmimic/metasim/configs/__init__.py b/roboverse_pack/tasks/beyondmimic/metasim/configs/__init__.py new file mode 100644 index 000000000..4f8ff5d7a --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/metasim/configs/__init__.py @@ -0,0 +1 @@ +"""Configs for environments, domain randomization classes, etc.""" diff --git a/roboverse_pack/tasks/beyondmimic/metasim/configs/cfg_base.py b/roboverse_pack/tasks/beyondmimic/metasim/configs/cfg_base.py new file mode 100644 index 000000000..ae28eb840 --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/metasim/configs/cfg_base.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from dataclasses import MISSING +from typing import Callable + +from metasim.utils import configclass + + +@configclass +class CallbacksCfg: + """Configuration for callbacks.""" + + setup: dict = {} + reset: dict = {} + pre_step: dict = {} + post_step: dict = {} + query: dict = {} + + +@configclass +class BaseEnvCfg: + """The base class of environment configuration for legged robots.""" + + max_episode_length_s = 10.0 + obs_len_history = 1 # number of past observations to include in the observation + priv_obs_len_history = 1 # number of past privileged observations to include in the privileged observation + decimation = 4 # task-level + + callbacks_setup: dict[str, tuple[Callable, dict] | Callable] = {} + callbacks_reset: dict[str, tuple[Callable, dict] | Callable] = {} + callbacks_pre_step: dict[str, tuple[Callable, dict] | Callable] = {} + callbacks_post_step: dict[str, tuple[Callable, dict] | Callable] = {} + callbacks_query: dict[str, tuple[Callable, dict] | Callable] = MISSING + + def __post_init__(self): + + def _normalize(value) -> dict: + return {} if value is MISSING else value + + self.callbacks = CallbacksCfg() + self.callbacks.query = _normalize(self.callbacks_query) + self.callbacks.setup = _normalize(self.callbacks_setup) + self.callbacks.reset = _normalize(self.callbacks_reset) + self.callbacks.pre_step = _normalize(self.callbacks_pre_step) + self.callbacks.post_step = _normalize(self.callbacks_post_step) + + # Type check for callbacks + for cb_attr in [ + "setup", + "reset", + "pre_step", + "post_step", + # "terminate", + "query", + ]: + cb_dict = getattr(self.callbacks, cb_attr) + for func_name, func_tuple in cb_dict.items(): + if not ( + callable(func_tuple) + or ( + isinstance(func_tuple, tuple) + and len(func_tuple) == 2 + and (callable(func_tuple[0]) or isinstance(func_tuple[0], object)) + and isinstance(func_tuple[1], dict) + ) + ): + raise ValueError( + f"Callback {func_name} in {cb_attr} must be a callable or a tuple of (callable, dict)." + ) diff --git a/roboverse_pack/tasks/beyondmimic/metasim/configs/cfg_queries.py b/roboverse_pack/tasks/beyondmimic/metasim/configs/cfg_queries.py new file mode 100644 index 000000000..2b0cdbb5b --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/metasim/configs/cfg_queries.py @@ -0,0 +1,117 @@ +from __future__ import annotations + +from collections import deque + +import numpy as np +import torch + +from metasim.sim.base import BaseQueryType, BaseSimHandler + +try: + import isaacgym +except ImportError: + pass + +try: + import mujoco +except ImportError: + pass + + +class ContactForces(BaseQueryType): + """Optional query to fetch per-body net contact forces for each robot. + + - For IsaacGym: uses the native net-contact tensor and maps it per-robot in handler indexing order. + - For IsaacSim: returns a zero tensor fallback per-robot (hook is in place; replace with real source when available). + """ + + def __init__(self, history_length: int = 3): + super().__init__() + self.history_length = history_length + self._current_contact_force = None + self._contact_forces_queue = deque(maxlen=history_length) + + def bind_handler(self, handler: BaseSimHandler, *args, **kwargs): + """Bind handler to the query.""" + super().bind_handler(handler, *args, **kwargs) + self.simulator = handler.scenario.simulator + self.num_envs = handler.scenario.num_envs + self.robots = handler.robots + if self.simulator in ["isaacgym", "mujoco"]: + self.body_ids_reindex = handler._get_body_ids_reindex(self.robots[0].name) + elif self.simulator == "isaacsim": + sorted_body_names = self.handler.get_body_names(self.robots[0].name, True) + self.body_ids_reindex = torch.tensor( + [self.handler.contact_sensor.body_names.index(name) for name in sorted_body_names], + dtype=torch.int, + device=self.handler.device, + ) + else: + raise NotImplementedError + self.initialize() + self.__call__() + + def initialize(self): + """Initialize the query.""" + for _ in range(self.history_length): + if self.simulator == "isaacgym": + self._current_contact_force = isaacgym.gymtorch.wrap_tensor( + self.handler.gym.acquire_net_contact_force_tensor(self.handler.sim) + ) + elif self.simulator == "isaacsim": + self._current_contact_force = self.handler.contact_sensor.data.net_forces_w + elif self.simulator == "mujoco": + self._current_contact_force = self._get_contact_forces_mujoco() + else: + raise NotImplementedError + self._contact_forces_queue.append( + self._current_contact_force.clone().view(self.num_envs, -1, 3)[:, self.body_ids_reindex, :] + ) + + def _get_contact_forces_mujoco(self) -> torch.Tensor: + """Compute net contact forces on each body. + + Returns: + torch.Tensor: shape (nbody, 3), contact forces for each body + """ + nbody = self.handler.physics.model.nbody + contact_forces = torch.zeros((nbody, 3), device=self.handler.device) + + for i in range(self.handler.physics.data.ncon): + contact = self.handler.physics.data.contact[i] + force = np.zeros(6, dtype=np.float64) + mujoco.mj_contactForce(self.handler.physics.model.ptr, self.handler.physics.data.ptr, i, force) + f_contact = torch.from_numpy(force[:3]).to(device=self.handler.device) + + body1 = self.handler.physics.model.geom_bodyid[contact.geom1] + body2 = self.handler.physics.model.geom_bodyid[contact.geom2] + + contact_forces[body1] += f_contact + contact_forces[body2] -= f_contact + + return contact_forces + + def __call__(self): + """Call the query.""" + if self.simulator == "isaacgym": + self.handler.gym.refresh_net_contact_force_tensor(self.handler.sim) + elif self.simulator == "isaacsim": + self._current_contact_force = self.handler.contact_sensor.data.net_forces_w + elif self.simulator == "mujoco": + self._current_contact_force = self._get_contact_forces_mujoco() + else: + raise NotImplementedError + self._contact_forces_queue.append( + self._current_contact_force.view(self.num_envs, -1, 3)[:, self.body_ids_reindex, :] + ) + return {self.robots[0].name: self} + + @property + def contact_forces_history(self) -> torch.Tensor: + """Get the contact forces history.""" + return torch.stack(list(self._contact_forces_queue), dim=1) # (num_envs, history_length, num_bodies, 3) + + @property + def contact_forces(self) -> torch.Tensor: + """Get the current contact forces.""" + return self._contact_forces_queue[-1] diff --git a/roboverse_pack/tasks/beyondmimic/metasim/configs/cfg_randomizers.py b/roboverse_pack/tasks/beyondmimic/metasim/configs/cfg_randomizers.py new file mode 100644 index 000000000..10a825d92 --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/metasim/configs/cfg_randomizers.py @@ -0,0 +1,592 @@ +from __future__ import annotations + +from copy import deepcopy +from typing import Literal + +import torch +from loguru import logger as log + +from metasim.sim.base import BaseQueryType, BaseSimHandler +from metasim.utils.math import sample_gaussian, sample_log_uniform, sample_uniform + +try: + import isaacgym # noqa: F401 +except ImportError: + pass + +try: + import mujoco # noqa: F401 +except ImportError: + pass + + +class MaterialRandomizer(BaseQueryType): + """Randomize the material properties of the bodies.""" + + handler: BaseSimHandler + + def __init__( + self, + obj_name: str, + body_names: list[str] | str | None = None, + static_friction_range: list | tuple = (1.0, 1.0), + dynamic_friction_range: list | tuple = (1.0, 1.0), + restitution_range: list | tuple = (0.0, 0.0), + num_buckets: int = 1, + make_consistent: bool = False, + ): + """Initialize the query.""" + super().__init__() + self.obj_name = obj_name + self.set_body_names = [body_names] if isinstance(body_names, str) else body_names + self.static_friction_range = static_friction_range + self.dynamic_friction_range = dynamic_friction_range + self.restitution_range = restitution_range + self.num_buckets = num_buckets + self.make_consistent = make_consistent + + def bind_handler(self, handler: BaseSimHandler, *args, **kwargs): + """Bind handler to the query.""" + super().bind_handler(handler, *args, **kwargs) + self.simulator_name = handler.scenario.simulator + self.initialize() + + def __call__(self, env_ids=None, **kwargs): + """Call the query.""" + # resolve environment ids + if env_ids is None: + env_ids = torch.arange(self.handler.num_envs, device="cpu") + else: + env_ids = torch.tensor(env_ids).cpu() + self.randomize(env_ids) + + def initialize(self): + """Initialize the query.""" + # sample material properties from the given ranges + # note: we only sample the materials once during initialization + # afterwards these are randomly assigned to the geometries of the asset + range_list = [ + self.static_friction_range, + self.dynamic_friction_range, + self.restitution_range, + ] + ranges = torch.tensor(range_list, device="cpu") + self.material_buckets = sample_uniform(ranges[:, 0], ranges[:, 1], (self.num_buckets, 3), device="cpu") + + # ensure dynamic friction is always less than static friction + if self.make_consistent: + self.material_buckets[:, 1] = torch.min(self.material_buckets[:, 0], self.material_buckets[:, 1]) + + self.body_names = self.handler.get_body_names(self.obj_name, sort=False) + self.set_body_ids = ( + torch.tensor( + [self.body_names.index(_name) for _name in self.set_body_names], + dtype=torch.int, + device="cpu", + ) + if self.set_body_names is not None + else torch.arange(len(self.body_names), dtype=torch.int, device="cpu") + ) + + self.all_robot_names = [robot.name for robot in self.handler.robots] + self.all_object_names = [obj.name for obj in self.handler.objects] + + self.set_shape_indices = self._get_set_shape_indices() + + def _get_set_shape_indices(self): + """Get the set shape indices.""" + num_shapes_per_body = None + if self.simulator_name == "isaacsim": + if self.obj_name in self.handler.scene.articulations: + obj_inst = self.handler.scene.articulations[self.obj_name] + # obtain number of shapes per body (needed for indexing the material properties correctly) + # note: this is a workaround since the Articulation does not provide a direct way to obtain the number of shapes + # per body. We use the physics simulation view to obtain the number of shapes per body. + num_shapes_per_body = [] + for link_path in obj_inst.root_physx_view.link_paths[0]: + link_physx_view = obj_inst._physics_sim_view.create_rigid_body_view(link_path) # type: ignore + num_shapes_per_body.append(link_physx_view.max_shapes) + # ensure the parsing is correct + expected_shapes = obj_inst.root_physx_view.max_shapes + elif self.simulator_name == "isaacgym": + if self.obj_name in self.all_robot_names: + _tmp_handle = self.handler._robot_handles[0] + elif self.obj_name in self.all_object_names: + _tmp_handle = self.handler._obj_handles[0][self.handler.objects.index(self.obj_name)] + body_shape_indices = self.handler.gym.get_actor_rigid_body_shape_indices(self.handler._envs[0], _tmp_handle) + num_shapes_per_body = [] + for body_shape in body_shape_indices: + num_shapes_per_body.append(body_shape.count) + expected_shapes = len(self.handler.gym.get_actor_rigid_shape_properties(self.handler._envs[0], _tmp_handle)) + elif self.simulator_name == "mujoco": + model = self.handler.physics.model + num_shapes_per_body = [0] * model.nbody + # geom_bodyid[j] = geom j belongs to body geom_bodyid[j] + for geom_bodyid in model.geom_bodyid: + num_shapes_per_body[geom_bodyid] += 1 + expected_shapes = model.ngeom + if num_shapes_per_body is not None and sum(num_shapes_per_body) != expected_shapes: + raise ValueError( + "Randomization term 'randomize_rigid_body_material' failed to parse the number of shapes per body." + f" Expected total shapes: {expected_shapes}, but got: {sum(num_shapes_per_body)}." + ) + # update material buffer with new samples + # NOTE this is part of `randomize_rigid_body_material.__call__()` in Isaac Lab + if num_shapes_per_body is not None: + set_shape_indices = [] + # sample material properties from the given ranges + for body_id in self.set_body_ids: + # obtain indices of shapes for the body + start_idx = sum(num_shapes_per_body[:body_id]) + end_idx = start_idx + num_shapes_per_body[body_id] + set_shape_indices.extend(list(range(start_idx, end_idx))) + # assign the new materials + else: + # assign all the materials + set_shape_indices = list(range(expected_shapes)) + + return set_shape_indices + + def randomize(self, env_ids: torch.Tensor): + """Randomize the query.""" + if self.simulator_name == "isaacsim": + self._randomize_isaacsim(env_ids) + elif self.simulator_name == "isaacgym": + self._randomize_isaacgym(env_ids) + elif self.simulator_name == "mujoco": + self._randomize_mujoco(env_ids) + else: + log.warning( + f"Material randomization not implemented for simulator: {self.simulator_name}. This randomization step will be skipped." + ) + + def _randomize_isaacsim(self, env_ids: torch.Tensor): + if self.obj_name in self.handler.scene.articulations: + obj_inst = self.handler.scene.articulations[self.obj_name] + elif self.obj_name in self.handler.scene.rigid_objects: + obj_inst = self.handler.scene.rigid_objects[self.obj_name] + else: + raise ValueError( + f"Randomization term 'randomize_rigid_body_material' not supported for asset: {self.obj_name}." + ) + + # retrieve material buffer from the physics simulation + materials = ( + obj_inst.root_physx_view.get_material_properties() + ) # (n_envs, n_shapes, 3) where 3 corresponds to static friction, dynamic friction, and restitution + # randomly assign material IDs to the geometries + total_num_shapes = obj_inst.root_physx_view.max_shapes + bucket_ids = torch.randint(0, self.num_buckets, (len(env_ids), total_num_shapes), device="cpu") + material_samples = self.material_buckets[bucket_ids] + + # update material buffer with new samples + materials[env_ids] = material_samples[:, self.set_shape_indices] + + # apply to simulation + obj_inst.root_physx_view.set_material_properties(materials, env_ids) + + def _randomize_isaacgym(self, env_ids: torch.Tensor): + """Randomize friction properties for IsaacGym simulator.""" + # Sample friction values for each environment + if self.obj_name in self.all_robot_names: + # For robot, get actor handle and modify rigid shape properties + _all_handles = self.handler._robot_handles + elif self.obj_name in self.all_object_names: + # For objects, find the corresponding object handle + _all_handles = [ + self.handler._obj_handles[i][self.handler.objects.index(self.obj_name)] + for i in range(self.handler.num_envs) + ] + else: + raise ValueError( + f"Randomization term 'randomize_rigid_body_material' not supported for asset: {self.obj_name}." + ) + max_body_shape = len(self.handler.gym.get_actor_rigid_shape_properties(self.handler._envs[0], _all_handles[0])) + bucket_ids = torch.randint(0, self.num_buckets, (len(env_ids), max_body_shape), device="cpu") + material_samples = self.material_buckets[bucket_ids] # static friction, dynamic friction and restitution + + roll_friction_factor = 0.05 + spin_friction_factor = 0.02 + for i, env_id in enumerate(env_ids): + env = self.handler._envs[env_id] + # For objects, find the corresponding object handle + _tmp_handle = _all_handles[env_id] + # Get current rigid shape properties + shape_props = self.handler.gym.get_actor_rigid_shape_properties(env, _tmp_handle) + + for _id in self.set_shape_indices: + shape_prop = shape_props[_id] + shape_prop.friction = material_samples[i, _id, 0] + shape_prop.rolling_friction = roll_friction_factor * material_samples[i, _id, 1] + shape_prop.torsion_friction = spin_friction_factor * material_samples[i, _id, 1] + shape_prop.restitution = material_samples[i, _id, 2] + + # Apply the modified properties + self.handler.gym.set_actor_rigid_shape_properties(env, _tmp_handle, shape_props) + + def _randomize_mujoco(self, env_ids: torch.Tensor): + """Randomize friction and restitution for MuJoCo simulator.""" + assert self.handler.num_envs == 1, "MuJoCo handler only supports single environment." + model = self.handler.physics.model + + bucket_ids = torch.randint(0, self.num_buckets, (len(env_ids), model.ngeom), device="cpu") + material_samples = self.material_buckets[bucket_ids] # static friction, dynamic friction and restitution + + static_friction = material_samples[env_ids, self.set_shape_indices, 0] + solimp_value = 0.1 * static_friction + model.geom_solimp[self.set_shape_indices, 0] = solimp_value + + # model.geom_friction --> friction for (slide, spin, roll) + dynamic_friction = material_samples[env_ids, self.set_shape_indices, 1] + model.geom_friction[self.set_shape_indices, 0] = dynamic_friction # slide friction + model.geom_friction[self.set_shape_indices, 1] = 0.01 * dynamic_friction # spin friction + model.geom_friction[self.set_shape_indices, 2] = 0.01 * dynamic_friction # roll friction + + # restitution and damping calculation + restitution_scale = 1.0 # from 0.5 - 2.0 + restitution = material_samples[env_ids, self.set_shape_indices, 2] * restitution_scale + 1e-6 + damping = (-torch.log(restitution) / torch.sqrt(torch.pi**2 + torch.log(restitution) ** 2)).clamp( + min=0.0, max=1.0 + ) + + # solref:timeconst & damping ratio + model.geom_solref[self.set_shape_indices, 1] = damping + + +class MassRandomizer(BaseQueryType): + """Randomize the mass of the bodies.""" + + handler: BaseSimHandler + + def __init__( + self, + obj_name: str, + body_names: list[str] | str | None = None, + mass_distribution_params: list | tuple = (-1.0, 3.0), + operation: Literal["add", "scale", "abs"] = "add", + distribution: Literal["uniform", "log_uniform", "gaussian"] = "uniform", + recompute_inertia: bool = True, + ): + super().__init__() + self.obj_name = obj_name + self.set_body_names = [body_names] if isinstance(body_names, str) else body_names + self.mass_distribution_params = mass_distribution_params + self.operation = operation + self.distribution = distribution + self.recompute_inertia = recompute_inertia + + def bind_handler(self, handler: BaseSimHandler, *args, **kwargs): + """Bind handler to the query.""" + super().bind_handler(handler, *args, **kwargs) + self.simulator_name = handler.scenario.simulator + self.initialize() + + def initialize(self): + """Initialize the query.""" + # check for valid operation + if self.operation == "scale": + _validate_scale_range( + self.mass_distribution_params, + "mass_distribution_params", + allow_zero=False, + ) + elif self.operation not in ("abs", "add"): + raise ValueError( + f"Randomization term 'randomize_rigid_body_mass' does not support operation: '{self.operation}'." + ) + + self.all_robot_names = [robot.name for robot in self.handler.robots] + self.all_object_names = [obj.name for obj in self.handler.objects] + self.body_names = self.handler.get_body_names(self.obj_name, sort=False) + self.set_body_ids = ( + torch.tensor( + [self.body_names.index(_name) for _name in self.set_body_names], + dtype=torch.int, + device="cpu", + ) + if self.set_body_names is not None + else torch.arange(len(self.body_names), dtype=torch.int, device="cpu") + ) + self.default_masses = deepcopy(self._get_masses()) + + def __call__(self, env_ids: torch.Tensor | None = None, **kwargs): + """Call the query.""" + # resolve environment ids + if env_ids is None: + env_ids = torch.arange(self.handler.num_envs, device="cpu") + else: + env_ids = torch.tensor(env_ids).cpu() + self.randomize(env_ids) + + def _get_masses(self): + if self.simulator_name == "isaacsim": + return self._get_masses_isaacsim() + elif self.simulator_name == "isaacgym": + return self._get_masses_isaacgym() + elif self.simulator_name == "mujoco": + return self._get_masses_mujoco() + + def _get_masses_isaacsim(self): + if self.obj_name in self.handler.scene.articulations: + obj_inst = self.handler.scene.articulations[self.obj_name] + elif self.obj_name in self.handler.scene.rigid_objects: + obj_inst = self.handler.scene.rigid_objects[self.obj_name] + else: + raise ValueError(f"Not found: {self.obj_name}.") + masses = obj_inst.root_physx_view.get_masses() + return masses + + def _get_masses_isaacgym(self): + """Get masses for IsaacGym simulator. Note that the isaacgym handler only support 1 robot and multiple objects, currently.""" + # Initialize masses tensor + masses = torch.zeros( + (self.handler.num_envs, len(self.body_names)), + dtype=torch.float32, + device=self.handler.device, + ) + + # Get masses from first environment (they should be the same across environments initially) + for ( + env_id, + env, + ) in enumerate(self.handler._envs): + if self.obj_name in self.all_robot_names: + _tmp_handle = self.handler._robot_handles[0] + elif self.obj_name in self.all_object_names: + # Find the object handle + _tmp_handle = self.handler._obj_handles[env_id][self.handler.objects.index(self.obj_name)] + else: + raise ValueError(f"Not found: {self.obj_name}.") + + body_props = self.handler.gym.get_actor_rigid_body_properties(env, _tmp_handle) + for i, prop in enumerate(body_props): + masses[env_id, i] = prop.mass + + return masses + + def _get_masses_mujoco(self): + """Get masses for MuJoCo simulator.""" + assert self.handler.num_envs == 1, "MuJoCo handler only supports single environment." + model = self.handler.physics.model + body_masses = model.body_mass + return torch.tensor( + body_masses, + dtype=torch.float32, + device=self.handler.device, + ).unsqueeze(0) # shape: (1, num_bodies) + + def _set_masses(self, masses: torch.Tensor, env_ids: torch.Tensor): + if self.simulator_name == "isaacsim": + self._set_masses_isaacsim(masses, env_ids) + elif self.simulator_name == "isaacgym": + self._set_masses_isaacgym(masses, env_ids) + elif self.simulator_name == "mujoco": + self._set_masses_mujoco(masses, env_ids) + + def _set_masses_isaacsim(self, masses: torch.Tensor, env_ids: torch.Tensor): + if self.obj_name in self.handler.scene.articulations: + obj_inst = self.handler.scene.articulations[self.obj_name] + elif self.obj_name in self.handler.scene.rigid_objects: + obj_inst = self.handler.scene.rigid_objects[self.obj_name] + obj_inst.root_physx_view.set_masses(masses, env_ids) + + def _set_masses_isaacgym(self, masses: torch.Tensor, env_ids: torch.Tensor): + """Set masses for IsaacGym simulator.""" + for env_id in env_ids: + env = self.handler._envs[env_id] + if self.obj_name in self.all_robot_names: + _tmp_handle = self.handler._robot_handles[env_id] + elif self.obj_name in self.all_object_names: + # Find the object handle + _tmp_handle = self.handler._obj_handles[env_id][self.handler.objects.index(self.obj_name)] + else: + raise ValueError(f"Not found: {self.obj_name}.") + + # Get current body properties + body_props = self.handler.gym.get_actor_rigid_body_properties(env, _tmp_handle) + + # Update masses for specified bodies + for body_idx in self.set_body_ids: + if body_idx < len(body_props): + body_props[body_idx].mass = float(masses[env_id, body_idx]) + + # Apply the modified properties + self.handler.gym.set_actor_rigid_body_properties(env, _tmp_handle, body_props) + + def _set_masses_mujoco(self, masses: torch.Tensor, env_ids: torch.Tensor): + model = self.handler.physics.model + model.body_mass[self.set_body_ids] = masses[env_ids, self.set_body_ids].cpu() + + def _recompute_inertias(self, ratios: torch.Tensor, env_ids: torch.Tensor): + # scale the inertia tensors by the the ratios + # since mass randomization is done on default values, we can use the default inertia tensors + if self.simulator_name == "isaacsim": + if self.obj_name in self.handler.scene.articulations: + obj_inst = self.handler.scene.articulations[self.obj_name] + inertias = obj_inst.root_physx_view.get_inertias() + # inertia has shape: (num_envs, num_bodies, 9) for articulation + inertias[env_ids[:, None], self.set_body_ids] = ( + obj_inst.data.default_inertia[env_ids[:, None], self.set_body_ids] * ratios[..., None] + ) + elif self.obj_name in self.handler.scene.rigid_objects: + obj_inst = self.handler.scene.rigid_objects[self.obj_name] + # inertia has shape: (num_envs, 9) for rigid object + inertias[env_ids] = obj_inst.data.default_inertia[env_ids] * ratios + # set the inertia tensors into the physics simulation + obj_inst.root_physx_view.set_inertias(inertias, env_ids) + elif self.simulator_name == "isaacgym": + # For IsaacGym, inertia recomputation is handled automatically by the physics engine + # when mass is changed, so we don't need to manually update inertias + + # a little delay refresh in isaacym handler + # self.gym.refresh_mass_matrix_tensors(self.sim) + pass + elif self.simulator_name == "mujoco": + model = self.handler.physics.model + model.body_inertia[self.set_body_ids] = ( + model.body_inertia[self.set_body_ids] * ratios.squeeze(0).numpy() + ) # only single env + + def randomize(self, env_ids: torch.Tensor): + """Randomize the query.""" + if self.simulator_name not in ("isaacsim", "isaacgym", "mujoco"): + log.warning( + f"Mass randomization not implemented for simulator: {self.simulator_name}. This randomization step will be skipped." + ) + return + # get the current masses of the bodies (num_assets, num_bodies) + masses = self._get_masses() # shape: (num_envs, num_bodies) + # apply randomization on default values + # this is to make sure when calling the function multiple times, the randomization is applied on the + # default values and not the previously randomized values + masses[env_ids[:, None], self.set_body_ids] = self.default_masses[env_ids[:, None], self.set_body_ids].clone() + # sample from the given range + # note: we modify the masses in-place for all environments + # however, the setter takes care that only the masses of the specified environments are modified + masses = randomize_prop_by_op( + masses, + self.mass_distribution_params, + env_ids, + self.set_body_ids, + operation=self.operation, + distribution=self.distribution, + ) + self._set_masses(masses, env_ids) + # recompute inertia tensors if needed + if self.recompute_inertia: + # compute the ratios of the new masses to the initial masses + ratios = ( + masses[env_ids[:, None], self.set_body_ids] / self.default_masses[env_ids[:, None], self.set_body_ids] + ) + self._recompute_inertias(ratios, env_ids) + + +# helper functions adapted from Isaac Lab + + +def randomize_prop_by_op( + data: torch.Tensor, + distribution_parameters: tuple[float | torch.Tensor, float | torch.Tensor], + dim_0_ids: torch.Tensor | None, + dim_1_ids: torch.Tensor | slice, + operation: Literal["add", "scale", "abs"], + distribution: Literal["uniform", "log_uniform", "gaussian"], +) -> torch.Tensor: + """Perform data randomization based on the given operation and distribution. + + Args: + data: The data tensor to be randomized. Shape is (dim_0, dim_1). + distribution_parameters: The parameters for the distribution to sample values from. + dim_0_ids: The indices of the first dimension to randomize. + dim_1_ids: The indices of the second dimension to randomize. + operation: The operation to perform on the data. Options: 'add', 'scale', 'abs'. + distribution: The distribution to sample the random values from. Options: 'uniform', 'log_uniform'. + + Returns: + The data tensor after randomization. Shape is (dim_0, dim_1). + + Raises: + NotImplementedError: If the operation or distribution is not supported. + """ + # resolve shape + # -- dim 0 + if dim_0_ids is None: + n_dim_0 = data.shape[0] + dim_0_ids = slice(None) + else: + n_dim_0 = len(dim_0_ids) + if not isinstance(dim_1_ids, slice): + dim_0_ids = dim_0_ids[:, None] + # -- dim 1 + if isinstance(dim_1_ids, slice): + n_dim_1 = data.shape[1] + else: + n_dim_1 = len(dim_1_ids) + + # resolve the distribution + if distribution == "uniform": + dist_fn = sample_uniform + elif distribution == "log_uniform": + dist_fn = sample_log_uniform + elif distribution == "gaussian": + dist_fn = sample_gaussian + else: + raise NotImplementedError( + f"Unknown distribution: '{distribution}' for joint properties randomization." + " Please use 'uniform', 'log_uniform', 'gaussian'." + ) + # perform the operation + if operation == "add": + data[dim_0_ids, dim_1_ids] += dist_fn(*distribution_parameters, (n_dim_0, n_dim_1), device=data.device) + elif operation == "scale": + data[dim_0_ids, dim_1_ids] *= dist_fn(*distribution_parameters, (n_dim_0, n_dim_1), device=data.device) + elif operation == "abs": + data[dim_0_ids, dim_1_ids] = dist_fn(*distribution_parameters, (n_dim_0, n_dim_1), device=data.device) + else: + raise NotImplementedError( + f"Unknown operation: '{operation}' for property randomization. Please use 'add', 'scale', or 'abs'." + ) + return data + + +def _validate_scale_range( + params: tuple[float, float] | None, + name: str, + *, + allow_negative: bool = False, + allow_zero: bool = True, +) -> None: + """Validates a (low, high) tuple used in scale-based randomization. + + This function ensures the tuple follows expected rules when applying a 'scale' + operation. It performs type and value checks, optionally allowing negative or + zero lower bounds. + + Args: + params (tuple[float, float] | None): The (low, high) range to validate. If None, + validation is skipped. + name (str): The name of the parameter being validated, used for error messages. + allow_negative (bool, optional): If True, allows the lower bound to be negative. + Defaults to False. + allow_zero (bool, optional): If True, allows the lower bound to be zero. + Defaults to True. + + Raises: + TypeError: If `params` is not a tuple of two numbers. + ValueError: If the lower bound is negative or zero when not allowed. + ValueError: If the upper bound is less than the lower bound. + + Example: + _validate_scale_range((0.5, 1.5), "mass_scale") + """ + if params is None: # caller didn’t request randomisation for this field + return + low, high = params + if not isinstance(low, (int, float)) or not isinstance(high, (int, float)): + raise TypeError(f"{name}: expected (low, high) to be a tuple of numbers, got {params}.") + if not allow_negative and not allow_zero and low <= 0: + raise ValueError(f"{name}: lower bound must be > 0 when using the 'scale' operation (got {low}).") + if not allow_negative and allow_zero and low < 0: + raise ValueError(f"{name}: lower bound must be ≥ 0 when using the 'scale' operation (got {low}).") + if high < low: + raise ValueError(f"{name}: upper bound ({high}) must be ≥ lower bound ({low}).") diff --git a/roboverse_pack/tasks/beyondmimic/metasim/configs/tracking_g1.py b/roboverse_pack/tasks/beyondmimic/metasim/configs/tracking_g1.py new file mode 100644 index 000000000..3058f2f8b --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/metasim/configs/tracking_g1.py @@ -0,0 +1,223 @@ +from __future__ import annotations + +from dataclasses import MISSING +from typing import Callable + +from metasim.utils import configclass +from roboverse_pack.tasks.beyondmimic.metasim.configs.cfg_randomizers import MassRandomizer, MaterialRandomizer +from roboverse_pack.tasks.beyondmimic.metasim.mdp import ( + events, + observations, + rewards, + terminations, +) +from roboverse_pack.tasks.beyondmimic.metasim.mdp.commands import MotionCommandCfg + +from .cfg_base import BaseEnvCfg +from .cfg_queries import ContactForces + +VELOCITY_RANGE = { + # linear velocity + "x": (-0.5, 0.5), + "y": (-0.5, 0.5), + "z": (-0.2, 0.2), + # angular velocity + "roll": (-0.52, 0.52), + "pitch": (-0.52, 0.52), + "yaw": (-0.78, 0.78), +} + + +@configclass +class CfgTerm: + """Configuration for terminal functions.""" + + func: Callable = MISSING + params: dict[str, any] = {} + + +@configclass +class ObsTerm(CfgTerm): + """Configuration for observation functions.""" + + noise_range: tuple[float, float] | None = None + + +@configclass +class RewTerm(CfgTerm): + """Configuration for reward functions.""" + + weight: float = 1.0 + + +@configclass +class DoneTerm(CfgTerm): + """Configuration for termination functions.""" + + time_out: bool = False + + +@configclass +class ObservationsCfg: + """Configuration for observations.""" + + @configclass + class PolicyCfg: + """Configuration for policy observations.""" + + command = ObsTerm(func=observations.generated_commands) + motion_anchor_pos_b = ObsTerm(func=observations.motion_anchor_pos_b, noise_range=(-0.25, 0.25)) + motion_anchor_ori_b = ObsTerm(func=observations.motion_anchor_ori_b, noise_range=(-0.05, 0.05)) + base_lin_vel = ObsTerm(func=observations.base_lin_vel, noise_range=(-0.5, 0.5)) + base_ang_vel = ObsTerm(func=observations.base_ang_vel, noise_range=(-0.2, 0.2)) + joint_pos = ObsTerm(func=observations.joint_pos_rel, noise_range=(-0.01, 0.01)) + joint_vel = ObsTerm(func=observations.joint_vel_rel, noise_range=(-0.5, 0.5)) + actions = ObsTerm(func=observations.last_action) + + @configclass + class PrivilegedCfg: + """Configuration for privileged observations.""" + + command = ObsTerm(func=observations.generated_commands) + motion_anchor_pos_b = ObsTerm(func=observations.motion_anchor_pos_b) + motion_anchor_ori_b = ObsTerm(func=observations.motion_anchor_ori_b) + body_pos = ObsTerm(func=observations.robot_body_pos_b) + body_ori = ObsTerm(func=observations.robot_body_ori_b) + base_lin_vel = ObsTerm(func=observations.base_lin_vel) + base_ang_vel = ObsTerm(func=observations.base_ang_vel) + joint_pos = ObsTerm(func=observations.joint_pos_rel) + joint_vel = ObsTerm(func=observations.joint_vel_rel) + actions = ObsTerm(func=observations.last_action) + + # observation groups + policy = PolicyCfg() + critic = PrivilegedCfg() + + +@configclass +class RewardsCfg: + """Configuration for rewards.""" + + motion_global_anchor_pos = RewTerm( + func=rewards.motion_global_anchor_position_error_exp, weight=0.5, params={"std": 0.3} + ) + motion_global_anchor_ori = RewTerm( + func=rewards.motion_global_anchor_orientation_error_exp, weight=0.5, params={"std": 0.4} + ) + motion_body_pos = RewTerm(func=rewards.motion_relative_body_position_error_exp, weight=1.0, params={"std": 0.3}) + motion_body_ori = RewTerm(func=rewards.motion_relative_body_orientation_error_exp, weight=1.0, params={"std": 0.4}) + motion_body_lin_vel = RewTerm( + func=rewards.motion_global_body_linear_velocity_error_exp, weight=1.0, params={"std": 1.0} + ) + motion_body_ang_vel = RewTerm( + func=rewards.motion_global_body_angular_velocity_error_exp, weight=1.0, params={"std": 3.14} + ) + action_rate_l2 = RewTerm(func=rewards.action_rate_l2, weight=-1e-1) + joint_limit = RewTerm(func=rewards.joint_pos_limits, weight=-10.0) + undesired_contacts = RewTerm( + func=rewards.undesired_contacts, + weight=-0.1, + params={ + "threshold": 1.0, + "body_names": r"^(?!left_ankle_roll_link$)(?!right_ankle_roll_link$)(?!left_wrist_yaw_link$)(?!right_wrist_yaw_link$).+$", + }, + ) + + +@configclass +class TerminationsCfg: + """Configuration for terminations.""" + + time_out = DoneTerm(func=terminations.time_out, time_out=True) + anchor_pos = DoneTerm(func=terminations.bad_anchor_pos_z_only, params={"threshold": 0.25}) + anchor_ori = DoneTerm(func=terminations.bad_anchor_ori, params={"threshold": 0.8}) + ee_body_pos = DoneTerm( + func=terminations.bad_motion_body_pos_z_only, + params={ + "threshold": 0.25, + "body_names": [ + "left_ankle_roll_link", + "right_ankle_roll_link", + "left_wrist_yaw_link", + "right_wrist_yaw_link", + ], + }, + ) + + +@configclass +class TrackingG1EnvCfg(BaseEnvCfg): + """Environment configuration for humanoid motion tracking task.""" + + commands = MotionCommandCfg( + anchor_body_name="torso_link", + body_names=[ # for indexing motion body links + "pelvis", + "left_hip_roll_link", + "left_knee_link", + "left_ankle_roll_link", + "right_hip_roll_link", + "right_knee_link", + "right_ankle_roll_link", + "torso_link", + "left_shoulder_roll_link", + "left_elbow_link", + "left_wrist_yaw_link", + "right_shoulder_roll_link", + "right_elbow_link", + "right_wrist_yaw_link", + ], + resampling_time_range=(1.0e9, 1.0e9), + pose_range={ + "x": (-0.05, 0.05), + "y": (-0.05, 0.05), + "z": (-0.01, 0.01), + "roll": (-0.1, 0.1), + "pitch": (-0.1, 0.1), + "yaw": (-0.2, 0.2), + }, + velocity_range=VELOCITY_RANGE, + joint_position_range=(-0.1, 0.1), + ) + observations = ObservationsCfg() + rewards = RewardsCfg() + terminations = TerminationsCfg() + + # NOTE extra obs will be included in `env_states.extras["contact_forces"]` + callbacks_query = {"contact_forces": ContactForces(history_length=3)} + + # TODO fully align domain randomization with BeyondMimic + callbacks_setup = { + "material_randomizer": MaterialRandomizer( + obj_name="g1_tracking", + static_friction_range=(0.3, 1.6), + dynamic_friction_range=(0.3, 1.2), + restitution_range=(0.0, 0.5), + num_buckets=64, + ), + # TODO change `MassRandomizer` to `randomize_rigid_body_com()` from BeyondMimic + "mass_randomizer": MassRandomizer( + obj_name="g1_tracking", + body_names="torso_link", + mass_distribution_params=(-1.0, 3.0), # TODO change this + operation="add", + ), + # NOTE `env` will be passed to the functions inside `LeggedRobotTask._bind_callbacks()` + "add_joint_default_pos": ( + events.randomize_joint_default_pos, + { + "pos_distribution_params": (-0.01, 0.01), + "operation": "add", + }, + ), + } + callbacks_post_step = { + # NOTE perhaps slightly different from how it's triggered in BeyondMimic + "push_robot": ( + events.push_by_setting_velocity, + { + "interval_range_s": (1.0, 3.0), + "velocity_range": VELOCITY_RANGE, + }, + ) + } diff --git a/roboverse_pack/tasks/beyondmimic/metasim/envs/__init__.py b/roboverse_pack/tasks/beyondmimic/metasim/envs/__init__.py new file mode 100644 index 000000000..64951550e --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/metasim/envs/__init__.py @@ -0,0 +1 @@ +"""Submodule containing environment classes written in MetaSim.""" diff --git a/roboverse_pack/tasks/beyondmimic/metasim/envs/base_legged_robot.py b/roboverse_pack/tasks/beyondmimic/metasim/envs/base_legged_robot.py new file mode 100644 index 000000000..73ea29f9a --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/metasim/envs/base_legged_robot.py @@ -0,0 +1,426 @@ +from __future__ import annotations + +import math +from copy import deepcopy +from dataclasses import asdict +from typing import Any, Sequence + +import torch + +from metasim.scenario.scenario import ScenarioCfg +from metasim.task.base import BaseTaskEnv +from metasim.task.rl_task import RLTaskEnv +from metasim.utils.state import TensorState, list_state_to_tensor +from roboverse_pack.robots.g1_tracking import G1TrackingCfg +from roboverse_pack.tasks.beyondmimic.metasim.configs.cfg_base import BaseEnvCfg, CallbacksCfg +from roboverse_pack.tasks.beyondmimic.metasim.mdp.commands import MotionCommand +from roboverse_pack.tasks.beyondmimic.metasim.utils.misc import get_axis_params +from roboverse_pack.tasks.beyondmimic.metasim.utils.string import find_bodies, pattern_match + + +class LeggedRobotTask(RLTaskEnv): + """Base environment for legged robots.""" + + def __init__( + self, + scenario: ScenarioCfg, + config: BaseEnvCfg, + device: str | torch.device | None = None, + ) -> None: + self.cfg = config + _callbacks_cfg = asdict(getattr(self.cfg, "callbacks", CallbacksCfg())) + self._query: dict = _callbacks_cfg.pop("query", {}) + self.robot = scenario.robots[0] + BaseTaskEnv.__init__(self, scenario=scenario, device=device) + self._initial_states = list_state_to_tensor(self.handler, self._get_initial_states(), self.device) + + self.extras: dict[str, Any] = {} + self.extras_buffer: dict[str, any] = {} + + # callbacks + self._bind_callbacks(callbacks=_callbacks_cfg) + + self.name = self.robot.name + self.total_action_dim = len(self.robot.actuators) + self.sim_dt = self.scenario.sim_params.dt + self.sorted_body_names = self.handler.get_body_names(self.name, sort=True) + self.sorted_joint_names = self.handler.get_joint_names(self.name, sort=True) + self.original_joint_names = self.handler.get_joint_names(self.name, sort=False) + + self._init_joint_cfg() + self._instantiate_cfg() + self._init_buffers() + self._setup() + # self.reset() + + def _bind_callbacks(self, callbacks: dict | None = None): + for _callbacks in callbacks.values(): + for _key, _val in _callbacks.items(): + if not isinstance(_val, tuple): + assert callable(_val) or isinstance(_val, object) + _callbacks[_key] = (_val, {}) + if hasattr(_callbacks[_key][0], "bind_handler"): + _callbacks[_key][0].bind_handler(self.handler) + + self.setup_callback = callbacks.pop("setup", {}) + assert isinstance(self.setup_callback, dict) + self.reset_callback = callbacks.pop("reset", {}) + assert isinstance(self.reset_callback, dict) + self.pre_physics_step_callback = callbacks.pop("pre_step", {}) + assert isinstance(self.pre_physics_step_callback, dict) + self.post_physics_step_callback = callbacks.pop("post_step", {}) + assert isinstance(self.post_physics_step_callback, dict) + self.terminate_callback = callbacks.pop("terminate", {}) + assert isinstance(self.terminate_callback, dict) + + def _init_joint_cfg(self): + """Set position limits, default joint positions, and default joint velocities.""" + robot: G1TrackingCfg = self.robot + sorted_joint_names: list[str] = self.sorted_joint_names + original_joint_names: list[str] = self.original_joint_names + + self.sorted_to_original_joint_indexes = torch.tensor( + find_bodies(original_joint_names, sorted_joint_names, preserve_order=True)[0], device=self.device + ) + self.original_to_sorted_joint_indexes = torch.tensor( + find_bodies(sorted_joint_names, original_joint_names, preserve_order=True)[0], device=self.device + ) + + # set position limits + dof_pos_limits = robot.joint_limits + sorted_dof_pos_limits = [dof_pos_limits[joint] for joint in sorted_joint_names] + self.dof_pos_limits = torch.tensor(sorted_dof_pos_limits, device=self.device) # (n_dofs, 2) + + soft_limit_factor = getattr(robot, "soft_joint_pos_limit_factor", 0.9) + _mid = (self.dof_pos_limits[:, 0] + self.dof_pos_limits[:, 1]) / 2.0 + _diff = self.dof_pos_limits[:, 1] - self.dof_pos_limits[:, 0] + + # NOTE same as `ArticulationData.soft_joint_pos_limits` in Isaac Lab + soft_dof_pos_limits = torch.zeros_like(self.dof_pos_limits, device=self.device) + soft_dof_pos_limits[:, 0] = _mid - 0.5 * _diff * soft_limit_factor + soft_dof_pos_limits[:, 1] = _mid + 0.5 * _diff * soft_limit_factor + self.soft_dof_pos_limits_sorted = soft_dof_pos_limits.unsqueeze(0).repeat( + self.num_envs, 1, 1 + ) # (n_envs, n_dofs, 2) + self.soft_dof_pos_limits_original = self.soft_dof_pos_limits_sorted[:, self.sorted_to_original_joint_indexes, :] + + # set default joint positions + default_joint_pos = robot.default_joint_positions + default_joint_pos = pattern_match(default_joint_pos, sorted_joint_names) + sorted_joint_pos = [default_joint_pos[name] for name in sorted_joint_names] + default_dof_pos = torch.tensor(sorted_joint_pos, device=self.device) # (n_dofs,) + self.default_dof_pos_sorted = default_dof_pos.unsqueeze(0).repeat(self.num_envs, 1) # (n_envs, n_dofs) + self.default_dof_pos_original = self.default_dof_pos_sorted[:, self.sorted_to_original_joint_indexes] + + # set default joint velocities + default_joint_vel = robot.default_joint_velocities + default_joint_vel = pattern_match(default_joint_vel, sorted_joint_names) + sorted_joint_vel = [default_joint_vel[name] for name in sorted_joint_names] + default_dof_vel = torch.tensor(sorted_joint_vel, device=self.device) # (n_dofs,) + self.default_dof_vel_sorted = default_dof_vel.unsqueeze(0).repeat(self.num_envs, 1) # (n_envs, n_dofs) + + def _instantiate_cfg(self): + self.decimation = self.cfg.decimation + self.step_dt = self.sim_dt * self.decimation + + # NOTE actions, action scale, and action offset are in the original order + self.action_clip = self.robot.action_clip + self.action_offset = self.default_dof_pos_original.clone() if self.robot.action_offset else 0.0 + + # per-actuator action scale + self.action_scale = ( + torch.tensor(list(self.robot.action_scale.values()), device=self.device) + .unsqueeze(0) + .repeat(self.num_envs, 1) + ) + + self.common_step_counter = 0 # counter for curriculum + self.commands = MotionCommand(env=self, cfg=self.cfg.commands) + + def _init_buffers(self): + self._action = torch.zeros(size=(self.num_envs, self.total_action_dim), device=self.device) + self._prev_action = torch.zeros_like(self._action) + + # prepare extra info to store individual termination term information + self._term_dones = dict() + for term_name in asdict(self.cfg.terminations).keys(): + self._term_dones[term_name] = torch.zeros(self.num_envs, device=self.device, dtype=torch.bool) + + # record terminated envs for adapting sampling + self.reset_terminated = torch.zeros(self.num_envs, dtype=torch.bool, device=self.device) + self.reset_time_outs = torch.zeros(self.num_envs, dtype=torch.bool, device=self.device) + + # set gravity vector + self.up_axis_idx = 2 + self.gravity_vec = torch.tensor( + get_axis_params(-1.0, self.up_axis_idx), + dtype=torch.float, + device=self.device, + ).repeat((self.num_envs, 1)) + + # reset commands + + # for logging + self.episode_rewards = { + name: torch.zeros( + self.num_envs, + dtype=torch.float, + device=self.device, + requires_grad=False, + ) + for name in asdict(self.cfg.rewards).keys() + } + + def _setup(self): + """Apply domain randomization of start-up mode.""" + for _setup_fn, _params in self.setup_callback.values(): + _setup_fn(env=self, **_params) + + def _pre_physics_step(self, actions: torch.Tensor): + """Apply pre-physics callbacks.""" + for pre_fn, _params in self.pre_physics_step_callback.values(): + pre_fn(self, **_params) + # NOTE corresponds to action clipping in `RslRlVecEnvWrapper.step()` in Isaac Lab + return torch.clip(actions, -self.action_clip, self.action_clip) if self.action_clip else actions + + def reset(self, env_ids: Sequence[int] | None = None): + """Reset the specified environments. Only called once when the task class is initialized.""" + if env_ids is None: + env_ids = torch.arange(self.num_envs, dtype=torch.int64, device=self.device) + + # reset state of scene + self._reset_idx(env_ids) + + # update articulation kinematics + # self.handler.scene.write_data_to_sim() + # self.handler.sim.forward() + + # update commands + self.commands.compute(self.handler.get_states()) + env_states = self.handler.get_states() + + # step interval events + for _step_fn, _params in self.post_physics_step_callback.values(): + _step_fn(self, env_states, **_params) + + # compute observations after resets + self._obs_buf = self._observation(self.handler.get_states()) + + def _reset_idx(self, env_ids: torch.Tensor | list[int] | None = None): + """Reset selected envs (defaults to all).""" + if env_ids is None: + env_ids = torch.tensor(list(range(self.num_envs)), device=self.device) + + self.extras["episode"] = {} + + # update the curriculum for environments that need a reset + + # reset the internal buffers of the scene elements (actions, sensors, etc.) + # TODO reset contact sensor (currently not supported by MetaSim) + + # apply events such as randomizations for environments that need a reset + for _reset_fn, _params in self.reset_callback.values(): + _ = _reset_fn(self, env_ids, **_params) + + # reset actions + self._prev_action[env_ids] = 0.0 + self._action[env_ids] = 0.0 + + # reset rewards + for key in self.episode_rewards.keys(): + self.extras["episode"]["Episode_Reward/" + key] = ( + torch.mean(self.episode_rewards[key][env_ids]) / self.cfg.max_episode_length_s + ) + self.episode_rewards[key][env_ids] = 0.0 + + # reset curriculum + + # reset commands + metrics = self.commands.reset(env_ids=env_ids) + for metric_name, metric_value in metrics.items(): + self.extras["episode"][f"Metrics_Motion/{metric_name}"] = metric_value + + # reset events + + # reset terminations + for key in self._term_dones.keys(): + self.extras["episode"]["Episode_Termination/" + key] = torch.count_nonzero( + self._term_dones[key][env_ids] + ).item() + + # reset the episode length buffer + self._episode_steps[env_ids] = 0 + + def step( + self, + actions: torch.Tensor, + ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, dict]: + """Apply actions, simulate for `decimation` steps, and compute RLTask-style outputs.""" + if actions.ndim == 1: + actions = actions.unsqueeze(0) + + # NOTE `actions` is in the original order since it's computed inside `OnPolicyRunner` + actions = self._pre_physics_step(actions) + self._prev_action[:] = self._action + self._action[:] = actions.to(self.device) + + processed_actions = self._action * self.action_scale + self.action_offset + if self.action_clip is not None: + processed_actions = processed_actions.clip(-self.action_clip, self.action_clip) + processed_actions = processed_actions.clone()[:, self.original_to_sorted_joint_indexes] # sorted order + + for _ in range(self.decimation): + env_states = self._physics_step(processed_actions) + + self._post_physics_step(env_states) + + # NOTE for RSL-RL v2.3.0 (needed in env wrapper) + self.extras["observations"] = self._obs_buf + + return ( + self._obs_buf, + self.reward_buf, + self.reset_terminated, + self.reset_time_outs, + self.extras, + ) + + def _post_physics_step(self, env_states: TensorState): + self._episode_steps += 1 # step in current episode (per env) + self.common_step_counter += 1 # total step (common for all envs) + + # check termination conditions and compute rewards + self.reset_buf = self._terminated(env_states) + self.reward_buf = self._reward(env_states) + + # reset envs and MDP + reset_env_ids = self.reset_buf.nonzero(as_tuple=False).squeeze(-1) + if len(reset_env_ids) > 0: + self._reset_idx(env_ids=reset_env_ids) + + # update articulation kinematics + # self.handler.scene.write_data_to_sim() + # self.handler.sim.forward() + + # update commands + self.commands.compute(self.handler.get_states()) + env_states = self.handler.get_states() + + # step interval events + for _step_fn, _params in self.post_physics_step_callback.values(): + _step_fn(self, env_states, **_params) + + # compute observations after resets + self._obs_buf = self._observation(self.handler.get_states()) + + def _physics_step(self, actions: torch.Tensor) -> TensorState: + """Issue low-level actions and simulate one physics step.""" + # FIXME both `set_dof_targets()` and `simulate()` call `articulation.write_data_to_sim()` + self.handler.set_dof_targets(actions) + self.handler.simulate() + return self.handler.get_states() + + def _reward(self, env_states: TensorState): + rew_buf = torch.zeros(self.num_envs, dtype=torch.float, device=self.device) + for name, term_cfg in asdict(self.cfg.rewards).items(): + value = term_cfg["func"](self, env_states, **term_cfg["params"]) * term_cfg["weight"] * self.step_dt + rew_buf += value # update total reward + self.episode_rewards[name] += value # update episodic sum + + return rew_buf + + def _terminated(self, env_states: TensorState | None) -> torch.BoolTensor: + self.reset_time_outs[:] = False + self.reset_terminated[:] = False + for name, term_cfg in asdict(self.cfg.terminations).items(): + value = term_cfg["func"](self, env_states, **term_cfg["params"]) + # store timeout signal separately + if term_cfg["time_out"]: + self.reset_time_outs |= value + else: + self.reset_terminated |= value + # add to episode dones + self._term_dones[name][:] = value + + return self.reset_time_outs | self.reset_terminated + + def _observation(self, env_states: TensorState) -> dict[str, torch.Tensor]: + """Return a dictionary with keys "policy" and "critic", each corresponding to a tensor.""" + raise NotImplementedError + + def _get_observations(self) -> dict[str, torch.Tensor]: + """For compatibility with Isaac Lab native RSL-RL env wrapper.""" + return self._observation(self.handler.get_states()) + + def _get_initial_states(self): + """Return list of per-env initial states derived from config.""" + sorted_joint_names = self.handler.get_joint_names(self.robot.name, sort=True) + + pos = self.robot.default_pos + rot = self.robot.default_rot + joint_pos = self.robot.default_joint_positions + joint_pos = pattern_match(joint_pos, sorted_joint_names) + + joint_vel = self.robot.default_joint_velocities + joint_vel = pattern_match(joint_vel, sorted_joint_names) + + template = { + "objects": {}, + "robots": { + self.robot.name: { + "pos": torch.tensor(pos, dtype=torch.float32), + "rot": torch.tensor(rot, dtype=torch.float32), + "dof_pos": {name: joint_pos[name] for name in joint_pos}, + "dof_vel": {name: joint_vel[name] for name in joint_vel}, + } + }, + } + return [deepcopy(template) for _ in range(self.scenario.num_envs)] + + def get_states(self) -> TensorState: + """Get the current simulator state.""" + return self.handler.get_states() + + def set_states(self, states: TensorState, env_ids: list[int] | None = None) -> None: + """Set simulator state for selected env indexes.""" + self.handler.set_states(states=states, env_ids=env_ids) + + def _extra_spec(self) -> dict: + """Expose optional sensor queries to the simulator handler.""" + return self._query + + @property + def max_episode_steps(self): + """Maximum episode length in steps.""" + return math.ceil(self.cfg.max_episode_length_s / self.step_dt) + + @property + def obs_buf(self) -> torch.Tensor: + """Policy (actor) observations in shape (num_envs, num_obs).""" + return self._obs_buf["policy"] + + @property + def priv_obs_buf(self) -> torch.Tensor: + """Critic observations in shape (num_envs, num_priv_obs).""" + return self._obs_buf["critic"] + + # for backward compatibility with RSL-RL env wrapper + + @property + def episode_length_buf(self) -> torch.Tensor: + """Current episode lengths of each env. Used in time-out computation.""" + return self._episode_steps + + @episode_length_buf.setter + def episode_length_buf(self, value: torch.Tensor): + self._episode_steps = value + + @property + def max_episode_length(self) -> int: + """Maximum episode length in environment steps.""" + return self.max_episode_steps + + @property + def num_actions(self) -> int: + """Total dimension of actions.""" + return self.total_action_dim diff --git a/roboverse_pack/tasks/beyondmimic/metasim/envs/tracking_g1.py b/roboverse_pack/tasks/beyondmimic/metasim/envs/tracking_g1.py new file mode 100644 index 000000000..9280228b3 --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/metasim/envs/tracking_g1.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +import copy +from dataclasses import asdict + +import torch + +from metasim.scenario.lights import DomeLightCfg +from metasim.scenario.scenario import ScenarioCfg +from metasim.scenario.simulator_params import SimParamCfg +from metasim.task.registry import register_task +from metasim.types import TensorState +from roboverse_learn.rl.configs.rsl_rl.ppo_tracking import RslRlPPOTrackingConfig +from roboverse_pack.tasks.beyondmimic.metasim.configs.tracking_g1 import TrackingG1EnvCfg + +from .base_legged_robot import LeggedRobotTask + + +@register_task("motion-tracking") +class TrackingG1Task(LeggedRobotTask): + """Registered BeyondMimic motion tracking task.""" + + scenario = ScenarioCfg( + robots=["g1_tracking"], + objects=[], + cameras=[], + num_envs=2, + simulator="isaacsim", + headless=True, + env_spacing=2.5, + decimation=1, # NOTE task-level decimation is defined by `self.cfg.decimation` + sim_params=SimParamCfg( + dt=0.005, + substeps=1, + num_threads=10, + solver_type=1, + num_position_iterations=255, + num_velocity_iterations=255, + bounce_threshold_velocity=0.5, + max_depenetration_velocity=1.0, + default_buffer_size_multiplier=5, + replace_cylinder_with_capsule=True, + friction_correlation_distance=0.025, + friction_offset_threshold=0.04, + ), + lights=[ + DomeLightCfg( + intensity=800.0, + color=(0.85, 0.9, 1.0), + ) + ], + ) + + def __init__( + self, + scenario: ScenarioCfg, + args: RslRlPPOTrackingConfig, + device: str | torch.device, + reset_in_env_wrapper: bool = False, + ) -> None: + scenario_copy = copy.deepcopy(scenario) + scenario_copy.__post_init__() + + cfg = TrackingG1EnvCfg() + cfg.commands.motion_file = args.motion_file + + super().__init__(scenario=scenario_copy, config=cfg, device=device) + if not reset_in_env_wrapper: + self.reset() + + def _compute_observation_group(self, env_states: TensorState, group_name: str): + """Compute all observations of a given group and concatenate them into a single tensor.""" + obs_terms = getattr(self.cfg.observations, group_name) + group_obs = [] + for term_cfg in asdict(obs_terms).values(): + if term_cfg["params"]: + obs: torch.Tensor = term_cfg["func"](self, env_states, **term_cfg["params"]).clone() + else: + obs: torch.Tensor = term_cfg["func"](self, env_states).clone() + if term_cfg["noise_range"]: + obs += ( + torch.rand_like(obs) * (term_cfg["noise_range"][1] - term_cfg["noise_range"][0]) + + term_cfg["noise_range"][0] + ) # [n_envs, n_dims] + group_obs.append(obs) + return torch.cat(group_obs, dim=-1) + + def _observation(self, env_states: TensorState): + obs_buf = dict() + for group_name in ["policy", "critic"]: + obs_buf[group_name] = self._compute_observation_group(env_states, group_name) + + return obs_buf diff --git a/roboverse_pack/tasks/beyondmimic/metasim/mdp/commands.py b/roboverse_pack/tasks/beyondmimic/metasim/mdp/commands.py new file mode 100644 index 000000000..59bdb1d90 --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/metasim/mdp/commands.py @@ -0,0 +1,404 @@ +from __future__ import annotations + +import math +import os +from dataclasses import MISSING +from typing import TYPE_CHECKING, Sequence + +import numpy as np +import torch + +from metasim.types import TensorState +from metasim.utils import configclass +from metasim.utils.math import ( + quat_apply, + quat_error_magnitude, + quat_from_euler_xyz, + quat_inv, + quat_mul, + sample_uniform, + yaw_quat, +) +from roboverse_pack.tasks.beyondmimic.metasim.utils.string import find_bodies + +if TYPE_CHECKING: + from roboverse_pack.tasks.beyondmimic.metasim.envs.tracking_g1 import TrackingG1Task + + +class MotionLoader: + """Load motion data from a file and provide access to target joint states and body positions and orientations in world frame.""" + + def __init__(self, motion_file: str, body_indexes: Sequence[int], device: str = "cpu"): + assert os.path.isfile(motion_file), f"Invalid file path: {motion_file}" + data = np.load(motion_file) + self.fps = data["fps"] + + # target joint states from motion file + self.joint_pos = torch.tensor( + data["joint_pos"], dtype=torch.float32, device=device + ) # [n_timesteps, n_dofs] -> [6574, 29] + self.joint_vel = torch.tensor(data["joint_vel"], dtype=torch.float32, device=device) # [n_timesteps, n_dofs] + # target body positions and orientations in world frame + self._body_pos_w = torch.tensor( + data["body_pos_w"], dtype=torch.float32, device=device + ) # [n_timesteps, n_bodies, 3] -> [6574, 30, 3] + self._body_quat_w = torch.tensor( + data["body_quat_w"], dtype=torch.float32, device=device + ) # [n_timesteps, n_bodies, 4] + + # target body velocities + self._body_lin_vel_w = torch.tensor( + data["body_lin_vel_w"], dtype=torch.float32, device=device + ) # [n_timesteps, n_bodies, 3] + self._body_ang_vel_w = torch.tensor( + data["body_ang_vel_w"], dtype=torch.float32, device=device + ) # [n_timesteps, n_bodies, 3] + self._body_indexes = body_indexes # [n_indexes] -> [14] + self.time_step_total = self.joint_pos.shape[0] + + @property + def body_pos_w(self) -> torch.Tensor: + """Get the body positions in world frame.""" + return self._body_pos_w[:, self._body_indexes] + + @property + def body_quat_w(self) -> torch.Tensor: + """Get the body quaternions in world frame.""" + return self._body_quat_w[:, self._body_indexes] + + @property + def body_lin_vel_w(self) -> torch.Tensor: + """Get the body linear velocities in world frame.""" + return self._body_lin_vel_w[:, self._body_indexes] + + @property + def body_ang_vel_w(self) -> torch.Tensor: + """Get the body angular velocities in world frame.""" + return self._body_ang_vel_w[:, self._body_indexes] + + +@configclass +class MotionCommandCfg: + """Configuration for motion commands.""" + + motion_file: str = MISSING + anchor_body_name: str = MISSING + body_names: list[str] = MISSING # a subset of body link names to track + + resampling_time_range: tuple[float, float] = MISSING + pose_range: dict[str, tuple[float, float]] = {} + velocity_range: dict[str, tuple[float, float]] = {} + joint_position_range: tuple[float, float] = (-0.52, 0.52) + + adaptive_kernel_size: int = 1 + adaptive_lambda: float = 0.8 + adaptive_uniform_ratio: float = 0.1 + adaptive_alpha: float = 0.001 + + +class MotionCommand: + """Motion command which handles updating, resampling, etc.""" + + def __init__(self, env: TrackingG1Task, cfg: MotionCommandCfg): + self.env = env + self.cfg = cfg + self.device = env.device + + # time left before resampling + self.time_left = torch.zeros(env.num_envs, device=env.device) + + self.robot_anchor_body_index = env.sorted_body_names.index(self.cfg.anchor_body_name) + self.motion_anchor_body_index = self.cfg.body_names.index(self.cfg.anchor_body_name) + self.body_indexes = torch.tensor( + find_bodies(self.cfg.body_names, env.sorted_body_names, preserve_order=True)[0] + ) + + # load motions + body_names_original = env.handler.get_body_names(env.robot.name, sort=False) + body_indexes_original = torch.tensor( + find_bodies(self.cfg.body_names, body_names_original, preserve_order=True)[0] + ) + self.motion = MotionLoader(cfg.motion_file, body_indexes_original, env.device) + + self.time_steps = torch.zeros(env.num_envs, dtype=torch.long, device=env.device) + self.body_pos_relative_w = torch.zeros(env.num_envs, len(self.cfg.body_names), 3, device=env.device) + self.body_quat_relative_w = torch.zeros(env.num_envs, len(self.cfg.body_names), 4, device=env.device) + self.body_quat_relative_w[:, :, 0] = 1.0 + + self.bin_count = int(self.motion.time_step_total // (1 / env.step_dt)) + 1 + self.bin_failed_count = torch.zeros(self.bin_count, dtype=torch.float, device=env.device) + self._current_bin_failed = torch.zeros(self.bin_count, dtype=torch.float, device=env.device) + self.kernel = torch.tensor( + [self.cfg.adaptive_lambda**i for i in range(self.cfg.adaptive_kernel_size)], device=env.device + ) + self.kernel = self.kernel / self.kernel.sum() + + # metrics used for logging + metrics = [ + "error_anchor_pos", + "error_anchor_rot", + "error_anchor_lin_vel", + "error_anchor_ang_vel", + "error_body_pos", + "error_body_rot", + "error_joint_pos", + "error_joint_vel", + "sampling_entropy", + "sampling_top1_prob", + "sampling_top1_bin", + ] + self.metrics = {k: torch.zeros(env.num_envs, device=env.device) for k in metrics} + + def _adaptive_sampling(self, env_ids: Sequence[int]): + episode_failed = self.env.reset_terminated[env_ids] # excluding time-out buf + if torch.any(episode_failed): + current_bin_index = torch.clamp( + (self.time_steps * self.bin_count) // max(self.motion.time_step_total, 1), 0, self.bin_count - 1 + ) + fail_bins = current_bin_index[env_ids][episode_failed] + self._current_bin_failed[:] = torch.bincount(fail_bins, minlength=self.bin_count) + + # sample + sampling_probabilities = self.bin_failed_count + self.cfg.adaptive_uniform_ratio / float(self.bin_count) + sampling_probabilities = torch.nn.functional.pad( + sampling_probabilities.unsqueeze(0).unsqueeze(0), + (0, self.cfg.adaptive_kernel_size - 1), # non-causal kernel + mode="replicate", + ) + sampling_probabilities = torch.nn.functional.conv1d(sampling_probabilities, self.kernel.view(1, 1, -1)).view(-1) + + sampling_probabilities = sampling_probabilities / sampling_probabilities.sum() + + sampled_bins = torch.multinomial(sampling_probabilities, len(env_ids), replacement=True) + + self.time_steps[env_ids] = ( + (sampled_bins + sample_uniform(0.0, 1.0, (len(env_ids),), device=self.device)) + / self.bin_count + * (self.motion.time_step_total - 1) + ).long() + + # metrics + H = -(sampling_probabilities * (sampling_probabilities + 1e-12).log()).sum() + H_norm = H / math.log(self.bin_count) + pmax, imax = sampling_probabilities.max(dim=0) + self.metrics["sampling_entropy"][:] = H_norm + self.metrics["sampling_top1_prob"][:] = pmax + self.metrics["sampling_top1_bin"][:] = imax.float() / self.bin_count + + def _resample_command(self, env_ids: Sequence[int], env_states: TensorState): + if len(env_ids) == 0: + return + self._adaptive_sampling(env_ids) + + root_pos = self.body_pos_w[:, 0].clone() # (n_envs, 3) + root_ori = self.body_quat_w[:, 0].clone() + root_lin_vel = self.body_lin_vel_w[:, 0].clone() + root_ang_vel = self.body_ang_vel_w[:, 0].clone() + + # perturb root position and orientation + range_list = [self.cfg.pose_range.get(key, (0.0, 0.0)) for key in ["x", "y", "z", "roll", "pitch", "yaw"]] + ranges = torch.tensor(range_list, device=self.device) + rand_samples = sample_uniform(ranges[:, 0], ranges[:, 1], (len(env_ids), 6), device=self.device) + root_pos[env_ids] += rand_samples[:, 0:3] + orientations_delta = quat_from_euler_xyz(rand_samples[:, 3], rand_samples[:, 4], rand_samples[:, 5]) # w first + root_ori[env_ids] = quat_mul(orientations_delta, root_ori[env_ids]) + + # perturb root velocities + range_list = [self.cfg.velocity_range.get(key, (0.0, 0.0)) for key in ["x", "y", "z", "roll", "pitch", "yaw"]] + ranges = torch.tensor(range_list, device=self.device) + rand_samples = sample_uniform(ranges[:, 0], ranges[:, 1], (len(env_ids), 6), device=self.device) + root_lin_vel[env_ids] += rand_samples[:, :3] + root_ang_vel[env_ids] += rand_samples[:, 3:] + + # perturb joint positions and velocities then clamp to limits + joint_pos = self.joint_pos.clone() + joint_vel = self.joint_vel.clone() + joint_pos += sample_uniform(*self.cfg.joint_position_range, joint_pos.shape, joint_pos.device) + + soft_joint_pos_limits = self.env.soft_dof_pos_limits_original[env_ids] + + joint_pos[env_ids] = torch.clip( + joint_pos[env_ids], soft_joint_pos_limits[:, :, 0], soft_joint_pos_limits[:, :, 1] + ) + + env_states.robots[self.env.name].joint_pos[env_ids] = joint_pos[ + env_ids.unsqueeze(1), self.env.original_to_sorted_joint_indexes + ] + env_states.robots[self.env.name].joint_vel[env_ids] = joint_vel[ + env_ids.unsqueeze(1), self.env.original_to_sorted_joint_indexes + ] + env_states.robots[self.env.name].root_state[env_ids, :] = torch.cat( + [root_pos[env_ids], root_ori[env_ids], root_lin_vel[env_ids], root_ang_vel[env_ids]], dim=-1 + ) + self.env.handler.set_states(env_states, env_ids) + + def _update_command(self, env_states: TensorState): + # pick new time steps using adaptive sampling for the envs that have reached the end of the motion + self.time_steps += 1 + env_ids = torch.where(self.time_steps >= self.motion.time_step_total)[0] + self._resample_command(env_ids, env_states) # resample when motion ends + + env_states = self.env.handler.get_states() + robot_state = env_states.robots[self.env.name] + + # put reference motion at the robot's XY (rotate it so its heading matches the robot's heading) while keeping the motion's height (Z) + anchor_pos_w_repeat = self.anchor_pos_w[:, None, :].repeat( + 1, len(self.cfg.body_names), 1 + ) # [n_envs, n_bodies, 3], n_bodies = 14 + anchor_quat_w_repeat = self.anchor_quat_w[:, None, :].repeat( + 1, len(self.cfg.body_names), 1 + ) # [n_envs, n_bodies, 4] + robot_anchor_pos_w_repeat = robot_state.body_state[ + :, self.robot_anchor_body_index : self.robot_anchor_body_index + 1, 0:3 + ].repeat(1, len(self.cfg.body_names), 1) # [n_envs, n_bodies, 3] + robot_anchor_quat_w_repeat = robot_state.body_state[ + :, self.robot_anchor_body_index : self.robot_anchor_body_index + 1, 3:7 + ].repeat(1, len(self.cfg.body_names), 1) # [n_envs, n_bodies, 4] + + # let XY come from the robot anchor, and Z comes from the motion anchor + # avoid penalizing global XY drift while preserving vertical posture from the motion + delta_pos_w = robot_anchor_pos_w_repeat + delta_pos_w[..., 2] = anchor_pos_w_repeat[..., 2] # overwrite Z + + # first compute relative rotation between robot and motion anchors, then keep only the yaw component + delta_ori_w = yaw_quat(quat_mul(robot_anchor_quat_w_repeat, quat_inv(anchor_quat_w_repeat))) + self.body_quat_relative_w = quat_mul(delta_ori_w, self.body_quat_w) # original order + self.body_pos_relative_w = delta_pos_w + quat_apply(delta_ori_w, self.body_pos_w - anchor_pos_w_repeat) + + self.bin_failed_count = ( + self.cfg.adaptive_alpha * self._current_bin_failed + (1 - self.cfg.adaptive_alpha) * self.bin_failed_count + ) + self._current_bin_failed.zero_() + + def _update_metrics(self, env_states: TensorState): + """Update metrics for logging.""" + robot_state = env_states.robots[self.env.name] + self.metrics["error_anchor_pos"] = torch.norm( + self.anchor_pos_w - robot_state.body_state[:, self.robot_anchor_body_index, :3], dim=-1 + ) + self.metrics["error_anchor_rot"] = quat_error_magnitude( + self.anchor_quat_w, robot_state.body_state[:, self.robot_anchor_body_index, 3:7] + ) + self.metrics["error_anchor_lin_vel"] = torch.norm( + self.anchor_lin_vel_w - robot_state.body_state[:, self.robot_anchor_body_index, 7:10], dim=-1 + ) + self.metrics["error_anchor_ang_vel"] = torch.norm( + self.anchor_ang_vel_w - robot_state.body_state[:, self.robot_anchor_body_index, 10:13], dim=-1 + ) + + self.metrics["error_body_pos"] = torch.norm( + self.body_pos_relative_w - robot_state.body_state[:, self.body_indexes, :3], dim=-1 + ).mean(dim=-1) + self.metrics["error_body_rot"] = quat_error_magnitude( + self.body_quat_relative_w, robot_state.body_state[:, self.body_indexes, 3:7] + ).mean(dim=-1) + + self.metrics["error_body_lin_vel"] = torch.norm( + self.body_lin_vel_w - robot_state.body_state[:, self.body_indexes, 7:10], dim=-1 + ).mean(dim=-1) + self.metrics["error_body_ang_vel"] = torch.norm( + self.body_ang_vel_w - robot_state.body_state[:, self.body_indexes, 10:13], dim=-1 + ).mean(dim=-1) + + self.metrics["error_joint_pos"] = torch.norm( + self.joint_pos - robot_state.joint_pos[:, self.env.sorted_to_original_joint_indexes], dim=-1 + ) + self.metrics["error_joint_vel"] = torch.norm( + self.joint_vel - robot_state.joint_vel[:, self.env.sorted_to_original_joint_indexes], dim=-1 + ) + + def reset(self, env_ids: Sequence[int] | None = None): + """Reset the motion command (similar to `CommandTerm.reset()` in Isaac Lab).""" + if env_ids is None: + env_ids = torch.arange(self.env.num_envs, dtype=torch.int64, device=self.device) + + # add logging metrics + extras = {} + for metric_name, metric_value in self.metrics.items(): + # compute the mean metric value + extras[metric_name] = torch.mean(metric_value[env_ids]).item() + # reset the metric value + metric_value[env_ids] = 0.0 + + # resample the command + self._resample(env_ids, self.env.handler.get_states()) + + return extras + + def compute(self, env_states: TensorState): + """Compute the motion command.""" + # update the metrics based on current state + self._update_metrics(env_states) + # reduce the time left before resampling by the timestep passed since the last call + self.time_left -= self.env.step_dt + # resample the command if necessary + resample_env_ids = (self.time_left <= 0.0).nonzero().flatten() + if len(resample_env_ids) > 0: + self._resample(resample_env_ids, env_states) + env_states = self.handler.get_states() + # update the command + self._update_command(env_states) + + def _resample(self, env_ids: Sequence[int], env_states: TensorState): + """Resample the command.""" + if len(env_ids) != 0: + # resample the time left before resampling + self.time_left[env_ids] = self.time_left[env_ids].uniform_(*self.cfg.resampling_time_range) + # resample the command + self._resample_command(env_ids, env_states) + + @property + def command(self) -> torch.Tensor: + """Get the command. Needed in observations.""" + return torch.cat([self.joint_pos, self.joint_vel], dim=1) + + @property + def joint_pos(self) -> torch.Tensor: + """Get the joint positions.""" + return self.motion.joint_pos[self.time_steps] + + @property + def joint_vel(self) -> torch.Tensor: + """Get the joint velocities.""" + return self.motion.joint_vel[self.time_steps] + + @property + def body_pos_w(self) -> torch.Tensor: + """Get the body positions in world frame.""" + # NOTE handler's `_set_states()` and `_get_states()` will handle `env_origins` internally (subtracts after getting states, and adds before setting states) + return self.motion.body_pos_w[self.time_steps] + + @property + def body_quat_w(self) -> torch.Tensor: + """Get the body quaternions in world frame.""" + return self.motion.body_quat_w[self.time_steps] + + @property + def body_lin_vel_w(self) -> torch.Tensor: + """Get the body linear velocities in world frame.""" + return self.motion.body_lin_vel_w[self.time_steps] + + @property + def body_ang_vel_w(self) -> torch.Tensor: + """Get the body angular velocities in world frame.""" + return self.motion.body_ang_vel_w[self.time_steps] + + @property + def anchor_pos_w(self) -> torch.Tensor: + """Get the anchor position in world frame.""" + return self.motion.body_pos_w[self.time_steps, self.motion_anchor_body_index] + + @property + def anchor_quat_w(self) -> torch.Tensor: + """Get the anchor quaternion in world frame.""" + return self.motion.body_quat_w[self.time_steps, self.motion_anchor_body_index] + + @property + def anchor_lin_vel_w(self) -> torch.Tensor: + """Get the anchor linear velocity in world frame.""" + return self.motion.body_lin_vel_w[self.time_steps, self.motion_anchor_body_index] + + @property + def anchor_ang_vel_w(self) -> torch.Tensor: + """Get the anchor angular velocity in world frame.""" + return self.motion.body_ang_vel_w[self.time_steps, self.motion_anchor_body_index] diff --git a/roboverse_pack/tasks/beyondmimic/metasim/mdp/events.py b/roboverse_pack/tasks/beyondmimic/metasim/mdp/events.py new file mode 100644 index 000000000..f0bd6bfa4 --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/metasim/mdp/events.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal + +import torch + +from metasim.types import TensorState +from metasim.utils.math import sample_uniform +from roboverse_pack.tasks.beyondmimic.metasim.configs.cfg_randomizers import randomize_prop_by_op + +if TYPE_CHECKING: + from roboverse_pack.tasks.beyondmimic.metasim.envs.base_legged_robot import LeggedRobotTask + + +def randomize_joint_default_pos( # startup + env: LeggedRobotTask, + env_ids: torch.Tensor | None = None, + joint_ids: torch.Tensor | None = None, + pos_distribution_params: tuple[float, float] | None = None, + operation: Literal["add", "scale", "abs"] = "abs", + distribution: Literal["uniform", "log_uniform", "gaussian"] = "uniform", +): + """Randomize the joint default positions which may be different from URDF due to calibration errors.""" + # save nominal value for export + env.default_dof_pos_nominal = torch.clone(env.default_dof_pos_sorted[0]) + + # resolve environment ids + if env_ids is None: + env_ids = torch.arange(env.scenario.num_envs, device=env.device) + + # resolve joint indices + if joint_ids is None: + joint_ids = torch.arange(len(env.sorted_joint_names), device=env.device) + + if pos_distribution_params is not None: + # pos = env.default_dof_pos.unsqueeze(0).repeat(env.scenario.num_envs, 1).to(env.device).clone() # FIXME + pos = env.default_dof_pos_sorted.to(env.device).clone() # [n_envs, n_dofs] + pos = randomize_prop_by_op( + pos, pos_distribution_params, env_ids, joint_ids, operation=operation, distribution=distribution + )[env_ids][:, joint_ids] + + if env_ids != slice(None) and joint_ids != slice(None): + env_ids = env_ids[:, None] + env.default_dof_pos_sorted[env_ids, joint_ids] = pos + + +def push_by_setting_velocity( + env: LeggedRobotTask, + env_states: TensorState, + interval_range_s: tuple | int, + velocity_range: dict[str, tuple[float, float]], +): + """Randomly set robot's root velocity to simulate a push. + + The function takes a dictionary of velocity ranges for each axis and rotation. The keys of the dictionary are "x", "y", "z", "roll", "pitch", and "yaw". The values are tuples of the form (min, max). + """ + if not hasattr(env, "push_interval"): + env.push_interval = ( + sample_uniform( + interval_range_s[0], + interval_range_s[1], + (env.num_envs, 1), + device=env.device, + ).flatten() + / env.step_dt # convert seconds to simulation steps + ).to(torch.int) + # TODO different from how interval events are triggered in Isaac Lab, consider adapting this + push_env_ids = ( + torch.logical_and(env._episode_steps % env.push_interval == 0, env._episode_steps > 0) + .nonzero(as_tuple=False) + .flatten() + ) + if len(push_env_ids) == 0: + return + ranges = torch.tensor( + [velocity_range.get(key, (0.0, 0.0)) for key in ["x", "y", "z", "roll", "pitch", "yaw"]], device=env.device + ) + env_states.robots[env.name].root_state[push_env_ids, 7:13] += sample_uniform( + ranges[:, 0], ranges[:, 1], (len(push_env_ids), 6), device=env.device + ) # add random velocity to root's linear and angular velocities + + env.handler.set_states(env_states, push_env_ids.tolist()) diff --git a/roboverse_pack/tasks/beyondmimic/metasim/mdp/observations.py b/roboverse_pack/tasks/beyondmimic/metasim/mdp/observations.py new file mode 100644 index 000000000..711cfa205 --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/metasim/mdp/observations.py @@ -0,0 +1,117 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import torch + +from metasim.types import TensorState +from metasim.utils.math import matrix_from_quat, quat_rotate_inverse, subtract_frame_transforms + +if TYPE_CHECKING: + from roboverse_pack.tasks.beyondmimic.metasim.envs.base_legged_robot import LeggedRobotTask + + +# NOTE all return values are in the original order to align with checkpoints trained by original BeyondMimic repo + + +def robot_body_pos_b(env: LeggedRobotTask, env_states: TensorState) -> torch.Tensor: + """Body positions relative to (robot) anchor frame.""" + robot_state = env_states.robots[env.name] + body_indexes = env.commands.body_indexes + anchor_index = env.commands.robot_anchor_body_index + + num_bodies = len(env.commands.cfg.body_names) + pos_b, _ = subtract_frame_transforms( + robot_state.body_state[:, anchor_index : anchor_index + 1, :3].repeat(1, num_bodies, 1), + robot_state.body_state[:, anchor_index : anchor_index + 1, 3:7].repeat(1, num_bodies, 1), + robot_state.body_state[:, body_indexes, :3], + robot_state.body_state[:, body_indexes, 3:7], + ) # [n_envs, n_bodies, 3] positions of each body relative to the anchor frame + + return pos_b.view(env.num_envs, -1) # [n_envs, n_bodies * 3] + + +def robot_body_ori_b(env: LeggedRobotTask, env_states: TensorState) -> torch.Tensor: + """Body orientations relative to anchor frame.""" + robot_state = env_states.robots[env.name] + body_indexes = env.commands.body_indexes + anchor_index = env.commands.robot_anchor_body_index + + num_bodies = len(env.commands.cfg.body_names) + _, ori_b = subtract_frame_transforms( + robot_state.body_state[:, anchor_index : anchor_index + 1, :3].repeat(1, num_bodies, 1), + robot_state.body_state[:, anchor_index : anchor_index + 1, 3:7].repeat(1, num_bodies, 1), + robot_state.body_state[:, body_indexes, :3], + robot_state.body_state[:, body_indexes, 3:7], + ) + mat = matrix_from_quat(ori_b) + return mat[..., :2].reshape(mat.shape[0], -1) + + +def motion_anchor_pos_b(env: LeggedRobotTask, env_states: TensorState) -> torch.Tensor: + """Target anchor position relative to anchor frame.""" + robot_state = env_states.robots[env.name] + anchor_index = env.commands.robot_anchor_body_index + + pos, _ = subtract_frame_transforms( + robot_state.body_state[:, anchor_index, :3], + robot_state.body_state[:, anchor_index, 3:7], + env.commands.anchor_pos_w, + env.commands.anchor_quat_w, + ) + + return pos.view(env.num_envs, -1) + + +def motion_anchor_ori_b(env: LeggedRobotTask, env_states: TensorState) -> torch.Tensor: + """Target anchor orientation relative to anchor frame.""" + robot_state = env_states.robots[env.name] + anchor_index = env.commands.robot_anchor_body_index + + _, ori = subtract_frame_transforms( + robot_state.body_state[:, anchor_index, :3], # [n_envs, 3] + robot_state.body_state[:, anchor_index, 3:7], # [n_envs, 4] + env.commands.anchor_pos_w, # [n_envs, 3] + env.commands.anchor_quat_w, # [n_envs, 4] + ) # [n_envs, 4] quaternion representing the relative rotation between the two frames + mat = matrix_from_quat(ori) # [n_envs, 3, 3] convert to rotation matrix + return mat[..., :2].reshape( + mat.shape[0], -1 + ) # [n_envs, 6] extract the first two rows because the third row can be derived from orthogonality + + +def generated_commands(env: LeggedRobotTask, env_states: TensorState) -> torch.Tensor: + """The generated command from command term in the command manager with the given name.""" + # return env.command_manager.get_command(command_name) + return env.commands.command + + +def base_lin_vel(env: LeggedRobotTask, env_states: TensorState) -> torch.Tensor: + """Root linear velocity in the robot's root frame.""" + robot_state = env_states.robots[env.name] + return quat_rotate_inverse(robot_state.root_state[:, 3:7], robot_state.root_state[:, 7:10]) + + +def base_ang_vel(env: LeggedRobotTask, env_states: TensorState) -> torch.Tensor: + """Root angular velocity in the robot's root frame.""" + robot_state = env_states.robots[env.name] + return quat_rotate_inverse(robot_state.root_state[:, 3:7], robot_state.root_state[:, 10:13]) + + +def joint_pos_rel(env: LeggedRobotTask, env_states: TensorState) -> torch.Tensor: + """The joint positions of the robot w.r.t. the default joint positions.""" + robot_state = env_states.robots[env.name] + joint_pos_sorted = robot_state.joint_pos - env.default_dof_pos_sorted + return joint_pos_sorted[:, env.sorted_to_original_joint_indexes] + + +def joint_vel_rel(env: LeggedRobotTask, env_states: TensorState): + """The joint velocities of the robot w.r.t. the default joint velocities.""" + robot_state = env_states.robots[env.name] + joint_vel_sorted = robot_state.joint_vel - env.default_dof_vel_sorted + return joint_vel_sorted[:, env.sorted_to_original_joint_indexes] # (n_envs, n_dofs) + + +def last_action(env: LeggedRobotTask, env_states: TensorState) -> torch.Tensor: + """The last input action to the environment.""" + return env._action diff --git a/roboverse_pack/tasks/beyondmimic/metasim/mdp/rewards.py b/roboverse_pack/tasks/beyondmimic/metasim/mdp/rewards.py new file mode 100644 index 000000000..f50c07b14 --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/metasim/mdp/rewards.py @@ -0,0 +1,121 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import torch + +from metasim.types import TensorState +from metasim.utils.math import quat_error_magnitude +from roboverse_pack.tasks.beyondmimic.metasim.configs.cfg_queries import ContactForces +from roboverse_pack.tasks.beyondmimic.metasim.utils.misc import get_body_indexes +from roboverse_pack.tasks.beyondmimic.metasim.utils.string import get_indexes_hash + +if TYPE_CHECKING: + from roboverse_pack.tasks.beyondmimic.metasim.envs.base_legged_robot import LeggedRobotTask + + +def motion_global_anchor_position_error_exp(env: LeggedRobotTask, env_states: TensorState, std: float) -> torch.Tensor: + """Global anchor position error.""" + robot_state = env_states.robots[env.name] + anchor_index = env.commands.robot_anchor_body_index + error = torch.sum(torch.square(env.commands.anchor_pos_w - robot_state.body_state[:, anchor_index, :3]), dim=-1) + return torch.exp(-error / std**2) + + +def motion_global_anchor_orientation_error_exp( + env: LeggedRobotTask, env_states: TensorState, std: float +) -> torch.Tensor: + """Global anchor orientation error.""" + robot_state = env_states.robots[env.name] + anchor_index = env.commands.robot_anchor_body_index + error = quat_error_magnitude(env.commands.anchor_quat_w, robot_state.body_state[:, anchor_index, 3:7]) ** 2 + return torch.exp(-error / std**2) + + +def motion_relative_body_position_error_exp( + env: LeggedRobotTask, env_states: TensorState, std: float, body_names: list[str] | None = None +) -> torch.Tensor: + """Relative body position error.""" + body_state = env_states.robots[env.name].body_state[:, env.commands.body_indexes, :] + body_indexes = get_body_indexes(env.commands, body_names) + error = torch.sum( + torch.square(env.commands.body_pos_relative_w[:, body_indexes] - body_state[:, body_indexes, :3]), dim=-1 + ) + return torch.exp(-error.mean(-1) / std**2) + + +def motion_relative_body_orientation_error_exp( + env: LeggedRobotTask, env_states: TensorState, std: float, body_names: list[str] | None = None +) -> torch.Tensor: + """Relative body orientation error.""" + body_state = env_states.robots[env.name].body_state[:, env.commands.body_indexes, :] + body_indexes = get_body_indexes(env.commands, body_names) + error = ( + quat_error_magnitude(env.commands.body_quat_relative_w[:, body_indexes], body_state[:, body_indexes, 3:7]) ** 2 + ) + return torch.exp(-error.mean(-1) / std**2) + + +def motion_global_body_linear_velocity_error_exp( + env: LeggedRobotTask, env_states: TensorState, std: float, body_names: list[str] | None = None +) -> torch.Tensor: + """Linear velocity tracking error.""" + body_state = env_states.robots[env.name].body_state[:, env.commands.body_indexes, :] + body_indexes = get_body_indexes(env.commands, body_names) + error = torch.sum( + torch.square(env.commands.body_lin_vel_w[:, body_indexes] - body_state[:, body_indexes, 7:10]), dim=-1 + ) + return torch.exp(-error.mean(-1) / std**2) + + +def motion_global_body_angular_velocity_error_exp( + env: LeggedRobotTask, env_states: TensorState, std: float, body_names: list[str] | None = None +) -> torch.Tensor: + """Angular velocity tracking error.""" + body_state = env_states.robots[env.name].body_state[:, env.commands.body_indexes, :] + body_indexes = get_body_indexes(env.commands, body_names) + error = torch.sum( + torch.square(env.commands.body_ang_vel_w[:, body_indexes] - body_state[:, body_indexes, 10:13]), dim=-1 + ) + return torch.exp(-error.mean(-1) / std**2) + + +def action_rate_l2(env: LeggedRobotTask, env_states: TensorState) -> torch.Tensor: + """Penalize the rate of change of the actions using L2 squared kernel.""" + # NOTE `env.actions` is already in the original order + return torch.sum(torch.square(env._action - env._prev_action), dim=1) # [n_envs, n_dims] + + +def joint_pos_limits(env: LeggedRobotTask, env_states: TensorState) -> torch.Tensor: + """Penalize joint positions if they cross the soft limits. + + This is computed as a sum of the absolute value of the difference between the joint position and the soft limits. + """ + robot_state = env_states.robots[env.name] + out_of_limits = -(robot_state.joint_pos - env.soft_dof_pos_limits_sorted[:, :, 0]).clip(max=0.0) + out_of_limits += (robot_state.joint_pos - env.soft_dof_pos_limits_sorted[:, :, 1]).clip(min=0.0) + return torch.sum(out_of_limits, dim=1) + + +def undesired_contacts( + env: LeggedRobotTask, + env_states: TensorState, + threshold: float, + body_names: str | tuple[str] = "(?!.*ankle.*).*", +) -> torch.Tensor: + """Penalize undesired contacts as the number of violations that are above a threshold.""" + # body_indexes = get_body_indexes(env.commands, body_names) + indexes = get_indexes_hash(env, body_names, env_states.robots[env.name].body_names) + contact_forces: ContactForces = env_states.extras["contact_forces"][env.name] + is_contact = ( + # TODO check correspondence with `contact_sensor.data.net_forces_w_history` + contact_forces.contact_forces_history[ + :, :, indexes, : + ] # [n_envs, history_length, n_indexes, 3] -> [n_envs, 3, 26, 3] + .norm(dim=-1) + .max(dim=1)[0] + > threshold + ) + + # sum over contacts for each environment + return torch.sum(is_contact, dim=1) # [n_envs] diff --git a/roboverse_pack/tasks/beyondmimic/metasim/mdp/terminations.py b/roboverse_pack/tasks/beyondmimic/metasim/mdp/terminations.py new file mode 100644 index 000000000..f5f123074 --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/metasim/mdp/terminations.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import torch + +from metasim.types import TensorState +from metasim.utils.math import quat_rotate_inverse +from roboverse_pack.tasks.beyondmimic.metasim.utils.misc import get_body_indexes + +if TYPE_CHECKING: + from roboverse_pack.tasks.beyondmimic.metasim.envs.base_legged_robot import LeggedRobotTask + + +# NOTE `env_states` is needed here for compatibility with how callbacks are triggered +def time_out(env: LeggedRobotTask, env_states: TensorState) -> torch.Tensor: + """Terminate the episode when the episode length exceeds the maximum episode length.""" + return env._episode_steps >= env.max_episode_steps + + +# NOTE bodies are in the original order + + +def bad_anchor_pos_z_only(env: LeggedRobotTask, env_states: TensorState, threshold: float) -> torch.Tensor: + """Bad anchor position z-only.""" + robot_state = env_states.robots[env.name] + anchor_index = env.commands.robot_anchor_body_index + return torch.abs(env.commands.anchor_pos_w[:, 2] - robot_state.body_state[:, anchor_index, 2]) > threshold + + +def bad_anchor_ori(env: LeggedRobotTask, env_states: TensorState, threshold: float) -> torch.Tensor: + """Bad anchor orientation.""" + robot_state = env_states.robots[env.name] + anchor_index = env.commands.robot_anchor_body_index + motion_projected_gravity_b = quat_rotate_inverse(env.commands.anchor_quat_w, env.gravity_vec) # [n_envs, 3] + robot_projected_gravity_b = quat_rotate_inverse(robot_state.body_state[:, anchor_index, 3:7], env.gravity_vec) + + # check whether the robot's tilt magnitude deviates too much (how relatively "upright"), and ignores which way it leans + return (motion_projected_gravity_b[:, 2] - robot_projected_gravity_b[:, 2]).abs() > threshold + + +def bad_motion_body_pos_z_only( + env: LeggedRobotTask, env_states: TensorState, threshold: float, body_names: list[str] +) -> torch.Tensor: + """Bad motion body position z-only.""" + body_state = env_states.robots[env.name].body_state[:, env.commands.body_indexes, :] + body_indexes = get_body_indexes(env.commands, body_names) + error = torch.abs(env.commands.body_pos_relative_w[:, body_indexes, 2] - body_state[:, body_indexes, 2]) + return torch.any(error > threshold, dim=-1) diff --git a/roboverse_pack/tasks/beyondmimic/metasim/utils/misc.py b/roboverse_pack/tasks/beyondmimic/metasim/utils/misc.py new file mode 100644 index 000000000..4421856f2 --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/metasim/utils/misc.py @@ -0,0 +1,213 @@ +from __future__ import annotations + +import argparse +import importlib +import os +import random +import re +from typing import TYPE_CHECKING + +import numpy as np +import torch +from loguru import logger as log + +from metasim.utils.setup_util import get_robot +from metasim.utils.string_util import is_camel_case, is_snake_case, to_camel_case + +if TYPE_CHECKING: + from roboverse_pack.tasks.beyondmimic.metasim.mdp.commands import MotionCommand + + +def get_args(): + """Get the command line arguments.""" + parser = argparse.ArgumentParser(description="Arguments for BeyondMimic motion tracking task") + parser.add_argument("--task", type=str, default=None, help="Name of the task") + parser.add_argument("--robots", type=str, default=None, help="Names of the robots to use") + parser.add_argument("--objects", type=str, default=None, help="Names of the objects to use") + parser.add_argument("--num_envs", type=int, default=4096, help="Number of environments to simulate") + parser.add_argument("--seed", type=int, default=None, help="Seed used for the environment") + parser.add_argument("--sim", type=str, default="isaacsim", help="Simulator type") + parser.add_argument("--headless", action="store_true", default=False, help="Run in headless mode") + + # for logging + parser.add_argument( + "--exp_name", type=str, default="tracking_g1", help="Name of the experiment folder where logs will be stored" + ) + parser.add_argument("--run_name", type=str, default=None, help="Run name suffix to the log directory") + parser.add_argument( + "--logger", type=str, default=None, choices={"wandb", "tensorboard", "neptune"}, help="Logger module to use." + ) + parser.add_argument( + "--log_project_name", type=str, default=None, help="Name of the logging project when using WandB or neptune" + ) + + # for loading + parser.add_argument( + "--resume", + type=bool, + default=False, + help="Whether to resume from a checkpoint. Should only be used for training", + ) + parser.add_argument( + "--load_run", type=str, default=None, help="Name of the local folder to resume from if not using WandB" + ) + parser.add_argument( + "--checkpoint", + type=str, + default=None, + help="Checkpoint file to resume from if not using WandB. Format: model_xxx.pt", + ) + + # evaluation args + parser.add_argument("--motion_file", type=str, default=None, help="Path to the local motion file") + parser.add_argument( + "--wandb_path", + type=str, + default=None, + help="The WandB run path to load from. Format: org/project/run_id(/model_xxx.pt)", + ) + + # training args + parser.add_argument("--max_iterations", type=int, default=30000, help="Max number of training iterations") + parser.add_argument("--registry_name", type=str, default=None, help="Name of the WandB registry") # required + + return parser.parse_args() + + +def set_seed(seed: int | None = None): + """Seed will be randomly initialized if it's None.""" + if not seed: + seed = np.random.randint(0, 10000) + log.info(f"Setting seed: {seed}") + + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + os.environ["PYTHONHASHSEED"] = str(seed) + torch.cuda.manual_seed(seed) + torch.cuda.manual_seed_all(seed) + + +def get_class(name: str, suffix: str, library="roboverse_learn.rl.beyondmimic"): + """Get the class wrappers. + + Example: + get_class("ReachOrigin", "Cfg") -> ReachOriginCfg + get_class("reach_origin", "Cfg") -> ReachOriginCfg + """ + if is_camel_case(name): + task_name_camel = name + elif is_snake_case(name): + task_name_camel = to_camel_case(name) + + wrapper_module = importlib.import_module(library) + wrapper_cls = getattr(wrapper_module, f"{task_name_camel}{suffix}") + return wrapper_cls + + +def make_robots(robots_str: str) -> list[any]: + """Make the robots.""" + robot_names = robots_str.split() + robots = [] + for _name in robot_names: + robots.append(get_robot(_name)) + return robots + + +def make_objects(objects_str: str) -> list[any]: + """Make the objects.""" + object_names = objects_str.split() + objects = [] + for _name in object_names: + objects.append( + get_class( + _name, + suffix="Cfg", + library="roboverse_learn.rl.unitree_rl.configs.cfg_objects", + )() + ) + return objects + + +def get_body_indexes(command: MotionCommand, body_names: list[str] | None = None) -> list[int]: + """Get the indexes of the bodies matching the body names.""" + return [i for i, name in enumerate(command.cfg.body_names) if (body_names is None) or (name in body_names)] + + +def get_axis_params(value, axis_idx, x_value=0.0, n_dims=3): + """Construct arguments to `Vec` according to axis index.""" + zs = torch.zeros((n_dims,)) + assert axis_idx < n_dims, "the axis dim should be within the vector dimensions" + zs[axis_idx] = 1.0 + params = torch.where(zs == 1.0, value, zs) + params[0] = x_value + return params.tolist() + + +# copied from `isaaclab_tasks.utils.parse_cfg.py` + + +def get_checkpoint_path( + log_path: str, + run_dir: str = ".*", + checkpoint: str = ".*", + other_dirs: list[str] | None = None, + sort_alpha: bool = True, +) -> str: + """Get path to the model checkpoint in input directory. + + The checkpoint file is resolved as: ``//<*other_dirs>/``, where the + :attr:`other_dirs` are intermediate folder names to concatenate. These cannot be regex expressions. + + If :attr:`run_dir` and :attr:`checkpoint` are regex expressions then the most recent (highest alphabetical order) + run and checkpoint are selected. To disable this behavior, set the flag :attr:`sort_alpha` to False. + + Args: + log_path: The log directory path to find models in. + run_dir: The regex expression for the name of the directory containing the run. Defaults to the most + recent directory created inside :attr:`log_path`. + other_dirs: The intermediate directories between the run directory and the checkpoint file. Defaults to + None, which implies that checkpoint file is directly under the run directory. + checkpoint: The regex expression for the model checkpoint file. Defaults to the most recent + torch-model saved in the :attr:`run_dir` directory. + sort_alpha: Whether to sort the runs by alphabetical order. Defaults to True. + If False, the folders in :attr:`run_dir` are sorted by the last modified time. + + Returns: + The path to the model checkpoint. + + Raises: + ValueError: When no runs are found in the input directory. + ValueError: When no checkpoints are found in the input directory. + + """ + # check if runs present in directory + try: + # find all runs in the directory that math the regex expression + runs = [ + os.path.join(log_path, run) for run in os.scandir(log_path) if run.is_dir() and re.match(run_dir, run.name) + ] + # sort matched runs by alphabetical order (latest run should be last) + if sort_alpha: + runs.sort() + else: + runs = sorted(runs, key=os.path.getmtime) + # create last run file path + if other_dirs is not None: + run_path = os.path.join(runs[-1], *other_dirs) + else: + run_path = runs[-1] + except IndexError as e: + raise ValueError(f"No runs present in the directory: '{log_path}' match: '{run_dir}'.") from e + + # list all model checkpoints in the directory + model_checkpoints = [f for f in os.listdir(run_path) if re.match(checkpoint, f)] + # check if any checkpoints are present + if len(model_checkpoints) == 0: + raise ValueError(f"No checkpoints in the directory: '{run_path}' match '{checkpoint}'.") + # sort alphabetically while ensuring that *_10 comes after *_9 + model_checkpoints.sort(key=lambda m: f"{m:0>15}") + # get latest matched checkpoint file + checkpoint_file = model_checkpoints[-1] + + return os.path.join(run_path, checkpoint_file) diff --git a/roboverse_pack/tasks/beyondmimic/metasim/utils/string.py b/roboverse_pack/tasks/beyondmimic/metasim/utils/string.py new file mode 100644 index 000000000..7ea41749b --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/metasim/utils/string.py @@ -0,0 +1,287 @@ +from __future__ import annotations + +import re +from collections.abc import Sequence +from functools import lru_cache +from typing import TYPE_CHECKING, Any + +import torch + +if TYPE_CHECKING: + from roboverse_pack.tasks.beyondmimic.metasim.envs.base_legged_robot import LeggedRobotTask + + +def get_indexes_from_substring( + candidates_list: list[str] | tuple[str] | str, + data_base: list[str], + fullmatch: bool = True, +) -> torch.Tensor: + """Get indexes of items matching the candidates patterns.""" + found_indexes = [] + if isinstance(candidates_list, str): + candidates_list = (candidates_list,) + assert isinstance(candidates_list, (list, tuple)), "candidates_list must be a list, tuple or string." + + for candidate in candidates_list: + # compile regex pattern for efficiency + try: + pattern = re.compile(candidate) + except re.error as e: + raise ValueError(f"Invalid regex pattern '{candidate}': {e}") from e + + for i, name in enumerate(data_base): + if fullmatch and pattern.fullmatch(name): + found_indexes.append(i) + elif not fullmatch and pattern.search(name): + found_indexes.append(i) + + # remove duplicates and sort + found_indexes = sorted(set(found_indexes)) + return torch.tensor(found_indexes, dtype=torch.int32, requires_grad=False) + + +@lru_cache(maxsize=128) +def hash_names(names: str | tuple[str]) -> str: + """Hash the names.""" + if isinstance(names, str): + names = (names,) + assert isinstance(names, tuple) and all(isinstance(_, str) for _ in names), ( + "body_names must be a string or a list of strings." + ) + hash_key = "_".join(sorted(names)) + return hash_key + + +def get_indexes_hash( # used by `undesired_contacts()` + env: LeggedRobotTask, sub_names: tuple[str] | str, all_names: list[str] | tuple[str] +) -> torch.Tensor: + """Get the indexes of the bodies matching the sub_names.""" + hash_key = hash_names(sub_names) + if hash_key not in env.extras_buffer: + env.extras_buffer[hash_key] = get_indexes_from_substring(sub_names, all_names, fullmatch=True).to(env.device) + return env.extras_buffer[hash_key] + + +def pattern_match(sub_names: dict[str, any], all_names: list[str]) -> dict[str, any]: + """Pattern match the sub_names to all_names using regex.""" + matched_names = {_key: 0.0 for _key in all_names} + for sub_key, sub_val in sub_names.items(): + pattern = re.compile(sub_key) + for name in all_names: + if pattern.fullmatch(name): + matched_names[name] = sub_val + return matched_names + + +# adapted from `isaaclab.utils.string.py` + + +def resolve_matching_names( + keys: str | Sequence[str], list_of_strings: Sequence[str], preserve_order: bool = False +) -> tuple[list[int], list[str]]: + """Match a list of query regular expressions against a list of strings and return the matched indices and names. + + When a list of query regular expressions is provided, the function checks each target string against each + query regular expression and returns the indices of the matched strings and the matched strings. + + If the :attr:`preserve_order` is True, the ordering of the matched indices and names is the same as the order + of the provided list of strings. This means that the ordering is dictated by the order of the target strings + and not the order of the query regular expressions. + + If the :attr:`preserve_order` is False, the ordering of the matched indices and names is the same as the order + of the provided list of query regular expressions. + + For example, consider the list of strings is ['a', 'b', 'c', 'd', 'e'] and the regular expressions are ['a|c', 'b']. + If :attr:`preserve_order` is False, then the function will return the indices of the matched strings and the + strings as: ([0, 1, 2], ['a', 'b', 'c']). When :attr:`preserve_order` is True, it will return them as: + ([0, 2, 1], ['a', 'c', 'b']). + + Note: + The function does not sort the indices. It returns the indices in the order they are found. + + Args: + keys: A regular expression or a list of regular expressions to match the strings in the list. + list_of_strings: A list of strings to match. + preserve_order: Whether to preserve the order of the query keys in the returned values. Defaults to False. + + Returns: + A tuple of lists containing the matched indices and names. + + Raises: + ValueError: When multiple matches are found for a string in the list. + ValueError: When not all regular expressions are matched. + """ + # resolve name keys + if isinstance(keys, str): + keys = [keys] + # find matching patterns + index_list = [] + names_list = [] + key_idx_list = [] + # book-keeping to check that we always have a one-to-one mapping + # i.e. each target string should match only one regular expression + target_strings_match_found = [None for _ in range(len(list_of_strings))] + keys_match_found = [[] for _ in range(len(keys))] + # loop over all target strings + for target_index, potential_match_string in enumerate(list_of_strings): + for key_index, re_key in enumerate(keys): + if re.fullmatch(re_key, potential_match_string): + # check if match already found + if target_strings_match_found[target_index]: + raise ValueError( + f"Multiple matches for '{potential_match_string}':" + f" '{target_strings_match_found[target_index]}' and '{re_key}'!" + ) + # add to list + target_strings_match_found[target_index] = re_key + index_list.append(target_index) + names_list.append(potential_match_string) + key_idx_list.append(key_index) + # add for regex key + keys_match_found[key_index].append(potential_match_string) + # reorder keys if they should be returned in order of the query keys + if preserve_order: + reordered_index_list = [None] * len(index_list) + global_index = 0 + for key_index in range(len(keys)): + for key_idx_position, key_idx_entry in enumerate(key_idx_list): + if key_idx_entry == key_index: + reordered_index_list[key_idx_position] = global_index + global_index += 1 + # reorder index and names list + index_list_reorder = [None] * len(index_list) + names_list_reorder = [None] * len(index_list) + for idx, reorder_idx in enumerate(reordered_index_list): + index_list_reorder[reorder_idx] = index_list[idx] + names_list_reorder[reorder_idx] = names_list[idx] + # update + index_list = index_list_reorder + names_list = names_list_reorder + # check that all regular expressions are matched + if not all(keys_match_found): + # make this print nicely aligned for debugging + msg = "\n" + for key, value in zip(keys, keys_match_found): + msg += f"\t{key}: {value}\n" + msg += f"Available strings: {list_of_strings}\n" + # raise error + raise ValueError( + f"Not all regular expressions are matched! Please check that the regular expressions are correct: {msg}" + ) + # return + return index_list, names_list + + +def find_bodies( + name_keys: str | Sequence[str], body_names: list[str], preserve_order: bool = False +) -> tuple[list[int], list[str]]: + """Find bodies in the articulation based on the name keys. + + Please check the :meth:`isaaclab.utils.string_utils.resolve_matching_names` function for more + information on the name matching. + + Args: + name_keys: A regular expression or a list of regular expressions to match the body names. + body_names: A list of body names to match. + preserve_order: Whether to preserve the order of the name keys in the output. Defaults to False. + + Returns: + A tuple of lists containing the body indices and names. + """ + return resolve_matching_names(name_keys, body_names, preserve_order) + + +def resolve_matching_names_values( + data: dict[str, Any], list_of_strings: Sequence[str], preserve_order: bool = False +) -> tuple[list[int], list[str], list[Any]]: + """Match a list of regular expressions in a dictionary against a list of strings and return the matched indices, names, and values. + + If the :attr:`preserve_order` is True, the ordering of the matched indices and names is the same as the order + of the provided list of strings. This means that the ordering is dictated by the order of the target strings + and not the order of the query regular expressions. + + If the :attr:`preserve_order` is False, the ordering of the matched indices and names is the same as the order + of the provided list of query regular expressions. + + For example, consider the dictionary is {"a|d|e": 1, "b|c": 2}, the list of strings is ['a', 'b', 'c', 'd', 'e']. + If :attr:`preserve_order` is False, then the function will return the indices of the matched strings, the + matched strings, and the values as: ([0, 1, 2, 3, 4], ['a', 'b', 'c', 'd', 'e'], [1, 2, 2, 1, 1]). When + :attr:`preserve_order` is True, it will return them as: ([0, 3, 4, 1, 2], ['a', 'd', 'e', 'b', 'c'], [1, 1, 1, 2, 2]). + + Args: + data: A dictionary of regular expressions and values to match the strings in the list. + list_of_strings: A list of strings to match. + preserve_order: Whether to preserve the order of the query keys in the returned values. Defaults to False. + + Returns: + A tuple of lists containing the matched indices, names, and values. + + Raises: + TypeError: When the input argument :attr:`data` is not a dictionary. + ValueError: When multiple matches are found for a string in the dictionary. + ValueError: When not all regular expressions in the data keys are matched. + """ + # check valid input + if not isinstance(data, dict): + raise TypeError(f"Input argument `data` should be a dictionary. Received: {data}") + # find matching patterns + index_list = [] + names_list = [] + values_list = [] + key_idx_list = [] + # book-keeping to check that we always have a one-to-one mapping + # i.e. each target string should match only one regular expression + target_strings_match_found = [None for _ in range(len(list_of_strings))] + keys_match_found = [[] for _ in range(len(data))] + # loop over all target strings + for target_index, potential_match_string in enumerate(list_of_strings): + for key_index, (re_key, value) in enumerate(data.items()): + if re.fullmatch(re_key, potential_match_string): + # check if match already found + if target_strings_match_found[target_index]: + raise ValueError( + f"Multiple matches for '{potential_match_string}':" + f" '{target_strings_match_found[target_index]}' and '{re_key}'!" + ) + # add to list + target_strings_match_found[target_index] = re_key + index_list.append(target_index) + names_list.append(potential_match_string) + values_list.append(value) + key_idx_list.append(key_index) + # add for regex key + keys_match_found[key_index].append(potential_match_string) + # reorder keys if they should be returned in order of the query keys + if preserve_order: + reordered_index_list = [None] * len(index_list) + global_index = 0 + for key_index in range(len(data)): + for key_idx_position, key_idx_entry in enumerate(key_idx_list): + if key_idx_entry == key_index: + reordered_index_list[key_idx_position] = global_index + global_index += 1 + # reorder index and names list + index_list_reorder = [None] * len(index_list) + names_list_reorder = [None] * len(index_list) + values_list_reorder = [None] * len(index_list) + for idx, reorder_idx in enumerate(reordered_index_list): + index_list_reorder[reorder_idx] = index_list[idx] + names_list_reorder[reorder_idx] = names_list[idx] + values_list_reorder[reorder_idx] = values_list[idx] + # update + index_list = index_list_reorder + names_list = names_list_reorder + values_list = values_list_reorder + # check that all regular expressions are matched + if not all(keys_match_found): + # make this print nicely aligned for debugging + msg = "\n" + for key, value in zip(data.keys(), keys_match_found): + msg += f"\t{key}: {value}\n" + msg += f"Available strings: {list_of_strings}\n" + # raise error + raise ValueError( + f"Not all regular expressions are matched! Please check that the regular expressions are correct: {msg}" + ) + # return + return index_list, names_list, values_list diff --git a/roboverse_pack/tasks/beyondmimic/scripts/convert_urdf.py b/roboverse_pack/tasks/beyondmimic/scripts/convert_urdf.py new file mode 100644 index 000000000..79e67dfb8 --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/scripts/convert_urdf.py @@ -0,0 +1,163 @@ +# Copyright (c) 2022-2025, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Utility to convert a URDF into USD format. + +Unified Robot Description Format (URDF) is an XML file format used in ROS to describe all elements of +a robot. For more information, see: http://wiki.ros.org/urdf + +This script uses the URDF importer extension from Isaac Sim (``isaacsim.asset.importer.urdf``) to convert a +URDF asset into USD format. It is designed as a convenience script for command-line use. For more +information on the URDF importer, see the documentation for the extension: +https://docs.isaacsim.omniverse.nvidia.com/latest/robot_setup/ext_isaacsim_asset_importer_urdf.html + + +positional arguments: + input The path to the input URDF file. + output The path to store the USD file. + +optional arguments: + -h, --help Show this help message and exit + --merge-joints Consolidate links that are connected by fixed joints. (default: False) + --fix-base Fix the base to where it is imported. (default: False) + --joint-stiffness The stiffness of the joint drive. (default: 100.0) + --joint-damping The damping of the joint drive. (default: 1.0) + --joint-target-type The type of control to use for the joint drive. (default: "position") + +""" + +"""Launch Isaac Sim Simulator first.""" + +import argparse + +from isaaclab.app import AppLauncher + +# add argparse arguments +parser = argparse.ArgumentParser(description="Utility to convert a URDF into USD format.") +parser.add_argument("input", type=str, help="The path to the input URDF file.") +parser.add_argument("output", type=str, help="The path to store the USD file.") +parser.add_argument( + "--merge-joints", + action="store_true", + default=False, + help="Consolidate links that are connected by fixed joints.", +) +parser.add_argument("--fix-base", action="store_true", default=False, help="Fix the base to where it is imported.") +parser.add_argument( + "--joint-stiffness", + type=float, + default=100.0, + help="The stiffness of the joint drive.", +) +parser.add_argument( + "--joint-damping", + type=float, + default=1.0, + help="The damping of the joint drive.", +) +parser.add_argument( + "--joint-target-type", + type=str, + default="position", + choices=["position", "velocity", "none"], + help="The type of control to use for the joint drive.", +) + +# append AppLauncher cli args +AppLauncher.add_app_launcher_args(parser) +# parse the arguments +args_cli = parser.parse_args() + +# launch omniverse app +app_launcher = AppLauncher(args_cli) +simulation_app = app_launcher.app + +"""Rest everything follows.""" + +import contextlib +import os + +import carb +import isaacsim.core.utils.stage as stage_utils +import omni.kit.app +from isaaclab.sim.converters import UrdfConverter, UrdfConverterCfg +from isaaclab.utils.assets import check_file_path +from isaaclab.utils.dict import print_dict +from loguru import logger as log + + +def main(): + """Main function to convert a URDF into USD format.""" + # check valid file path + urdf_path = args_cli.input + if not os.path.isabs(urdf_path): + urdf_path = os.path.abspath(urdf_path) + if not check_file_path(urdf_path): + raise ValueError(f"Invalid file path: {urdf_path}") + # create destination path + dest_path = args_cli.output + if not os.path.isabs(dest_path): + dest_path = os.path.abspath(dest_path) + + # Create Urdf converter config + urdf_converter_cfg = UrdfConverterCfg( + asset_path=urdf_path, + usd_dir=os.path.dirname(dest_path), + usd_file_name=os.path.basename(dest_path), + fix_base=args_cli.fix_base, + merge_fixed_joints=args_cli.merge_joints, + force_usd_conversion=True, + joint_drive=UrdfConverterCfg.JointDriveCfg( + gains=UrdfConverterCfg.JointDriveCfg.PDGainsCfg( + stiffness=args_cli.joint_stiffness, + damping=args_cli.joint_damping, + ), + target_type=args_cli.joint_target_type, + ), + ) + + # Print info + log.info("-" * 80) + log.info("-" * 80) + log.info(f"Input URDF file: {urdf_path}") + log.info("URDF importer config:") + print_dict(urdf_converter_cfg.to_dict(), nesting=0) + log.info("-" * 80) + log.info("-" * 80) + + # Create Urdf converter and import the file + urdf_converter = UrdfConverter(urdf_converter_cfg) + # print output + log.info("URDF importer output:") + log.info(f"Generated USD file: {urdf_converter.usd_path}") + log.info("-" * 80) + log.info("-" * 80) + + # Determine if there is a GUI to update: + # acquire settings interface + carb_settings_iface = carb.settings.get_settings() + # read flag for whether a local GUI is enabled + local_gui = carb_settings_iface.get("/app/window/enabled") + # read flag for whether livestreaming GUI is enabled + livestream_gui = carb_settings_iface.get("/app/livestream/enabled") + + # Simulate scene (if not headless) + if local_gui or livestream_gui: + # Open the stage with USD + stage_utils.open_stage(urdf_converter.usd_path) + # Reinitialize the simulation + app = omni.kit.app.get_app_interface() + # Run simulation + with contextlib.suppress(KeyboardInterrupt): + while app.is_running(): + # perform step + app.update() + + +if __name__ == "__main__": + # run the main function + main() + # close sim app + simulation_app.close() diff --git a/roboverse_pack/tasks/beyondmimic/scripts/csv_to_npz.py b/roboverse_pack/tasks/beyondmimic/scripts/csv_to_npz.py new file mode 100644 index 000000000..35eeaabd3 --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/scripts/csv_to_npz.py @@ -0,0 +1,384 @@ +from __future__ import annotations + +"""This script replays a motion from a csv file and outputs it to a npz file. + +.. code-block:: bash + + # 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 +""" + +"""Launch Isaac Sim Simulator first.""" + +import argparse + +import numpy as np +from isaaclab.app import AppLauncher + +# add argparse arguments +parser = argparse.ArgumentParser(description="Replay motion from csv file and output to npz file.") +parser.add_argument("--input_file", type=str, required=True, help="The path to the input motion csv file.") +parser.add_argument("--input_fps", type=int, default=30, help="The fps of the input motion.") +parser.add_argument( + "--frame_range", + nargs=2, + type=int, + metavar=("START", "END"), + help=( + "frame range: START END (both inclusive). The frame index starts from 1. If not provided, all frames will be" + " loaded." + ), +) +parser.add_argument("--output_name", 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.") + +# append AppLauncher cli args +AppLauncher.add_app_launcher_args(parser) +# parse the arguments +args_cli = parser.parse_args() + +# launch omniverse app +app_launcher = AppLauncher(args_cli) +simulation_app = app_launcher.app + +"""Rest everything follows.""" + +import isaaclab.sim as sim_utils +import torch +from isaaclab.assets import ArticulationCfg, AssetBaseCfg +from isaaclab.scene import InteractiveScene, InteractiveSceneCfg +from isaaclab.sim import SimulationContext +from isaaclab.utils import configclass +from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR +from isaaclab.utils.math import axis_angle_from_quat, quat_conjugate, quat_mul, quat_slerp +from loguru import logger as log + +## +# Pre-defined configs +## +from roboverse_pack.tasks.beyondmimic.isaaclab.robots.g1 import G1_CYLINDER_CFG + + +@configclass +class ReplayMotionsSceneCfg(InteractiveSceneCfg): + """Configuration for a replay motions scene.""" + + # ground plane + ground = AssetBaseCfg(prim_path="/World/defaultGroundPlane", spawn=sim_utils.GroundPlaneCfg()) + + # lights + sky_light = AssetBaseCfg( + prim_path="/World/skyLight", + spawn=sim_utils.DomeLightCfg( + intensity=750.0, + texture_file=f"{ISAAC_NUCLEUS_DIR}/Materials/Textures/Skies/PolyHaven/kloofendal_43d_clear_puresky_4k.hdr", + ), + ) + + # articulation + robot: ArticulationCfg = G1_CYLINDER_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + + +class MotionLoader: + """Loads and processes motion data from a csv file.""" + + def __init__( + self, + motion_file: str, + input_fps: int, + output_fps: int, + device: torch.device, + frame_range: tuple[int, int] | None, + ): + self.motion_file = motion_file + self.input_fps = input_fps + self.output_fps = output_fps + self.input_dt = 1.0 / self.input_fps + self.output_dt = 1.0 / self.output_fps + self.current_idx = 0 + self.device = device + self.frame_range = frame_range + self._load_motion() + self._interpolate_motion() + self._compute_velocities() + + def _load_motion(self): + """Loads the motion from the csv file.""" + if self.frame_range is None: + motion = torch.from_numpy(np.loadtxt(self.motion_file, delimiter=",")) + else: + motion = torch.from_numpy( + np.loadtxt( + self.motion_file, + delimiter=",", + skiprows=self.frame_range[0] - 1, + max_rows=self.frame_range[1] - self.frame_range[0] + 1, + ) + ) + motion = motion.to(torch.float32).to( + self.device + ) # [n_frames, 3 (base position) + 4 (base rotation) + n_dofs (each DoF corresponds to one changing value)] + self.motion_base_poss_input = motion[:, :3] + self.motion_base_rots_input = motion[:, 3:7] + self.motion_base_rots_input = self.motion_base_rots_input[:, [3, 0, 1, 2]] # convert to wxyz + self.motion_dof_poss_input = motion[:, 7:] # [n_frames, n_dofs] + + self.input_frames = motion.shape[0] + self.duration = (self.input_frames - 1) * self.input_dt + log.info(f"Motion loaded ({self.motion_file}), duration: {self.duration} sec, frames: {self.input_frames}") + + def _interpolate_motion(self): + """Interpolates the motion to the output fps.""" + times = torch.arange(0, self.duration, self.output_dt, device=self.device, dtype=torch.float32) + self.output_frames = times.shape[0] + index_0, index_1, blend = self._compute_frame_blend(times) + self.motion_base_poss = self._lerp( + self.motion_base_poss_input[index_0], + self.motion_base_poss_input[index_1], + blend.unsqueeze(1), + ) + self.motion_base_rots = self._slerp( + self.motion_base_rots_input[index_0], + self.motion_base_rots_input[index_1], + blend, + ) + self.motion_dof_poss = self._lerp( + self.motion_dof_poss_input[index_0], + self.motion_dof_poss_input[index_1], + blend.unsqueeze(1), + ) + log.info( + f"Motion interpolated, input frames: {self.input_frames}, input fps: {self.input_fps}, output frames:" + f" {self.output_frames}, output fps: {self.output_fps}" + ) + + def _lerp(self, a: torch.Tensor, b: torch.Tensor, blend: torch.Tensor) -> torch.Tensor: + """Linear interpolation between two tensors.""" + return a * (1 - blend) + b * blend + + def _slerp(self, a: torch.Tensor, b: torch.Tensor, blend: torch.Tensor) -> torch.Tensor: + """Spherical linear interpolation between two quaternions.""" + slerped_quats = torch.zeros_like(a) + for i in range(a.shape[0]): + slerped_quats[i] = quat_slerp(a[i], b[i], blend[i]) + return slerped_quats + + def _compute_frame_blend(self, times: torch.Tensor) -> torch.Tensor: + """Computes the frame blend for the motion.""" + phase = times / self.duration + index_0 = (phase * (self.input_frames - 1)).floor().long() + index_1 = torch.minimum(index_0 + 1, torch.tensor(self.input_frames - 1)) + blend = phase * (self.input_frames - 1) - index_0 + return index_0, index_1, blend + + def _compute_velocities(self): + """Computes the velocities of the motion.""" + self.motion_base_lin_vels = torch.gradient(self.motion_base_poss, spacing=self.output_dt, dim=0)[ + 0 + ] # [n_frames_out, 3] + self.motion_dof_vels = torch.gradient(self.motion_dof_poss, spacing=self.output_dt, dim=0)[ + 0 + ] # [n_frames_out, n_dofs] + self.motion_base_ang_vels = self._so3_derivative(self.motion_base_rots, self.output_dt) # [n_frames_out, 3] + + def _so3_derivative(self, rotations: torch.Tensor, dt: float) -> torch.Tensor: + """Computes the derivative of a sequence of SO3 rotations. + + Args: + rotations: shape (B, 4). + dt: time step. + + Returns: + shape (B, 3). + """ + q_prev, q_next = rotations[:-2], rotations[2:] + q_rel = quat_mul(q_next, quat_conjugate(q_prev)) # shape (B−2, 4) + + omega = axis_angle_from_quat(q_rel) / (2.0 * dt) # shape (B−2, 3) + omega = torch.cat([omega[:1], omega, omega[-1:]], dim=0) # repeat first and last sample + return omega + + def get_next_state( + self, + ) -> tuple[ + torch.Tensor, + torch.Tensor, + torch.Tensor, + torch.Tensor, + torch.Tensor, + torch.Tensor, + ]: + """Gets the next state of the motion.""" + state = ( + self.motion_base_poss[self.current_idx : self.current_idx + 1], + self.motion_base_rots[self.current_idx : self.current_idx + 1], + self.motion_base_lin_vels[self.current_idx : self.current_idx + 1], + self.motion_base_ang_vels[self.current_idx : self.current_idx + 1], + self.motion_dof_poss[self.current_idx : self.current_idx + 1], + self.motion_dof_vels[self.current_idx : self.current_idx + 1], + ) + self.current_idx += 1 + reset_flag = False + if self.current_idx >= self.output_frames: + self.current_idx = 0 + reset_flag = True + return state, reset_flag + + +def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene, joint_names: list[str]): + """Runs the simulation loop.""" + # Load motion + motion = MotionLoader( + motion_file=args_cli.input_file, + input_fps=args_cli.input_fps, + output_fps=args_cli.output_fps, + device=sim.device, + frame_range=args_cli.frame_range, + ) + + # Extract scene entities + robot = scene["robot"] + robot_joint_indexes = robot.find_joints(joint_names, preserve_order=True)[ + 0 + ] # Get the corresponding joint's index for a given joint name + + # ------- data logger ------------------------------------------------------- + log = { + "fps": [args_cli.output_fps], + "joint_pos": [], + "joint_vel": [], + "body_pos_w": [], + "body_quat_w": [], + "body_lin_vel_w": [], + "body_ang_vel_w": [], + } + file_saved = False + # -------------------------------------------------------------------------- + + # Simulation loop + while simulation_app.is_running(): + ( + ( + motion_base_pos, + motion_base_rot, + motion_base_lin_vel, + motion_base_ang_vel, + motion_dof_pos, + motion_dof_vel, + ), + reset_flag, + ) = motion.get_next_state() + + # set root state + root_states = robot.data.default_root_state.clone() + root_states[:, :3] = motion_base_pos + root_states[:, :2] += scene.env_origins[:, :2] + root_states[:, 3:7] = motion_base_rot + root_states[:, 7:10] = motion_base_lin_vel + root_states[:, 10:] = motion_base_ang_vel + robot.write_root_state_to_sim(root_states) + + # set joint state + joint_pos = robot.data.default_joint_pos.clone() + joint_vel = robot.data.default_joint_vel.clone() + joint_pos[:, robot_joint_indexes] = motion_dof_pos + joint_vel[:, robot_joint_indexes] = motion_dof_vel + robot.write_joint_state_to_sim(joint_pos, joint_vel) + sim.render() # We don't want physic (sim.step()) + scene.update(sim.get_physics_dt()) + + pos_lookat = root_states[0, :3].cpu().numpy() + sim.set_camera_view(pos_lookat + np.array([2.0, 2.0, 0.5]), pos_lookat) + + if not file_saved: + log["joint_pos"].append(robot.data.joint_pos[0, :].cpu().numpy().copy()) + log["joint_vel"].append(robot.data.joint_vel[0, :].cpu().numpy().copy()) + log["body_pos_w"].append(robot.data.body_pos_w[0, :].cpu().numpy().copy()) + log["body_quat_w"].append(robot.data.body_quat_w[0, :].cpu().numpy().copy()) + log["body_lin_vel_w"].append(robot.data.body_lin_vel_w[0, :].cpu().numpy().copy()) + log["body_ang_vel_w"].append(robot.data.body_ang_vel_w[0, :].cpu().numpy().copy()) + + if reset_flag and not file_saved: + file_saved = True + for k in ( + "joint_pos", + "joint_vel", + "body_pos_w", + "body_quat_w", + "body_lin_vel_w", + "body_ang_vel_w", + ): + log[k] = np.stack(log[k], axis=0) + + np.savez("/tmp/motion.npz", **log) + + import wandb + + COLLECTION = args_cli.output_name + run = wandb.init(project="csv_to_npz", name=COLLECTION) + log.info(f"[INFO]: Logging motion to wandb: {COLLECTION}") + REGISTRY = "motions" + # declare the NPZ file as the output of this run + logged_artifact = run.log_artifact(artifact_or_path="/tmp/motion.npz", name=COLLECTION, type=REGISTRY) + # link artifact to the registry (`target_path`) + run.link_artifact(artifact=logged_artifact, target_path=f"wandb-registry-{REGISTRY}/{COLLECTION}") + log.info(f"[INFO]: Motion saved to wandb registry: {REGISTRY}/{COLLECTION}") + + +def main(): + """Main function.""" + # 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 = InteractiveScene(scene_cfg) + # Play the simulator + sim.reset() + # Now we are ready! + log.info("[INFO]: Setup complete...") + # Run the simulator + run_simulator( + sim, + scene, + joint_names=[ + "left_hip_pitch_joint", + "left_hip_roll_joint", + "left_hip_yaw_joint", + "left_knee_joint", + "left_ankle_pitch_joint", + "left_ankle_roll_joint", + "right_hip_pitch_joint", + "right_hip_roll_joint", + "right_hip_yaw_joint", + "right_knee_joint", + "right_ankle_pitch_joint", + "right_ankle_roll_joint", + "waist_yaw_joint", + "waist_roll_joint", + "waist_pitch_joint", + "left_shoulder_pitch_joint", + "left_shoulder_roll_joint", + "left_shoulder_yaw_joint", + "left_elbow_joint", + "left_wrist_roll_joint", + "left_wrist_pitch_joint", + "left_wrist_yaw_joint", + "right_shoulder_pitch_joint", + "right_shoulder_roll_joint", + "right_shoulder_yaw_joint", + "right_elbow_joint", + "right_wrist_roll_joint", + "right_wrist_pitch_joint", + "right_wrist_yaw_joint", + ], + ) + + +if __name__ == "__main__": + # run the main function + main() + # close sim app + simulation_app.close() diff --git a/roboverse_pack/tasks/beyondmimic/scripts/replay_npz.py b/roboverse_pack/tasks/beyondmimic/scripts/replay_npz.py new file mode 100644 index 000000000..837d5808f --- /dev/null +++ b/roboverse_pack/tasks/beyondmimic/scripts/replay_npz.py @@ -0,0 +1,129 @@ +"""This script demonstrates how to use the interactive scene interface to setup a scene with multiple prims. + +.. code-block:: bash + + # Usage + python replay_motion.py --motion_file source/whole_body_tracking/whole_body_tracking/assets/g1/motions/lafan_walk_short.npz +""" + +"""Launch Isaac Sim Simulator first.""" + +import argparse + +import numpy as np +import torch +from isaaclab.app import AppLauncher + +# add argparse arguments +parser = argparse.ArgumentParser(description="Replay converted motions.") +parser.add_argument("--registry_name", type=str, required=True, help="The name of the wand registry.") + +# append AppLauncher cli args +AppLauncher.add_app_launcher_args(parser) +# parse the arguments +args_cli = parser.parse_args() + +# launch omniverse app +app_launcher = AppLauncher(args_cli) +simulation_app = app_launcher.app + +"""Rest everything follows.""" + +import isaaclab.sim as sim_utils +from isaaclab.assets import Articulation, ArticulationCfg, AssetBaseCfg +from isaaclab.scene import InteractiveScene, InteractiveSceneCfg +from isaaclab.sim import SimulationContext +from isaaclab.utils import configclass +from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR + +from roboverse_pack.tasks.beyondmimic.isaaclab.mdp.commands import MotionLoader + +## +# Pre-defined configs +## +from roboverse_pack.tasks.beyondmimic.isaaclab.robots.g1 import G1_CYLINDER_CFG + + +@configclass +class ReplayMotionsSceneCfg(InteractiveSceneCfg): + """Configuration for a replay motions scene.""" + + ground = AssetBaseCfg(prim_path="/World/defaultGroundPlane", spawn=sim_utils.GroundPlaneCfg()) + + sky_light = AssetBaseCfg( + prim_path="/World/skyLight", + spawn=sim_utils.DomeLightCfg( + intensity=750.0, + texture_file=f"{ISAAC_NUCLEUS_DIR}/Materials/Textures/Skies/PolyHaven/kloofendal_43d_clear_puresky_4k.hdr", + ), + ) + + # articulation + robot: ArticulationCfg = G1_CYLINDER_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + + +def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene): + """Runs the simulation loop.""" + # Extract scene entities + robot: Articulation = scene["robot"] + # Define simulation stepping + sim_dt = sim.get_physics_dt() + + registry_name = args_cli.registry_name + if ":" not in registry_name: # Check if the registry name includes alias, if not, append ":latest" + registry_name += ":latest" + import pathlib + + import wandb + + api = wandb.Api() + artifact = api.artifact(registry_name) + motion_file = str(pathlib.Path(artifact.download()) / "motion.npz") + + motion = MotionLoader( + motion_file, + torch.tensor([0], dtype=torch.long, device=sim.device), + sim.device, + ) + time_steps = torch.zeros(scene.num_envs, dtype=torch.long, device=sim.device) + + # Simulation loop + while simulation_app.is_running(): + time_steps += 1 + reset_ids = time_steps >= motion.time_step_total + 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: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] + + robot.write_root_state_to_sim(root_states) + robot.write_joint_state_to_sim(motion.joint_pos[time_steps], motion.joint_vel[time_steps]) + scene.write_data_to_sim() + sim.render() # We don't want physic (sim.step()) + scene.update(sim_dt) + + pos_lookat = root_states[0, :3].cpu().numpy() + sim.set_camera_view(pos_lookat + np.array([2.0, 2.0, 0.5]), pos_lookat) + + +def main(): + """Main function to run the simulator.""" + 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 = InteractiveScene(scene_cfg) + sim.reset() + # Run the simulator + run_simulator(sim, scene) + + +if __name__ == "__main__": + # run the main function + main() + # close sim app + simulation_app.close() diff --git a/roboverse_pack/tasks/calvin/base_table.py b/roboverse_pack/tasks/calvin/base_table.py index 9cdb16bd6..e30035a7b 100644 --- a/roboverse_pack/tasks/calvin/base_table.py +++ b/roboverse_pack/tasks/calvin/base_table.py @@ -41,18 +41,32 @@ class BaseCalvinTableTask(BaseTaskEnv): default_position=[-0.34, -0.46, 0.24], default_orientation=[1, 0, 0, 0], actuators={ - "panda_joint1": BaseActuatorCfg(velocity_limit=2.175, torque_limit=87, stiffness=280, damping=10), - "panda_joint2": BaseActuatorCfg(velocity_limit=2.175, torque_limit=87, stiffness=280, damping=10), - "panda_joint3": BaseActuatorCfg(velocity_limit=2.175, torque_limit=87, stiffness=280, damping=10), - "panda_joint4": BaseActuatorCfg(velocity_limit=2.175, torque_limit=87, stiffness=280, damping=10), - "panda_joint5": BaseActuatorCfg(velocity_limit=2.61, torque_limit=12.0, stiffness=200, damping=5), - "panda_joint6": BaseActuatorCfg(velocity_limit=2.61, torque_limit=12.0, stiffness=200, damping=5), - "panda_joint7": BaseActuatorCfg(velocity_limit=2.61, torque_limit=12.0, stiffness=200, damping=5), + "panda_joint1": BaseActuatorCfg( + velocity_limit=2.175, effort_limit_sim=87, stiffness=280, damping=10 + ), + "panda_joint2": BaseActuatorCfg( + velocity_limit=2.175, effort_limit_sim=87, stiffness=280, damping=10 + ), + "panda_joint3": BaseActuatorCfg( + velocity_limit=2.175, effort_limit_sim=87, stiffness=280, damping=10 + ), + "panda_joint4": BaseActuatorCfg( + velocity_limit=2.175, effort_limit_sim=87, stiffness=280, damping=10 + ), + "panda_joint5": BaseActuatorCfg( + velocity_limit=2.61, effort_limit_sim=12.0, stiffness=200, damping=5 + ), + "panda_joint6": BaseActuatorCfg( + velocity_limit=2.61, effort_limit_sim=12.0, stiffness=200, damping=5 + ), + "panda_joint7": BaseActuatorCfg( + velocity_limit=2.61, effort_limit_sim=12.0, stiffness=200, damping=5 + ), "panda_finger_joint1": BaseActuatorCfg( - velocity_limit=0.2, torque_limit=20.0, is_ee=True, stiffness=30000, damping=1000 + velocity_limit=0.2, effort_limit_sim=20.0, is_ee=True, stiffness=30000, damping=1000 ), "panda_finger_joint2": BaseActuatorCfg( - velocity_limit=0.2, torque_limit=20.0, is_ee=True, stiffness=30000, damping=1000 + velocity_limit=0.2, effort_limit_sim=20.0, is_ee=True, stiffness=30000, damping=1000 ), }, default_joint_positions={ diff --git a/roboverse_pack/tasks/humanoid/base/base_legged_robot.py b/roboverse_pack/tasks/humanoid/base/base_legged_robot.py index 9202849cb..6c60f6456 100644 --- a/roboverse_pack/tasks/humanoid/base/base_legged_robot.py +++ b/roboverse_pack/tasks/humanoid/base/base_legged_robot.py @@ -66,7 +66,7 @@ def _init_joint_cfg(self): torque_limits = ( robot.torque_limits if hasattr(robot, "torque_limits") - else {name: actuator_cfg.torque_limit for name, actuator_cfg in robot.actuators.items()} + else {name: actuator_cfg.effort_limit_sim for name, actuator_cfg in robot.actuators.items()} ) sorted_limits = [torque_limits[name] for name in sorted_joint_names] diff --git a/roboverse_pack/utils/curriculum_utils.py b/roboverse_pack/utils/curriculum_utils.py index 6349ce05e..a616a7b8d 100644 --- a/roboverse_pack/utils/curriculum_utils.py +++ b/roboverse_pack/utils/curriculum_utils.py @@ -7,7 +7,7 @@ from roboverse_pack.tasks.humanoid.base.types import EnvTypes -def lin_vel_cmd_levels( +def lin_vel_cmd_levels( # used env: EnvTypes, env_ids: list[int] | torch.Tensor, reward_term_name: str = "track_lin_vel_xy", @@ -23,7 +23,7 @@ def lin_vel_cmd_levels( if reward > reward_term_scales * 0.8: delta_command = torch.tensor([-0.1, 0.1], device=env.device) - ranges.lin_vel_x = torch.clamp( + ranges.lin_vel_x = torch.clamp( # ensure new ranges don't exceed the hard limits torch.tensor(ranges.lin_vel_x, device=env.device) + delta_command, limit_ranges.lin_vel_x[0], limit_ranges.lin_vel_x[1], diff --git a/roboverse_pack/utils/humanoid_utils.py b/roboverse_pack/utils/humanoid_utils.py index ced4b4093..5c2ee2e0c 100644 --- a/roboverse_pack/utils/humanoid_utils.py +++ b/roboverse_pack/utils/humanoid_utils.py @@ -1,10 +1,140 @@ from __future__ import annotations +import argparse +import os +import random import re from functools import lru_cache from typing import Callable +import numpy as np import torch +from loguru import logger as log + + +def parse_arguments(description="humanoid rl task arguments", custom_parameters=None): + """Parse command line arguments.""" + if custom_parameters is None: + custom_parameters = [] + parser = argparse.ArgumentParser(description=description) + for argument in custom_parameters: + if ("name" in argument) and ("type" in argument or "action" in argument): + help_str = "" + if "help" in argument: + help_str = argument["help"] + + if "type" in argument: + if "default" in argument: + parser.add_argument( + argument["name"], + type=argument["type"], + default=argument["default"], + help=help_str, + ) + else: + parser.add_argument(argument["name"], type=argument["type"], help=help_str) + elif "action" in argument: + parser.add_argument(argument["name"], action=argument["action"], help=help_str) + + else: + log.error("ERROR: command line argument name, type/action must be defined, argument not added to parser") + log.error("supported keys: name, type, default, action, help") + + return parser.parse_args() + + +def get_args(test=False): + """Get the command line arguments.""" + custom_parameters = [ + { + "name": "--task", + "type": str, + "default": "walk_g1_dof29", + "help": "Task name for training/testing.", + }, + {"name": "--robots", "type": str, "default": "", "help": "The used robots."}, + { + "name": "--objects", + "type": str, + "default": None, + "help": "The used objects.", + }, + { + "name": "--num_envs", + "type": int, + "default": 128, + "help": "number of parallel environments.", + }, + { + "name": "--iter", + "type": int, + "default": 15000, + "help": "Max number of training iterations.", + }, + { + "name": "--sim", + "type": str, + "default": "isaacgym", + "help": "simulator type, currently only isaacgym is supported", + }, + { + "name": "--headless", + "action": "store_true", + "default": True, + "help": "Force display off at all times", + }, + { + "name": "--resume", # TODO + "type": str, + "default": None, + "help": "Resume training from a checkpoint", + }, + { + "name": "--checkpoint", # TODO + "type": int, + "default": -1, + "help": "Saved model checkpoint number. If -1: will load the last checkpoint. Overrides config file if provided.", + }, + { + "name": "--seed", + "type": int, + "default": -1, + "help": "The random seed for the run. If -1, will be randomly generated.", + }, + { + "name": "--eval", + "action": "store_true", + "default": False, + "help": "Whether to run in eval mode", + }, + { + "name": "--jit_load", + "action": "store_true", + "default": False, + "help": "Whether to load the JIT model", + }, + # {"name": "--run_name", "type": str, "required": True if not test else False, "help": "Name of the run. Overrides config file if provided."}, + # {"name": "--load_run", "type": str, "default": None, "help": "Path to the config file. If provided, will override command line arguments."}, + # {"name": "--use_wandb", "action": "store_true", "default": True, "help": "Use wandb for logging"}, + # {"name": "--wandb", "type": str, "default": "g1_walking", "help": "Wandb project name"}, + # {"name": "--log", "type": str, "default": None, "help": "log directory. If None, will be set automatically."}, + ] + args = parse_arguments(custom_parameters=custom_parameters) + return args + + +def set_seed(seed=-1): + """Set the seed for the random number generators.""" + if seed == -1: + seed = np.random.randint(0, 10000) + log.info(f"Setting seed: {seed}") + + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + os.environ["PYTHONHASHSEED"] = str(seed) + torch.cuda.manual_seed(seed) + torch.cuda.manual_seed_all(seed) def get_indices_from_substring( From f302b4a8aba4cea7e06925f7475f9ad458ef6300 Mon Sep 17 00:00:00 2001 From: Mingyuan Sun <64095400+myuansun@users.noreply.github.com> Date: Fri, 2 Jan 2026 18:09:17 +0800 Subject: [PATCH 40/50] [fix] effort limit & pre-merge test (#742) * [fix] effort limit * [fix] usd path issue * [update] premerge-ci --- .github/workflows/premerge-ci.yml | 257 ++++++++++-------- docs/source/metasim/concept/config.md | 2 +- .../metasim/features/support_matrix.rst | 2 +- metasim/scenario/robot.py | 4 +- metasim/sim/isaacsim/isaacsim.py | 2 +- .../robots/brainco_hand_left_cfg.py | 10 +- .../robots/brainco_hand_right_cfg.py | 10 +- .../robots/inspire_hand_left_cfg.py | 12 +- .../robots/inspire_hand_right_cfg.py | 12 +- roboverse_pack/robots/robot.py | 8 +- roboverse_pack/robots/robot_template.py | 4 +- 11 files changed, 179 insertions(+), 144 deletions(-) diff --git a/.github/workflows/premerge-ci.yml b/.github/workflows/premerge-ci.yml index ed991710b..666c1a199 100644 --- a/.github/workflows/premerge-ci.yml +++ b/.github/workflows/premerge-ci.yml @@ -119,25 +119,6 @@ jobs: echo "pr_number=${PR_NUM}" >> "$GITHUB_OUTPUT" echo "reason=${REASON}" >> "$GITHUB_OUTPUT" - - name: Log routing decision - run: | - echo "run_direct: ${{ steps.decide.outputs.run_direct }}" - echo "pr_number: ${{ steps.decide.outputs.pr_number }}" - echo "reason: ${{ steps.decide.outputs.reason }}" - - - name: Ensure promotion token exists - if: > - github.event_name == 'pull_request_target' && - steps.decide.outputs.reason == 'fork PR requires promotion' && - (env.PRIV_CI_PUSH_TOKEN == '' || env.PRIV_CI_PUSH_TOKEN == null) - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ steps.decide.outputs.pr_number }} - run: | - gh pr comment "$PR_NUMBER" --body "🔒 Privileged CI promotion skipped: secret \`PRIV_CI_PUSH_TOKEN\` is not configured." - echo "Missing PRIV_CI_PUSH_TOKEN; exiting router after notifying PR." - exit 0 - - name: Check actor permission id: perm if: > @@ -154,22 +135,6 @@ jobs: echo "permission=$perm" >> "$GITHUB_OUTPUT" echo "Actor: $ACTOR, permission: $perm" - - name: Skip promotion without push access - if: > - github.event_name == 'pull_request_target' && - steps.decide.outputs.reason == 'fork PR requires promotion' && - steps.perm.outputs.permission != '' && - (steps.perm.outputs.permission == 'read' || steps.perm.outputs.permission == 'triage' || steps.perm.outputs.permission == 'none') - env: - ACTOR: ${{ github.actor }} - REPO: ${{ github.repository }} - PR_NUMBER: ${{ steps.decide.outputs.pr_number }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh pr comment "$PR_NUMBER" --body "🔒 Privileged CI promotion skipped: @${ACTOR} does not have push permission on ${REPO}." - echo "Insufficient permission; exiting router after notifying PR." - exit 0 - - name: Checkout repository for promotion if: > github.event_name == 'pull_request_target' && @@ -188,7 +153,7 @@ jobs: env: PR_NUMBER: ${{ steps.decide.outputs.pr_number }} REPO: ${{ github.repository }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.PRIV_CI_PUSH_TOKEN }} run: | set -euo pipefail BRANCH="ci/pr-${PR_NUMBER}" @@ -197,17 +162,6 @@ jobs: gh pr checkout "$PR_NUMBER" git push origin HEAD:"$BRANCH" - - name: Comment promotion result - if: > - github.event_name == 'pull_request_target' && - steps.decide.outputs.reason == 'fork PR requires promotion' && - (steps.perm.outputs.permission == 'write' || steps.perm.outputs.permission == 'maintain' || steps.perm.outputs.permission == 'admin') - env: - PR_NUMBER: ${{ steps.decide.outputs.pr_number }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - BRANCH="ci/pr-${PR_NUMBER}" - gh pr comment "$PR_NUMBER" --body "🔐 Privileged CI has been promoted to branch \`${BRANCH}\`. Tests will run from that branch." pre-merge-tests: needs: prepare-or-promote @@ -222,6 +176,8 @@ jobs: # change to the source code directory - name: Checkout code uses: actions/checkout@v4 + with: + token: ${{ secrets.PRIV_CI_PUSH_TOKEN }} - run: aws --version ############# Prebuild ############ - name: pre_build @@ -279,10 +235,10 @@ jobs: # Create ECR repository if it doesn't exist aws ecr describe-repositories --repository-names $ECR_REPOSITORY || \ aws ecr create-repository --repository-name $ECR_REPOSITORY - echo "Waiting for instance $INSTANCE_ID to be running..." - aws ec2 wait instance-running \ - --instance-ids $INSTANCE_ID \ - --region $REGION + echo "Waiting for instance $INSTANCE_ID to be running..." + aws ec2 wait instance-running \ + --instance-ids $INSTANCE_ID \ + --region $REGION echo "Getting instance IP address..." EC2_INSTANCE_IP=$(aws ec2 describe-instances \ @@ -301,7 +257,7 @@ jobs: --output text > ~/.ssh/id_rsa.pub echo "$SSH_KEY" > ~/.ssh/id_rsa chmod 400 ~/.ssh/id_* - echo "Host $EC2_INSTANCE_IP\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile=/dev/null\n" >> ~/.ssh/config + printf "Host %s\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile=/dev/null\n" "$EC2_INSTANCE_IP" >> ~/.ssh/config echo "Sending SSH public key to instance..." aws ec2-instance-connect send-ssh-public-key \ @@ -316,7 +272,7 @@ jobs: echo "====Copying source code...====" wait_time=$RETRY_WAIT_TIME SRC_DIR=$(basename $GITHUB_WORKSPACE) - echo ""====Check environment variables..."====" + echo "====Check environment variables...====" echo "GITHUB_WORKSPACE=$GITHUB_WORKSPACE" echo "CODEBUILD_SRC_DIR=$CODEBUILD_SRC_DIR" echo "EC2_USER_NAME=$EC2_USER_NAME" @@ -333,7 +289,7 @@ jobs: retry_count=0 - # change to parent directory t + # change to parent directory to copy files cd .. while [ $retry_count -lt $MAX_RETRIES ]; do @@ -557,7 +513,7 @@ jobs: if: always() # always try to terminate the instance run: | echo "Cleaning up resources..." - if [ ! -z "$INSTANCE_ID" ]; then + if [ -n "$INSTANCE_ID" ]; then echo "Terminating EC2 instance $INSTANCE_ID..." aws ec2 terminate-instances --instance-ids $INSTANCE_ID --region $REGION || true fi @@ -584,82 +540,66 @@ jobs: if-no-files-found: warn retention-days: 7 - - name: Comment result to PR + + - name: Report commit status to PR if: always() uses: actions/github-script@v7 env: JOB_STATUS: ${{ job.status }} + PRIV_CI_PUSH_TOKEN: ${{ secrets.PRIV_CI_PUSH_TOKEN }} with: + github-token: ${{ secrets.PRIV_CI_PUSH_TOKEN }} script: | - const fs = require('fs'); + const status = process.env.JOB_STATUS; + const ref = context.ref; - const status = process.env.JOB_STATUS; // success / failure / cancelled - const ref = context.ref; // refs/heads/ci/pr-123 or merge_group scenario - - // only reply comments to branches ci/pr-* branches, skip commenting in merge_group runs + // Only report status for ci/pr-* branches const match = ref.match(/^refs\/heads\/ci\/pr-(\d+)$/); if (!match) { - core.info(`Ref ${ref} is not a ci/pr-* branch, skip PR comment.`); + core.info(`Ref ${ref} is not a ci/pr-* branch, skip status reporting.`); return; } const prNumber = Number(match[1]); - let emoji, msg; + // Get the PR's HEAD SHA + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber + }); + + const sha = pr.head.sha; + core.info(`Reporting status to PR #${prNumber} commit ${sha}`); + + // Map job status to commit status state + let state, description; if (status === 'success') { - emoji = '✅'; - msg = `Privileged CI passed on branch \`${ref.split('/').pop()}\`.`; + state = 'success'; + description = 'Privileged CI tests passed'; } else if (status === 'cancelled') { - emoji = '⚪️'; - msg = `Privileged CI was cancelled on branch \`${ref.split('/').pop()}\`.`; + state = 'error'; + description = 'Privileged CI tests were cancelled'; } else { - emoji = '❌'; - msg = `Privileged CI failed on branch \`${ref.split('/').pop()}\`.`; + state = 'failure'; + description = 'Privileged CI tests failed'; } const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; - const artifactUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}#artifacts`; - - // Read test exit codes if available (copied to workspace by previous step) - let testSummary = ''; - try { - const exitCodesPath = 'test_exit_codes.txt'; - if (fs.existsSync(exitCodesPath)) { - const exitCodesContent = fs.readFileSync(exitCodesPath, 'utf8'); - const exitCodes = {}; - exitCodesContent.split('\n').forEach(line => { - const [key, value] = line.split('='); - if (key && value) exitCodes[key.trim()] = parseInt(value.trim()); - }); - - const testSuites = [ - { name: 'General', code: exitCodes['GENERAL_TEST_EXIT_CODE'] }, - { name: 'MuJoCo', code: exitCodes['MUJOCO_TEST_EXIT_CODE'] }, - { name: 'Sapien3', code: exitCodes['SAPIEN3_TEST_EXIT_CODE'] }, - { name: 'IsaacSim', code: exitCodes['ISAACSIM_TEST_EXIT_CODE'] }, - { name: 'IsaacGym', code: exitCodes['ISAACGYM_TEST_EXIT_CODE'] } - ]; - - testSummary = '\n\n**Test Suite Results:**\n'; - testSuites.forEach(suite => { - const icon = (suite.code === 0 || suite.code === undefined) ? '✅' : '❌'; - testSummary += `- ${icon} ${suite.name} tests\n`; - }); - - testSummary += `\n📦 [Download test logs](${artifactUrl})`; - } - } catch (error) { - core.warning(`Failed to read test exit codes: ${error.message}`); - } - - const body = `${emoji} ${msg}${testSummary}\n\n🔗 [View full details](${runUrl})`; - await github.rest.issues.createComment({ + // Create commit status + await github.rest.repos.createCommitStatus({ owner: context.repo.owner, repo: context.repo.repo, - issue_number: prNumber, - body + sha: sha, + state: state, + context: 'pre-merge-tests', + description: description, + target_url: runUrl }); + + core.info(`✅ Reported status '${state}' to commit ${sha}`); + - name: Clean up ci/pr-* branch if: always() run: | @@ -675,12 +615,107 @@ jobs: echo "Not a ci/pr-* branch (ref: $REF), skipping cleanup" fi - fork-merge-group-placeholder: + fork-merge-group-check: needs: prepare-or-promote if: needs.prepare-or-promote.outputs.reason == 'merge_group_contains_fork' runs-on: ubuntu-latest + permissions: + statuses: write steps: - - name: Skip privileged tests for fork merge group + - name: Check pre-merge-tests status for fork PRs + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_EVENT_PATH: ${{ github.event_path }} + run: | + set -euo pipefail + + echo "merge_group contains fork PR(s). Checking pre-merge-tests commit statuses..." + + # Extract PR numbers from merge_group payload + PRS=$(jq -r '.merge_group.pull_requests[].number // empty' "$GITHUB_EVENT_PATH" 2>/dev/null || true) + + if [ -z "$PRS" ]; then + echo "⚠️ Warning: No pull requests listed in merge_group payload." + echo "This is unexpected. Proceeding with caution (passing)." + exit 0 + fi + + ALL_PASSED=true + for PR in $PRS; do + # Check if this is a fork PR + PR_DATA=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR}" 2>/dev/null || echo "{}") + HEAD_REPO=$(echo "$PR_DATA" | jq -r '.head.repo.full_name // ""') + BASE_REPO=$(echo "$PR_DATA" | jq -r '.base.repo.full_name // ""') + HEAD_SHA=$(echo "$PR_DATA" | jq -r '.head.sha // ""') + + if [ "$HEAD_REPO" = "$BASE_REPO" ]; then + echo "PR #$PR is same-repo, skipping status check." + continue + fi + + if [ -z "$HEAD_SHA" ]; then + echo "❌ Could not get HEAD SHA for PR #$PR." + ALL_PASSED=false + continue + fi + + echo "PR #$PR is from fork ($HEAD_REPO). Checking commit status for $HEAD_SHA..." + + # Get the combined status for the commit + STATUS_DATA=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${HEAD_SHA}/status" 2>/dev/null || echo "{}") + + # Find the pre-merge-tests status + PRIV_STATUS=$(echo "$STATUS_DATA" | jq -r '.statuses[] | select(.context == "pre-merge-tests") | .state' | head -1) + + echo " Commit: $HEAD_SHA" + echo " pre-merge-tests status: ${PRIV_STATUS:-not found}" + + if [ -z "$PRIV_STATUS" ]; then + echo "❌ No pre-merge-tests status found for PR #$PR. Please ensure the ci/pr-* run completed." + ALL_PASSED=false + elif [ "$PRIV_STATUS" = "pending" ]; then + echo "⏳ pre-merge-tests is still pending for PR #$PR. Please wait for completion." + ALL_PASSED=false + elif [ "$PRIV_STATUS" != "success" ]; then + echo "❌ pre-merge-tests status is '$PRIV_STATUS' for PR #$PR." + ALL_PASSED=false + else + echo "✅ pre-merge-tests passed for PR #$PR." + fi + done + + if [ "$ALL_PASSED" = "true" ]; then + echo "" + echo "All fork PR privileged tests have passed. Merge queue check succeeded." + exit 0 + else + echo "" + echo "One or more fork PR privileged tests have not passed. Failing merge queue check." + exit 1 + fi + + - name: Report pre-merge-tests status to merge group commit + if: always() + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MERGE_GROUP_SHA: ${{ github.event.merge_group.head_sha }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + JOB_STATUS: ${{ job.status }} run: | - echo "merge_group contains fork PR(s)." - echo "Skipping MetaSim EC2 tests on merge_group and relying on ci/pr-* runs for privileged CI." + if [ "$JOB_STATUS" = "success" ]; then + STATE="success" + DESCRIPTION="Fork PR privileged tests verified" + else + STATE="failure" + DESCRIPTION="Fork PR privileged tests failed" + fi + + echo "Reporting pre-merge-tests status ($STATE) to merge group commit ${MERGE_GROUP_SHA}..." + gh api "repos/${{ github.repository }}/statuses/${MERGE_GROUP_SHA}" \ + -X POST \ + -f state=$STATE \ + -f context=pre-merge-tests \ + -f description="$DESCRIPTION" \ + -f target_url="${RUN_URL}" + echo "✅ Successfully reported pre-merge-tests status." diff --git a/docs/source/metasim/concept/config.md b/docs/source/metasim/concept/config.md index e76c91eb7..16dd8f44a 100644 --- a/docs/source/metasim/concept/config.md +++ b/docs/source/metasim/concept/config.md @@ -141,6 +141,6 @@ RobotCfg( enabled_gravity=True, control_type={"joint1": "position", "joint2": "effort"}, actuators={"joint1": BaseActuatorCfg(stiffness=500, damping=10), - "joint2": BaseActuatorCfg(torque_limit=50)} + "joint2": BaseActuatorCfg(effort_limit_sim=50)} ) ``` diff --git a/docs/source/metasim/features/support_matrix.rst b/docs/source/metasim/features/support_matrix.rst index 2a7d5c0e6..d520f4838 100644 --- a/docs/source/metasim/features/support_matrix.rst +++ b/docs/source/metasim/features/support_matrix.rst @@ -94,7 +94,7 @@ Robot Configuration - - - - * - ``torque_limit`` + * - ``effort_limit_sim`` - `✓ `_ - - diff --git a/metasim/scenario/robot.py b/metasim/scenario/robot.py index b97249d97..1ad088ccd 100644 --- a/metasim/scenario/robot.py +++ b/metasim/scenario/robot.py @@ -104,7 +104,7 @@ class RobotCfg(ArticulationObjCfg): actuators = { "joint1": BaseActuatorCfg( velocity_limit=2.0, # Velocity limit (rad/s) - torque_limit=100.0, # Torque limit (N⋅m) + effort_limit_sim=100.0, # Torque (effort) limit (N⋅m) stiffness=1000.0, # Stiffness coefficient damping=100.0, # Damping coefficient fully_actuated=True, # Whether fully actuated @@ -112,7 +112,7 @@ class RobotCfg(ArticulationObjCfg): ), "gripper_joint": BaseActuatorCfg( velocity_limit=0.2, - torque_limit=10.0, + effort_limit_sim=10.0, stiffness=1000.0, damping=100.0, is_ee=True # Mark as end effector diff --git a/metasim/sim/isaacsim/isaacsim.py b/metasim/sim/isaacsim/isaacsim.py index 5c95c7977..c47f46fd5 100644 --- a/metasim/sim/isaacsim/isaacsim.py +++ b/metasim/sim/isaacsim/isaacsim.py @@ -657,7 +657,7 @@ def _add_robot(self, robot: ArticulationObjCfg) -> None: self._manual_pd_on.append(manual_pd) spawn_cfg = sim_utils.UsdFileCfg( - usd_path=robot.usd_path, + usd_path=os.path.abspath(robot.usd_path), activate_contact_sensors=True, rigid_props=sim_utils.RigidBodyPropertiesCfg( disable_gravity=not robot.enabled_gravity, diff --git a/roboverse_pack/robots/brainco_hand_left_cfg.py b/roboverse_pack/robots/brainco_hand_left_cfg.py index eac23f695..8119325d8 100644 --- a/roboverse_pack/robots/brainco_hand_left_cfg.py +++ b/roboverse_pack/robots/brainco_hand_left_cfg.py @@ -58,19 +58,19 @@ class BraincoHandLeftCfg(RobotCfg): # Thumb (2 actuated + 1 passive = 3 joints) "left_thumb_metacarpal_joint": BaseActuatorCfg(stiffness=10.0, damping=1.0), "left_thumb_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=1.0), - "left_thumb_distal_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + "left_thumb_distal_joint": BaseActuatorCfg(fully_actuated=False, effort_limit_sim=0.0), # Passive/coupled # Index finger (1 actuated + 1 passive = 2 joints) "left_index_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=1.0), - "left_index_distal_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + "left_index_distal_joint": BaseActuatorCfg(fully_actuated=False, effort_limit_sim=0.0), # Passive/coupled # Middle finger (1 actuated + 1 passive = 2 joints) "left_middle_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=1.0), - "left_middle_distal_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + "left_middle_distal_joint": BaseActuatorCfg(fully_actuated=False, effort_limit_sim=0.0), # Passive/coupled # Ring finger (1 actuated + 1 passive = 2 joints) "left_ring_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=1.0), - "left_ring_distal_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + "left_ring_distal_joint": BaseActuatorCfg(fully_actuated=False, effort_limit_sim=0.0), # Passive/coupled # Pinky finger (1 actuated + 1 passive = 2 joints) "left_pinky_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=1.0), - "left_pinky_distal_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + "left_pinky_distal_joint": BaseActuatorCfg(fully_actuated=False, effort_limit_sim=0.0), # Passive/coupled } # Joint limits from MJCF (in radians) diff --git a/roboverse_pack/robots/brainco_hand_right_cfg.py b/roboverse_pack/robots/brainco_hand_right_cfg.py index 73673ec80..6290547fc 100644 --- a/roboverse_pack/robots/brainco_hand_right_cfg.py +++ b/roboverse_pack/robots/brainco_hand_right_cfg.py @@ -58,19 +58,19 @@ class BraincoHandRightCfg(RobotCfg): # Thumb (2 actuated + 1 passive = 3 joints) "right_thumb_metacarpal_joint": BaseActuatorCfg(stiffness=10.0, damping=1.0), "right_thumb_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=1.0), - "right_thumb_distal_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + "right_thumb_distal_joint": BaseActuatorCfg(fully_actuated=False, effort_limit_sim=0.0), # Passive/coupled # Index finger (1 actuated + 1 passive = 2 joints) "right_index_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=1.0), - "right_index_distal_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + "right_index_distal_joint": BaseActuatorCfg(fully_actuated=False, effort_limit_sim=0.0), # Passive/coupled # Middle finger (1 actuated + 1 passive = 2 joints) "right_middle_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=1.0), - "right_middle_distal_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + "right_middle_distal_joint": BaseActuatorCfg(fully_actuated=False, effort_limit_sim=0.0), # Passive/coupled # Ring finger (1 actuated + 1 passive = 2 joints) "right_ring_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=1.0), - "right_ring_distal_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + "right_ring_distal_joint": BaseActuatorCfg(fully_actuated=False, effort_limit_sim=0.0), # Passive/coupled # Pinky finger (1 actuated + 1 passive = 2 joints) "right_pinky_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=1.0), - "right_pinky_distal_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + "right_pinky_distal_joint": BaseActuatorCfg(fully_actuated=False, effort_limit_sim=0.0), # Passive/coupled } # Joint limits from MJCF (in radians) diff --git a/roboverse_pack/robots/inspire_hand_left_cfg.py b/roboverse_pack/robots/inspire_hand_left_cfg.py index fe5c31748..3a5b9e9b2 100644 --- a/roboverse_pack/robots/inspire_hand_left_cfg.py +++ b/roboverse_pack/robots/inspire_hand_left_cfg.py @@ -46,20 +46,20 @@ class InspireHandLeftCfg(RobotCfg): # Thumb (2 actuated + 2 passive = 4 joints) "L_thumb_proximal_yaw_joint": BaseActuatorCfg(stiffness=10.0, damping=2.0), "L_thumb_proximal_pitch_joint": BaseActuatorCfg(stiffness=10.0, damping=2.0), - "L_thumb_intermediate_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled - "L_thumb_distal_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + "L_thumb_intermediate_joint": BaseActuatorCfg(fully_actuated=False, effort_limit_sim=0.0), # Passive/coupled + "L_thumb_distal_joint": BaseActuatorCfg(fully_actuated=False, effort_limit_sim=0.0), # Passive/coupled # Index finger (1 actuated + 1 passive = 2 joints) "L_index_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=2.0), - "L_index_intermediate_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + "L_index_intermediate_joint": BaseActuatorCfg(fully_actuated=False, effort_limit_sim=0.0), # Passive/coupled # Middle finger (1 actuated + 1 passive = 2 joints) "L_middle_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=2.0), - "L_middle_intermediate_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + "L_middle_intermediate_joint": BaseActuatorCfg(fully_actuated=False, effort_limit_sim=0.0), # Passive/coupled # Ring finger (1 actuated + 1 passive = 2 joints) "L_ring_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=2.0), - "L_ring_intermediate_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + "L_ring_intermediate_joint": BaseActuatorCfg(fully_actuated=False, effort_limit_sim=0.0), # Passive/coupled # Pinky finger (1 actuated + 1 passive = 2 joints) "L_pinky_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=2.0), - "L_pinky_intermediate_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + "L_pinky_intermediate_joint": BaseActuatorCfg(fully_actuated=False, effort_limit_sim=0.0), # Passive/coupled } joint_limits: dict[str, tuple[float, float]] = { diff --git a/roboverse_pack/robots/inspire_hand_right_cfg.py b/roboverse_pack/robots/inspire_hand_right_cfg.py index 663429ddb..1c8ef098a 100644 --- a/roboverse_pack/robots/inspire_hand_right_cfg.py +++ b/roboverse_pack/robots/inspire_hand_right_cfg.py @@ -46,20 +46,20 @@ class InspireHandRightCfg(RobotCfg): # Thumb (2 actuated + 2 passive = 4 joints) "R_thumb_proximal_yaw_joint": BaseActuatorCfg(stiffness=10.0, damping=2.0), "R_thumb_proximal_pitch_joint": BaseActuatorCfg(stiffness=10.0, damping=2.0), - "R_thumb_intermediate_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled - "R_thumb_distal_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + "R_thumb_intermediate_joint": BaseActuatorCfg(fully_actuated=False, effort_limit_sim=0.0), # Passive/coupled + "R_thumb_distal_joint": BaseActuatorCfg(fully_actuated=False, effort_limit_sim=0.0), # Passive/coupled # Index finger (1 actuated + 1 passive = 2 joints) "R_index_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=2.0), - "R_index_intermediate_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + "R_index_intermediate_joint": BaseActuatorCfg(fully_actuated=False, effort_limit_sim=0.0), # Passive/coupled # Middle finger (1 actuated + 1 passive = 2 joints) "R_middle_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=2.0), - "R_middle_intermediate_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + "R_middle_intermediate_joint": BaseActuatorCfg(fully_actuated=False, effort_limit_sim=0.0), # Passive/coupled # Ring finger (1 actuated + 1 passive = 2 joints) "R_ring_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=2.0), - "R_ring_intermediate_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + "R_ring_intermediate_joint": BaseActuatorCfg(fully_actuated=False, effort_limit_sim=0.0), # Passive/coupled # Pinky finger (1 actuated + 1 passive = 2 joints) "R_pinky_proximal_joint": BaseActuatorCfg(stiffness=10.0, damping=2.0), - "R_pinky_intermediate_joint": BaseActuatorCfg(fully_actuated=False, torque_limit=0.0), # Passive/coupled + "R_pinky_intermediate_joint": BaseActuatorCfg(fully_actuated=False, effort_limit_sim=0.0), # Passive/coupled } joint_limits: dict[str, tuple[float, float]] = { diff --git a/roboverse_pack/robots/robot.py b/roboverse_pack/robots/robot.py index 9f3a1f58f..b7e230f42 100644 --- a/roboverse_pack/robots/robot.py +++ b/roboverse_pack/robots/robot.py @@ -13,8 +13,8 @@ class BaseActuatorCfg: velocity_limit: float | None = None """Velocity limit of the actuator. If not specified, use the value specified in the asset file and interpreted by the simulator.""" - torque_limit: float | None = None - """Torque limit of the actuator. If not specified, use the value specified in the asset file and interpreted by the simulator.""" + effort_limit_sim: float | None = None + """Torque (effort) limit of the actuator. If not specified, use the value specified in the asset file and interpreted by the simulator. Note that this corresponds to `effort_limit_sim` in Isaac Lab.""" damping: float | None = None """Damping of the actuator. If not specified, use the value specified in the asset file and interpreted by the simulator.""" @@ -92,7 +92,7 @@ class RobotCfg(ArticulationObjCfg): actuators = { "joint1": BaseActuatorCfg( velocity_limit=2.0, # Velocity limit (rad/s) - torque_limit=100.0, # Torque limit (N⋅m) + effort_limit_sim=100.0, # Torque (effort) limit (N⋅m) stiffness=1000.0, # Stiffness coefficient damping=100.0, # Damping coefficient fully_actuated=True, # Whether fully actuated @@ -100,7 +100,7 @@ class RobotCfg(ArticulationObjCfg): ), "gripper_joint": BaseActuatorCfg( velocity_limit=0.2, - torque_limit=10.0, + effort_limit_sim=10.0, stiffness=1000.0, damping=100.0, is_ee=True # Mark as end effector diff --git a/roboverse_pack/robots/robot_template.py b/roboverse_pack/robots/robot_template.py index 74930ae4c..2769351ed 100644 --- a/roboverse_pack/robots/robot_template.py +++ b/roboverse_pack/robots/robot_template.py @@ -48,7 +48,7 @@ class RobotTemplateCfg(RobotCfg): actuators = { "joint1": BaseActuatorCfg( velocity_limit=2.0, # Velocity limit (rad/s) - torque_limit=100.0, # Torque limit (N⋅m) + effort_limit_sim=100.0, # Torque (effort) limit (N⋅m) stiffness=1000.0, # Stiffness coefficient damping=100.0, # Damping coefficient fully_actuated=True, # Whether fully actuated @@ -56,7 +56,7 @@ class RobotTemplateCfg(RobotCfg): ), "gripper_joint": BaseActuatorCfg( velocity_limit=0.2, - torque_limit=10.0, + effort_limit_sim=10.0, stiffness=1000.0, damping=100.0, is_ee=True # Mark as end effector From e8dab678c3811bf2d75ae2e7d45786b44639aa61 Mon Sep 17 00:00:00 2001 From: Mingyuan Sun <64095400+myuansun@users.noreply.github.com> Date: Sat, 3 Jan 2026 01:13:12 +0800 Subject: [PATCH 41/50] [update] premerge-ci (#745) --- .github/workflows/premerge-ci.yml | 140 +----------------------------- 1 file changed, 3 insertions(+), 137 deletions(-) diff --git a/.github/workflows/premerge-ci.yml b/.github/workflows/premerge-ci.yml index 666c1a199..d2111394a 100644 --- a/.github/workflows/premerge-ci.yml +++ b/.github/workflows/premerge-ci.yml @@ -57,38 +57,9 @@ jobs: case "$GITHUB_EVENT_NAME" in merge_group) - BASE_REPO_NAME="${BASE_REPO}" - HAS_FORK="false" - PRS=$(jq -r '.merge_group.pull_requests[].number // empty' "$GITHUB_EVENT_PATH" 2>/dev/null || true) - - if [ -z "$PRS" ]; then - echo "No pull requests listed in merge_group payload; defaulting to same-repo." - fi - - for PR in $PRS; do - HEAD_REPO_NAME=$(gh api "repos/${BASE_REPO_NAME}/pulls/${PR}" --jq '.head.repo.full_name' 2>/dev/null || echo "") - BASE_REPO_FROM_PR=$(gh api "repos/${BASE_REPO_NAME}/pulls/${PR}" --jq '.base.repo.full_name' 2>/dev/null || echo "$BASE_REPO_NAME") - - if [ -z "$HEAD_REPO_NAME" ]; then - echo "Warning: could not determine head repo for PR #$PR; assuming same-repo." - continue - fi - - if [ "$HEAD_REPO_NAME" != "$BASE_REPO_FROM_PR" ]; then - HAS_FORK="true" - echo "Detected fork PR #$PR (head repo: $HEAD_REPO_NAME, base repo: $BASE_REPO_FROM_PR)" - else - echo "PR #$PR is same-repo (head repo: $HEAD_REPO_NAME)" - fi - done - - if [ "$HAS_FORK" = "true" ]; then - RUN_DIRECT="false" - REASON="merge_group_contains_fork" - else - RUN_DIRECT="true" - REASON="merge_group_same_repo_only" - fi + echo "Event is merge_group. Running tests directly in merge queue context." + RUN_DIRECT="true" + REASON="merge_group_execution" ;; push) if [[ "$GITHUB_REF" =~ ^refs/heads/ci/pr- ]]; then @@ -614,108 +585,3 @@ jobs: else echo "Not a ci/pr-* branch (ref: $REF), skipping cleanup" fi - - fork-merge-group-check: - needs: prepare-or-promote - if: needs.prepare-or-promote.outputs.reason == 'merge_group_contains_fork' - runs-on: ubuntu-latest - permissions: - statuses: write - steps: - - name: Check pre-merge-tests status for fork PRs - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_REPOSITORY: ${{ github.repository }} - GITHUB_EVENT_PATH: ${{ github.event_path }} - run: | - set -euo pipefail - - echo "merge_group contains fork PR(s). Checking pre-merge-tests commit statuses..." - - # Extract PR numbers from merge_group payload - PRS=$(jq -r '.merge_group.pull_requests[].number // empty' "$GITHUB_EVENT_PATH" 2>/dev/null || true) - - if [ -z "$PRS" ]; then - echo "⚠️ Warning: No pull requests listed in merge_group payload." - echo "This is unexpected. Proceeding with caution (passing)." - exit 0 - fi - - ALL_PASSED=true - for PR in $PRS; do - # Check if this is a fork PR - PR_DATA=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR}" 2>/dev/null || echo "{}") - HEAD_REPO=$(echo "$PR_DATA" | jq -r '.head.repo.full_name // ""') - BASE_REPO=$(echo "$PR_DATA" | jq -r '.base.repo.full_name // ""') - HEAD_SHA=$(echo "$PR_DATA" | jq -r '.head.sha // ""') - - if [ "$HEAD_REPO" = "$BASE_REPO" ]; then - echo "PR #$PR is same-repo, skipping status check." - continue - fi - - if [ -z "$HEAD_SHA" ]; then - echo "❌ Could not get HEAD SHA for PR #$PR." - ALL_PASSED=false - continue - fi - - echo "PR #$PR is from fork ($HEAD_REPO). Checking commit status for $HEAD_SHA..." - - # Get the combined status for the commit - STATUS_DATA=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${HEAD_SHA}/status" 2>/dev/null || echo "{}") - - # Find the pre-merge-tests status - PRIV_STATUS=$(echo "$STATUS_DATA" | jq -r '.statuses[] | select(.context == "pre-merge-tests") | .state' | head -1) - - echo " Commit: $HEAD_SHA" - echo " pre-merge-tests status: ${PRIV_STATUS:-not found}" - - if [ -z "$PRIV_STATUS" ]; then - echo "❌ No pre-merge-tests status found for PR #$PR. Please ensure the ci/pr-* run completed." - ALL_PASSED=false - elif [ "$PRIV_STATUS" = "pending" ]; then - echo "⏳ pre-merge-tests is still pending for PR #$PR. Please wait for completion." - ALL_PASSED=false - elif [ "$PRIV_STATUS" != "success" ]; then - echo "❌ pre-merge-tests status is '$PRIV_STATUS' for PR #$PR." - ALL_PASSED=false - else - echo "✅ pre-merge-tests passed for PR #$PR." - fi - done - - if [ "$ALL_PASSED" = "true" ]; then - echo "" - echo "All fork PR privileged tests have passed. Merge queue check succeeded." - exit 0 - else - echo "" - echo "One or more fork PR privileged tests have not passed. Failing merge queue check." - exit 1 - fi - - - name: Report pre-merge-tests status to merge group commit - if: always() - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - MERGE_GROUP_SHA: ${{ github.event.merge_group.head_sha }} - RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - JOB_STATUS: ${{ job.status }} - run: | - if [ "$JOB_STATUS" = "success" ]; then - STATE="success" - DESCRIPTION="Fork PR privileged tests verified" - else - STATE="failure" - DESCRIPTION="Fork PR privileged tests failed" - fi - - echo "Reporting pre-merge-tests status ($STATE) to merge group commit ${MERGE_GROUP_SHA}..." - gh api "repos/${{ github.repository }}/statuses/${MERGE_GROUP_SHA}" \ - -X POST \ - -f state=$STATE \ - -f context=pre-merge-tests \ - -f description="$DESCRIPTION" \ - -f target_url="${RUN_URL}" - echo "✅ Successfully reported pre-merge-tests status." From 0ac36ab92b00138f17fe4256f80f27e9fdcd2b18 Mon Sep 17 00:00:00 2001 From: Mingyuan Sun <64095400+myuansun@users.noreply.github.com> Date: Sat, 3 Jan 2026 01:48:53 +0800 Subject: [PATCH 42/50] [update] all with merge_group (#748) --- .github/workflows/premerge-ci.yml | 189 +----------------------------- 1 file changed, 1 insertion(+), 188 deletions(-) diff --git a/.github/workflows/premerge-ci.yml b/.github/workflows/premerge-ci.yml index d2111394a..447ccfda5 100644 --- a/.github/workflows/premerge-ci.yml +++ b/.github/workflows/premerge-ci.yml @@ -10,9 +10,6 @@ on: branches: - main - develop - push: - branches: - - ci/pr-** env: REGION: us-west-2 @@ -26,117 +23,8 @@ env: ECR_REPOSITORY: "roboverse-dev" jobs: - prepare-or-promote: - name: prepare-or-promote - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - issues: write - env: - PRIV_CI_PUSH_TOKEN: ${{ secrets.PRIV_CI_PUSH_TOKEN }} - outputs: - run_direct: ${{ steps.decide.outputs.run_direct }} - pr_number: ${{ steps.decide.outputs.pr_number }} - reason: ${{ steps.decide.outputs.reason }} - steps: - - name: Route CI flow - id: decide - env: - GITHUB_EVENT_NAME: ${{ github.event_name }} - GITHUB_REF: ${{ github.ref }} - PR_NUMBER: ${{ github.event.pull_request.number || '' }} - HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name || '' }} - BASE_REPO: ${{ github.repository }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_EVENT_PATH: ${{ github.event_path }} - run: | - RUN_DIRECT="false" - PR_NUM="${PR_NUMBER}" - REASON="" - - case "$GITHUB_EVENT_NAME" in - merge_group) - echo "Event is merge_group. Running tests directly in merge queue context." - RUN_DIRECT="true" - REASON="merge_group_execution" - ;; - push) - if [[ "$GITHUB_REF" =~ ^refs/heads/ci/pr- ]]; then - RUN_DIRECT="true" - REASON="ci/pr-* push" - PR_NUM="${GITHUB_REF#refs/heads/ci/pr-}" - else - REASON="unsupported push ref" - fi - ;; - workflow_dispatch) - RUN_DIRECT="true" - REASON="manual dispatch" - ;; - pull_request_target) - if [ "$HEAD_REPO" = "$BASE_REPO" ]; then - REASON="same-repo PR, no promotion needed" - else - REASON="fork PR requires promotion" - fi - ;; - *) - REASON="unsupported event" - ;; - esac - - echo "run_direct=${RUN_DIRECT}" >> "$GITHUB_OUTPUT" - echo "pr_number=${PR_NUM}" >> "$GITHUB_OUTPUT" - echo "reason=${REASON}" >> "$GITHUB_OUTPUT" - - - name: Check actor permission - id: perm - if: > - github.event_name == 'pull_request_target' && - steps.decide.outputs.reason == 'fork PR requires promotion' && - env.PRIV_CI_PUSH_TOKEN != '' && - env.PRIV_CI_PUSH_TOKEN != null - env: - ACTOR: ${{ github.actor }} - REPO: ${{ github.repository }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - perm=$(gh api "repos/$REPO/collaborators/$ACTOR/permission" --jq .permission 2>/dev/null || echo "none") - echo "permission=$perm" >> "$GITHUB_OUTPUT" - echo "Actor: $ACTOR, permission: $perm" - - - name: Checkout repository for promotion - if: > - github.event_name == 'pull_request_target' && - steps.decide.outputs.reason == 'fork PR requires promotion' && - (steps.perm.outputs.permission == 'write' || steps.perm.outputs.permission == 'maintain' || steps.perm.outputs.permission == 'admin') - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.PRIV_CI_PUSH_TOKEN }} - - - name: Promote fork PR to ci/pr-* branch - if: > - github.event_name == 'pull_request_target' && - steps.decide.outputs.reason == 'fork PR requires promotion' && - (steps.perm.outputs.permission == 'write' || steps.perm.outputs.permission == 'maintain' || steps.perm.outputs.permission == 'admin') - env: - PR_NUMBER: ${{ steps.decide.outputs.pr_number }} - REPO: ${{ github.repository }} - GH_TOKEN: ${{ secrets.PRIV_CI_PUSH_TOKEN }} - run: | - set -euo pipefail - BRANCH="ci/pr-${PR_NUMBER}" - git config --global user.name "github-actions[bot]" - git config --global user.email "github-actions[bot]@users.noreply.github.com" - gh pr checkout "$PR_NUMBER" - git push origin HEAD:"$BRANCH" - - pre-merge-tests: - needs: prepare-or-promote - if: needs.prepare-or-promote.outputs.run_direct == 'true' + if: github.event_name == 'merge_group' || github.event_name == 'workflow_dispatch' permissions: contents: write # Need write access to delete ci/pr-* branches pull-requests: write @@ -510,78 +398,3 @@ jobs: test_exit_codes.txt if-no-files-found: warn retention-days: 7 - - - - name: Report commit status to PR - if: always() - uses: actions/github-script@v7 - env: - JOB_STATUS: ${{ job.status }} - PRIV_CI_PUSH_TOKEN: ${{ secrets.PRIV_CI_PUSH_TOKEN }} - with: - github-token: ${{ secrets.PRIV_CI_PUSH_TOKEN }} - script: | - const status = process.env.JOB_STATUS; - const ref = context.ref; - - // Only report status for ci/pr-* branches - const match = ref.match(/^refs\/heads\/ci\/pr-(\d+)$/); - if (!match) { - core.info(`Ref ${ref} is not a ci/pr-* branch, skip status reporting.`); - return; - } - - const prNumber = Number(match[1]); - - // Get the PR's HEAD SHA - const { data: pr } = await github.rest.pulls.get({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: prNumber - }); - - const sha = pr.head.sha; - core.info(`Reporting status to PR #${prNumber} commit ${sha}`); - - // Map job status to commit status state - let state, description; - if (status === 'success') { - state = 'success'; - description = 'Privileged CI tests passed'; - } else if (status === 'cancelled') { - state = 'error'; - description = 'Privileged CI tests were cancelled'; - } else { - state = 'failure'; - description = 'Privileged CI tests failed'; - } - - const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; - - // Create commit status - await github.rest.repos.createCommitStatus({ - owner: context.repo.owner, - repo: context.repo.repo, - sha: sha, - state: state, - context: 'pre-merge-tests', - description: description, - target_url: runUrl - }); - - core.info(`✅ Reported status '${state}' to commit ${sha}`); - - - name: Clean up ci/pr-* branch - if: always() - run: | - REF="${GITHUB_REF}" - # Only delete if we're running on a ci/pr-* branch - if [[ "$REF" =~ ^refs/heads/ci/pr-[0-9]+$ ]]; then - BRANCH="${REF#refs/heads/}" - echo "Cleaning up temporary branch: $BRANCH" - # Delete the branch from remote (ignore errors if already deleted) - git push origin --delete "$BRANCH" 2>/dev/null || echo "Branch $BRANCH already deleted or not found" - echo "✓ Cleanup complete" - else - echo "Not a ci/pr-* branch (ref: $REF), skipping cleanup" - fi From 65f864abc2281b5b4cb444a597d10ce58185068c Mon Sep 17 00:00:00 2001 From: Mingyuan Sun <64095400+myuansun@users.noreply.github.com> Date: Sat, 3 Jan 2026 02:32:06 +0800 Subject: [PATCH 43/50] [update] add content check (#749) --- .github/workflows/premerge-ci.yml | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/.github/workflows/premerge-ci.yml b/.github/workflows/premerge-ci.yml index 447ccfda5..da14da4b0 100644 --- a/.github/workflows/premerge-ci.yml +++ b/.github/workflows/premerge-ci.yml @@ -26,17 +26,14 @@ jobs: pre-merge-tests: if: github.event_name == 'merge_group' || github.event_name == 'workflow_dispatch' permissions: - contents: write # Need write access to delete ci/pr-* branches + contents: read pull-requests: write - issues: write # Post status comment back to PR + issues: write runs-on: codebuild-EC2_Launcher2-${{ github.run_id }}-${{ github.run_attempt }} timeout-minutes: 720 steps: - # change to the source code directory - name: Checkout code uses: actions/checkout@v4 - with: - token: ${{ secrets.PRIV_CI_PUSH_TOKEN }} - run: aws --version ############# Prebuild ############ - name: pre_build @@ -398,3 +395,27 @@ jobs: test_exit_codes.txt if-no-files-found: warn retention-days: 7 + + workflow-integrity-check: + runs-on: ubuntu-latest + if: github.event_name == 'pull_request_target' + permissions: + pull-requests: read + steps: + - name: Check for workflow changes + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + set -euo pipefail + echo "Checking if .github/workflows/premerge-ci.yml is modified in PR #$PR_NUMBER..." + CHANGES=$(gh pr diff "$PR_NUMBER" --name-only) + + if echo "$CHANGES" | grep -q "^.github/workflows/premerge-ci.yml$"; then + echo "❌ Critical workflow modification detected!" + echo "For security reasons, this workflow file cannot be modified via Pull Request." + echo "Please revert changes to .github/workflows/premerge-ci.yml to pass this check." + exit 1 + fi + + echo "✅ Workflow integrity verified (file not modified)." From 9fd82273d5670dfbcb1b49ef98173cf39c0633e6 Mon Sep 17 00:00:00 2001 From: Mingyuan Sun <64095400+myuansun@users.noreply.github.com> Date: Sat, 3 Jan 2026 03:46:54 +0800 Subject: [PATCH 44/50] [fix] git repo error & workflow integrity check (#752) --- .github/workflows/premerge-ci.yml | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/.github/workflows/premerge-ci.yml b/.github/workflows/premerge-ci.yml index da14da4b0..94178c2e0 100644 --- a/.github/workflows/premerge-ci.yml +++ b/.github/workflows/premerge-ci.yml @@ -23,7 +23,7 @@ env: ECR_REPOSITORY: "roboverse-dev" jobs: - pre-merge-tests: + pre-merge-tests-impl: if: github.event_name == 'merge_group' || github.event_name == 'workflow_dispatch' permissions: contents: read @@ -396,6 +396,26 @@ jobs: if-no-files-found: warn retention-days: 7 + pre-merge-tests: + if: always() + needs: [workflow-integrity-check, pre-merge-tests-impl] + runs-on: ubuntu-latest + steps: + - run: | + if [[ "${{ github.event_name }}" == "pull_request_target" ]]; then + if [[ "${{ needs.workflow-integrity-check.result }}" != "success" ]]; then + echo "❌ Workflow integrity check failed." + exit 1 + fi + echo "✅ Workflow integrity verified. Ready for merge queue." + elif [[ "${{ github.event_name }}" == "merge_group" || "${{ github.event_name }}" == "workflow_dispatch" ]]; then + if [[ "${{ needs.pre-merge-tests-impl.result }}" != "success" ]]; then + echo "❌ Tests failed." + exit 1 + fi + echo "✅ Tests passed." + fi + workflow-integrity-check: runs-on: ubuntu-latest if: github.event_name == 'pull_request_target' @@ -409,7 +429,7 @@ jobs: run: | set -euo pipefail echo "Checking if .github/workflows/premerge-ci.yml is modified in PR #$PR_NUMBER..." - CHANGES=$(gh pr diff "$PR_NUMBER" --name-only) + CHANGES=$(gh pr diff "$PR_NUMBER" --name-only -R ${{ github.repository }}) if echo "$CHANGES" | grep -q "^.github/workflows/premerge-ci.yml$"; then echo "❌ Critical workflow modification detected!" From c54f566c94750a30861dfb332818a299e0a0fafe Mon Sep 17 00:00:00 2001 From: Boshi An <41010290+boshi-an@users.noreply.github.com> Date: Thu, 8 Jan 2026 16:41:44 +0100 Subject: [PATCH 45/50] fixed gripper control direction (#740) Co-authored-by: boshi-an --- scripts/advanced/teleop_keyboard.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/advanced/teleop_keyboard.py b/scripts/advanced/teleop_keyboard.py index 8b79913d3..40761e998 100644 --- a/scripts/advanced/teleop_keyboard.py +++ b/scripts/advanced/teleop_keyboard.py @@ -436,7 +436,7 @@ def save_all_episodes_to_file(): curr_ee_quat_local = quat_mul(quat_inv(robot_quat), curr_ee_quat) if keyboard_client is not None: - d_pos, d_rot_local, close_gripper = process_kb_input(keyboard_client, dpos=0.0002, drot=0.0003) + d_pos, d_rot_local, close_gripper = process_kb_input(keyboard_client, dpos=0.02, drot=0.03) else: # Handle keyboard input using pynput key states d_pos = [0.0, 0.0, 0.0] @@ -496,7 +496,7 @@ def save_all_episodes_to_file(): # Process gripper command (convert boolean to float for consistency) gripper_widths = process_gripper_command( - torch.tensor(float(close_gripper), dtype=torch.float32, device=device), scenario.robots[0], device + torch.tensor(float(not close_gripper), dtype=torch.float32, device=device), scenario.robots[0], device ) # Compose joint action From 5609929be27d24e054c79e1090f5086587013d3a Mon Sep 17 00:00:00 2001 From: Mingyuan Sun <64095400+myuansun@users.noreply.github.com> Date: Thu, 15 Jan 2026 12:32:41 +0800 Subject: [PATCH 46/50] [fix] adaptive ground size to num_envs (#753) * [fix] adapative terrain size to num_envs * [update] simplify code * [fix] remove default lidar sensor * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: OMNILACRIM Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- metasim/sim/isaacsim/isaacsim.py | 12 +++++++++--- .../tasks/humanoid/locomotion/walk_g1_dof29.py | 6 +----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/metasim/sim/isaacsim/isaacsim.py b/metasim/sim/isaacsim/isaacsim.py index c47f46fd5..30d607ad6 100644 --- a/metasim/sim/isaacsim/isaacsim.py +++ b/metasim/sim/isaacsim/isaacsim.py @@ -886,16 +886,22 @@ def _load_terrain(self) -> None: except Exception as e: log.warning(f"Failed to download terrain material {mdl_path}: {e}") + num_cols = math.ceil(math.sqrt(self._num_envs)) + num_rows = num_cols + # make each tile at least env_spacing (add a margin so robot never touches tile boundary) + tile = 1.25 * self.scenario.env_spacing + plane_gen_cfg = TerrainGeneratorCfg( - size=(100.0, 100.0), # ground size (in total) + size=(tile, tile), + num_rows=num_rows, + num_cols=num_cols, horizontal_scale=0.1, - vertical_scale=0.0, + vertical_scale=0.005, slope_threshold=None, use_cache=False, sub_terrains={ "flat": mesh_cfg.MeshPlaneTerrainCfg( proportion=1.0, - size=(10.0, 10.0), ), }, ) diff --git a/roboverse_pack/tasks/humanoid/locomotion/walk_g1_dof29.py b/roboverse_pack/tasks/humanoid/locomotion/walk_g1_dof29.py index e379ef36a..91a2265bd 100644 --- a/roboverse_pack/tasks/humanoid/locomotion/walk_g1_dof29.py +++ b/roboverse_pack/tasks/humanoid/locomotion/walk_g1_dof29.py @@ -20,7 +20,6 @@ step_funcs, termination_funcs, ) -from roboverse_pack.queries.lidar import LidarPointCloud from roboverse_pack.randomization.humanoid import ( MassRandomizer, MaterialRandomizer, @@ -113,10 +112,7 @@ class RewardsScales: }, ) - callbacks_query = { - "contact_forces": ContactForces(history_length=3), - "lidar_point_cloud": LidarPointCloud(enabled=False), - } + callbacks_query = {"contact_forces": ContactForces(history_length=3)} callbacks_setup = { "material_randomizer": MaterialRandomizer( obj_name="g1_dof29", From a8e373bc9964cd5701b4d6b70d70c2e92493fce3 Mon Sep 17 00:00:00 2001 From: Mingyuan Sun <64095400+myuansun@users.noreply.github.com> Date: Fri, 16 Jan 2026 00:01:00 +0800 Subject: [PATCH 47/50] [update] humanoid training pipeline (#754) * [fix] wandb logging * [fix] optimize contact sensor & fix g1_dof12 observation buffer * [fix] fix isaacgym CUDA error * [update] only set collapse_fixed_joints to True for isaacgym * [fix] align checkpoint directory of eval.py * [fix] obs_groups & collapse fixed joints for g1 * [fix] isaacgym render with redundant physics step * [fix] add ground padding for isaacsim * [update] increase ground padding of isaacsim * [update] change default obs_group in eval.py --- metasim/sim/isaacgym/isaacgym.py | 12 +++--------- metasim/sim/isaacsim/isaacsim.py | 3 ++- roboverse_learn/rl/configs/rsl_rl/ppo.py | 7 ++++++- roboverse_learn/rl/rsl_rl/eval.py | 17 ++++++++--------- roboverse_pack/robots/g1_cfg.py | 2 +- .../tasks/humanoid/base/base_legged_robot.py | 14 +++++++++----- 6 files changed, 29 insertions(+), 26 deletions(-) diff --git a/metasim/sim/isaacgym/isaacgym.py b/metasim/sim/isaacgym/isaacgym.py index f75218d0b..571efe84d 100644 --- a/metasim/sim/isaacgym/isaacgym.py +++ b/metasim/sim/isaacgym/isaacgym.py @@ -422,9 +422,8 @@ def _load_robot_assets(self) -> None: asset_options.fix_base_link = self.robots[0].fix_base_link asset_options.disable_gravity = not self.robots[0].enabled_gravity asset_options.flip_visual_attachments = self.robots[0].isaacgym_flip_visual_attachments - asset_options.collapse_fixed_joints = self.robots[0].collapse_fixed_joints + asset_options.collapse_fixed_joints = getattr(self.robots[0], "collapse_fixed_joints", False) asset_options.default_dof_drive_mode = gymapi.DOF_MODE_NONE - # Defaults are set to free movement and will be updated based on the configuration in actuator_cfg below. asset_options.replace_cylinder_with_capsule = self.scenario.sim_params.replace_cylinder_with_capsule robot_asset = self.gym.load_asset(self.sim, asset_root, robot_asset_file, asset_options) # configure robot dofs @@ -761,7 +760,6 @@ def _get_states(self, env_ids: list[int] | None = None) -> list[DictEnvState]: camera_states = {} - self.refresh_render() self.gym.start_access_image_tensors(self.sim) for cam_id, cam in enumerate(self.cameras): @@ -864,12 +862,6 @@ def set_actions(self, actions: torch.Tensor) -> None: else: self.gym.set_dof_position_target_tensor(self.sim, gymtorch.unwrap_tensor(action_input)) - def refresh_render(self) -> None: - # Step the physics - self.gym.simulate(self.sim) - self.gym.fetch_results(self.sim, True) - self._render() - def _simulate_one_physics_step(self): self.gym.simulate(self.sim) if self.device == "cpu": @@ -891,6 +883,8 @@ def _simulate(self) -> None: def _render(self) -> None: """Listen for keyboard events, step graphics and render the environment""" + if self.device != "cpu": + self.gym.fetch_results(self.sim, True) if not self.headless: for evt in self.gym.query_viewer_action_events(self.viewer): if evt.action == "toggle_viewer_sync" and evt.value > 0: diff --git a/metasim/sim/isaacsim/isaacsim.py b/metasim/sim/isaacsim/isaacsim.py index 30d607ad6..2ec4f85aa 100644 --- a/metasim/sim/isaacsim/isaacsim.py +++ b/metasim/sim/isaacsim/isaacsim.py @@ -886,7 +886,8 @@ def _load_terrain(self) -> None: except Exception as e: log.warning(f"Failed to download terrain material {mdl_path}: {e}") - num_cols = math.ceil(math.sqrt(self._num_envs)) + ground_padding = 8 + num_cols = math.ceil(math.sqrt(self._num_envs)) + ground_padding num_rows = num_cols # make each tile at least env_spacing (add a margin so robot never touches tile boundary) tile = 1.25 * self.scenario.env_spacing diff --git a/roboverse_learn/rl/configs/rsl_rl/ppo.py b/roboverse_learn/rl/configs/rsl_rl/ppo.py index dc9e2913b..092c7e7d5 100644 --- a/roboverse_learn/rl/configs/rsl_rl/ppo.py +++ b/roboverse_learn/rl/configs/rsl_rl/ppo.py @@ -100,7 +100,7 @@ def __post_init__(self) -> None: self.model_dir = os.path.join("outputs", name, self.task, log_dir) if self.obs_groups is None: - self.obs_groups = {"policy": ["policy"], "critic": ["policy", "critic"]} + self.obs_groups = {"policy": ["policy"], "critic": ["critic"]} # Build runner training config for RSL-RL policy_cfg = self.policy.to_dict() if hasattr(self.policy, "to_dict") else dict(self.policy.__dict__) @@ -129,5 +129,10 @@ def __post_init__(self) -> None: if self.clip_actions is not None: self.train_cfg["clip_actions"] = self.clip_actions + # Sync logger with use_wandb flag + if self.use_wandb: + self.logger = "wandb" + self.train_cfg["logger"] = "wandb" + __all__ = ["RslRlPPOConfig"] diff --git a/roboverse_learn/rl/rsl_rl/eval.py b/roboverse_learn/rl/rsl_rl/eval.py index 3d9a34402..6d018b23c 100644 --- a/roboverse_learn/rl/rsl_rl/eval.py +++ b/roboverse_learn/rl/rsl_rl/eval.py @@ -21,11 +21,11 @@ from roboverse_learn.rl.rsl_rl.env_wrapper import RslRlEnvWrapper from metasim.task.registry import get_task_class -def get_log_dir(robot_name: str, task_name: str, now=None) -> str: - """Get the log directory.""" +def get_log_dir(exp_name: str, task_name: str, now=None) -> str: + """Get the log directory (aligned with ppo.py saving logic).""" if now is None: - now = datetime.datetime.now().strftime("%Y_%m%d_%H%M%S") - log_dir = f"./outputs/{robot_name}/{task_name}/{now}" + now = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + log_dir = f"./outputs/{exp_name}/{task_name}/{now}" if not os.path.exists(log_dir): os.makedirs(log_dir, exist_ok=True) log.info("Log directory: {}", log_dir) @@ -84,10 +84,12 @@ def evaluate(args: RslRlPPOConfig): raise ValueError("Please provide --resume (timestamp/log dir) for evaluation.") # Convert resume string to full log directory path + # Use exp_name to match ppo.py's saving logic (defaults to experiment_name -> task) + exp_name = args.exp_name or args.experiment_name or args.task log_dir = ( args.resume if os.path.isdir(args.resume) - else get_log_dir(robot_name=args.robot, task_name=args.task, now=args.resume) + else get_log_dir(exp_name=exp_name, task_name=args.task, now=args.resume) ) # Use get_load_path helper to handle checkpoint loading logic @@ -109,10 +111,7 @@ def evaluate(args: RslRlPPOConfig): obs = wrapped_env.get_observations() - # Resolve obs_groups (mimicking OnPolicyRunner.__init__) - default_sets = ["critic"] - args.obs_groups = resolve_obs_groups(obs, {}, default_sets) - obs_groups = args.obs_groups + obs_groups = args.obs_groups or {"policy": ["policy"], "critic": ["critic"]} # Extract policy config policy_cfg = args.policy diff --git a/roboverse_pack/robots/g1_cfg.py b/roboverse_pack/robots/g1_cfg.py index 31ef1a19b..241bea1b3 100644 --- a/roboverse_pack/robots/g1_cfg.py +++ b/roboverse_pack/robots/g1_cfg.py @@ -21,7 +21,7 @@ class G1Dof12Cfg(RobotCfg): enabled_self_collisions: bool = True isaacgym_read_mjcf = False isaacgym_flip_visual_attachments: bool = False - collapse_fixed_joints: bool = False + collapse_fixed_joints: bool = True actuators: dict[str, BaseActuatorCfg] = { # N7520-14.3: hip_pitch, hip_yaw (stiffness 100, damping 2, torque 88, vel 32) diff --git a/roboverse_pack/tasks/humanoid/base/base_legged_robot.py b/roboverse_pack/tasks/humanoid/base/base_legged_robot.py index 6c60f6456..8441690dd 100644 --- a/roboverse_pack/tasks/humanoid/base/base_legged_robot.py +++ b/roboverse_pack/tasks/humanoid/base/base_legged_robot.py @@ -353,11 +353,15 @@ def _post_physics_step(self, env_states: TensorState): # Compute "true" next observations BEFORE reset (for off-policy RL) # This is the observation that would be returned if the env didn't auto-reset true_obs_single, _ = self._compute_task_observations(env_states) - # Temporarily append to queue to compute obs with full history - self.obs_buf_queue.append(true_obs_single) - true_next_obs_with_history = self.obs_buf.clone() - # Remove the temporary observation - self.obs_buf_queue.pop() + # Compute history as if we appended without mutating the queue + if self.obs_buf_queue is None or len(self.obs_buf_queue) == 0: + true_next_obs_with_history = true_obs_single.clone() + else: + obs_queue = list(self.obs_buf_queue) + if self.obs_buf_queue.maxlen is not None and len(obs_queue) == self.obs_buf_queue.maxlen: + obs_queue = obs_queue[1:] + obs_queue.append(true_obs_single) + true_next_obs_with_history = torch.cat(obs_queue, dim=1) # reset envs reset_env_idx = self.reset_buf.nonzero(as_tuple=False).squeeze(-1) From 691b3a3af00a75815fb6c94116b01cebdaf4a5eb Mon Sep 17 00:00:00 2001 From: King_North <1831647528@qq.com> Date: Fri, 16 Jan 2026 16:40:03 +0800 Subject: [PATCH 48/50] [Feature] adpat agibot a2 dof12 into rl locomotion (#736) * feat:adpat agibot a2 dof12 into unitree rl locomotion * fix:1.Modify the model training path2.Fix device settings and model weights loading in the eval file * fix:1.Modify the model training path2.Fix device settings and model weights loading in the eval file * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix: add missing docstrings * fix: reworked the logic for saving path * fix: update task registration identifier for WalkAgibotA2Dof12 * fix: modify evaluation script for Mujoco compatibility and performance logging - Added time tracking for evaluation steps to monitor simulation and elapsed time. - Adjusted device settings to default to CPU when using Mujoco. - Removed unnecessary observation device transfer function to streamline the evaluation process. * fix: update actuator configuration and enable USD path for AgibotA2Dof12 - Changed actuator configuration to use 'effort_limit_sim' instead of 'torque_limit'. - Enabled the USD path for the AgibotA2Dof12 robot configuration. * feat: add joint effort limits function and adjust reward scales for WalkAgibotA2Dof12 - Implemented a new function to penalize joint efforts exceeding torque limits. - Updated reward scales for linear and angular velocity, joint acceleration, action rate, and added joint effort limits to enhance locomotion performance. - Adjusted target height for stability in the environment configuration. * correct contributors in alphabetical order * Reorder Yikai Tang in contributors list * accept develop branch change --------- Co-authored-by: wangbei Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: zhangyi Co-authored-by: Mingyuan Sun <64095400+myuansun@users.noreply.github.com> --- CONTRIBUTORS.md | 2 + roboverse_learn/rl/configs/rsl_rl/ppo.py | 4 +- roboverse_learn/rl/rsl_rl/eval.py | 20 +- .../callback_funcs/humanoid/reward_funcs.py | 20 ++ roboverse_pack/robots/__init__.py | 1 + roboverse_pack/robots/agibot_a2_cfg.py | 132 +++++++ roboverse_pack/tasks/humanoid/cfg_base.py | 11 + .../locomotion/walk_agibot_a2_dof12.py | 336 ++++++++++++++++++ roboverse_pack/utils/humanoid_utils.py | 13 + 9 files changed, 530 insertions(+), 9 deletions(-) create mode 100644 roboverse_pack/robots/agibot_a2_cfg.py create mode 100644 roboverse_pack/tasks/humanoid/locomotion/walk_agibot_a2_dof12.py diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 5dfd0af59..4549a9b8c 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -21,6 +21,7 @@ Guidelines for modifications: ## Contributors * Bangjun Wang +* Bei Wang * Boqi Zhao * Chaoyi Xu * Chengyang Zhao @@ -41,6 +42,7 @@ Guidelines for modifications: * Songlin Wei * Xinjie Wang * Xinying Guo +* Yi Zhang * Yikai Tang * Yongce Liu * Yu Hong diff --git a/roboverse_learn/rl/configs/rsl_rl/ppo.py b/roboverse_learn/rl/configs/rsl_rl/ppo.py index 092c7e7d5..6f55e2356 100644 --- a/roboverse_learn/rl/configs/rsl_rl/ppo.py +++ b/roboverse_learn/rl/configs/rsl_rl/ppo.py @@ -4,6 +4,7 @@ from datetime import datetime from metasim.utils import configclass +from datetime import datetime SimBackend = Literal[ "isaacgym", @@ -96,8 +97,7 @@ def __post_init__(self) -> None: if self.model_dir is None: name = self.exp_name or self.experiment_name - log_dir = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") - self.model_dir = os.path.join("outputs", name, self.task, log_dir) + self.model_dir = os.path.join("outputs", name, self.task, datetime.now().strftime("%Y%m%d_%H%M%S")) if self.obs_groups is None: self.obs_groups = {"policy": ["policy"], "critic": ["critic"]} diff --git a/roboverse_learn/rl/rsl_rl/eval.py b/roboverse_learn/rl/rsl_rl/eval.py index 6d018b23c..c19d6b45a 100644 --- a/roboverse_learn/rl/rsl_rl/eval.py +++ b/roboverse_learn/rl/rsl_rl/eval.py @@ -2,6 +2,7 @@ import os import random +import time try: import isaacgym # noqa: F401 @@ -72,6 +73,10 @@ def evaluate(args: RslRlPPOConfig): torch.manual_seed(args.seed) torch.backends.cudnn.deterministic = args.torch_deterministic + if args.sim == "mujoco": + args.cuda = False + args.device = "cpu" + device = torch.device(args.device if torch.cuda.is_available() and args.cuda else "cpu") print(f"Using device: {device}") @@ -127,10 +132,12 @@ def evaluate(args: RslRlPPOConfig): init_noise_std=policy_cfg.init_noise_std, ).to(device) + state_dict = checkpoint['model_state_dict'] + state_dict = {k: v for k, v in state_dict.items() if 'critic' not in k} # Load the model weights - actor_critic.load_state_dict(checkpoint['model_state_dict']) + actor_critic.load_state_dict(state_dict, strict=False) + actor_critic.to(device) actor_critic.eval() - # Create inference policy (just the actor part) policy = actor_critic.act_inference @@ -142,8 +149,9 @@ def evaluate(args: RslRlPPOConfig): env.reset() obs, _, _, _, _ = env.step(torch.zeros(env.num_envs, env.num_actions, device=device)) obs = wrapped_env.get_observations() - print(f"Starting evaluation for 1000000 steps...") + + t0 = time.time() for i in range(1000000): # set fixed command env.commands_manager.value[:, 0] = 0.5 @@ -151,10 +159,8 @@ def evaluate(args: RslRlPPOConfig): env.commands_manager.value[:, 2] = 0.0 actions = policy(obs) obs, _, _, _ = wrapped_env.step(actions) - - if (i + 1) % 1000 == 0: - print(f"Step {i + 1}/1000000") - + if (i + 1) % 100 == 0: + print(f"Step {i + 1}/1000000 | Simulation time: {(i + 1) * env.step_dt:.2f}s | Elapsed time: {time.time() - t0:.2f}s") print("Evaluation complete!") diff --git a/roboverse_pack/callback_funcs/humanoid/reward_funcs.py b/roboverse_pack/callback_funcs/humanoid/reward_funcs.py index 6c3ad78ea..c285e98bb 100644 --- a/roboverse_pack/callback_funcs/humanoid/reward_funcs.py +++ b/roboverse_pack/callback_funcs/humanoid/reward_funcs.py @@ -93,6 +93,26 @@ def joint_pos_limits(env: EnvTypes, env_states: TensorState) -> torch.Tensor: return torch.sum(out_of_limits, dim=1) +def joint_effort_limits(env: EnvTypes, env_states: TensorState, soft_limit_factor: float = 1.0) -> torch.Tensor: + """Penalize joint efforts that exceed torque limits using an L2 squared kernel.""" + robot_state = env_states.robots[env.name] + if env.manual_pd_on: + processed_actions = (env.actions * env.action_scale + env.actions_offset).clip( + -env.action_clip, + env.action_clip, + ) + effort = env.p_gains * (processed_actions - robot_state.joint_pos) - env.d_gains * robot_state.joint_vel + else: + effort = robot_state.joint_effort_target + if effort is None: + return torch.zeros(env.num_envs, dtype=torch.float, device=env.device) + + torque_limits = env.torque_limits * soft_limit_factor + excess = torch.abs(effort) - torque_limits + excess = excess.clamp(min=0.0) + return torch.sum(torch.square(excess), dim=1) + + def energy(env: EnvTypes, env_states: TensorState) -> torch.Tensor: r"""Sum |qdot|*|tau| across joints ("energy" usage).""" base = env_states.robots[env.name] diff --git a/roboverse_pack/robots/__init__.py b/roboverse_pack/robots/__init__.py index c55ad98d2..2ac0d5d51 100644 --- a/roboverse_pack/robots/__init__.py +++ b/roboverse_pack/robots/__init__.py @@ -2,6 +2,7 @@ from metasim.scenario.robot import RobotCfg +from .agibot_a2_cfg import AgibotA2Dof12Cfg from .allegrohand_cfg import AllegroHandCfg from .aloha import AlohaCfg from .ant_cfg import AntCfg diff --git a/roboverse_pack/robots/agibot_a2_cfg.py b/roboverse_pack/robots/agibot_a2_cfg.py new file mode 100644 index 000000000..a92993127 --- /dev/null +++ b/roboverse_pack/robots/agibot_a2_cfg.py @@ -0,0 +1,132 @@ +from __future__ import annotations + +from typing import Literal + +from metasim.scenario.robot import BaseActuatorCfg, RobotCfg +from metasim.utils import configclass + + +@configclass +class AgibotA2Dof12Cfg(RobotCfg): + name: str = "agibot_a2_dof12" + num_joints: int = 12 + urdf_path: str = "roboverse_data/robots/agibot_a2/urdf/agibot_a2_dof12.urdf" + usd_path: str = "roboverse_data/robots/agibot_a2/usd/agibot_a2_dof12.usd" + xml_path: str = "roboverse_data/robots/agibot_a2/mjcf/agibot_a2_dof12.mjcf" + mjcf_path: str = xml_path + + isaacgym_flip_visual_attachments: bool = False + enabled_self_collisions: bool = False + + actuators: dict[str, BaseActuatorCfg] = { + "idx01_left_hip_roll": BaseActuatorCfg(stiffness=130, damping=5.0, effort_limit_sim=76.8, velocity_limit=12.0), + "idx02_left_hip_yaw": BaseActuatorCfg(stiffness=130, damping=3.0, effort_limit_sim=76.8, velocity_limit=12.0), + "idx03_left_hip_pitch": BaseActuatorCfg( + stiffness=200, damping=6.0, effort_limit_sim=216.0, velocity_limit=12.0 + ), + "idx04_left_tarsus": BaseActuatorCfg(stiffness=220, damping=7.0, effort_limit_sim=216.0, velocity_limit=12.0), + "idx05_left_toe_pitch": BaseActuatorCfg(stiffness=50, damping=1.5, effort_limit_sim=38.4, velocity_limit=12.0), + "idx06_left_toe_roll": BaseActuatorCfg(stiffness=50, damping=1.5, effort_limit_sim=38.4, velocity_limit=12.0), + "idx07_right_hip_roll": BaseActuatorCfg(stiffness=130, damping=5.0, effort_limit_sim=76.8, velocity_limit=12.0), + "idx08_right_hip_yaw": BaseActuatorCfg(stiffness=130, damping=3.0, effort_limit_sim=76.8, velocity_limit=12.0), + "idx09_right_hip_pitch": BaseActuatorCfg( + stiffness=200, damping=6.0, effort_limit_sim=216.0, velocity_limit=12.0 + ), + "idx10_right_tarsus": BaseActuatorCfg(stiffness=220, damping=7.0, effort_limit_sim=216.0, velocity_limit=12.0), + "idx11_right_toe_pitch": BaseActuatorCfg(stiffness=50, damping=1.5, effort_limit_sim=38.4, velocity_limit=12.0), + "idx12_right_toe_roll": BaseActuatorCfg(stiffness=50, damping=1.5, effort_limit_sim=38.4, velocity_limit=12.0), + } + + joint_limits: dict[str, tuple[float, float]] = { + # Hips & legs + "idx01_left_hip_roll": (-0.698, 0.698), + "idx02_left_hip_yaw": (-1.57, 1.57), + "idx03_left_hip_pitch": (-1.919862144, 0.78539815), + "idx04_left_tarsus": (-0.087266461, 2.443460911), + "idx05_left_toe_pitch": (-1.047197533, 0.523598767), + "idx06_left_toe_roll": (-1.972222, 1.972222), + "idx07_right_hip_roll": (-0.698, 0.698), + "idx08_right_hip_yaw": (-1.57, 1.57), + "idx09_right_hip_pitch": (-1.919862144, 0.78539815), + "idx10_right_tarsus": (-0.087266461, 2.443460911), + "idx11_right_toe_pitch": (-1.047197533, 0.523598767), + "idx12_right_toe_roll": (-1.972222, 1.972222), + } + + default_joint_positions: dict[str, float] = { + # Hips & legs + "idx01_left_hip_roll": 0.0, + "idx02_left_hip_yaw": 0.0, + "idx03_left_hip_pitch": -0.115, + "idx04_left_tarsus": 0.267, + "idx05_left_toe_pitch": -0.152, + "idx06_left_toe_roll": 0.0, + "idx07_right_hip_roll": 0.0, + "idx08_right_hip_yaw": 0.0, + "idx09_right_hip_pitch": -0.115, + "idx10_right_tarsus": 0.267, + "idx11_right_toe_pitch": -0.152, + "idx12_right_toe_roll": 0.0, + } + + control_type: dict[str, Literal["position", "effort"]] = { + # Hips & legs + "idx01_left_hip_roll": "effort", + "idx02_left_hip_yaw": "effort", + "idx03_left_hip_pitch": "effort", + "idx04_left_tarsus": "effort", + "idx05_left_toe_pitch": "effort", + "idx06_left_toe_roll": "effort", + "idx07_right_hip_roll": "effort", + "idx08_right_hip_yaw": "effort", + "idx09_right_hip_pitch": "effort", + "idx10_right_tarsus": "effort", + "idx11_right_toe_pitch": "effort", + "idx12_right_toe_roll": "effort", + } + + # rigid body name substrings, to find indices of different rigid bodies. + feet_links: list[str] = ["left_toe_roll", "right_toe_roll"] + knee_links: list[str] = ["left_tarsus", "right_tarsus"] + torso_links: list[str] = ["base_link"] + elbow_links: list[str] = [ + "left_arm_link05", + "right_arm_link05", + ] # TODO(zhangyi): find elbow links and add them here + terminate_contacts_links = [ + "base_link", + "left_hip_roll", + "left_hip_yaw", + "left_hip_pitch", + "left_tarsus", + "left_toe_pitch", + "left_toe_roll", + "right_hip_roll", + "right_hip_yaw", + "right_hip_pitch", + "right_tarsus", + "right_toe_pitch", + "right_toe_roll", + ] + penalized_contacts_links: list[str] = [ + "left_hip_roll", + "left_hip_yaw", + "left_hip_pitch", + "left_tarsus", + "left_toe_pitch", + "left_toe_roll", + "right_hip_roll", + "right_hip_yaw", + "right_hip_pitch", + "right_tarsus", + "right_toe_pitch", + "right_toe_roll", + ] + + # joint substrings, to find indices of joints. + left_hip_yaw_roll_joints: list[str] = ["idx02_left_hip_yaw", "idx01_left_hip_roll"] + right_hip_yaw_roll_joints: list[str] = ["idx08_right_hip_yaw", "idx07_right_hip_roll"] + + soft_joint_pos_limit_factor = 0.95 + # From default joint armature in XML + armature: float = 0.01 diff --git a/roboverse_pack/tasks/humanoid/cfg_base.py b/roboverse_pack/tasks/humanoid/cfg_base.py index 30a3c6569..7e29523b5 100644 --- a/roboverse_pack/tasks/humanoid/cfg_base.py +++ b/roboverse_pack/tasks/humanoid/cfg_base.py @@ -107,6 +107,17 @@ class InitialStates: "right_wrist_roll_joint": -0.15, }, }, + "agibot_a2_dof12": { + "pos": [0.0, 0.0, 0.98], + "default_joint_pos": { + ".*_hip_roll": 0.0, + ".*_hip_yaw": 0.0, + ".*_hip_pitch": -0.115, + ".*_tarsus": 0.267, + ".*_toe_pitch": -0.152, + ".*_toe_roll": 0.0, + }, + }, } initial_states = InitialStates() diff --git a/roboverse_pack/tasks/humanoid/locomotion/walk_agibot_a2_dof12.py b/roboverse_pack/tasks/humanoid/locomotion/walk_agibot_a2_dof12.py new file mode 100644 index 000000000..44c3f49c4 --- /dev/null +++ b/roboverse_pack/tasks/humanoid/locomotion/walk_agibot_a2_dof12.py @@ -0,0 +1,336 @@ +from __future__ import annotations + +import copy +import math + +import torch + +from metasim.queries import ContactForces +from metasim.scenario.lights import DomeLightCfg +from metasim.scenario.scenario import ScenarioCfg +from metasim.scenario.simulator_params import SimParamCfg +from metasim.task.registry import register_task +from metasim.types import TensorState +from metasim.utils import configclass +from metasim.utils.math import euler_xyz_from_quat, quat_rotate_inverse +from roboverse_pack.callback_funcs.humanoid import ( + reset_funcs, + reward_funcs, + step_funcs, + termination_funcs, +) +from roboverse_pack.randomization.humanoid import ( + MassRandomizer, + MaterialRandomizer, +) +from roboverse_pack.tasks.humanoid.base import LeggedRobotTask +from roboverse_pack.tasks.humanoid.cfg_base import BaseEnvCfg +from roboverse_pack.utils.curriculum_utils import lin_vel_cmd_levels +from roboverse_pack.utils.humanoid_utils import Indexer + + +@configclass +class WalkAgibotA2Dof12EnvCfg(BaseEnvCfg): + """Configuration for the 12-DOF A2 walking task.""" + + episode_length_s = 20.0 + obs_len_history = 1 + priv_obs_len_history = 1 + + control = BaseEnvCfg.Control( + action_scale=0.5, + action_clip=100, + soft_joint_pos_limit_factor=0.95, + decimation=5, + ) + + @configclass + class RewardsScales: + """Reward weights for gait stability and efficiency.""" + + track_lin_vel_xy = (2.0, {"std": math.sqrt(0.25)}) + track_ang_vel_z = (1.0, {"std": math.sqrt(0.25)}) + lin_vel_z = -2.0 + ang_vel_xy = -0.05 + flat_orientation = -1.0 + base_height = (-100.0, {"target_height": 0.98}) + joint_acc = -1e-7 + joint_vel = -0.001 + action_rate = -0.2 + joint_pos_limits = -5.0 + joint_effort_limits = (-1e-5, {"soft_limit_factor": 0.95}) + is_alive = 0.3 + joint_deviation_legs = ( + -1.0, + {"joint_names": (".*_hip_roll.*", ".*_hip_yaw.*", ".*_toe_roll.*")}, + reward_funcs.joint_deviation_l1, + ) + feet_slide = (-0.2, {"body_names": (".*toe_roll.*")}) + feet_clearance = ( + 1.0, + { + "std": math.sqrt(0.05), + "tanh_mult": 2.0, + "target_height": 0.18, + "body_names": (".*toe_roll.*"), + }, + ) + feet_gait = ( + 0.18, + { + "period": 1.0, + "offset": [0.0, 0.5], + "threshold": 0.55, + "body_names": (".*toe_roll.*"), + }, + ) + # energy = -1e-5 + ######################## + + rewards = BaseEnvCfg.Rewards(scales=RewardsScales(), only_positive_rewards=True) + + commands = BaseEnvCfg.Commands( + value=None, + resample=step_funcs.resample_commands, + heading_command=True, + resampling_time=10.0, + rel_standing_envs=0.02, + ranges=BaseEnvCfg.Commands.Ranges( + lin_vel_x=(-1.0, 1.0), + lin_vel_y=(-0.5, 0.5), + ang_vel_yaw=(-1.0, 1.0), + heading=(-3.14, 3.14), + ), + limit_ranges=BaseEnvCfg.Commands.Ranges( + lin_vel_x=(-1.0, 1.0), + lin_vel_y=(-0.5, 0.5), + ang_vel_yaw=(-1.0, 1.0), + heading=(-3.14, 3.14), + ), + ) + + curriculum = BaseEnvCfg.Curriculum( + enabled=False, + funcs={ + "lin_vel_cmd_levels": lin_vel_cmd_levels, + }, + ) + + callbacks_query = {"contact_forces": ContactForces(history_length=3)} + callbacks_setup = { + "material_randomizer": MaterialRandomizer( + obj_name="agibot_a2_dof12", + static_friction_range=(0.1, 1.25), + dynamic_friction_range=(0.1, 1.25), + restitution_range=(0.0, 0.0), + num_buckets=64, + ), + "mass_randomizer": MassRandomizer( + obj_name="agibot_a2_dof12", + body_names="base_link", + mass_distribution_params=(-1.0, 3.0), + operation="add", + ), + } + callbacks_reset = { + "random_root_state": ( + reset_funcs.random_root_state, + { + "pose_range": [ + [0.0, 0.0, 0, 0, 0, 0], + [0.0, 0.0, 0, 0, 0, 0], + ], + "velocity_range": [[-0.5] * 6, [0.5] * 6], + }, + ), + "reset_joints_by_scale": ( + reset_funcs.reset_joints_by_scale, + {"position_range": (0.5, 1.5), "velocity_range": (1.0, 1.0)}, + ), + } + callbacks_post_step = { + "push_robot": ( + step_funcs.push_by_setting_velocity, + { + "interval_range_s": (5.0, 5.0), + "velocity_range": [[-1.5, -1.5, 0.0], [1.5, 1.5, 0.0]], + }, + ) + } + callbacks_terminate = { + "time_out": termination_funcs.time_out, + "undesired_contact": ( + termination_funcs.undesired_contact, + { + # TODO(zhangyi): add more undesired contact names here + "contact_names": [ + ".*base_link.*", + ], + "limit_range": 1.0, + }, + ), + "bad_orientation": (termination_funcs.bad_orientation, {"limit_angle": 0.8}), + "root_height_below_minimum": (termination_funcs.root_height_below_minimum, {"minimum_height": 0.7}), + } + + +@register_task( + "agibot_a2.walk_agibot_a2_dof12", + "walk_agibot_a2_dof12", +) +class WalkAgibotA2Dof12Task(LeggedRobotTask): + """Registered task wrapper with scenario defaults and cfg hooks.""" + + env_cfg_cls = WalkAgibotA2Dof12EnvCfg + task_name = "walk_agibot_a2_dof12" + + scenario = ScenarioCfg( + robots=["agibot_a2_dof12"], + objects=[], + cameras=[], + num_envs=128, + simulator="isaacgym", + headless=True, + env_spacing=2.5, + decimation=1, + sim_params=SimParamCfg( + dt=0.002, + substeps=1, + num_threads=10, + solver_type=1, + num_position_iterations=4, + num_velocity_iterations=0, + contact_offset=0.01, + rest_offset=0.0, + bounce_threshold_velocity=0.5, + max_depenetration_velocity=1.0, + default_buffer_size_multiplier=5, + replace_cylinder_with_capsule=True, + friction_correlation_distance=0.025, + friction_offset_threshold=0.04, + ), + lights=[ + DomeLightCfg( + intensity=800.0, + color=(0.85, 0.9, 1.0), + ) + ], + ) + + def __init__( + self, + scenario: ScenarioCfg | None = None, + device: str | torch.device | None = None, + env_cfg: WalkAgibotA2Dof12EnvCfg | None = None, + ) -> None: + scenario_copy = copy.deepcopy(scenario or type(self).scenario) + scenario_copy.__post_init__() + + if env_cfg is None: + env_cfg = type(self).env_cfg_cls() + + if device is None: + device = "cpu" if scenario_copy.simulator == "mujoco" else ("cuda" if torch.cuda.is_available() else "cpu") + + super().__init__(scenario=scenario_copy, config=env_cfg, device=device) + + def _init_buffers(self): + # ---------- obs slice ---------- + indexer = Indexer() + + s_sin = indexer.take(1) + s_cos = indexer.take(1) + s_cmd = indexer.take(3) # [lin_x, lin_y, yaw] + s_cmd_lin = slice(s_cmd.start, s_cmd.start + 2) # only use the first two dimensions for scaling + s_dof_pos = indexer.take(self.num_actions) + s_dof_vel = indexer.take(self.num_actions) + s_prev_act = indexer.take(self.num_actions) + s_base_ang = indexer.take(3) + s_base_euler = indexer.take(3) + + self.num_obs_single = indexer.i # should be 47 + + s_base_lin = indexer.take(3) + + self.num_priv_obs_single = indexer.i # should be 50 + + # ---------- init buffer ---------- + self.obs_clip_limit = 100.0 + self.obs_scale = torch.ones(self.num_obs_single, dtype=torch.float, device=self.device) + self.priv_obs_scale = torch.ones(self.num_priv_obs_single, dtype=torch.float, device=self.device) + self.obs_noise = torch.zeros(self.num_obs_single, dtype=torch.float, device=self.device) + + ####### for observation scale ####### + self.obs_scale[s_cmd_lin] = 2.0 + self.obs_scale[s_dof_vel] = 0.05 + + ####### for priviliged observation scale ####### + self.priv_obs_scale[s_cmd_lin] = 2.0 + self.priv_obs_scale[s_dof_vel] = 0.05 + self.priv_obs_scale[s_base_lin] = 2.0 + + ####### for observation noise ####### + self.obs_noise[s_dof_pos] = 0.02 + self.obs_noise[s_dof_vel] = 1.5 + self.obs_noise[s_base_ang] = 0.2 + self.obs_noise[s_base_euler] = 0.05 + + return super()._init_buffers() + + def gait_phase(self, period: float = 0.8) -> torch.Tensor: + """Compute gait phase based on episode length buffer.""" + global_phase = (self._episode_steps * self.step_dt) % period / period + + phase = torch.zeros(self.num_envs, 2, device=self.device) + phase[:, 0] = torch.sin(global_phase * torch.pi * 2.0) + phase[:, 1] = torch.cos(global_phase * torch.pi * 2.0) + return phase + + def _compute_task_observations(self, env_states: TensorState): + robot_state = env_states.robots[self.robot.name] + base_quat = robot_state.root_state[:, 3:7] + base_lin_vel = quat_rotate_inverse(base_quat, robot_state.root_state[:, 7:10]) + base_ang_vel = quat_rotate_inverse(base_quat, robot_state.root_state[:, 10:13]) + roll, pitch, yaw = euler_xyz_from_quat(base_quat) + base_euler_xyz = torch.stack([roll, pitch, yaw], dim=-1) + + gait_phase = self.gait_phase() + + q = env_states.robots[self.robot.name].joint_pos - self.default_dof_pos + dq = env_states.robots[self.robot.name].joint_vel - self.default_dof_vel + prev_act = self.actions + + obs_buf = torch.cat( + ( + gait_phase, # 2 + self.commands_manager.value, # 3 + q, # num_actions + dq, # num_actions + prev_act, # num_actions + base_ang_vel, # 3 + base_euler_xyz, # 3 + ), + dim=-1, + ) + + priv_obs_buf = torch.cat( + ( + gait_phase, # 2 + self.commands_manager.value, # 3 + q, # num_actions + dq, # num_actions + prev_act, # num_actions + base_ang_vel, # 3 + base_euler_xyz, # 3 + base_lin_vel, # 3 + ), + dim=-1, + ) + + obs_buf += (2 * torch.rand_like(obs_buf) - 1) * self.obs_noise + + # clip observations -> scale observations + obs_buf = obs_buf.clip(-self.obs_clip_limit, self.obs_clip_limit) * self.obs_scale + priv_obs_buf = priv_obs_buf.clip(-self.obs_clip_limit, self.obs_clip_limit) * self.priv_obs_scale + + return obs_buf, priv_obs_buf diff --git a/roboverse_pack/utils/humanoid_utils.py b/roboverse_pack/utils/humanoid_utils.py index 5c2ee2e0c..46bdc5489 100644 --- a/roboverse_pack/utils/humanoid_utils.py +++ b/roboverse_pack/utils/humanoid_utils.py @@ -226,3 +226,16 @@ def hash_names(names: str | tuple[str]) -> str: ) hash_key = "_".join(sorted(names)) return hash_key + + +class Indexer: + """Utility that assigns consecutive slice ranges.""" + + def __init__(self): + self.i = 0 + + def take(self, n: int) -> slice: + """Return slice(i, i+n) and advance internal counter.""" + s = slice(self.i, self.i + n) + self.i += n + return s From 902c65614f2e494016ee0f7ecf597edeec539704 Mon Sep 17 00:00:00 2001 From: Mingyuan Sun <64095400+myuansun@users.noreply.github.com> Date: Fri, 23 Jan 2026 13:54:23 +0800 Subject: [PATCH 49/50] [Feature] Newton Handler (#755) * [add] initial newton handler supporting get started 0 * [fix] add GUI viewer code * [add] robot control * [update] add env spacing and njmax, nconmax to pass get started 0-3 * [fix] camera support env origin * [update] g1 sim2sim ready, but looks quite slow * [add] pytest for newton * [add] per-body gravity compensation * [fix] per-object joint name resolution, skip joint limit test for newton * [fix] decouple camera from OpenGL GUI, align urdf loading details and effort control details * [fix] validate inertia * [fix] contact sensor layout * [update] drop dedundant test case * [update] set newton_use_mujoco_contacts to false for training in newton * [update] add mass and material randomizer * [update] optimize training speed and fix a weird contact bug * docs: add newton support and compressed assets --- ...aaclab.png => 0_static_scene_isaacsim.png} | Bin .../standard_output/0_static_scene_newton.png | Bin 0 -> 76108 bytes .../1_control_robot_newton.gif | Bin 0 -> 195256 bytes .../standard_output/1_move_robot_newton.mp4 | Bin 0 -> 380149 bytes .../2_add_new_robot_newton.gif | Bin 0 -> 220383 bytes .../2_add_new_robot_newton.mp4 | Bin 0 -> 474247 bytes .../3_parallel_envs_newton.gif | Bin 0 -> 268757 bytes .../3_parallel_envs_newton.mp4 | Bin 0 -> 344384 bytes .../tasks/generate_task_docs.py | 2 +- .../metasim/developer_guide/autotest.md | 6 +- docs/source/metasim/features/cross_sim.md | 4 +- .../metasim/features/support_matrix.rst | 46 +- .../advanced/rl_example/infrastructure.md | 9 +- .../metasim/get_started/installation.rst | 4 + .../metasim/get_started/prerequisite.md | 10 +- .../get_started/quick_start/0_static_scene.md | 13 +- .../quick_start/1_control_robot.md | 17 +- .../quick_start/2_add_new_robot.md | 17 +- .../quick_start/3_parallel_envs.md | 17 +- .../metasim/get_started/quick_start/guide.md | 2 +- get_started/0_static_scene.py | 1 + get_started/1_control_robot.py | 1 + get_started/2_add_new_robot.py | 1 + get_started/3_parallel_envs.py | 4 +- metasim/constants.py | 1 + metasim/queries/contact_force.py | 131 +- metasim/scenario/objects.py | 1 + metasim/scenario/simulator_params.py | 6 +- metasim/sim/newton/__init__.py | 5 + metasim/sim/newton/newton.py | 2253 +++++++++++++++++ metasim/test/conftest.py | 5 +- metasim/test/sim/test_dof_control.py | 45 +- metasim/test/sim/test_parallel_handler.py | 10 +- metasim/test/sim/test_state_consistency.py | 2 +- metasim/test/sim/test_state_modes.py | 8 +- .../test/test_scenario_cfg/test_1_robot.py | 2 +- .../test/test_scenario_cfg/test_2_robots.py | 2 +- .../test_robot_cfg/test_collision.py | 4 +- .../test_robot_cfg/test_default_pos.py | 2 +- .../test_robot_cfg/test_default_qpos.py | 2 +- .../test_robot_cfg/test_qpos_limit.py | 1 + metasim/test/test_utils.py | 4 +- metasim/utils/setup_util.py | 8 + pyproject.toml | 8 + roboverse_learn/rl/configs/rsl_rl/ppo.py | 2 + roboverse_learn/rl/rsl_rl/eval.py | 4 + roboverse_learn/rl/rsl_rl/ppo.py | 4 + roboverse_pack/randomization/humanoid.py | 202 +- 48 files changed, 2764 insertions(+), 102 deletions(-) rename docs/source/_static/standard_output/{0_static_scene_isaaclab.png => 0_static_scene_isaacsim.png} (100%) create mode 100644 docs/source/_static/standard_output/0_static_scene_newton.png create mode 100644 docs/source/_static/standard_output/1_control_robot_newton.gif create mode 100644 docs/source/_static/standard_output/1_move_robot_newton.mp4 create mode 100644 docs/source/_static/standard_output/2_add_new_robot_newton.gif create mode 100644 docs/source/_static/standard_output/2_add_new_robot_newton.mp4 create mode 100644 docs/source/_static/standard_output/3_parallel_envs_newton.gif create mode 100644 docs/source/_static/standard_output/3_parallel_envs_newton.mp4 create mode 100644 metasim/sim/newton/__init__.py create mode 100644 metasim/sim/newton/newton.py diff --git a/docs/source/_static/standard_output/0_static_scene_isaaclab.png b/docs/source/_static/standard_output/0_static_scene_isaacsim.png similarity index 100% rename from docs/source/_static/standard_output/0_static_scene_isaaclab.png rename to docs/source/_static/standard_output/0_static_scene_isaacsim.png diff --git a/docs/source/_static/standard_output/0_static_scene_newton.png b/docs/source/_static/standard_output/0_static_scene_newton.png new file mode 100644 index 0000000000000000000000000000000000000000..2b1134d452a60212eac56100917e2e5637229c83 GIT binary patch literal 76108 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 z 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

TJ1cXxMpcXtWy65Nvj36_NF?6-Tm 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�E zklYJU-EVxNj9x1ikA#ZNQZnebVSQ#b#PvOZduxdAW5`+o9x`D7Vs232yM|adpeS%q zf(JSxYgiscuD)=i!gvw^`JO#c(%G%{D6ff zbCkGyGBVQ%tc8oWTJT!~TvheZC9$;n#}Moij5^QER2L(>1J9cEpt+R6iRBPY0vO|> z6hih8(RY1a)@gq29%GlOGr{3<^FpbWX zYQSPaz&s!BxF8r5l+1_#Zr1+}!mI^j0y_oc{@-E~L2#sEaR3EycQEL`YJdNCY%<

_7E<5dYm$*cyxAb%U4r?O1dp2H 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 M16MKdkD2-Z0KgskF#rGn literal 0 HcmV?d00001 diff --git a/docs/source/_static/standard_output/2_add_new_robot_newton.gif b/docs/source/_static/standard_output/2_add_new_robot_newton.gif new file mode 100644 index 0000000000000000000000000000000000000000..620eb0bbe975989e989bd292d0a0e2774e2527b6 GIT binary patch literal 220383 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 I`X#de23Ug(V*mgE literal 0 HcmV?d00001 diff --git a/docs/source/_static/standard_output/2_add_new_robot_newton.mp4 b/docs/source/_static/standard_output/2_add_new_robot_newton.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..bbf2ed8868ddeb57fe7cb9588cf2f67a5a47e073 GIT binary patch literal 474247 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 ya*@K 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

TT6NgnE7+GoH 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 z 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%0An40MX|;Th5l(zx%1J|?syx> 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%0Da?Hi2ufZR`{gxfh}XDHmCzMW6^x%?PF

HH4|Apug}rj7mLNO!(4(bkvMY4>W- zzG$$eCS9?~4FAz(UL5ih>5O6nCF~6VPH%P&3=CdZ+1_x%Kf;P3vwSBXoAe`sj6<1t zQ|aevG7jh%ZQZKL_r_CSYskr{BZ}=}-*sI+kJ*58r%- z>Ey44X1Emgl5C>T8Tg};B^WP~7Y*xYfQNVU! z|7xDM)59E$0H^9IZf;?Uvvsxr$9z*PtU-iuVN%KqoibeV9}&9L3k_OZwtvQ#Q6>|% z?fsy+(>$JCv1QL4064 zn!j_w>rESe|L)U>zb42|8_~5ViHFmHxMOBS)p#ISsqQ>gkZZs5AcU>@$x+4yrTbTP z$8PzZ#o39xdEE;YMfi{NFv*u3%(8(ng)6mP0b3+(snz4k>98L#(-^cgn>kfhxV>9W2kk$*a^$nc+z z@=i0U+VbH;T~1(jXOyK-$^L3XKU<%GJtbCaSc1Y$dG6gG@&lVS+`FtBlSP_HCzQAi zXqeAO;#6(vEIREouMUL1JarSCMV962>u7hOy-ko0jjswZ(`PQuPD zw@`&HV!~dmuHL%`9C<5+q}X}#(7*T*2-nPrsIaZ}*K9VtnCFP&F=Ja8sL{F;FcWCX z<-FcLPgPIDjRA_<)ncVYt~2g&2lcPd?=OVZh)9)5(oSK_TMPhCK(N1b(i=sCwzzOV z^$!y<>ga@PSNg{mF2#uC?WWK$!#9dD1-izYSv&=HsWAbp#HzAoziByBePs}OQat{k zPAG?Om?*5bO013_06%u_jg(czE-5bZcSB-_n>7>@k@~3__#A zOPD4Z)hCVG%|cdedTr_P5d^_*M6FLD=iW#nyHZ8X%=&z0+ApSF9R&!t#-^XFol{t+7XzA3i=_VbT>of40Q~%l=JP1JL(S~*n#Xq∈C}R~E)ji9(B~y-~Yxn%KRjr^N8F zd4ftS0DY{w^cCZudv}mP!Bc!KGYMwE?f+s!#@7z>38n=(HNSh8_}G79WSSR&SjZw!6u-QCrG80*%r`xr8t zusVyWNL?s`OWwBP3c}Zp@PEC=95&ui%7NfivW{Qq2v!P#mT6efw&z%7aT~(%5M`h= z1n!vWUrn+u3P%(@fAA0dPUUMlTR;Na z@M(PDSh45mK%U$ttK!JW+@${w85!9_*2E zmlE?~!ZK#oVcB^wvA+zw{a>rckO{Cz1M_WhmOIlERRI#1c0$}IO~aXj$}{{!UMqv8 zP>kYyNt^<11C6-WiXD7eu*ArEeK;y8KQ-VnjlGgF^nhE>1&UC|S+tZTreRQnu6mM0WwrTI*HJN66+2;`x0=WIX^|^5}xFT6Ck~ zJdi4mu+GtuI!fHlC?y5q-EeBi0=cFdnp$z>1J)|tei%?>(Nuo9ThXk|tqG3y#MU@Y zvWaGh@m630CVmJZ{g{1fg_|7rOu6_xgxb2nO0;J)q6-2pHm~Kj!j=8)JOGlAfBUbS znW-Cw0B=!t)pC4H5$tq zB9FPjy#p_>aA;rEa=Jq?rg#t%7X%^+V=k(!P`6ewf~JhKxPt&EpuOE(rMU00sqr7~ z2s`e-W+GmVzp|uq^zj#KSd8=jhg1-_@m9tgt|b=b>|k!LpueQ9usXK`AF|DR~6%d*{6Mp_rAkA zlGE&Iq`|fHZNYMTop4GUMHZ^EDxB*#k}$pBh2>nG02#&-^n&FY`&gkdQINEeeBXHh zTpS(iA*rBwm-Kr!CyyxpKm}?C#`79;#vzRPhcHJ*;|1z3LimVXTF0KKwRg13{(ZoZ zTmS$bLP46PN#PGBQvwtJfC~F#T5{ejD?E5}O~xXt+Me5=kWd()1VdDq@dM;N8md)9 z?URRhroqGP5Mrr=08(UoKXG3z@SI#?H zdQbgch5+IM+6I%3=wr#sEa^$AkJ zV~JNRj8#!&KX0j_s$V(cJrE)m9Tg8u+MW06qts+`Wl8@#a9WfM^O&S|Sb)_8+IUVO z!zqNgBVm8CPbw0L7*o74&@B$JQyQxgw`--7)-YK6TbyxeF=nT6C(_$Ywh^5EQiqk5 zL#jjK8mI2BQCG;u**iEBCcOM?`!bh-{Si^XEqc(Fh(SF$rFnEf@)8M5=AbYQ&T*k2 z1Kip*zE)5fM|zS`sps-Fv#>M4sCxEnWj^8RWV)KlXX-1TZ)2maWdSU`l|d9~TJV6v zK-RkVkT|p4DNj9QM6!r+kL3Hw)FJ}0NXX4mY8a!y{p0lw*@O%0k(eADDA&+GnqX2A zIgM&Uu1}+SBn7GPlu!1G>sSxJ{MGqj(KDSK=P5KSK|UTNS{>6IEx|}^oZ|ED~fK9=~o?`Zt|ge4xAe} zyd-A9?omUNR)J-@zM#XF!&&Y*|BWOD(@dm^2*N`z}F+woQiivix4M(}7`5YM!=YzQ7VL35q?%9Wsj*%J~08B=< z5e=b7v2k5}z8U+RO_DdgG-BW{cgd02?E-Gvcqt=#aZL%z8kZ0wu5KgNfEKcc%k}hN z$Zf=zM?G=ZSI%h#3w@%U23B`GvUL&8wte~n(2z-YZP$a=nKXP5I}m&Y=&ndYPh5!K2jsw0UnNqC~=2x^i-oE=k zm(%IR+N822tFWCq>q_zE$q2vz~jB4f?0(%()^bR(kF;TO-fqF5#9LGq7Nk zW-1_p8#9RG-iTpD6fYo z?LevUKRsxPCd+pVv${qyo>3nlaV{`I_wT)>cbZcal9N$V|zYg%KguVi3VI`{uil{4d;%tJYuG9h*di|OI&MnW? z0(l2YBAF%F`UEab{8C2QAu8Jk+kj$cfWwqB^omJJZSK$+`Be2TKo8Ed?bhJzNyIT- zafo@rMHtO{(vscyJ*psTN-rHNh!^AvVK#>yw;-TJ?!HUnTg7v^%zV z8TRW&xVrqLw1hk2q*_?2XcmF#cb80dPd0+wXlb z68Q|qa*f9pA#B#f6aam&H8Nq+NuV2GS-6-c%{3Fy*uSh=%(wmOZqp)-9H5zc_1!?y zVGvS<)yhwwgIwM1$V$>%r*S;OJGbFR{t6lbLvgBr?f#9LjDeA~l!t&mn#bC{{=S z0O93M4?y+WK)Xr0yh`tIU$M;*+N*!#$N&3OZ=tS{*~hk3a@PcQ19?}-WxiLvFYc;k z$CXbyRkBL!igcOYjN@n^n_&UBBx5CpDPILdPuzpB_=rPot3!Q(}viI zvd144)7?A!z(0J1mAd2Ex3s{jhd5Jf8>QR+iTezK)GXpv3z_$bJs}R;7!HXa1>>;23pv%!Q@In2cu=nPvj|C5;_keMnd^erx z5kXua8ia3PhLoKqlo}!+#KSO&su}Vw&aHC&;|&YkfRv!$hId%f+k>xu3ba3dH1Nx- z{!S`Q_^x6BO{lL|_bAoDKzS6K%Bark-q>sLAx@QGC5VyvlHVi*^M>1KCo~U~iYi8E z$W~!5aD3aFJo@uR7fszp)FV{jIkIaD4aS<^760x1E3Y6LB{Yz&*)1?u#PoO+lEP^5 ztjP?%T%DVUNwg^o9c)mpJo2y4t!^vWoa*SyKR|aJLQ$TrIe&UAj2R1%^fmi;7Ez~I z1(1Pm!Z(#H+WRs)ZAn>yRerdFZI6kbg-b~J# zz!eEvP}?Vf)l;bu-hD*F-z?}7cjXcOopiWgpBoIHD9&5xczHTDU-YLn%T~Kr>QSnx z8_5WBx_iOZ!#?#w**10f2pJO%4sKFA5%V1ps>QPpbF+K!@>}0i2EUjw^yfV4$W%@1 zQd5GG8v1qO%DGu+A;EHTcgME>JE8AL&3_<@O>u0d7&&1YJEmK35a46n-%R~EJ&g<; zbQ-tDGLDVz(cQ4H9qgoJrv=on7YjsHQ;L#~wjw_qAd<;nac;OJ!_BMkvwk83Xz>zB zzB_VUs2c&iUg?VMu)kZ=#RRu_3_>(QYF@$kVq(dA3b#gWrZ$-o{ZfPX4#BxY2||)? zd@gkUdJK&*lJD||-aiG4eg^;?NSfsx+j&%4+tU(tSa*(JyXnffRQp`cOc0k*JP!_H zP|%vCrGYNdwBh5>2d@B?N%LF#4nB7-enicUaQ97XsQF`Y4biGF!dBz^=Z3mYJ$JuW zpS!6;zlylh3ZPS%8+f-EtP7s<-p}<(X7?EBiXLT_^=^OPQr+RsZtQo{Uf459N7I>C zVe*BCBNBtvfcMg2@|bu4QQylUCMldvYq{NkAfOxWGh?gEu*4n1AFG1m2)qb9P%Gc*Z)t6)on~$` zJo8v|>IC5LI$fYA*IpMc$+1+!ShMmCT_mxQ9JT`cF#nF#bh_lti++o+4mf<61P4|= z_~0&iZq5Uog;hEG#V@?>#tz~BgX;A?v^#A?dEeyWWD=UdqaW?J9JTY|Jm>&;NcLuG z(PntO!Cs2(*$G|fSJ+g6#@7b5EF4mOIU@}!7rwu>H=yTQkx9jO1j)e{qgx2>ZoJ*5H5rj%OHbB0z|mYf3`Pin zldAb5lFb6LajVae0-H-|)e zgkK#jNKB+8&o}U?NHY-vpV4~2GgL=!Rg9TUPIG>({=wW3unN%&oWImBO4>LXifmr(IW6u(4IuTSuHtoa8Q7vhU70=|ZRn}TJAwZ(Z{iWGVbrNz zO;Kp1^Zy3Ch%fB6VfuXjIW8_D*}#yx~WJmXhePWfBnZp z1LDw^Na{U1qRze&K7_PwjXaNNH-9wMEMD=1*K(e!7v_N_DVzl?+S>*m000s_IM`Kq z5)Eb}bw=pZl>V8~87yVlq86^|YelPds`oZQ2&bH$)0;Wr#V8`ghO*B9Arsnbj4I#xTeglHY{ zpN{ZPu~grq!x8ORz$m>JpISV_pw}gr#t)1c6R4JJpA*=_rdcR?+EBk3`}|}z0l%y+ zLI*O1U?-{W?R?UoTKPWNQlYnGqL?R&sXgNxiU@iehZuAy^?CJe{`+Q#e*}qE{{H=* zsV91USa>`Hv4r~6YB|2}ufs(7Q<5+}dk}^1me)g z#I^sr;$_bMBGuP!EQ3B6SoEQQ!RLT$DZh~Z69cdx*R9OrSiMed04kGUZTr&zX<)?C z+W>7N&H}_I@Zn_=Dxp8F6Z$*7Bi%}#s-`2s59*6Y1*7)msaN4~tS(L_R3vc?)%AAm z=?BzM1v*fQOYQ}2?+4p!N{+jB@Y-Mg12KO{eT4;mhOt}A!2&m>HJrqdZo*>Q-P24y ztLbpFf&<6pYg-S>1NJ~q_X^#yTqO@DCou=1k_eA;P-_PHr<2v38Kc_T7G2nDWZx_P zM3{irFq_0dnCk(((3y?1plH5T6Yz2Klp*@)&7ZQjK9Zn?I#Bl2W%hLf-yVkQ=Bm*T ziL91e-*C*ALP5{ld&Dcy$H;#eIz z5G|~SPD48vz>Tsv#L&sC>fz)unl%0pQdwyEici6sBwMFbN7FbMpTrKla}n>*TK1C^ zAj{%NiwNyx=rlYSi8MjUuYObq`;Jt&kJ5D^RA0^>{PgEvSh)RuCcK8W453}LM$Dlx z7(9Xm*ICS}%E=EO0~Uc#T>45j-=HKVu1;Og(_r$=_i_sVK%%=&T+T2NG}sd7+UwCP z>%Jmmkdz-g-oi7fEt5qOU3+Q{459x8BBQTg)}2fE@6F7ZsAfS>a&saZh?vFdDK!N0SZ!p>LP9;jO{f){Z{c4#ozRRy}kl@o4n;As313c6@!EnA4sf`kf=NpXYK_bA&15>Xez<_Vb443uA`I4D*XZ){F zaKidgBOpxeUtQ<$EIIcm#5{9@2ORhHiu@XV7T%E$yTV>2Q6lzf7uy&b8qR2MH%SoK z+=A-cDkNzvPawZ5W^B&u?}5{?tyaz57V=>l?gFs`vRk#%`7CrcR@8ypTK`Qu_l{7BA+xrBB9ZE-pnQws1wAL>;N z`S8&N-fsqP+pedQO9bI1qs&0FfZ(uLo#A*W!O`77dd?&%yDJb(d1-%&Rv+`C^V&Irph%O_Y? z^s%6czDz%@%#Vb#C|4SKT@EZTlQL45i>lRE?nx5n@){d`&3C$96L{L2_KJCnTD9TH z58r!qh!xo{knFjEn||XCH=8q9(-ZNR=FBr$9(&vz3&h$r8@~rN8x)ID4r_t>S|3#< ztXhk1&lf`X1dl~50iav&$Xsm1CWKXVzoC+;@bl;{f_w`#*Y5*3h@oEVoXL|v6|e)u z9=lptIH?n2ZBK^XGb{u`BzbgJe2#?iMTHwEf-{9Li(9UKG1>Z$HaL4Mf8Ek$QdqSB z_ueN&2IQ)sNp+!7N}zUN)>BMesW)YrW=Kuv>#zcGAL`b9`#l!v1IV{L=BOv=J9a zZ%HQQ2A3$O5B{y4Kenvl(1`|D)m4>MX5S)hCU#}`PhrJ`GG!BCFaZ<{s|QA5;;GMY zYbqmq#9E2D-OAtcfRxJk4}rhH6#5v{OOZLnRUM+E!^CN!QyF125|J>{GG_zX+<+$F zty7r(@G+uYJ69JUp`o?f*dAoyp+n;`0s*Le^cFyLY~A&Db9gl0eceGN=&dJ4l%@K7 zIuGJF5v5HWG1v;%VLgv5MHu#mwfa@@wZ3U>g{!@&cXXNADkCW=$%Nl8M#sOtXHp}( z-F?CoYfxfBVN^ETI~UYO!heYOk!5z+7kA1<2DFhn4}dHd0n0ILK8ho0h21kZz!2`tvcvfGd7I3Q z@J;O|2p_02T@3SBLxq*fsEVdRtP1DrEan_Cl@$Dq_W64rz(iP~F$9=tY$8m|mu(&~ zBrPtFSOm{pAIm2Ld*I1<*W-w zD0yl5D!feMz-#3{Eg2@Q_=a^PWo4|sgQOHWv@ z3Y#X#c$P?z7dwgdb4y0rvWaKctv>huXkuhh!*zLw^JJ2u#_BpGk?bmt5_^_55WRoE zahG_>DDxffsXrj0SpqWj3PgXI*^VZ)+j&H_(prM03ah;Bg>d%Oa)Lb;{5E>(dgZ2!y^ajG=H#wI9RK^QU>^E z^*1szNZai`P~JSAc7Ug~2!k z7LfEjV|zHF9US`s+V+0g6bW*nXbu9K2Hg|_D&5uRx!Qb^e8cMAa<+$!lFL4}W!}Za zhkl`t1O?1W&UnaL2}*HofgHVAHlCYmC$gC7*$dX<;T6@W2*r^Pmi%4p^mPO|x zML6|>m`0W;QPQCDmm@Qr+i>cv(SCK!FrL5WrX>HPL+rVo_i-lF86GeyUR}TwH-_z_ z31#eAJxLrg`ggVocZt|0cv?rBJ{LX@5Jz8vIK2ftwtMV;k^&PaseioAt(IZH-x=Oo zQ>)x4gnyLmU)x+h>x*hTL}w%CYYcQ5u5FjDkp3$;Qtw3+E5AMirtWBJ9K65n?hSpy zE}ZwFODil4ujmzdtlgkIe(I@ zKKB(VX<~wv)hsy`gRbF>pY<&?P=6xWWjgS(rLNbBP(0BUU%v?D&Co?YK7uBQeR!*itYK%yb#00>}ujpz0wEFVSFQ(dbeqeld7JE$AW zYN6EfbuGG$d^za@c+S#3zI8+Aux6yB?GX7+%ZCxlfZwf@kiA|_XGuaPC@U}T_2{6N zWK>*@b}_oZy~DBm@$p|YqEu$lM1DW^!-CxCkty%xV|rD^%znI%(AnVoppKv9E_+0= zSd6@GnRg3=(`0|ns&rQ)>ZxBPp-aMS5$4&$6@etURl!jkTGL090p3QK`9?07uB{*~ zO1P2KT3>uFw*;5CaXW$b_$1y;!xWW35noPU0ooBp=wn&O0RDdYj^{t6x94I?89_l% zrLUipevyQm8ESNm;&qh$9rL{7%)Brtc*;T_ft?{1wd1v__{knDZ7v>LR!ukkLz;L~ zptN{H<{{2LSGCQtWHG+D?%)!|S0ZTlN>N$=1NI#4Kc2n72e=7WY@z?}xJ;1sw;WX4|`xwnApKeZUTc}56P#eJn%=ny?;E?6A ztIZG9C(s;w)Zo6osFhd*0p=LF6P#{rCaC=Redt@6C*|#wvw}T2J=GOHcKkPy4d|3J z-n8-tQUs z5~I_oP;7-k#*R8__IT)_WRfULr2+KJfC>n`r1Jm}>_2e=)+Jhh3kjtvleYQVx5v?K zi$TBu01*yBn#M`t4<=Ir6#vo22|8Ig4r?0W*9yUHg>90%C7hSm*bh=+ho~Kzls_!LXTp;p(e7w;kNxLJ6AZIMLD8sFyEdy zCsZ?S@2mrn*yF^MWMTH08Q%1jFRsNG`5(B(q^!SWi=#62iS*=Gx4@k$YgMy=-QBuY z*^QhVLwCgdz26RQIzRQnbkh%tH_gqrfB*mxKM^LOraRV-sy|suj_L5Xb9}K!#yMGF z()$-v0&xM24?Z>~xE-iU`v1sdND-;O!dNDa^I)xUpH9i@jcl|Ds}Ts^6@4hL=_;;# zX8E**Dg4Z#U+NKh+hh$Ew~}Z7B{G>aw2X7@kf#@P;O&$|0=*b-q-SSDJPeIX{Kt5A z7lZV2C!6@6x54fkSeETG4xj)GH@Ux%m~`(>4U7DvAGpPCpu-UA74r2ywxbJz1(T(2 zqQHTL=M|+5Mf>Q>gX_ZltZKBsn{v3m0ms zpa0y=QGb88dG!;DQl5aw-IJiJjo9qSew&m?bHd_Zc5sbbtOZ`h$$cx#$o+Vd$QErb zacpzErfRkWUK%q??vt`R$cto(I-9C#!6EG$k^V}gq(b~4b%us=>XIKhCJ3K?!{Y}e zOqC>vctF(}qOMuM=1+afW|tyezXFK9xl;z8v8w@-P>uNLy1~GlO=R6Kk@7V73WLnz zDO3Rn^}>mqt}-4Y)Uab^1mjP7Sf%3pJX;^=*^4}>Yb{1(e}1T<^tk3weupP~z)0#P zDn8O>;4!(91&>QW&S7>k2o1;$iNp|-3EG`l)jtU8g>v6Un7&>o4eq2falB(rm1BnZQScJQBL4v zv#0rKUb&OVJqLq&%|(`G)*?jvlHvN*_S?F^&XJ@85#C~5bC2(=SWWFfBN|l-uWzR* zAyKWbUst34gGWvgK9yB z3TCF%g!wURLY@o{9=K#9Jj$43Bp!;NJeI!1RUuZKu6@ zB8&I+y2HN)LnyWV`X}bMsz(i47{PZTT&bw*1K#eBrlD6}Hv=w#pv9#r+56x1)%TeT ziKABu*7^{0enPZ53nBaM8G&BT5E9&DCs;`12az64GvUuJT80K|xDA!~~@WYjGk3|}SoIHtJ8Q}lJyUjF9 z$0o&BoJg2HDN=}lz(AZj?EQ<;_G|S$QH|DJA)eoZU>a-RY%r=9v(uZrV`i?C?JaYH zx|OQ>bh#6R(3;Fo0kss|KAAJ@3NVb%x;)FKiH6UUt5k71$k1Dn!Jw#tcD#Gr4%~h1 zi@A=feVeqH4A<8tGz(T-6)!F<8xEYo9eEe$mlhE0D*3LIA~ z{PO4OOFjmOrDC}jhy_lB+9dg5%$vvF3^l$p7&w_bfRg@S=ov<*+_N0iix(&9wb`e} z*h3E5}6J5(GK+f16U?NJH|s1NzMC3&JrF49~--)Fet9eEC|gkU@GV zu#i8%#o!r3?bX62qVnMYC0O&o8k+TJDbHYvBCgbV+p#FFQC_qsQ$EH-14P58Z5QU1 znbcLHv-|Sv$nO+A(W0^)I$dXiUQ!DN2a`cgn*fyrr4y^~t5m|3!9to1mwcfb(8}oV z_EJ3C$Wb_G)*zCJn03D)?|j*+Iy97EH-ZTxeAApF)8~`;($FfZAts@wBPM9xEL-7u zYF0@mFm2<;&;o)945!t3i>}DY#A3+M;n8!QPZ{Pbr8^m6P|ZtW()(y;R;F?6j4pI4 zV$$7&8dR@_7(nYD#OFHZTGPn4>#TYS12go0j`bf}0Hgr6)bz+W$bAG1*2oC25p3Qa z=;h3S!px=wSpB%j-i|F>$eJtp8m-@s1b+Qmjp%&rK`LiNlmteJ@R8;K*|s}9s}NsV zSV9>BcmnwrE!FB0Z(IJq^GOZ!ACA1{P|Tpl5pAKCq|i5l1`0iVBIb=s$Z8rEX!b`} z9fKM%JO}^sK`jfZajBdt)r-^H-Se~&^MHnbW<~xW9U)8)@|#WDy1-$vh?v+=_uOVu z*UUIcdQTrIbZAtK{=LzpJgUz3s;gFyo34F<*7@H9MZY8sPEU!6>{oXyOm^R2vMDmK zY|TU1oF=-$QtudA!Qrl4<}4^|w&HSm;Ti#&yXAWVoFw(|8SjJGEGDyCFJj!JwZW_g3Q}EF4;Ck83hPcXy(@l`zh&<0z@7KOjjIBqqcOmJx(H96m zZf2W|>%dV8^KHj~t6j^VDG>@}RI17%!DG^Ql@We)TFN^g7GbW?0F2{3(lt}2!8;4w zr7fdW7|RuVsHB=OPXlpjx89za{IHN9)g&FkK^BdkmWxv^Y^t8!Yw{N?D8T{n z<8RMco{t8X8^&nEPjB0Ya{BU~E+EXutg}vjyGW zdd`ZfO=y(=_%m8`ZV*N50w>AclTjW?!~WEQq9qv2BIZ=Ez`~Q=v4QAOAYH{$TcLo? zVE;aiXG%ZZ%@NkxlqA(FKRQcdMZjBn+*e-e851@g0LL0#K5IMrogb!ujLD9rCxW(k z`s|11Mm1g@hL<~fja2*EkhR2!CPF^Z3VS6QzN|8)_vGd*IJd4ZAL~XGS;q_(Ppp-) zla{OZrgmHcm@DoY-X`Jr+V?Ab+RoUc=lnD+_IK!e@X*H;BNCs}j5-5#|F!q*yF$Ng z>ETeYgyH?VRodwP1((ei5;1z|c9uncvP{xj!&N(CNs_haN(TfZhluTuP*!AKEKU;{ zl`*8OMEGrBww(Y-drLCE_l15i79!l*l6O56FgA!D zqxs083Iz3p3mDG;N=@ebpCe)E-+r~24v(}*4BHas@r(`jng+*AHLt39s*PK#wP<_? z9GNavJKwIUj`htBWQWelSPcX=iVUT4IN3irKsh0T|;sv0>`5;I?85CyIaQQ^uN1B?8eM& z$7*A*ONT|g?`vqo*C8p|@BmpV638$9LV%HB6|TH#S1YF(tIh0i3TaY)BAGkM}B2q&{ z-&>)D1CN!wHc$h(RU1Re`HgGfI)a}YK7%|?bUF4>4s0I=H(l_?UbyhPop1B!?Nj;+z6Ai} znO@R=U-3ijC`9jVi2r2uqkL8+l3lUCU^Y7ww+LLe&z-w-U&(?Cb`InJiJkmSpk~aR zj>3dEpA8oAX(~gr>C*7OL3P}to@?JUFg3m&l($wAWxsFs62Pv%KUL`gp_}A@>Z(ovri!xCJ#~-= zt8+7Y>l-`T_2)nUB7e+AD;tj+T?$4D|Czd`%l&}cgBR;hb?%;GhLLzAl$P4;YSwbW z=eC3It=h!}Dq_`|Nh{89KMlEdbt--uohOti!_~Q^u=^tb0#*Rq6VemwC$C$$Ota_<}UlA3Sg5y%vfqNYFBZ;@!WN1ksV@CDgdr6 z--NM_Ld2AjbTAYW!shDpUk~ql$Kka2ZmXN%`DZx;M2=Qn+`gt-Do=Coqe37n8*<1O z5QfAy=X{_gvt&B3b*p7X!6+<_A{$xt!%wQMmwU&uokMs@zYHk$hLfE-VnXK2aTvju z3?4((O`p!=uAIQGs-EBIcSEiiD==rC0b};`Fg~camLXJKI z;vzr!6#xJR@j;sBN#PGBQvw(Ni&U#h_zn3{aHY zpA5hN&t+ufIf0jH>ZU{i;7?uEdYI91M7^bl>v!dN08n5As=G8FSo<2WkI@ltu|uJ1 zO$|68Nx@AsPPIa8D=6tpi>4cg$gCoRi&eV-MxmwF;u2dxXH-422txFaH&R%JtY!dW z2jPI0CWYw;gc!pi%{()D^~5?A_=)^|rf(dP^@@;;qMp+-(9Yv4_EPn7wDbkV+S2V$ z^p;mcht+BU74mLqgv|joZ}`bd`k!W#fu0f7@917ooI4h|`fmsj$qX3jEMBXrNm1-%e^SLr&5Tj3-(ZA z=@T7&D-bSm{syzSgKRkEZ?9GvJEjoJt*=v~EqfG#JeSBRaZ zb*!_G2)bdTE|EJAmi*q#E4<5ds)Hr4Sb>AD)+9I!F%-oH<9<6cpFeT9x>Wjl^9X73 zH9>^AEVy*xot6g)-t$p1ab^s478SaZ?q$6i?#Ka5(QJ9zeH)}iKL+v=R~fiv-9 zoH(w%k!i+`VAPRu??TqF#^J!12WH?tpkV5OpA1Rwro5L-1_Uff0KYU(q=%IVPfJCm zOG?g9V5q+#Wd~NYZ;pu?Zy4-Dpa4_sIDD@YGN&Ot9JC$Hh8V$f)z6ncC4{4j&)55llR4S?KfBX zkx7(sFHYo~TNGu28wWV-&w=W*z$>Yw9=R9vo@c5YLzOw@{e$Y~oW)o_lh4rCu2VJ7#4eL{B~WlDRf%b(XKw_f1*RfsT0(X%t7 z%P3s3haH*aGpQ$_f$ZageI!Zw#_Z0!`ZuLxPp%>BmFFYoPJZHcqj`X9?>vt3BMb$1 z$+OUZITI$0$a49kJ;~0-Gv0?o0+}s)`U*of?>PwfhOn@2*Ke?478oXPH+KsXHQyFg zem`IUf|o;J7XSS$+~`=_+2O%pqj=r9X^IPy-yT!=kpii|XOv4D**_{LuLq+2fEI*@ z@oW`D6#P7E2FKzO@=HE3OjBo#$*5>U@q$2dqtJIRKkqTVqBk^Aw_12?}GGq{KOQy;(iyX0y|~uc_e-UmGxK<6t*T{j>I?~OpzA7B> zx+x7UkGU_BEM%)AW{e*F@REKglVyUynh4z_V<_LfIt$|B{re|;>KuwSL6rMaXm>3O z-K`UGBUIn?`Is4ytuQ}0Y$nm{zVyU1%4-dwZ!Od*MNS1`7&`yz#G8_gC-t zMbX(33%xgnIf5RV!Gjww4iU1qX}Mu2r&s7{o-u}se1NwT6tS_$>}kf z!Gxbg^@;yq&2sQHp&kzg{(_(|7-jisUy?y!48v*em`L+jyqwk!@V>JMF~HFic`doT zy4Z!X)ehIWvqP=uLjTv&VQ~Ne2KPam2ua}&CQ||z|Ne}y000938V_5@g`Yzx&jACg zsSt)36d35|>a;?Zs{j^&0!#r+7QlvB4w@~TX2cz>pWhM*S6j94c6p`COJe?vh{C7@ z0AuK^GDiJ0tS-Nj`a1sKqpgVwHwfLKS zH|wZ~&KaRsd&(7xhT5lHpm$bp*ihDu9f!YS?y-*$XRgpIQC%B7~LDEC-tF|7kXke;bB3I~+*S#j#Mfk511;p_F zp>3j&yrxb^SsQ^at3Z_E59uI_NS!)(H(CP($r~Wmp#7_V?U02ue4S(kLn=-61KR(kWfi-AI>o zN+Vs;UD7BiE!`ag@7(Hho^#GWp66WG`{DiYZf3F9-fPX?zge@_n%ehF=@)r-6`*0B zT?oz`5z&!CC7+dE&f?Q_xB1Xpnk!!&)cit!0Egs&0!euhr7Lowddc?w=B#FGfB%;8wiE3F5<^wjpE z2RF<|d90avtb20}e5LPUDmhQY`rfy)&Y`Pw5FQIu<~>u+tvo)Q7vM$httnvL*Otvs z$q*>!(2bzj_Y;w?9E+D|x%ETjtz@gbq=#YDmKe6!97}Ewos<5$9oqB)!35Uj;{``B zHRt~PaLZj*7`KYW^~6dPQrbN?w-zE7rM(@&A|)aflVZ1B|3_bE5Drf@SVaz|_TZVF zirJ($^CcWf%%54uZMvg1p0`kPCBik3R zQb=k zZrih8yXWv3qIQ6dmdO+260?6&X0>p7Y7%4$=ol9 zBE5#a%C?8K-A)FLmxFjE&~ zLG}_}UgfprOteI*)Ab{D-4xw?K~uTS{??<7CCj=RfyGZYi8e2rqEfl9DNSl@9$Arg z+`866^Mbr_o95lQjV=9=2ch|eP^7nRx|NFK<)r162Z@E&MN3T>XXNO!x3b`S_s&n+ z6>0da9teb5opZETY50W1Pv1|RO)!Lsm5CBR^uhV?dFZK&I=+)KZrhp|g`$g^;FG*J z`=d0>@w(CI=_`RMmpRg}K0gvI4pA{AR4F z&>RjY68F82)MQy)P3`OxyAh#hW*3j&AmPt_{8sNeXM)vIe{d$hTqjfiA#&x@;yoq| z@*EuLQJZR?h{Y}9xoXNI#O@YZqKt;Eh7ty;XO2%PD4W%a*L|L>&?MPtr{eY2R(L_s zS;`hqP#Sz&b~~%CwHLd$8~12mWUdw4Q>?VyM+>HG=v;^)r5kVX%V{!doM;_ae!{w{ zXsyrWCW7w{_W5Q(C}fU^z?7Y^25sJrCfKM*IIcAay!^X;mu=N z+Il0)4;xW96m{vf`C-AwqXm#S{79&wZ;rcg7nmyD2oB&`Yo4v%hIU}VKfC9*i7AyF z%vKokSm?^$`^eI~orfT=SK?exxIH&6nZtO7RgH46CjX&jz7(!+Gqp>E6rcX4T|(z) z7fi&-U0T`IpcK*m>?40x4;b0@g28IJPx;YvH17ASO)$q2_5YY zZLV!4RmdOtZ>dYUakqeKLo) zHp}n~9P)6_Fg;ZBu@WjG4-!-j%|qnQs#WCppeL-+f+>AITW|I`?X}xF013%m(1rcJ zUtY-h2=Vyd$Ne|ElWTVHG8X>H5Vmmmbh>Z@90UcKwI<1@vHN`i@U09AhIRby5rvXp z3s`AHQNo(QcNCZzv}U78;hVkSB(COigf}j-uR0ts7|cvnsGx&ufO6r%k9W?*T>33WAmWv7JSxiL(lo#{_(y}J6R4}h}slAKc z{6|w#oI>u4MYE&x3qK6_!4wnujEDU*(_~nn-emEpTA-dm9XAo&8p=1SPmV5?$QEBM-2|S~7Oo4cM44O~tdFq#b9%LawXM_g_?Y zgTM7We9^OZ;EU1^8#@rn4GB?xojSf>fsEfYd^vlhyj%fSy7z-%`-S-mvMfYafQXAOIlqrbe| zxa~Q^gIyT&6foArE$0c9T+O)}tQ02ss8&X*Kcmgjqg>{aw}}3J{9{AU%xxi2j7sP0 z1KPnVARwb0(UWbAn0h1CqqMD!X`qU5$7N;@e>ylh7lJA#r@-R(cDF2e2}%Rmk*iMjVd5pZGl+UE>@6Sg7s`IzOsr+%cgbtkNSXuf$H>B*ye`p}3T|Xh>{VTlk^8`}6IqE)4>N z(W*L9YZSauvL7?J66lxtnObnT1=ca|mitHgy7I*enf;I7$xdP&kuH+=55+NYJ##(i zd4!MRf#wlY!_rUi2)Qw(rslbav6u|$XaXD3z*rI58fqb4YLO6wnXz(6XXCbxMcr*i zn7Ut3uADUvO&o6=O`Kff+2Lq;)x!;6$C1M0c75b@drQkkjIRaiKiKZ;2%TO{yA|T7 zV(r={O6l(jd~4fTZxpbM43EpkDGMj`2S0&;yre9>g_p(2=19?i%nRwn{4_U}KM`(Y z<)4}?dC?uD<>nN2Z$__Ijn{oJAZYYsA#tPG$x441>9Zf75a?m%{KNS^i}CW%OxUi( zKHy6^>&BXPo8qOaG$>vEpkEDhYsB`0Xi60rV{L4|q}8xMf9PaQPI1KHF9gq3R4K^G zCE%Qp>0OBRX8g{_{cY{{%z|3n$FcN7?U=q}MNDk9omm4RjHb5Da{;2qN@~joAI~dKa5?IxlQVgR z2x*t>$x^!BBcParGrhkxQHV0l!)VnIm{br-9O=&q)Ia7)SLT!?JTQviNPl|CpgFnX z_*gB+i~n)2oReX|$!O~kW242q#cSn1d=M5uw+4Amfn@WaUI>rD{rv7W4gIIQ<(g#3 z4wA z%fbH53l(nL1{DgM87jC7dk;g}E_)-6P;FR4jS&E=y>CPQqikZ4=GUc}`K-&-;ptp< zLH5NQ2_2r}cFx#{cK zZGNS~@yeOj!VG1;!*V{b8DUaz@|UVFlQnmTar^M=x8t%uByVM%)4go+Q(esy?e#o| zk=fcBT*510iP@kdvmzXKeo`2^a0>I-L)g# z{yX3SwX(};q)OVJ0A7K7>HNIr`0H0RrRfw4Gy;x^OCn(iwy|5D>dR5tN*2!6_w_$w z`cO>iZWb&r8oG8hVy5H-Jk)33$A14Z{Q^Up87Et1e}*~kJ_W0R5Ry*mbK3}mTEv*; zmpBW>5VnT5(K^i|ulDOaX$66OltACt{W5oVmB+X1`G*_LfwdJZqvwx~-=^g`cs+dr z|F~h*MSaR7Nq%mxG?I9j?={b?-9f1a=Xt;=+!xs$SGn?9w6jg;r~1MQ=0q;avlXj8 z45TBJZOUR&v;kcGNYalewAtQTz1mPTM1J1TIH0st*pnCtbuoFy5AGsDypu!ORQkShQ?&y4Gk?Ye4Cj-pqOU6 z35pdq%_Xg7x6}F>FGpJ|(lNH+%vVs!jUPtOQi)jF$I}v1n{VgA95_T_)X}-@xTg)P z_FZhzEs%tX5WYEHz)O@2dHAj_P3%=!85#GXRL~<8OVwh2sU46$@Qh2Fu?Ux6n3Mpm!~gdC1&|%;e=3Rpa1=% z{r5*JV;MD=4q+?8s6iQjdJkL^06;t;>7izVp1?uJ9LZKEfR(yFjKLJ&y7|%chd+X8 z-p7T*l8WK*rWb_4dy+f&FSW`2YJ{hjiu^xef7sxJZ}qHpIn=}cG`B?>#s8vna&c`a z2rV~b9Uvgp6s+n-<`Pd5*g{eehX?YieoT5?!$*vE&maUXPhnGtjwxqjmK(tL1S9S` zhm-lH)@~6QUbF2w!qt{sy&EZ_R-SS9Ls<>lkSh#PovVl0>RUBiu55XOb=e^4P_1Wn z3fa*DyleZZ@{KXbZZ9<5JycP0ENw4$9}^Wkbg9PJZ7z;Q zU~=HhXuMY<&CGV4ptRZ0ITcJywx~aP>BD2Y!};Uv773dOQ&M~c`F=q{Kv#*ZX?xZ1 z(9wz0;KZxb*p@}r<>abCmRN)q8#n`5wFd>^YFC!fQwQh*k?me+1)1*x$>F~O*gwTD zs0K{+-i^-4ciTyyBoY(&$CvP){e^Yw0OoAjYif8Ea~-p`hJ&go7P;E;%d)49C{bo3 z>1GmykVxD%o{{lrJcQUEqbS=7k7L)-N&;i)MBzu}_yF5ciYD(GDvCwMQS$>%DtCWr zLWgDcCMg=kgS-7tA`7Zkz9=wjuuh^J7w`>^Sl2tx#3!BdH>Yfjolr+(GyU%DXDv1< zus4o1Z)7|7yq3BZp2)_e5{%oRNTJ=|#lo64Z>H!aL4W(D8%?#mH3$Hj`qYvyLZyrD zm9f{8BE|N35qdf#f$v|YNy(;Bjpm6R%!4`X>OjhV9)WJ^t5LDA81UhCg&vj1o4aD+ zC@x=?1$D8~eLUVA@mo6Y!^CV8p6*pYq-@gQ_YA$V%{JpNf{MKwMQZ4&iqzZsDo+cK zix_*15NWoC@XbTZeq2P^QHb7Ok{F?lu#7cOdJ4cV%ezk}Um0$0lxbM&k2lE(CvWoM zMa`r5CkVF<*yJLOAL2|W25Z$VW#DIFBtG3l9lm228hdN|PClKDqN|bfgS_OoA^q#{ zz86`Sia+X9>C;Ugn|ws56m^+Ki}0f$aLPP;?=VkLKsP6!MLL|UWLRVF74QQ{k*mX|WY>a9qWn)y zv_fdd4AlF%*G`BX1}g-X(>~YGF2zKdv+a4Q5xL%rEwnmnWjGT~0;lLsZeB7Hf99Ks z)i*?*o%(W)GKlERC+8fqm$d%QwL(26)so?3Fm66r!IL&=HKUj?ab*#l%2A8^@OjYsk9FFd71bO zY`i0+NH;$rvGkRomi+Aa^ogT5b7oHFcRJB_$WqO$W>(8`hjAMWY{ZwsQd9@zY(Q&z zmfcd+xyoYHyFhEzU9UWM1-&eruHSm4y1lt_WVO;hK)zBNmse!E7x&&c*W!EI(uV@= zI;9&Z`dzz)FLg)*6Bfmm`IZxjc2`1i-q0ypQATq*C-G9lw{;mkcnB}0Z98n3;jq?g z_R)dNpN9DDW|HWEQuzcyZpUMsLI6z3LW1J?66k? zktW;R%|6`L>tU8LPoCIFMZpl!5~a7|Ym0N0Zl#xu4|(r0Mc*=zv!A5~%-GN7`8xB) zvQGFU4Z#JU@O+qEleuP6mNHUs3(CWJ8_EHdn~v*0LQ{ zHF9P9oWs^kkfZULh`2d2K#Irt=>4{|(okhThmGo_Wnz-`#F>xPCqKgnHXRAxbHTnt zG$T`69o$yoOwF}iF--u;` z)Y^kN-O|%uvCiU>xcuTS}2*1y=tys0u$hZ0x*A$WM=Fg{zV;!~v1N zmW19$o+K?as?K11I1;z6rNGML5Wg#d@m&tXj3s@hZGP6ad~&2mDnx$*f%*r!|9XaT zZf(Eqi?yDWOom5v$IY%3mFW+$^*v;+@Dg?pAJ0kC960oz%AJ0WtxrzvtCM+t6;IB= zlTf%`O{;aM$a%DRX@}}q+L0HOkjsIZRF;fGy{(`qT{C;!j5s@{I^7ER&|mIvF%c+z zk7g}sJTs)p4|(TAKYpM_g|ZnthR)?~^jHC__s05e098?vke1+-nozCtwRl9ohCZrA z)B3oHSc(ID?ERX>#18AH5^Y490j`|y(JG{-o&dtd*#-E~h-&tJ9VmRW5(#S4aCGB= zIQsUN*P=7#k9j`H@hWcnS`uHDTm^<#L|Uw#Ip?MlZm6%VL4s@Tk?(6nnveT9kg$7A z6|LEkJNO;f==(4?al=PS4T|Eg4ZKPB_4GAnd=PZftPa0ziS_sIYAKd2HraO@qbk?9o1{9=x`sPXcu2BqMRp6=wUk#6 zkwoF(a>}UsdP3I5Q!4BIZIbW=pL{W_U{5kccDJe_kJ;64<3A#t9bp+Oj=x}SR~@$O z#tBmMwjHqvu5)p(jdzF502oxId7($GU(1uOhle1WBamGB>k63QA7|b_RftIS>&K%k z+?@0G047;1`p5lVjPkL0*)4h1vGbevQ9>J3t+etD;t;;Vt$Hypz`VYSK}S5$MvE;f zr8p#)ic}ijlL?~1X2(jXF7#I1GR;}+HjTiW*J5kSi%dq>!WLq+D;0?2RIWIM&u*N< zHKwI7AEdjgO_&h3GGEAjf2`yrGli=!$uN86`Z;WU%Akft?RIV#?Y%N5_n6JF*m(_> zu9O@W=`(_mI^R7xh$QN_Jr~&$=1?C9B>}%<#l?xb^0!*&mxiq}8MEz_(lyk2+*g}S zu0rX$vuaFRYZ5v*TWta!_a{2I<38$Z7S+B{m{~}urwko%K&E0r7JOnPPh_$EW~u0w zKhP)}zF}W6&S<7+Sd{Alzhde<(Wvc(h*n-eQuItbM*pkXU_+`U%v7|fzzB8Po!iHY z0A{*DFV#%5>OH4YXIEWDlbNXntLD)k5-JR;RLGIrH;v-j-F!|}CU(vb?b2;B7~9b- zgVOO}?@f}F_6FcDo%j+B<;|rx1~6(FEtbotjup&LX`rXlK-T@~{MS)JW%+AG1gXx+ znlo8%ss((W1%E<8PDSi(ov8G!T<*ABPNmwaHgdYQ!O!;<`>I=ZAG2V1TT{iu|P2bHh;)#NbP8*CN1T1Z+s@w=MYm{HTAXF;|UAAkH+q$Mbut|-JGu^IFT>)kzi0G z1&aqCt#WZrD>q8@bPPQ%i%dzsEq|rJsC`|o5wh3v6{C44{HTU7UfwGB+bEPN+*>qB(LnOIf%H%%vvFk?m>q9zxal`y%YM4t@( zBX(@uaFd#SeGrb+$h>Adu~*d>MVd`$Y#iNh21!axyZL4>=|0da!jI)|N55F^In>hU zdA5m@Q5-V{f<-E z8!C~H*o@#K{E^k_BHL>p|Ma^~A-WdvUBCKX(ByPLqTXh8u016%Z$!6VNP*!gYnV_j z;$XcVzeu;z{QxUHAiS+5ntcpzT|wF|(Mys4ZTdzw7Git@4V|CFwWug3Kde0-5_hP< zt2Fb|5gNj!>tyU6o42*^6mz}^k9Un(NWLnsL}Q?Bb(p82b}Xj~W4VxBX`{C1`AXMy zOUh|bW%6yB;X`lS_g)0oA`J@aP=?H!zo&pbyhp_8PZyHinas}#9`46mw z?T=Zs$l@hzee??!BHpF4uJVEGXV(@YWU$BLEJA`H1!OTL-6y;O77@n@7UO3Wx!`I&g z!81GRJeqsJzKM8Ohd)V?C1O(J0!!ewgEX#DL}B9XpBFTlRzU4r-K=$#F~NO47!?e^ zo`syi`7yUKaZ6yAq)?}Z4sVH$K+aZ%iW3*J;-prX1uHrz^A5*3S7}s{)!gz`?8Xqx zx)WiA3(Q;TdYQ~5jH3`pL{zeU{D+tIL#m{*Vy&lVrB$We5vL^uc#5^!>E5>U`zofW zCQf65FAw(^@GVx|Jd8IKnNU21RP-&%t#RN-0wr#|90k+I8nu9(?p4$ExZUYx-! zn4*=_Ik@PsIA5gVs+W;VnX{*#FfI_!qJK{+a@v0XPB}e=m%TaJemMLo|L2+ZsNy^h zu?(+sn%%f|q|ukU*XIqNdm8H5Kld|d>bgsvK(7veFEFwo!zhq^^vA6(_CFmNhUAEK zz7_ItyMTVpZ72bYM|r+>TD{L2gBbvj<#tQ}q#Lvyk0O+rg&?5HM$}-fG}NdS$rROQ z9yLkX<424@s}-68fXe$-4bNLx+Lxi{a&mbkocW0NO>91oAKHzkGs7hbEn^i7$LWMZ zD7yj`Efay&`X{m%4J==$d0`SKBM>+=w5D!u&u1ylr|1Yps|h zuP}ElnZij8T=#?8Osl++zLs+UrssMD2j(PiRViLyyw0x)a6dbwoNGrUpnAUStIJ(+ zzicR9{7n^Vivy|65)#U&{ly)L(`T9Q%2f)aOW0_wbK7&dN5i5k@taduZke>6C8Sr2 zq8f0ER6Y}FO-zJ?{8ZDSw22Jirm9~BA>gnNtSzo zxU+l5QQ8Br3H8sUm)R+{6h)$AFxo!Wx=_t2n5bxPzhESf(0X;Ox-&MpSie%0BYkDB zK!UarmKeRH9Jx4?^tuBlzs4j*Y&xZ$XCkDL{mm1n-i$BlESbWQ0rWKIE{me(1Y8rf zI~z6J?tD%Cb2aLpuFaS+26n3K*^2YJAg=?Y-DNd4y}y0Say5TOF#f^&E}M+~?* zx;wkvv;@xR6LW^J+8Dzdg*I5dK6UEX3Wm}0S8bOGwW|K;FcnBowtWzY&1>0Bd1dT| ztZO|J+cIqGZoVCO{~p`KQsnyZCTZnH%@cv$3BodcfRdf^_3_K@!?(VV?CdGA>|2gV zR=kZ)YZ6^wnDeUGh_N`KPgQAZW_URrTo!ddHGifnC%A_&S>CGQ6R#e@XlqCJG;8Ft zP2A9Do!}SNYhRbPyC&~H&VFaGow zw>I#C>n$V=<6kbgGHQ;*AtN}`R3#%moB$j*5=-gEoi#dBX~{iVQYP5-ebX$KbB%}8 zf)BKszgrV@#J+EaBe?%6wREX>3E{296wIFBAXz;lM$*HkGTMX0RLh4BPquVvi+7~$ z=o7kX^obIad2rTl9%VZDVboMfZ=HVG7$&^y^0A|Pz{!$r^}%7!FN}GCn+h}c0*mLM zi0939c;NSXy|+xr29~Y~9?za;$Zj&fCe|E4q>Kd2^VL@eg zF=I&Ez~VCE&HLdFZExGW$@=Q_s`j(lIRn*n;~lei60G|n*`OO@v@%Pa%%tgHoA0KM`MW$5=#=C;_A3B-M(l+}asItS~W}y!?e)QRt0Igbyptc6M*dyT&)|i1B4R=vI=&yI=1X z2*Jr#MjuV{5-LzQN8$Q8r+vwrb*dM~9IIXk^d!M;=08*U7CJhw-L}JPq%e$bgUK6W zWWTnLu;z82NB#22=%TT8YIFcX6j8M){u{rjSz!*_4Ez}v-Lqndw0ea4*_oG0mGi`|~EHw~%S1eKp;E1VQH>cd|@cDrG4g!jrISdqPK% zP_2r#U^T$UOaL|PV((CoM|#36%A?|>`(sm0X8?i1!;F3icbxYwAc6`$;|DAtX$tm& zo>8N0eXfZ3;r+56DhS;0O+2YOz|g`@f*kU@$14QFs9hJycn9iS}G42*9EyHKjFnlnU%s5+R3aerO_+LkAw4&LZ zgB?;q(v*JsTD|9=RUWV-iSGm&g||Nt8me`b*n!fGbc7q&bOTFf!cz_xAnO zG$MF<426&YZu9@9_Y(rBm-tsu>wZB^1EHG#EvV4l3_@)b!TFm|Xa6SDZ+`{#w^iH! zQ>cr74b|bNRntlT3M$lERFHJYUqSt4)ntVK5NbNrUqk&h0Mdp2CRCBXg8Iv<;ADXR zYyhO2{590yRt@}{P=o&p>Tj!t|EExk{~GFVs}B54sDpn6^|w{W{x(#OpKg_Y@Yhg( zSvBp!UqOXJ03*P!lZgK%SpBkU67FxP#{YUV?7v1g%B7!B!9f3aBLFl&|D9mJW$3M8fewyNyi4fLMl|F}Rjej#o6+eqjB#t=l1 zx3IEu1Y0(@a5OOh^M6XP8vuYt13FG2v||7YF*%EJHvyt}=v?i)~~ zY5(SDpWsISBmHg-?DyZ(zxDIq^$Q2C10^s~85-EzfjP0ExgFGXp$TjO+AgT^vn{w& z3jh& zQ%fU|!FIIx+2}vRrZV^?BQ~%#gz7*ORF3LkYfkoyhtsjM*E0wCGCO-a12Bgss7{8i zqyE#MrQh&>^FK2<=tepY7@6=Z~KN!1QWBDcf^&kHZJdFU@ zKrJDN0YG-Q1P~a5dGRzqKMaB&bQpq7!U6}}I^C)e0KsH?Wq_jf=HddnHnyIvaHtg#-YegZk9q zZ+0FJg2SN$lrI9n(?|ed?F9)eQ{MuBC!XGkavO3{S^Rk(7=yMfg8Zv4FCdc0KoGA092>|fE^sKLZx88AW=zxI-=lf z!!W_Ibqdlo*p@ulW&lXwV<3nxm~Vo17z6Wska|D@?>51>f*Z^Uk}XIecIY?;pgsxq z|C9fJp$y{u2OexYdwnag3{BAe2bJkM7})EAxtfLH&m3d{@_!C+U{JKxwYG-FpnoJr h(_c}Ps=(d~+90uk?$4E>jRIh#F#;nE_{Yrre*lUTy?Ouu literal 0 HcmV?d00001 diff --git a/docs/source/_static/standard_output/3_parallel_envs_newton.gif b/docs/source/_static/standard_output/3_parallel_envs_newton.gif new file mode 100644 index 0000000000000000000000000000000000000000..94b1f8fdc8b3f018e385eb82b903531459e2602d GIT binary patch literal 268757 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

_+9lCy7H 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

UAjC~ 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

@MlW1ZyjOWa;-!B@N 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 literal 0 HcmV?d00001 diff --git a/docs/source/_static/standard_output/3_parallel_envs_newton.mp4 b/docs/source/_static/standard_output/3_parallel_envs_newton.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..14d74bac63500de1bd6a1649c191c4e41cccbef1 GIT binary patch literal 344384 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ƪ~L^Uym`;LSh@HD0KsW25Aa_HJJ>u4az>( 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%0DPPcREh8|mse9&>Y-N{hkE(B+w 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

I_3-=jHJB@O>!Q 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

XYQ9T@^QEbUkF!czUf*&P}k>`Nd0 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

wmAn6Otx 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

?v6;#S8>2y>=$2>p6Qc0Eavp3U0hh;I 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

SEyRg8%CBpz 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�yNpU$-ySo>Nx*Y0g>Aro^)&N_i6O$wKMx;B~w#YKWya4+WF8SF1}8 zjl;rN%EX^B{bKl~+Sro6mxvi%)iUwW26bqDfpw?%)fd4R6ccMQy zZg8`e3%iae6!fLJ^1uXy9{i9pf7`AtOi1l8HIHD6?TwNtv-JypmS7fAG;JbObX6T* z%S3r)>ewvJO~t>mQ5wmKihM6Y^~60LrSPOH$cf$eqAjZMstVgMf%5l#@*1sawg-#g zqpb4DMMs?+LZhxbUXIaOu=V{O2S2@8gpiSLf$XPCyFRo12r$gF1yzQkdPV;=f#vqM zO^2$}7h3ci37Cn?2oVP@op7mTIi*E{g|`oEB|cB#F8uY)AEos(T<{MOB+@FWzbZv> z>yOi<4|-o+i7XjObGkTKchg4;aCC()7}W~y(Es|5AR+c#j+#Fj@GQVZyJi{zNhwMa zGJbZ#2VNM@IK^n$X6(5wObd3p?u;~3=8S_6Qwp>FeLlMEKU-u}B$7@cZn@akpOTh` z^pwuyApnz$NK2l7nOXMRc!LxY(d1j#*cNes36$mK(6=CmIr5#ayqA~pCi8qf3{UGf zsBtHU=myqD>7v-@H;I<}L43vt)-)u=X_J?~CaNyh$E=lMwe@A6uXI>~(QC=uOA(E# zs)@`AseRX1i{06iZy%d}YxtC~u1!%felMZv%!p^|!T0p+dJLF|^~-sz)-2x| zqwLh#<^N zmWVHN`}>C1s>wzz*tGTGm$9bG@An&|i8$}ZWvojuTE3M;mx^%Pd{Y|{5WZxw$!DT< z#-nRMx9#{s)B!O(A*xwTd(s^Q{a`o8dY?=k2x99sFyjCHnIgo=BV);5@M=&2%fV{q zlLN-evAAL4(ABW;+c+tYJ&JL+PwS!LKSEF_`U0(f`LKURx!O5!$AJTf{NZw+OwrKO$#K;UPQdB5?tEcEAJMHsvgaMKx7i|zt)^96?PaTkx=`q2| zAgRVd7{c#0dys_C@)WLtn~6S27w@~tI{5K92%fuE^>FoaBE-tdJ37vD_&J}8-rFV= z^AV47(H=3OF_Nt*_QMi>B=69mYoMZdHPo-FGj4i}@kJ^9*i|_j-pH^znaAlEN7U(Nv0_DfPb!AN1hZG6oOu0E>UB5;`G$2^Tav zFrdv`^5i+`ppG*Nyn_@-yNIOUv1EbtSp6JA#?oWA#k=f7o^`IBKt73F{iVVy~LtuQM>%V}OR#C8GW{LGz>2O_@XKvJe; zWQa98`Pujw&8F_<2C;*n!`zgS{}5(=Kr zrg$J2dkty)mgc0hA5Dfo|LpcL2}RL58h-ZYe72-FgNRc{Z?sAzRo;(Vy>9TTN?HV7 z+42f0EtLSFaK8wMjqozPLX*hi(6zx+@a@Q*S^MdX^mSjGIJH4H71kyZ5-P9(7Zu(z?Y2m_OifxhYfn` zBV>_kvg?<%*uncx+BK?djBYG3vMF@jIP`GXZy%4f9{gt>g?e=Q>Ia;Dief#p&IZBu z!G8P<(U=KuI23k5fQ}1O07BK`ZP%wrl1UK1HpMLerHp~A>{LBrm=~jnGKt`fAQ|mCmh|@$gd+DDIn=ou2e+BYpF^S87P1Nd;Kj)&>Qu{4_pr-Ck^xqu$hZDRi{X z=1K8lkQFZ$co3T3O!hyoEWjYiQxpV(TD5m{^Z)@Lw6XWFG6VR(5qK9EUD1FbK(D`l z(SIZW^?#*B{^j|gM{-26^?#H})`P1^6rdd(6KSaA?3`%K+jlUg`hy_*?yNTfCPS zAjJhl6^AebvAwK=@GSvexgbI+BC;#;dbR)?RB%6%c_0J`#_C|LxSsLp_VzUdmWi9o z-<68wlnr_X2!!YA_E&|!+Fx@ADhX(7z!x}<*M@kdSN$tkO#kHzv<8U#uSWUH1_LBt zeg4{)@gI63AcpCy0ib`zcx?nghA0DUK7U#M-T-=bp#IQ+hDKciPGDUCYXAa(HTi%# zM-B#fKLFs43+43$mjbH465xOic@V%Bpw|F63c#@d{sj1n0x})|prarT06L(|C=CF- z00e?S2ta&92LO>k8n7YV0X{YWfOPLTLz~uk{*699(*;DJ0V02|PWUgIG{11JFi95<35 z2m~Jl$X0&?=?CBoJQ#%toJUCc0Ahf&Hc(G`fE_qSB%n?ZfqaoL03HGWr~@P*4tx#3 zfpQ}<0vwQ$r2&uu>1iN+0LUpo92J1S)&Wr7*D@d#0(h+l763w^zs>_wM}WT)1OPw} zGI2L^GY0s3d-K0IAcDyLy|V!w)Wz7z38=S!5tH@bJ({ZA&G9uLv6=B-JHG}6$s2oE OaPly7b1}2Bu>3!^|LHaW literal 0 HcmV?d00001 diff --git a/docs/source/dataset_benchmark/tasks/generate_task_docs.py b/docs/source/dataset_benchmark/tasks/generate_task_docs.py index 25de29b6e..1653f3cda 100644 --- a/docs/source/dataset_benchmark/tasks/generate_task_docs.py +++ b/docs/source/dataset_benchmark/tasks/generate_task_docs.py @@ -24,7 +24,7 @@ "Metaworld", "Rlafford", ] -PLATFORMS = ["isaaclab", "mujoco", "isaacgym", "sapien3", "genesis"] +PLATFORMS = ["isaacsim", "mujoco", "isaacgym", "sapien3", "genesis", "newton"] def parse_docstring_metadata(docstring: str): diff --git a/docs/source/metasim/developer_guide/autotest.md b/docs/source/metasim/developer_guide/autotest.md index 077410f4d..78b921756 100644 --- a/docs/source/metasim/developer_guide/autotest.md +++ b/docs/source/metasim/developer_guide/autotest.md @@ -2,7 +2,7 @@ ## Overview -RoboVerse provides a comprehensive testing infrastructure built on pytest that enables efficient, cross-backend testing of simulation functionality. The system is designed around handler reuse and scenario sharing, dramatically reducing test execution time while maintaining full coverage across all supported simulator backends (MuJoCo, MJX, IsaacGym, IsaacSim). +RoboVerse provides a comprehensive testing infrastructure built on pytest that enables efficient, cross-backend testing of simulation functionality. The system is designed around handler reuse and scenario sharing, dramatically reducing test execution time while maintaining full coverage across all supported simulator backends (MuJoCo, MJX, IsaacGym, IsaacSim, Newton). Key features of the testing system include: @@ -38,6 +38,7 @@ Markers declare which simulator backends a test requires: - `@pytest.mark.mjx`: Test runs on MuJoCo MJX backend - `@pytest.mark.isaacgym`: Test runs on IsaacGym backend - `@pytest.mark.isaacsim`: Test runs on IsaacSim backend +- `@pytest.mark.newton`: Test runs on Newton backend - `@pytest.mark.sim("sim1", "sim2")`: Test runs on multiple specified backends - `@pytest.mark.general`: Test requires no simulator/handler (pure unit test) @@ -321,6 +322,9 @@ python metasim/test/isaacgym_entry.py metasim/test/ -k isaacgym # IsaacSim only pytest metasim/test/ -k isaacsim +# Newton only +pytest metasim/test/ -k newton + # General tests (no simulator) pytest metasim/test/ -k general ``` diff --git a/docs/source/metasim/features/cross_sim.md b/docs/source/metasim/features/cross_sim.md index 50b7d0d9c..8068af87a 100644 --- a/docs/source/metasim/features/cross_sim.md +++ b/docs/source/metasim/features/cross_sim.md @@ -1,7 +1,7 @@ # Cross Simulator ## Basic usage -By default, the simulator is set to `isaaclab`. You can change it to other simulators by setting the `sim` argument. Currently, we support: -- `isaaclab` +By default, the simulator is set to `isaacsim`. You can change it to other simulators by setting the `sim` argument. Currently, we support: +- `isaacsim` - `isaacgym` - `pyrep` diff --git a/docs/source/metasim/features/support_matrix.rst b/docs/source/metasim/features/support_matrix.rst index d520f4838..fa0f668ac 100644 --- a/docs/source/metasim/features/support_matrix.rst +++ b/docs/source/metasim/features/support_matrix.rst @@ -9,7 +9,7 @@ Supported Simulators There are 3 levels of supportance for each simulator: -- **Actively supported**: ``isaaclab``, ``isaacgym``, ``mujoco``, ``sapien2``, ``sapien3``, ``genesis``, ``pybullet`` . These simulators should always be guaranteed to work on the main branch. +- **Actively supported**: ``isaacsim``, ``isaacgym``, ``mujoco``, ``sapien2``, ``sapien3``, ``genesis``, ``pybullet``, ``newton`` . These simulators should always be guaranteed to work on the main branch. - **Inactively supported**: ``pyrep``. These simulators won't be actively supported. They will only be guaranteed to work when a major version is released. - **Experimental**: ``mjx``, ``blender``. These simulators (renderers) are still in experimental stage and will be added to "actively supported" list in the future. @@ -26,15 +26,16 @@ Simulation Configuration .. list-table:: :header-rows: 1 - :widths: 20 20 20 20 20 20 20 + :widths: 15 15 15 15 15 15 15 15 * - Parameter - - IsaacLab + - IsaacSim - IsaacGym - MuJoCo - Genesis - SAPIEN3 - PyBullet + - Newton * - ``dt`` - `1/60 `_ - `1/60 `_ @@ -42,6 +43,7 @@ Simulation Configuration - `1/100 `_ - 1/100 - `1/240 `_ + - 1/60 * - ``solver_type`` - `✓ `_ - `✓ `_ @@ -49,6 +51,7 @@ Simulation Configuration - - - + - * - ``env_spacing`` - - ✓ @@ -56,6 +59,7 @@ Simulation Configuration - ✓ - - + - ✓ @@ -64,15 +68,16 @@ Robot Configuration .. list-table:: :header-rows: 1 - :widths: 20 20 20 20 20 20 20 + :widths: 15 15 15 15 15 15 15 15 * - Parameter - - IsaacLab + - IsaacSim - IsaacGym - MuJoCo - Genesis - SAPIEN3 - PyBullet + - Newton * - ``stiffness`` - `✓ `_ - ✓ @@ -80,6 +85,7 @@ Robot Configuration - - ✓ - + - * - ``damping`` - `✓ `_ - ✓ @@ -87,6 +93,7 @@ Robot Configuration - - ✓ - + - * - ``velocity_limit`` - `✓ `_ - @@ -94,6 +101,7 @@ Robot Configuration - - - + - * - ``effort_limit_sim`` - `✓ `_ - @@ -101,6 +109,7 @@ Robot Configuration - - - + - * - ``fully_actuated`` - ✓ - ✓ @@ -108,6 +117,7 @@ Robot Configuration - - - + - Physics Engine Configuration @@ -115,15 +125,16 @@ Physics Engine Configuration .. list-table:: :header-rows: 1 - :widths: 20 20 20 20 20 20 20 + :widths: 15 15 15 15 15 15 15 15 * - Parameter - - IsaacLab + - IsaacSim - IsaacGym - MuJoCo - Genesis - SAPIEN3 - PyBullet + - Newton * - ``bounce_threshold_velocity`` - `✓ `_ - `✓ `_ @@ -131,6 +142,7 @@ Physics Engine Configuration - - - + - * - ``contact_offset`` - - `✓ `_ @@ -138,6 +150,7 @@ Physics Engine Configuration - - - + - * - ``friction_correlation_distance`` - `✓ `_ - `✓ `_ @@ -145,6 +158,7 @@ Physics Engine Configuration - - - + - * - ``friction_offset_threshold`` - `✓ `_ - `✓ `_ @@ -152,6 +166,7 @@ Physics Engine Configuration - - - + - * - ``num_position_iterations`` - - `✓ `_ @@ -159,6 +174,7 @@ Physics Engine Configuration - - - + - * - ``num_velocity_iterations`` - - `✓ `_ @@ -166,6 +182,7 @@ Physics Engine Configuration - - - + - * - ``rest_offset`` - - `✓ `_ @@ -173,6 +190,7 @@ Physics Engine Configuration - - - + - * - ``max_depenetration_velocity`` - - `✓ `_ @@ -180,6 +198,7 @@ Physics Engine Configuration - - - + - * - ``default_buffer_size_multiplier`` - - `✓ `_ @@ -187,21 +206,23 @@ Physics Engine Configuration - - - + - Resource Management Configuration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. list-table:: :header-rows: 1 - :widths: 20 20 20 20 20 20 20 + :widths: 15 15 15 15 15 15 15 15 * - Parameter - - IsaacLab + - IsaacSim - IsaacGym - MuJoCo - Genesis - SAPIEN3 - PyBullet + - Newton * - ``num_threads`` - - `✓ `_ @@ -209,21 +230,23 @@ Resource Management Configuration - - - + - Misc Configuration ~~~~~~~~~~~~~~~~~~ .. list-table:: :header-rows: 1 - :widths: 20 20 20 20 20 20 20 + :widths: 15 15 15 15 15 15 15 15 * - Parameter - - IsaacLab + - IsaacSim - IsaacGym - MuJoCo - Genesis - SAPIEN3 - PyBullet + - Newton * - ``replace_cylinder_with_capsule`` - - `✓ `_ @@ -231,3 +254,4 @@ Misc Configuration - - - + - diff --git a/docs/source/metasim/get_started/advanced/rl_example/infrastructure.md b/docs/source/metasim/get_started/advanced/rl_example/infrastructure.md index f1a2fbfcc..d3c0c0441 100644 --- a/docs/source/metasim/get_started/advanced/rl_example/infrastructure.md +++ b/docs/source/metasim/get_started/advanced/rl_example/infrastructure.md @@ -26,7 +26,7 @@ python get_started/rl/0_ppo.py --num-envs 256 --headless # Different simulators python get_started/rl/0_ppo.py --sim mujoco python get_started/rl/0_ppo.py --sim genesis -python get_started/rl/0_ppo.py --sim isaaclab +python get_started/rl/0_ppo.py --sim isaacsim ``` **Arguments:** @@ -34,7 +34,7 @@ python get_started/rl/0_ppo.py --sim isaaclab - `--task`: Task name (default: `reach_origin`) - `--robot`: Robot type (default: `franka`) - `--num-envs`: Number of parallel environments (default: `128`) -- `--sim`: Simulator backend (`isaacgym`, `isaaclab`, `mujoco`, `genesis`, `mjx`) +- `--sim`: Simulator backend (`isaacgym`, `isaacsim`, `mujoco`, `genesis`, `mjx`, `newton`) - `--headless`: Run without GUI (flag) **Outputs:** @@ -63,7 +63,7 @@ python get_started/rl/0_ppo_gym.py --task reach_origin --robot franka --num-envs - `--task`: Task name (default: `reach_origin`) - `--robot`: Robot type (default: `franka`) - `--num-envs`: Number of environments (default: `128`) -- `--sim`: Simulator (`isaaclab`, `isaacgym`, `mujoco`, `genesis`, `mjx`) +- `--sim`: Simulator (`isaacsim`, `isaacgym`, `mujoco`, `genesis`, `mjx`, `newton`) - `--headless`: Headless mode (flag) - `--device`: Device (`cuda`, `cpu`) @@ -114,10 +114,11 @@ CONFIG = { ### Simulator Backends 1. **Isaac Gym**: NVIDIA's physics simulation -2. **Isaac Lab**: Next-generation Isaac simulation +2. **Isaac Sim**: Next-generation Isaac simulation 3. **MuJoCo**: Fast physics simulation 4. **Genesis**: Multi-physics simulation 5. **MJX**: JAX-based MuJoCo implementation +6. **Newton**: GPU-accelerated physics simulation ## Dependencies diff --git a/docs/source/metasim/get_started/installation.rst b/docs/source/metasim/get_started/installation.rst index d16635976..af6009f6f 100644 --- a/docs/source/metasim/get_started/installation.rst +++ b/docs/source/metasim/get_started/installation.rst @@ -46,6 +46,10 @@ MuJoCo, SAPIEN2, SAPIEN3, Genesis, and PyBullet can be installed directly via `` - ``uv pip install -e ".[pybullet]"`` - 3.6-3.11 - 3.10 + * - Newton + - ``uv pip install -e ".[newton]"`` + - 3.10-3.12 + - 3.10 * - IsaacSim v4.5.0 - See below - 3.10 diff --git a/docs/source/metasim/get_started/prerequisite.md b/docs/source/metasim/get_started/prerequisite.md index fa57849c0..8d776fccf 100644 --- a/docs/source/metasim/get_started/prerequisite.md +++ b/docs/source/metasim/get_started/prerequisite.md @@ -8,11 +8,11 @@ You may choose one or more of the following simulators, according to your operat For beginners, we recommend choosing one specific simulator for each environment, which makes environment configuration simpler and provides better isolation. -| OS | IsaacSim | IsaacGym | MuJoCo | SAPIEN2 | SAPIEN3 | Genesis | PyBullet | -|---------|----------|----------|--------|---------|---------|---------|----------| -| MacOS | | | ✓ | | ✓ | ✓ | | -| Windows | ✓ | | ✓ | | ✓ | ✓ | | -| Ubuntu | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| OS | IsaacSim | IsaacGym | MuJoCo | SAPIEN2 | SAPIEN3 | Genesis | PyBullet | Newton | +|---------|----------|----------|--------|---------|---------|---------|----------|--------| +| MacOS | | | ✓ | | ✓ | ✓ | | | +| Windows | ✓ | | ✓ | | ✓ | ✓ | | | +| Ubuntu | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ```{note} RoboVerse team hasn't got the chance to fully test MetaSim on MacOS and Windows. Please let us know if you have any issues. diff --git a/docs/source/metasim/get_started/quick_start/0_static_scene.md b/docs/source/metasim/get_started/quick_start/0_static_scene.md index f522280cd..266464d49 100644 --- a/docs/source/metasim/get_started/quick_start/0_static_scene.md +++ b/docs/source/metasim/get_started/quick_start/0_static_scene.md @@ -43,18 +43,27 @@ python get_started/0_static_scene.py --sim sapien3 python get_started/0_static_scene.py --sim pybullet ``` +#### Newton +```bash +python get_started/0_static_scene.py --sim newton +``` + You will get the following image: --- -| Isaac Lab | Isaac Gym | Mujoco | +| Isaac Sim | Isaac Gym | Mujoco | |:---:|:---:|:---:| -| ![Isaac Lab](../../../_static/standard_output/0_static_scene_isaaclab.png) | ![Isaac Gym](../../../_static/standard_output/0_static_scene_isaacgym.png) | ![Mujoco](../../../_static/standard_output/0_static_scene_mujoco.png) | +| ![Isaac Sim](../../../_static/standard_output/0_static_scene_isaacsim.png) | ![Isaac Gym](../../../_static/standard_output/0_static_scene_isaacgym.png) | ![Mujoco](../../../_static/standard_output/0_static_scene_mujoco.png) | | Genesis | Sapien | PyBullet | |:---:|:---:|:---:| | ![Genesis](../../../_static/standard_output/0_static_scene_genesis.png) | ![Sapien](../../../_static/standard_output/0_static_scene_sapien3.png) | ![Pybullet](../../../_static/standard_output/0_static_scene_pybullet.png) | +| Newton | +|:---:| +| ![Newton](../../../_static/standard_output/0_static_scene_newton.png) | + ## Code Highlights **Object Configuration**: Objects are added to `scenario.objects` with different types: diff --git a/docs/source/metasim/get_started/quick_start/1_control_robot.md b/docs/source/metasim/get_started/quick_start/1_control_robot.md index c752bd83c..853cfd086 100644 --- a/docs/source/metasim/get_started/quick_start/1_control_robot.md +++ b/docs/source/metasim/get_started/quick_start/1_control_robot.md @@ -46,6 +46,11 @@ python get_started/1_control_robot.py --sim sapien3 python get_started/1_control_robot.py --sim pybullet ``` +#### Newton +```bash +python get_started/1_control_robot.py --sim newton +``` + You will get the following videos: @@ -53,9 +58,9 @@ You will get the following videos:

+
+
+ +

Newton

+
+
## Code Highlights diff --git a/docs/source/metasim/get_started/quick_start/2_add_new_robot.md b/docs/source/metasim/get_started/quick_start/2_add_new_robot.md index f23e7a9c3..ad5d529cf 100644 --- a/docs/source/metasim/get_started/quick_start/2_add_new_robot.md +++ b/docs/source/metasim/get_started/quick_start/2_add_new_robot.md @@ -46,6 +46,11 @@ python get_started/2_add_new_robot.py --sim sapien3 python get_started/2_add_new_robot.py --sim pybullet ``` +#### Newton +```bash +python get_started/2_add_new_robot.py --sim newton +``` + You will get the following videos: @@ -53,9 +58,9 @@ You will get the following videos:
-

Isaac Lab

+

Isaac Sim

+
+
+ +

Newton

+
+
## Code Highlights diff --git a/docs/source/metasim/get_started/quick_start/3_parallel_envs.md b/docs/source/metasim/get_started/quick_start/3_parallel_envs.md index b25ed0d9f..e31db4e78 100644 --- a/docs/source/metasim/get_started/quick_start/3_parallel_envs.md +++ b/docs/source/metasim/get_started/quick_start/3_parallel_envs.md @@ -36,6 +36,11 @@ python get_started/3_parallel_envs.py --sim mujoco --num_envs 4 --headless ``` **If you are on mac**, please avoid running this task without the `headless` tag. +#### Newton +```bash +python get_started/3_parallel_envs.py --sim newton --num_envs 4 +``` + We can open multiple environments at the same time.

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 diff --git a/roboverse_learn/il/act/assets/vx300s_8_gripper_prop.stl b/roboverse_learn/il/act/assets/vx300s_8_gripper_prop.stl deleted file mode 100644 index 36099b4221155b491a2ad61fb799c7fd7d443b77..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31684 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 VOkg%qW)~|6R}onK;5kP~{2$;m(f0rV diff --git a/roboverse_learn/il/act/assets/vx300s_9_gripper_bar.stl b/roboverse_learn/il/act/assets/vx300s_9_gripper_bar.stl deleted file mode 100644 index eba3caa21990c28559553fe0003ae379514e7215..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 379484 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:WfBSN(sYU3!tO?rYNF z51gR@A3cUWZQo3ATI|Co;6wAl8Ya+o^?okOxqaG>Tj_}E6WQk|%=d}js$j1Yjnc)ed)6?aZzpiJs}+T(b&u@EuDPxw(*&tHaL0Izhk54C`$xv_17EDYHyPX z+aV&k|>f{1*u zhKc-QN1rfbOsuTfs#fZLn~zz4-IGZWkq_1|F>H?W@xs>8F&}JI`xWP-?RR%)5=7*K zHB7wxyYq2%tGYKN{ z!5St;FL6FrA2KZFgRK@{;e32_aF9t5kq_1|aqdUX$2%|F7W2VYJ100FU$h;bNf40_ z)-Z9;7UyH-iMPaju+_x5&PQRV3i=$N5C2;)c2C&elZ_xrT$L%q)(KOh=_b}T1;pe=6}>X z=7X)Y+`^LLs6HYh^1*2_p=}|rOV5}Ow$l1PZrSxwyNZa&2dBk^w&@nX-Wc=2R@#m) z-E?i#KO!RX!D%s}ee~F=p-1>ta6GO2?J&!*0=d z6%mmSPKya0E8iY@ZOjK->G(M7;me|NHzFb*oE8&0=C@yXb<77_={W!F>z72=kBEqT za9T|0wQKINSH*m=m0qt#~Yw&`1uZa0zE4}W%R{O%}dL9vx4^E2- zy_dLV!R0X@Y^C=P?_7F*bYBt?kq=Ib3B4!!^Q&$#A8e)fLBH-jFS@^qh{y-0#f0A5 z{n07zgRS&_u0ezIqWi>%h&dVhX zYnaIY*3nT*R3C<|UTEp8(00owSi?m6VMj+jD)PZrKc>goe4r1OPq2oGk)526Xk>_d zu+{e$I3E~G$|qREM8_J=M>N7kKG^(G@51!B(lJ&IhhhiYW^Fh*m+ z-i;MCw%90(vX7u*?8ev;|Hc9cx}qXB$|!4HD~UZ8Y)R}9R2F0(cWlw9QLNF}jmF*^ zioEA{&YYRw-I)jS@=3X$J?DGRxp!_kx6C|eXsLc6n{UT5b<(gAx&ZFM}ma zcpaA4IIQLMhBZ!!$ZfEM32zJeTFr@swfy;Gt=5nTEMdaibiU@yZLpTNW2`wF5`iU5 zc;C#|&bbZN^8Sdmb3-Dqgb5#G@;!ju25b4af<1tSL|_RMK33*?8MzJC^6_y~*~@51 z1eP%2V}8CTliOe|ALp?r(~t-(VZ!IGd~YeY!CF3FVQ;A+5m>^6&%yZ~S#E>1eBQ+# zSwkYQgb9x&^1Z&?25WizfW5wkL|_RM9uwtzmbneq@;C^4mJNwKzCvD1cx;#Nz2-Jp z%i}rhy~f1LqhH0FNa61jC)7-v!YJVcYN z>6Tk|C6NDG4^8U--8?7SG+!n>ayA>ksK2_6+bC<9S1}>w(07+m4whKj2wwr9eDVc> z62V$l3o&u^ng{6{BQNwgrN$CV8{sP%JWcuLa*1HA))ZPX(d*P3)W!){99LtBrH$|v z8CqosWrDR@Q)tD+oF86L8&B>#qQ(+S8{sQU^sW%f1Z%aX(29vGf9Rnn0M8#dq{b3U z8{sQtj1nP~3D#;&p%oM3KUzg?eDK6BHI`V~2w&-Av7L}VdC4#kDQ)tD+=uHk)8xKAAX){YKZG`WHAsUdA%o4#`ttqr( z;`h%Stv0?qeO5C|ENz7E=pm|+H^WK{{HM{mRQ;d-o7DAa| zt=1G;G4b|wKsk4L$A7ik( zkH(UWXibr?k3P_fiEM-mGA7np!i0~N*rkZZ#EfW7k*|-U(29v{qz*E+*IB}ZkNMbb ziN^MfkU2{7_1OSgF_FzULFTABOPKJv3%f?q9F-BRDf0DM4O%hLVsmZy?7$&)mN4OS zFm}J>U88JvE{N6?`TFb(t(eFnfFLnLoh3|oEP-9TC}zlr))e`AlmV@n$Re2_u}_^P zOn6L$-N-2R$%xie_Yn0SKx+!En3%Md#&F^B9;eh<;_(#-kL|Fl8bU@S1!4Jm)CaAY z_^Cx>(eTv733Zk*;V~+9hvkb|MkEE%nj&A1UZE8eh^g&gQfCPh9=DqfSC`N|BU)2P zJccjQZTffhO4kPPit}Q^R~A-RQVtMWjP5*?HQ32gym~mHU`Ip%pYn4^9M_q@SPW%qcS2X2+P;BY%Yn3 zxo`bWZCrUv&pJz(@Es(ZYcnD#2+P;BY)*`czMJYdox=0aeA>YhCVVH%Vup-J3c~U= zEt}h8LgErF2jUWzFyT9T7W-sGQV^D}X<3XC6A~@iGne>^B~19vq{VOQly2xOmRQ;d-YE_tBa(t( zEsK3);=|0w6ZZ`rz!FOv;kIpUkP%5iu$IN}F+p|J%n~mL^7T5jH4fEPMp(XHyU>aW zSrzFs$2y87On6(cwOU3b1z~F(fBw*l32M8|EMdaiw5>U*?Pi3n)x5PsD<-H9HnW5Y z@0+%EraqVvw&wI+3$2)-u|&RtVJ8@cmaU3?jIlic8cQ;QwQQ{(6Er5)SmNUq^7XOO z_A+Qp%m~ZZM^R|S1dZ)AmN4OCzU|4-*q#x#m*KMkv|@ths2WR{@VU$OmS~R32(cmg z`m6@6n2_BlZM)dNVhIyI2iqQ5MkEDcdrLk$Ln|gIW~i}*36CXgua9DejIcd2k20VY z6BPT@Si*$IM7C#1u}?iV{hf4=xZyN7DwuaNA^zdPS*1x+p{y(WfYuK>PjF?Pm{C#FY zBv3b^(1N|;!)9f47eW9d#?8ApbsOX7okr-&R+ki}Yz?gX$`Q0k^?gg;x~>AhqFnu~(>d|GOw^{%)fvtvwqnM{wRdp% z;ybL*d;r7+VxzF$FVXLPf8VU@cDDyWz17 zZzWa><`vTX%*#Z%b}IP~20k*Y*e($*tl`($V)^EMMNZC@y*EO1KvO?kzvAE6xk) z7goJqc7*?;HcA9gwc(N~0<_xD)-EZz>+JpfI?315TV-h*kIMwwm3&)7wY-hlIEIpE zbrsoQEpL4d2`LB0M9T|lCh%mth3Q2(5-&%rg^|-dED