Timsty commited on
Commit
a89a522
·
verified ·
1 Parent(s): 8cf4ead

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/environments/locomanipulation/__init__.py +13 -0
  2. GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/environments/locomanipulation/base.py +1658 -0
  3. GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/environments/locomanipulation/locomanip.py +83 -0
  4. GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/environments/locomanipulation/locomanip_basic.py +732 -0
  5. GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/environments/locomanipulation/locomanip_dc.py +15 -0
  6. GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/environments/locomanipulation/locomanip_pnp.py +99 -0
  7. GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/examples/third_party_controller/__init__.py +0 -0
  8. GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/examples/third_party_controller/default_mink_ik_g1_gear_wbc.json +113 -0
  9. GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/examples/third_party_controller/default_mink_ik_g1_gear_wbc_gc.json +117 -0
  10. GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite/__init__.py +6 -0
  11. GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite/bin.py +205 -0
  12. GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite/box_pattern_object.py +124 -0
  13. GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite/lid.py +136 -0
  14. GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite/needle.py +109 -0
  15. GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite/pot_with_handles.py +396 -0
  16. GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite/ring_tripod.py +194 -0
  17. GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite_body/bin_with_handles.py +162 -0
  18. GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite_body/coffee_machine.py +244 -0
  19. GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite_body/inverse_stacked_cylinder.py +139 -0
  20. GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite_body/lightbulb.py +153 -0
  21. GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite_body/sliding_box.py +132 -0
  22. GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite_body/stacked_box.py +111 -0
  23. GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite_body/stacked_cylinder.py +127 -0
  24. GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite_body/stove_plug.py +396 -0
  25. GR00T-WholeBodyControl/decoupled_wbc/docker/entrypoint/bash.sh +5 -0
  26. GR00T-WholeBodyControl/decoupled_wbc/docker/entrypoint/deploy.sh +20 -0
  27. GR00T-WholeBodyControl/decoupled_wbc/docker/entrypoint/install_deps.sh +23 -0
  28. GR00T-WholeBodyControl/decoupled_wbc/tests/control/__init__.py +0 -0
  29. GR00T-WholeBodyControl/decoupled_wbc/tests/control/main/__init__.py +0 -0
  30. GR00T-WholeBodyControl/decoupled_wbc/tests/control/main/teleop/__init__.py +0 -0
  31. GR00T-WholeBodyControl/decoupled_wbc/tests/control/main/teleop/test_g1_control_loop.py +469 -0
  32. GR00T-WholeBodyControl/decoupled_wbc/tests/control/main/test_data_exporter_loop.py +403 -0
  33. GR00T-WholeBodyControl/decoupled_wbc/tests/control/policy/__init__.py +0 -0
  34. GR00T-WholeBodyControl/decoupled_wbc/tests/control/policy/interpolation_policy/__init__.py +0 -0
  35. GR00T-WholeBodyControl/decoupled_wbc/tests/control/policy/interpolation_policy/test_interpolation_policy.py +47 -0
  36. GR00T-WholeBodyControl/decoupled_wbc/tests/control/policy/interpolation_policy/test_interpolation_ramp_up.py +78 -0
  37. GR00T-WholeBodyControl/decoupled_wbc/tests/control/policy/interpolation_policy/trajectory.png +0 -0
  38. GR00T-WholeBodyControl/decoupled_wbc/tests/control/robot_model/__init__.py +0 -0
  39. GR00T-WholeBodyControl/decoupled_wbc/tests/control/robot_model/robot_model_test.py +911 -0
  40. GR00T-WholeBodyControl/decoupled_wbc/tests/control/teleop/__init__.py +0 -0
  41. GR00T-WholeBodyControl/decoupled_wbc/tests/control/teleop/test_teleop_retargeting_ik.py +196 -0
  42. GR00T-WholeBodyControl/decoupled_wbc/tests/control/visualization/__init__.py +0 -0
  43. GR00T-WholeBodyControl/decoupled_wbc/tests/control/visualization/test_meshcat_visualizer_env.py +88 -0
  44. GR00T-WholeBodyControl/decoupled_wbc/tests/data/test_exporter.py +522 -0
  45. GR00T-WholeBodyControl/decoupled_wbc/tests/sim/test_sim_data_collection.py +64 -0
  46. GR00T-WholeBodyControl/external_dependencies/XRoboToolkit-PC-Service-Pybind_X86_and_ARM64/CLAUDE.md +108 -0
  47. GR00T-WholeBodyControl/external_dependencies/XRoboToolkit-PC-Service-Pybind_X86_and_ARM64/CMakeLists.txt +54 -0
  48. GR00T-WholeBodyControl/external_dependencies/XRoboToolkit-PC-Service-Pybind_X86_and_ARM64/LICENSE +21 -0
  49. GR00T-WholeBodyControl/external_dependencies/XRoboToolkit-PC-Service-Pybind_X86_and_ARM64/README.md +169 -0
  50. GR00T-WholeBodyControl/external_dependencies/XRoboToolkit-PC-Service-Pybind_X86_and_ARM64/setup.py +148 -0
GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/environments/locomanipulation/__init__.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Full list of loco-manipulation tasks.
3
+
4
+ GroundOnly - ground only environments
5
+
6
+ locomanip_pnp - factory environments, pick and place tasks:
7
+ LMBottlePnP
8
+ LMBoxPnP
9
+ """
10
+
11
+ from .base import REGISTERED_LOCOMANIPULATION_ENVS
12
+
13
+ ALL_LOCOMANIPULATION_ENVIRONMENTS = REGISTERED_LOCOMANIPULATION_ENVS.keys()
GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/environments/locomanipulation/base.py ADDED
@@ -0,0 +1,1658 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from copy import deepcopy
2
+ import os
3
+ from typing import Optional, Type
4
+ import warnings
5
+ import xml.etree.ElementTree as ET
6
+
7
+ import mujoco
8
+ import numpy as np
9
+ import robosuite
10
+ from robosuite.environments.base import EnvMeta
11
+ from robosuite.environments.manipulation.manipulation_env import ManipulationEnv
12
+ from robosuite.models.arenas import Arena
13
+ from robosuite.models.tasks import ManipulationTask
14
+ from robosuite.utils.mjcf_utils import array_to_string, find_elements, xml_path_completion
15
+ from robosuite.utils.observables import Observable, sensor
16
+
17
+ import robocasa
18
+ from robocasa.models.objects.objects import MJCFObject
19
+ from robocasa.models.scenes import GroundArena
20
+ import robocasa.utils.camera_utils as CamUtils
21
+ from robocasa.utils.dexmg_utils import DexMGConfigHelper
22
+ from robocasa.utils.object_utils import check_obj_upright
23
+ from robocasa.utils.visuals_utls import Gradient, randomize_materials_rgba
24
+
25
+ REGISTERED_LOCOMANIPULATION_ENVS = {}
26
+
27
+
28
+ def register_locomanipulation_env(target_class):
29
+ REGISTERED_LOCOMANIPULATION_ENVS[target_class.__name__] = target_class
30
+
31
+
32
+ class LocoManipulationEnvMeta(EnvMeta):
33
+ """Metaclass for registering robocasa environments"""
34
+
35
+ def __new__(meta, name, bases, class_dict):
36
+ cls = super().__new__(meta, name, bases, class_dict)
37
+ register_locomanipulation_env(cls)
38
+ return cls
39
+
40
+
41
+ class CameraPoseRandomizer:
42
+ @staticmethod
43
+ def randomize_cameras(
44
+ env: "LocoManipulationEnv",
45
+ cam_names: list[str],
46
+ pos_range: tuple[np.ndarray, np.ndarray],
47
+ euler_range: tuple[np.ndarray, np.ndarray],
48
+ ):
49
+ """
50
+ Randomize camera poses while maintaining their relative transforms.
51
+
52
+ Args:
53
+ env: The environment instance
54
+ cam_names: List of camera names to randomize together
55
+ pos_range: Tuple of (min_pos, max_pos) as 3D arrays for position randomization
56
+ euler_range: Tuple of (min_euler, max_euler) as 3D arrays for euler angle randomization (in radians)
57
+ """
58
+ if len(cam_names) == 0:
59
+ return
60
+
61
+ # Sample random transform offset
62
+ random_pos_offset = env.rng.uniform(pos_range[0], pos_range[1])
63
+ random_euler_offset = env.rng.uniform(euler_range[0], euler_range[1])
64
+
65
+ # Convert euler offset to quaternion
66
+ quat_offset = np.zeros(4, dtype=float)
67
+ mujoco.mju_euler2Quat(quat_offset, random_euler_offset, "xyz")
68
+
69
+ # Apply the same transform to all specified cameras
70
+ for cam_name in cam_names:
71
+ if cam_name not in env._cam_configs:
72
+ warnings.warn(f"Camera {cam_name} not found in camera configs. Skipping.")
73
+ continue
74
+
75
+ cam_config = env._cam_configs[cam_name]
76
+
77
+ # Get original position and quaternion
78
+ original_pos = np.array(cam_config["pos"], dtype=float)
79
+ original_quat = np.array(cam_config["quat"], dtype=float)
80
+
81
+ # Apply rotation offset to position (rotate position offset by the random rotation)
82
+ rotated_offset = np.zeros(3, dtype=float)
83
+ mujoco.mju_rotVecQuat(rotated_offset, random_pos_offset, original_quat)
84
+ new_pos = original_pos + rotated_offset
85
+
86
+ # Compose quaternions: new_quat = quat_offset * original_quat
87
+ new_quat = np.zeros(4, dtype=float)
88
+ mujoco.mju_mulQuat(new_quat, quat_offset, original_quat)
89
+
90
+ # Update camera config
91
+ cam_config["pos"] = new_pos.tolist()
92
+ cam_config["quat"] = new_quat.tolist()
93
+
94
+ # Update in simulation if already created
95
+ if hasattr(env, "sim") and env.sim is not None:
96
+ try:
97
+ cam_id = env.sim.model.camera_name2id(cam_name)
98
+ env.sim.model.cam_pos[cam_id] = new_pos
99
+ env.sim.model.cam_quat[cam_id] = new_quat
100
+ except:
101
+ # Camera might not be in the model yet
102
+ pass
103
+
104
+
105
+ class RobotPoseRandomizer:
106
+ @staticmethod
107
+ def set_pose(
108
+ env: "LocoManipulationEnv",
109
+ x_range: [tuple[float, float]],
110
+ y_range: [tuple[float, float]],
111
+ yaw_range: [tuple[float, float]],
112
+ ):
113
+ new_x = env.rng.uniform(*x_range)
114
+ new_y = env.rng.uniform(*y_range)
115
+ new_yaw = env.rng.uniform(*yaw_range)
116
+
117
+ if env.robots[0].name == "G1":
118
+ base_offset = env.ROBOT_POS_OFFSETS[env.robots[0].robot_model.__class__.__name__]
119
+ target_pos = np.array([new_x, new_y, base_offset[2]], dtype=float)
120
+ quat = np.zeros(4, dtype=float)
121
+ mujoco.mju_euler2Quat(quat, np.array([0.0, 0.0, new_yaw]), "xyz")
122
+ base_freejoint = f"{env.robots[0].robot_model.naming_prefix}base"
123
+ if base_freejoint in env.sim.model.joint_names:
124
+ env.sim.data.set_joint_qpos(base_freejoint, np.concatenate([target_pos, quat]))
125
+ else:
126
+ warnings.warn(f"Base joint {base_freejoint} not found in the model.")
127
+ else:
128
+ base_joint_pos = np.array([new_x, new_y, new_yaw])
129
+ base_joint_names = [
130
+ "mobilebase0_joint_mobile_forward",
131
+ "mobilebase0_joint_mobile_side",
132
+ "mobilebase0_joint_mobile_yaw",
133
+ ]
134
+ for i, base_joint_name in enumerate(base_joint_names):
135
+ if base_joint_name not in env.sim.model.joint_names:
136
+ warnings.warn(
137
+ f"Base joint {base_joint_name} not found in the model. "
138
+ f"Skipping randomization of {base_joint_name}."
139
+ )
140
+ else:
141
+ env.sim.data.set_joint_qpos(base_joint_name, base_joint_pos[i])
142
+
143
+ @staticmethod
144
+ def set_arm(env: ManipulationEnv, elbow_qpos: float, shoulder_pitch_qpos: float):
145
+ """Helper function to reinitialize G1 robot arm configuration."""
146
+ robot = env.robots[0]
147
+ if "G1" not in robot.name:
148
+ # avoid reinitializing arm configuration for non-G1 robots
149
+ return
150
+
151
+ joint_names = robot.robot_joints
152
+ joint_pos_indices = robot._ref_joint_pos_indexes
153
+ for joint_name, pos_idx in zip(joint_names, joint_pos_indices):
154
+ if "elbow" in joint_name:
155
+ print(f"reinitializing G1 {joint_name} with idx {pos_idx} to {elbow_qpos}")
156
+ env.sim.data.qpos[pos_idx] = elbow_qpos
157
+ elif "shoulder_pitch" in joint_name:
158
+ print(f"reinitializing G1 {joint_name} with idx {pos_idx} to {shoulder_pitch_qpos}")
159
+ env.sim.data.qpos[pos_idx] = shoulder_pitch_qpos
160
+
161
+
162
+ class LocoManipulationEnv(ManipulationEnv, metaclass=LocoManipulationEnvMeta):
163
+ """
164
+ Initialized a Base Ground Standing environment.
165
+ """
166
+
167
+ MUJOCO_ARENA_CLS: Type[Arena] = GroundArena
168
+
169
+ ROBOT_POS_OFFSETS: dict[str, list[float]] = {
170
+ "PandaOmron": [0, 0, 0],
171
+ "GR1FloatingBody": [0, 0, 0.97],
172
+ "GR1": [0, 0, 0.97],
173
+ "GR1FixedLowerBody": [0, 0, 0.97],
174
+ "GR1FixedLowerBodyInspireHands": [0, 0, 0.97],
175
+ "GR1FixedLowerBodyFourierHands": [0, 0, 0.97],
176
+ "GR1ArmsOnly": [0, 0, 0.97],
177
+ "GR1ArmsOnlyInspireHands": [0, 0, 0.97],
178
+ "GR1ArmsOnlyFourierHands": [0, 0, 0.97],
179
+ "GR1ArmsAndWaistFourierHands": [0, 0, 0.97],
180
+ "G1": [0, 0, 0.793],
181
+ "G1FixedBase": [0, 0, 0.793],
182
+ "G1FixedLowerBody": [0, 0, 0.793],
183
+ "G1ArmsOnly": [0, 0, 0.793],
184
+ "G1ArmsOnlyFloating": [0, 0, 0.793],
185
+ "G1FloatingBody": [0, 0, 0.793],
186
+ "G1FloatingBodyWithVertical": [0, 0, 0.793],
187
+ }
188
+
189
+ def __init__(
190
+ self,
191
+ translucent_robot: bool = False,
192
+ use_object_obs: bool = False,
193
+ randomize_cameras: bool = False,
194
+ *args,
195
+ **kwargs,
196
+ ):
197
+ self.mujoco_objects = []
198
+ self.randomize_cameras = randomize_cameras
199
+
200
+ super().__init__(
201
+ *args,
202
+ **kwargs,
203
+ )
204
+
205
+ self.translucent_robot = translucent_robot
206
+
207
+ def _load_model(self):
208
+ super()._load_model()
209
+
210
+ self.mujoco_arena = self.MUJOCO_ARENA_CLS()
211
+ self.mujoco_arena.set_origin([0, 0, 0])
212
+ self.set_cameras()
213
+
214
+ self.model = ManipulationTask(
215
+ mujoco_arena=self.mujoco_arena,
216
+ mujoco_robots=[robot.robot_model for robot in self.robots],
217
+ mujoco_objects=self.mujoco_objects,
218
+ )
219
+
220
+ robot_base_pos = self.ROBOT_POS_OFFSETS[self.robots[0].robot_model.__class__.__name__]
221
+ robot_model = self.robots[0].robot_model
222
+ robot_model.set_base_xpos(robot_base_pos)
223
+ # robot_model.set_base_ori(robot_base_ori)
224
+
225
+ def set_cameras(self):
226
+ """
227
+ Adds new tabletop-relevant cameras to the environment. Will randomize cameras if specified.
228
+ """
229
+
230
+ self._cam_configs = deepcopy(CamUtils.CAM_CONFIGS)
231
+
232
+ for robot in self.robots:
233
+ if hasattr(robot.robot_model, "get_camera_configs"):
234
+ self._cam_configs.update(robot.robot_model.get_camera_configs())
235
+
236
+ for cam_name, cam_cfg in self._cam_configs.items():
237
+ if cam_cfg.get("parent_body", None) is not None:
238
+ continue
239
+
240
+ self.mujoco_arena.set_camera(
241
+ camera_name=cam_name,
242
+ pos=cam_cfg["pos"],
243
+ quat=cam_cfg["quat"],
244
+ camera_attribs=cam_cfg.get("camera_attribs", None),
245
+ )
246
+
247
+ self.mujoco_arena.set_camera(
248
+ camera_name="egoview",
249
+ pos=[0.078, 0, 1.308],
250
+ quat=[0.66491268, 0.24112495, -0.24112507, -0.66453637],
251
+ camera_attribs=dict(fovy="90"),
252
+ )
253
+
254
+ def visualize(self, vis_settings):
255
+ """
256
+ In addition to super call, make the robot semi-transparent
257
+
258
+ Args:
259
+ vis_settings (dict): Visualization keywords mapped to T/F, determining whether that specific
260
+ component should be visualized. Should have "grippers" keyword as well as any other relevant
261
+ options specified.
262
+ """
263
+ # Run superclass method first
264
+ super().visualize(vis_settings=vis_settings)
265
+
266
+ visual_geom_names = []
267
+
268
+ for robot in self.robots:
269
+ robot_model = robot.robot_model
270
+ visual_geom_names += robot_model.visual_geoms
271
+
272
+ for name in visual_geom_names:
273
+ rgba = self.sim.model.geom_rgba[self.sim.model.geom_name2id(name)]
274
+ if self.translucent_robot:
275
+ rgba[-1] = 0.10
276
+ else:
277
+ rgba[-1] = 1.0
278
+
279
+ def reward(self, action=None):
280
+ """
281
+ Reward function for the task. The reward function is based on the task
282
+ and to be implemented in the subclasses. Returns 0 by default.
283
+
284
+ Returns:
285
+ float: Reward for the task
286
+ """
287
+ reward = 0
288
+ if self._check_success():
289
+ reward = 1.0
290
+ return reward
291
+
292
+ def _check_success(self):
293
+ """
294
+ Checks if the task has been successfully completed.
295
+ Success condition is based on the task and to be implemented in the
296
+ subclasses. Returns False by default.
297
+
298
+ Returns:
299
+ bool: True if the task is successfully completed, False otherwise
300
+ """
301
+ return False
302
+
303
+ def edit_model_xml(self, xml_str):
304
+ """
305
+ This function postprocesses the model.xml collected from a MuJoCo demonstration
306
+ for retrospective model changes.
307
+
308
+ Args:
309
+ xml_str (str): Mujoco sim demonstration XML file as string
310
+
311
+ Returns:
312
+ str: Post-processed xml file as string
313
+ """
314
+ xml_str = super().edit_model_xml(xml_str)
315
+
316
+ tree = ET.fromstring(xml_str)
317
+ root = tree
318
+ worldbody = root.find("worldbody")
319
+ actuator = root.find("actuator")
320
+ asset = root.find("asset")
321
+ meshes = asset.findall("mesh")
322
+ textures = asset.findall("texture")
323
+ all_elements = meshes + textures
324
+
325
+ robosuite_path_split = os.path.split(robosuite.__file__)[0].split("/")
326
+ robocasa_path_split = os.path.split(robocasa.__file__)[0].split("/")
327
+
328
+ # replace robocasa-specific asset paths
329
+ for elem in all_elements:
330
+ old_path = elem.get("file")
331
+ if old_path is None:
332
+ continue
333
+
334
+ old_path_split = old_path.split("/")
335
+ # maybe replace all paths to robosuite assets
336
+ if "models/assets" in old_path:
337
+ if "/robosuite/" in old_path:
338
+ check_lst = [
339
+ loc for loc, val in enumerate(old_path_split) if val == "robosuite"
340
+ ]
341
+ ind = max(check_lst) # last occurrence index
342
+ new_path_split = robosuite_path_split + old_path_split[ind + 1 :]
343
+ elif "/robocasa/" in old_path:
344
+ check_lst = [loc for loc, val in enumerate(old_path_split) if val == "robocasa"]
345
+ ind = max(check_lst) # last occurrence index
346
+ new_path_split = robocasa_path_split + old_path_split[ind + 1 :]
347
+ else:
348
+ raise ValueError
349
+
350
+ new_path = "/".join(new_path_split)
351
+ elem.set("file", new_path)
352
+
353
+ # set cameras
354
+ for cam_name, cam_config in self._cam_configs.items():
355
+ parent_body = cam_config.get("parent_body", None)
356
+
357
+ cam_root = worldbody
358
+ if parent_body is not None:
359
+ cam_root = find_elements(root=worldbody, tags="body", attribs={"name": parent_body})
360
+ if cam_root is None:
361
+ # camera config refers to body that doesnt exist on the robot
362
+ continue
363
+
364
+ cam = find_elements(root=cam_root, tags="camera", attribs={"name": cam_name})
365
+
366
+ if cam is None:
367
+ old_cam = find_elements(root=worldbody, tags="camera", attribs={"name": cam_name})
368
+ if old_cam is not None:
369
+ # old camera associated with different body
370
+ continue
371
+
372
+ cam = ET.Element("camera")
373
+ cam.set("mode", "fixed")
374
+ cam.set("name", cam_name)
375
+ cam_root.append(cam)
376
+
377
+ cam.set("pos", array_to_string(cam_config["pos"]))
378
+ cam.set("quat", array_to_string(cam_config["quat"]))
379
+ for k, v in cam_config.get("camera_attribs", {}).items():
380
+ cam.set(k, v)
381
+
382
+ # replace base -> mobilebase (this is needed for old PandaOmron demos)
383
+ for elem in find_elements(
384
+ root=worldbody, tags=["geom", "site", "body", "joint"], return_first=False
385
+ ):
386
+ if elem.get("name") is None:
387
+ continue
388
+ if elem.get("name").startswith("base0_"):
389
+ old_name = elem.get("name")
390
+ new_name = "mobilebase0_" + old_name[6:]
391
+ elem.set("name", new_name)
392
+ for elem in find_elements(
393
+ root=actuator,
394
+ tags=["velocity", "position", "motor", "general"],
395
+ return_first=False,
396
+ ):
397
+ if elem.get("name") is None:
398
+ continue
399
+ if elem.get("name").startswith("base0_"):
400
+ old_name = elem.get("name")
401
+ new_name = "mobilebase0_" + old_name[6:]
402
+ elem.set("name", new_name)
403
+ for elem in find_elements(
404
+ root=actuator,
405
+ tags=["velocity", "position", "motor", "general"],
406
+ return_first=False,
407
+ ):
408
+ if elem.get("joint") is None:
409
+ continue
410
+ if elem.get("joint").startswith("base0_"):
411
+ old_joint = elem.get("joint")
412
+ new_joint = "mobilebase0_" + old_joint[6:]
413
+ elem.set("joint", new_joint)
414
+
415
+ # result = ET.tostring(root, encoding="utf8").decode("utf8")
416
+ result = ET.tostring(root).decode("utf8")
417
+
418
+ # # replace with generative textures
419
+ # if (self.generative_textures is not None) and (
420
+ # self.generative_textures is not False
421
+ # ):
422
+ # # sample textures
423
+ # assert self.generative_textures == "100p"
424
+ # self._curr_gen_fixtures = get_random_textures(self.rng)
425
+
426
+ # cab_tex = self._curr_gen_fixtures["cab_tex"]
427
+ # counter_tex = self._curr_gen_fixtures["counter_tex"]
428
+ # wall_tex = self._curr_gen_fixtures["wall_tex"]
429
+ # floor_tex = self._curr_gen_fixtures["floor_tex"]
430
+
431
+ # result = replace_cab_textures(
432
+ # self.rng, result, new_cab_texture_file=cab_tex
433
+ # )
434
+ # result = replace_counter_top_texture(
435
+ # self.rng, result, new_counter_top_texture_file=counter_tex
436
+ # )
437
+ # result = replace_wall_texture(
438
+ # self.rng, result, new_wall_texture_file=wall_tex
439
+ # )
440
+ # result = replace_floor_texture(
441
+ # self.rng, result, new_floor_texture_file=floor_tex
442
+ # )
443
+
444
+ return result
445
+
446
+ def _setup_references(self):
447
+ super()._setup_references()
448
+
449
+ self.obj_body_id = {}
450
+
451
+ def _randomize_robot_cameras(self):
452
+ """Randomize the poses of robot-mounted cameras while preserving their relative transforms."""
453
+ cam_names = ["robot0_oak_egoview", "robot0_oak_left_monoview", "robot0_oak_right_monoview"]
454
+
455
+ # Define randomization ranges
456
+ pos_range = (
457
+ np.array([-0.02, -0.02, -0.02]), # min position offset [x, y, z] in meters
458
+ np.array([0.02, 0.02, 0.02]), # max position offset [x, y, z] in meters
459
+ )
460
+ euler_range = (
461
+ np.array([-0.1, -0.1, -0.1]), # min euler angles [roll, pitch, yaw] in radians
462
+ np.array([0.1, 0.1, 0.1]), # max euler angles [roll, pitch, yaw] in radians
463
+ )
464
+
465
+ CameraPoseRandomizer.randomize_cameras(
466
+ env=self, cam_names=cam_names, pos_range=pos_range, euler_range=euler_range
467
+ )
468
+
469
+ def _reset_internal(self):
470
+ super()._reset_internal()
471
+
472
+ if self.randomize_cameras:
473
+ self._randomize_robot_cameras()
474
+
475
+ def _reset_observables(self):
476
+ if self.hard_reset:
477
+ self._observables = self._setup_observables()
478
+
479
+ # these sensors need a lot of computation, so we disable them by default for speed up simulation
480
+ disabled_sensors = [
481
+ "base_to_left_eef_pos",
482
+ "base_to_left_eef_quat",
483
+ "base_to_left_eef_quat_site",
484
+ "base_to_right_eef_pos",
485
+ "base_to_right_eef_quat",
486
+ "base_to_right_eef_quat_site",
487
+ ]
488
+ for name in disabled_sensors:
489
+ for robot in self.robots:
490
+ robot_name_prefix = robot.robot_model.naming_prefix
491
+ if f"{robot_name_prefix}{name}" in self._observables:
492
+ self._observables[f"{robot_name_prefix}{name}"].set_enabled(False)
493
+ self._observables[f"{robot_name_prefix}{name}"].set_active(False)
494
+
495
+ def get_state(self):
496
+ return {"states": self.sim.get_state().flatten()}
497
+
498
+
499
+ class GroundOnly(LocoManipulationEnv):
500
+ def __init__(self, *args, **kwargs):
501
+ super().__init__(*args, **kwargs)
502
+
503
+
504
+ class PrimitiveBottle:
505
+ DEFAULT_RGB = [0.3, 0.7, 0.8]
506
+
507
+ def __init__(
508
+ self,
509
+ name="bottle",
510
+ radius: float = 0.03,
511
+ half_height: float = 0.075,
512
+ rgb: Optional[list[float]] = None,
513
+ ):
514
+ self.name = name
515
+ self.assets = [
516
+ ET.Element(
517
+ "texture",
518
+ type="2d",
519
+ name=f"{name}_tex",
520
+ builtin="flat",
521
+ rgb1=" ".join(map(str, self.DEFAULT_RGB if rgb is None else rgb)),
522
+ width="512",
523
+ height="512",
524
+ ),
525
+ ET.Element(
526
+ "material",
527
+ name=f"{name}_mat",
528
+ texture=f"{name}_tex",
529
+ texuniform="true",
530
+ reflectance="0.1",
531
+ ),
532
+ ]
533
+
534
+ self.body = ET.Element("body", name=f"{self.name}_body", pos="0.35 0 0.8")
535
+ bottle_vis_geom = ET.Element(
536
+ "geom",
537
+ name=f"{name}_vis",
538
+ pos="0 0 0",
539
+ size=f"{radius} {half_height}",
540
+ type="cylinder",
541
+ material=f"{name}_mat",
542
+ group="1",
543
+ conaffinity="0",
544
+ contype="0",
545
+ )
546
+ self.body.append(bottle_vis_geom)
547
+
548
+ # Cylinder collider approximation for stable contacts
549
+ self.contact_geoms = []
550
+ n_sides = 3
551
+ half_width = radius * np.tan(np.pi / n_sides / 2)
552
+ for i in range(n_sides):
553
+ coll_name = f"{self.name}_collider_{i}"
554
+ angle = np.pi / n_sides * i
555
+ quat = np.zeros(4)
556
+ euler = np.array([0, 0, angle])
557
+ mujoco.mju_euler2Quat(quat, euler, "xyz")
558
+ box_geom = ET.Element(
559
+ "geom",
560
+ name=coll_name,
561
+ type="box",
562
+ pos="0 0 0",
563
+ size=f"{radius} {half_width} {half_height}",
564
+ quat=" ".join(map(str, quat)),
565
+ solimp="0.998 0.998 0.001",
566
+ solref="0.001 2",
567
+ density="100",
568
+ friction="0.95 0.3 0.1",
569
+ )
570
+ self.body.append(box_geom)
571
+ self.contact_geoms.append(coll_name)
572
+
573
+ bottle_joint = ET.Element(
574
+ "joint",
575
+ name=f"{self.name}_joint",
576
+ type="free",
577
+ damping="0.0005",
578
+ )
579
+ self.body.append(bottle_joint)
580
+
581
+
582
+ class PrimitiveFixture:
583
+ DEFAULT_RGB = [0.8, 0.8, 0.8]
584
+
585
+ def __init__(
586
+ self,
587
+ name: str,
588
+ pos: np.ndarray = np.array([0.0, 0.0, 0.8]),
589
+ half_size: np.ndarray = np.array([0.1, 0.1, 0.001]),
590
+ rgb: Optional[str] = None,
591
+ ):
592
+ """
593
+ A simple primitive fixture as a flat box.
594
+
595
+ Args:
596
+ half_size: Half-sizes in [x, y, z] directions. Default creates a 20cm x 20cm x 2mm box.
597
+ """
598
+ self.half_size = half_size
599
+
600
+ self.assets = [
601
+ ET.Element(
602
+ "texture",
603
+ type="2d",
604
+ name=f"{name}",
605
+ builtin="flat",
606
+ rgb1=" ".join(map(str, self.DEFAULT_RGB if rgb is None else rgb)),
607
+ width="512",
608
+ height="512",
609
+ ),
610
+ ET.Element(
611
+ "material",
612
+ name=f"{name}",
613
+ texture=f"{name}",
614
+ texuniform="true",
615
+ reflectance="0.05", # Less reflective than bottle
616
+ ),
617
+ ]
618
+
619
+ self.body = ET.Element("body", name=f"{name}_body", pos=array_to_string(pos))
620
+
621
+ # Visual geometry
622
+ fixture_vis_geom = ET.Element(
623
+ "geom",
624
+ name=f"{name}_vis",
625
+ pos="0 0 0",
626
+ size=f"{half_size[0]} {half_size[1]} {half_size[2]}",
627
+ type="box",
628
+ material=f"{name}",
629
+ group="1",
630
+ conaffinity="0",
631
+ contype="0",
632
+ )
633
+ self.body.append(fixture_vis_geom)
634
+
635
+ # Collision geometry - just a single box since it's already a simple shape
636
+ self.contact_geoms = []
637
+ fixture_collider = ET.Element(
638
+ "geom",
639
+ name=f"{name}_collider",
640
+ type="box",
641
+ pos="0 0 0",
642
+ size=f"{half_size[0]} {half_size[1]} {half_size[2]}",
643
+ solimp="0.998 0.998 0.001",
644
+ solref="0.001 2",
645
+ density="100",
646
+ friction="0.6 0.01 0.001", # Similar to add_fixture_body friction
647
+ )
648
+ self.body.append(fixture_collider)
649
+ self.contact_geoms.append("fixture_collider")
650
+
651
+
652
+ class PnPBottle(LocoManipulationEnv, DexMGConfigHelper):
653
+ TABLE_GRADIENT: Gradient = Gradient(
654
+ np.array([0.68, 0.34, 0.07, 1.0]), np.array([1.0, 1.0, 1.0, 1.0])
655
+ )
656
+ DEFAULT_BOTTLE_POS: np.ndarray = np.array([0.4, 0, 0.77])
657
+ BOTTLE_POS_RANGE_X = (-0.08, 0.04)
658
+ BOTTLE_POS_RANGE_Y = (-0.08, 0.08)
659
+
660
+ def __init__(self, *args, **kwargs):
661
+ self.objects = {}
662
+ super().__init__(*args, **kwargs)
663
+
664
+ def _load_model(self):
665
+ self.mujoco_objects = [self._create_table("table_body", [0.5, 0, 0], [0, 0, np.pi / 2])]
666
+
667
+ super()._load_model()
668
+
669
+ self.bottle = self._create_bottle()
670
+
671
+ @staticmethod
672
+ def _create_table(name: str, position: list[float], euler: list[float]) -> MJCFObject:
673
+ table = MJCFObject(
674
+ name=name,
675
+ mjcf_path=xml_path_completion(
676
+ "objects/omniverse/locomanip/lab_table/model.xml", root=robocasa.models.assets_root
677
+ ),
678
+ scale=1.0,
679
+ solimp=(0.998, 0.998, 0.001),
680
+ solref=(0.001, 1),
681
+ density=10,
682
+ friction=(1, 1, 1),
683
+ static=True,
684
+ )
685
+ table.set_pos(position)
686
+ table.set_euler(euler)
687
+ return table
688
+
689
+ def _create_bottle(
690
+ self, name: str = "bottle", rgb: Optional[list[float]] = None
691
+ ) -> PrimitiveBottle:
692
+ bottle = PrimitiveBottle(name=name, radius=0.03, half_height=0.075, rgb=rgb)
693
+ self.model.asset.extend(bottle.assets)
694
+ self.model.worldbody.append(bottle.body)
695
+ self.objects[name] = {"name": f"{name}_body"}
696
+ return bottle
697
+
698
+ def _reset_internal(self):
699
+ """
700
+ Resets simulation internal configurations.
701
+ """
702
+ super()._reset_internal()
703
+
704
+ if not self.deterministic_reset:
705
+ self._randomize_bottle_placement()
706
+ self._randomize_table_texture()
707
+
708
+ def _randomize_bottle_placement(
709
+ self, name: str = "bottle", base_pos: Optional[np.ndarray] = None
710
+ ):
711
+ if not self.deterministic_reset:
712
+ bottle_joint = f"{name}_joint"
713
+ base_pos = self.DEFAULT_BOTTLE_POS if base_pos is None else base_pos
714
+
715
+ random_x = self.rng.uniform(*self.BOTTLE_POS_RANGE_X)
716
+ random_y = self.rng.uniform(*self.BOTTLE_POS_RANGE_Y)
717
+ new_pos = base_pos + np.array([random_x, random_y, 0])
718
+
719
+ current_qpos = self.sim.data.get_joint_qpos(bottle_joint)
720
+ new_qpos = current_qpos.copy()
721
+ new_qpos[:3] = new_pos
722
+
723
+ self.sim.data.set_joint_qpos(bottle_joint, new_qpos)
724
+
725
+ def _randomize_table_texture(self):
726
+ table = self.mujoco_objects[0]
727
+ randomize_materials_rgba(
728
+ rng=self.rng, mjcf_obj=table, gradient=self.TABLE_GRADIENT, linear=True
729
+ )
730
+
731
+ def _setup_references(self):
732
+ super()._setup_references()
733
+
734
+ self.obj_body_id = {}
735
+ for name, model in self.objects.items():
736
+ self.obj_body_id[name] = self.sim.model.body_name2id(model["name"])
737
+
738
+ def _check_success(self):
739
+ check_grasp = self._check_grasp(self.robots[0].gripper["right"], self.bottle.contact_geoms)
740
+
741
+ bottle_z = self.sim.data.body_xpos[self.obj_body_id["bottle"]][2]
742
+ table_z = self.mujoco_objects[0].top_offset[2]
743
+ check_bottle_in_air = bottle_z > table_z + 0.2
744
+ # check bottle and table collision
745
+ # check_bottle_in_air = not self.check_contact("bottle", "table")
746
+ return check_grasp and check_bottle_in_air
747
+
748
+ def get_object(self):
749
+ return dict(
750
+ bottle=dict(obj_name=self.objects["bottle"]["name"], obj_type="body"),
751
+ )
752
+
753
+ def get_subtask_term_signals(self):
754
+ signals = dict()
755
+ signals["grasp_bottle"] = int(
756
+ self._check_grasp(self.robots[0].gripper["right"], self.bottle.contact_geoms)
757
+ )
758
+ return signals
759
+
760
+ @staticmethod
761
+ def task_config():
762
+ task = DexMGConfigHelper.AttrDict()
763
+ task.task_spec_0.subtask_1 = dict(
764
+ object_ref="bottle",
765
+ subtask_term_signal=None,
766
+ subtask_term_offset_range=None,
767
+ selection_strategy="random",
768
+ selection_strategy_kwargs=None,
769
+ action_noise=0.05,
770
+ num_interpolation_steps=5,
771
+ num_fixed_steps=0,
772
+ apply_noise_during_interpolation=False,
773
+ )
774
+ task.task_spec_1.subtask_1 = dict(
775
+ object_ref=None,
776
+ subtask_term_signal=None,
777
+ subtask_term_offset_range=None,
778
+ selection_strategy="random",
779
+ selection_strategy_kwargs=None,
780
+ action_noise=0.05,
781
+ num_interpolation_steps=5,
782
+ num_fixed_steps=0,
783
+ apply_noise_during_interpolation=False,
784
+ )
785
+ return task.to_dict()
786
+
787
+
788
+ def create_shelf(pos: list[float], euler: list[float]) -> MJCFObject:
789
+ shelf = MJCFObject(
790
+ name="shelf_body",
791
+ mjcf_path=xml_path_completion(
792
+ "objects/aigc/shelf/model.xml", root=robocasa.models.assets_root
793
+ ),
794
+ scale=[1.0, 1.0, 1.0],
795
+ solimp=(0.998, 0.998, 0.001),
796
+ solref=(0.001, 1),
797
+ density=10,
798
+ friction=(1, 1, 1),
799
+ static=True,
800
+ )
801
+ shelf.set_pos(pos)
802
+ shelf.set_euler(euler)
803
+ return shelf
804
+
805
+
806
+ class PickBottleShelf(PnPBottle):
807
+ def _load_model(self):
808
+ # Create both the original table and the target table
809
+ self.mujoco_objects = [create_shelf(pos=[0.8, 0.4, 0], euler=[0, 0, np.pi / 2])]
810
+
811
+ LocoManipulationEnv._load_model(self)
812
+
813
+ self.bottle = self._create_bottle()
814
+
815
+ def _reset_internal(self):
816
+ """
817
+ Resets simulation internal configurations.
818
+ """
819
+ LocoManipulationEnv._reset_internal(self)
820
+
821
+ if not self.deterministic_reset:
822
+ # Base position on ground (z=0.075 is bottle radius)
823
+ # Level 2 of shelf
824
+ self._randomize_bottle_placement(base_pos=np.array([0.7, 0.4, 0.376660 + 0.075 + 0.02]))
825
+ self._randomize_table_texture()
826
+ RobotPoseRandomizer.set_arm(self, elbow_qpos=-0.5, shoulder_pitch_qpos=0.5)
827
+
828
+
829
+ class PnPBottleHigh(PnPBottle):
830
+ def _load_model(self):
831
+ self.mujoco_objects = [self._create_table("table_body", [0.5, 0, 0.1], [0, 0, np.pi / 2])]
832
+
833
+ LocoManipulationEnv._load_model(self)
834
+
835
+ self.bottle = self._create_bottle()
836
+
837
+ def _reset_internal(self):
838
+ """
839
+ Resets simulation internal configurations.
840
+ """
841
+ LocoManipulationEnv._reset_internal(self)
842
+
843
+ # Randomize bottle position within +/- 0.1 range on x and y axes
844
+ if not self.deterministic_reset:
845
+ # Base position of the bottle
846
+ base_pos = np.array([0.4, 0, 0.875])
847
+
848
+ # Add random offset within +/- 0.1 range for x and y
849
+ random_x = np.random.uniform(-0.1, 0.1)
850
+ random_y = np.random.uniform(-0.1, 0.1)
851
+ # New randomized position (keep z constant)
852
+ new_pos = base_pos + np.array([random_x, random_y, 0])
853
+
854
+ # Set the bottle position using the free joint
855
+ # For free joints, qpos includes [x, y, z, qw, qx, qy, qz]
856
+ current_qpos = self.sim.data.get_joint_qpos("bottle_joint")
857
+ new_qpos = current_qpos.copy()
858
+ new_qpos[:3] = new_pos # Update position (x, y, z)
859
+
860
+ self.sim.data.set_joint_qpos("bottle_joint", new_qpos)
861
+
862
+ def _setup_observables(self):
863
+ observables = super()._setup_observables()
864
+
865
+ @sensor(modality="object")
866
+ def obj_pos(obs_cache):
867
+ return self.sim.data.body_xpos[self.obj_body_id["bottle"]]
868
+
869
+ @sensor(modality="object")
870
+ def obj_quat(obs_cache):
871
+ return self.sim.data.body_xquat[self.obj_body_id["bottle"]]
872
+
873
+ @sensor(modality="object")
874
+ def obj_linear_vel(obs_cache):
875
+ return self.sim.data.get_body_xvelp("bottle_body")
876
+
877
+ @sensor(modality="object")
878
+ def obj_angular_vel(obs_cache):
879
+ return self.sim.data.get_body_xvelr("bottle_body")
880
+
881
+ sensors = [obj_pos, obj_quat, obj_linear_vel, obj_angular_vel]
882
+ names = [s.__name__ for s in sensors]
883
+
884
+ for name, s in zip(names, sensors):
885
+ observables[name] = Observable(
886
+ name=name,
887
+ sensor=s,
888
+ sampling_rate=self.control_freq,
889
+ )
890
+
891
+ return observables
892
+
893
+ def get_privileged_obs_keys(self):
894
+ return {
895
+ "obj_pos": (3,),
896
+ "obj_quat": (4,),
897
+ "obj_linear_vel": (3,),
898
+ "obj_angular_vel": (3,),
899
+ }
900
+
901
+
902
+ class NavPickBottle(PnPBottle):
903
+ """
904
+ Pick-and-Place Bottle environment with robot position randomized at reset.
905
+ """
906
+
907
+ def _reset_internal(self):
908
+ super()._reset_internal()
909
+
910
+ if not self.deterministic_reset:
911
+ RobotPoseRandomizer.set_pose(self, (-0.3, -0.16), (-0.2, 0.2), (-np.pi / 6, np.pi / 6))
912
+
913
+
914
+ class PnPBottleRandRobotPose(NavPickBottle):
915
+ pass
916
+
917
+
918
+ class VisualReach(LocoManipulationEnv):
919
+ def __init__(self, *args, **kwargs):
920
+ super().__init__(*args, **kwargs)
921
+
922
+ def _load_model(self):
923
+ super()._load_model()
924
+
925
+ self.create_visual_only_goal_cube()
926
+
927
+ def create_visual_only_goal_cube(self):
928
+ cube_tex = ET.Element(
929
+ "texture",
930
+ type="2d",
931
+ name="cube",
932
+ builtin="flat",
933
+ rgb1="1.0 0.0 0.0",
934
+ width="512",
935
+ height="512",
936
+ )
937
+ cube_mat = ET.Element(
938
+ "material",
939
+ name="cube",
940
+ texture="cube",
941
+ texuniform="true",
942
+ reflectance="0.1",
943
+ )
944
+ self.model.asset.append(cube_tex)
945
+ self.model.asset.append(cube_mat)
946
+
947
+ self.objects = {}
948
+ cube_body = ET.Element("body", name="cube_body", pos="0.4 0 0.875")
949
+
950
+ cube_vis_geom = ET.Element(
951
+ "geom",
952
+ name="cube_vis",
953
+ pos="0 0 0",
954
+ size="0.0375 0.0375 0.0375",
955
+ type="box",
956
+ material="cube",
957
+ group="1",
958
+ conaffinity="0",
959
+ contype="0",
960
+ )
961
+
962
+ cube_body.append(cube_vis_geom)
963
+ self.model.worldbody.append(cube_body)
964
+ self.objects["cube"] = {"name": "cube_body"}
965
+
966
+ def _setup_references(self):
967
+ super()._setup_references()
968
+
969
+ self.obj_body_id = {}
970
+ for name, model in self.objects.items():
971
+ self.obj_body_id[name] = self.sim.model.body_name2id(model["name"])
972
+
973
+ def _check_success(self):
974
+ # check_grasp = self._check_grasp(self.robots[0].gripper["right"], self.objects["bottle"])
975
+ # check_reach = self._check_reach(self.objects["bottle"])
976
+ return True
977
+
978
+ def _check_reach(self, obj_name):
979
+ raise NotImplementedError
980
+ # To be implemented by the subclass
981
+
982
+ def get_object(self):
983
+ return dict(
984
+ cube=dict(obj_name=self.objects["cube"].root_body, obj_type="body"),
985
+ )
986
+
987
+ def reset_obj_pos(self):
988
+ # reset object pos randomly around bottle_body pos="0.4 0 0.875"
989
+ init_pos = np.array([0.4, 0, 0.875])
990
+ random_x = np.random.uniform(-0.3, 0.15)
991
+ random_y = np.random.uniform(-0.15, 0.15)
992
+ random_z = np.random.uniform(-0.15, 0.30)
993
+ self.sim.model.body_pos[self.obj_body_id["cube"]] = init_pos + np.array(
994
+ [random_x, random_y, random_z]
995
+ )
996
+
997
+ def set_cameras(self):
998
+ super().set_cameras()
999
+ self.mujoco_arena.set_camera(
1000
+ camera_name="egoview",
1001
+ pos=[0.078, 0, 1.308],
1002
+ quat=[0.66491268, 0.24112495, -0.24112507, -0.66453637],
1003
+ camera_attribs=dict(fovy="90"),
1004
+ )
1005
+
1006
+ def _setup_observables(self):
1007
+ observables = super()._setup_observables()
1008
+
1009
+ @sensor(modality="object")
1010
+ def obj_pos(obs_cache):
1011
+ return self.sim.data.body_xpos[self.obj_body_id["cube"]]
1012
+
1013
+ @sensor(modality="object")
1014
+ def obj_quat(obs_cache):
1015
+ return self.sim.data.body_xquat[self.obj_body_id["cube"]]
1016
+
1017
+ @sensor(modality="object")
1018
+ def obj_linear_vel(obs_cache):
1019
+ return self.sim.data.get_body_xvelp("cube_body")
1020
+
1021
+ @sensor(modality="object")
1022
+ def obj_angular_vel(obs_cache):
1023
+ return self.sim.data.get_body_xvelr("cube_body")
1024
+
1025
+ sensors = [obj_pos, obj_quat, obj_linear_vel, obj_angular_vel]
1026
+ names = [s.__name__ for s in sensors]
1027
+
1028
+ for name, s in zip(names, sensors):
1029
+ observables[name] = Observable(
1030
+ name=name,
1031
+ sensor=s,
1032
+ sampling_rate=self.control_freq,
1033
+ )
1034
+
1035
+ return observables
1036
+
1037
+ def get_privileged_obs_keys(self):
1038
+ return {
1039
+ "obj_pos": (3,),
1040
+ "obj_quat": (4,),
1041
+ "obj_linear_vel": (3,),
1042
+ "obj_angular_vel": (3,),
1043
+ }
1044
+
1045
+
1046
+ class PnPBottleFixtureToFixture(PnPBottle):
1047
+ """
1048
+ Task: Robot picks up bottle and places it on a fixture.
1049
+
1050
+ Initialization: bottle rests on a source fixture.
1051
+
1052
+ Idea: by changing the location of the target fixture, we can change the data generation task layout for
1053
+ these placement related tasks.
1054
+ """
1055
+
1056
+ SEPARATION_THRESH_M: float = 0.0005 # 0.05 cm
1057
+ DISTMAX_SCAN_M: float = 0.05 # 5 cm window for distance queries
1058
+ _SRC_NAME = "start_fixture"
1059
+ _TGT_NAME = "target_fixture"
1060
+ _FIXTURE_HALF_SIZE = np.array([0.05, 0.05, 0.001])
1061
+ _BOTTLE_HALF_HEIGHT = 0.075
1062
+ _X_SRC_RANGE = (0.30, 0.55)
1063
+ _X_TGT_RANGE = (0.30, 0.55)
1064
+ _Y_SRC_RANGE = (-0.20, -0.05)
1065
+ _Y_TGT_RANGE = (0.05, 0.20)
1066
+ _SRC_FIXTURE_VISIBLE = False
1067
+ _TGT_FIXTURE_VISIBLE = True
1068
+
1069
+ def _load_model(self):
1070
+ self.mujoco_objects = [self._create_table("table_body", [0.5, 0, 0], [0, 0, np.pi / 2])]
1071
+ LocoManipulationEnv._load_model(self)
1072
+ self.bottle = self._create_bottle()
1073
+ self._create_fixture(self._SRC_NAME, visible=self._SRC_FIXTURE_VISIBLE, rgb="1 0 0")
1074
+ self._create_fixture(self._TGT_NAME, visible=self._TGT_FIXTURE_VISIBLE, rgb="0 1 0")
1075
+ self._src_body = f"{self._SRC_NAME}_body"
1076
+ self._tgt_body = f"{self._TGT_NAME}_body"
1077
+ self._src_coll = f"{self._SRC_NAME}_collider"
1078
+ self._tgt_coll = f"{self._TGT_NAME}_collider"
1079
+
1080
+ def _create_fixture(self, name: str, visible: bool, rgb: Optional[str] = None) -> None:
1081
+ """Create a flat box fixture; add assets + body to the compiled model."""
1082
+ fx = PrimitiveFixture(
1083
+ name=name, pos=np.array([0.0, 0.0, 0.0]), half_size=self._FIXTURE_HALF_SIZE, rgb=rgb
1084
+ )
1085
+
1086
+ # Make source fixture invisible (keep collision only)
1087
+ if not visible:
1088
+ # Find the visual geom and hide it
1089
+ for child in list(fx.body):
1090
+ if child.tag == "geom" and child.get("name") == f"{name}_vis":
1091
+ child.set("rgba", "0 0 0 0") # invisible visual
1092
+ break
1093
+
1094
+ # Register assets + body into the scene graph
1095
+ self.model.asset.extend(fx.assets)
1096
+ self.model.worldbody.append(fx.body)
1097
+
1098
+ def _setup_references(self):
1099
+ super()._setup_references()
1100
+ # Table root body is "<name>_main" (same convention as target_table above)
1101
+ self.table_body_id = self.sim.model.body_name2id("table_body_main")
1102
+ self.src_fixture_id = self.sim.model.body_name2id(self._src_body)
1103
+ self.tgt_fixture_id = self.sim.model.body_name2id(self._tgt_body)
1104
+
1105
+ def _check_success(self) -> bool:
1106
+ """Bottle touches target fixture collider and is upright."""
1107
+ bottle_on_target = self.check_contact(self.bottle.contact_geoms, [self._tgt_coll])
1108
+ bottle_upright = check_obj_upright(self, "bottle", threshold=0.8, symmetric=True)
1109
+ return bottle_on_target and bottle_upright
1110
+
1111
+ # --- runtime table height ---
1112
+ def _table_top_z(self) -> float:
1113
+ base_z = float(self.sim.data.body_xpos[self.table_body_id][2])
1114
+ top_offset_z = float(self.mujoco_objects[0].top_offset[2])
1115
+ return base_z + top_offset_z
1116
+
1117
+ def _reset_internal(self):
1118
+ LocoManipulationEnv._reset_internal(self)
1119
+
1120
+ if not self.deterministic_reset:
1121
+ # Sample fixture XY, compute Z from current table pose
1122
+ x_src = self.rng.uniform(*self._X_SRC_RANGE)
1123
+ y_src = self.rng.uniform(*self._Y_SRC_RANGE)
1124
+
1125
+ x_tgt = self.rng.uniform(*self._X_TGT_RANGE)
1126
+ y_tgt = self.rng.uniform(*self._Y_TGT_RANGE)
1127
+ # y_tgt = self.rng.uniform(*self._Y_TGT_RANGE)
1128
+
1129
+ z_top = self._table_top_z() # dynamic table top
1130
+ src_pos = np.array([x_src, y_src, z_top])
1131
+ tgt_pos = np.array([x_tgt, y_tgt, z_top])
1132
+
1133
+ # Reset fixture body poses (static bodies): write to model; MuJoCo will use it after forward()
1134
+ self.sim.model.body_pos[self.src_fixture_id] = src_pos
1135
+ self.sim.model.body_pos[self.tgt_fixture_id] = tgt_pos
1136
+
1137
+ # Place bottle on source fixture: top of fixture + bottle half-height + tiny clearance
1138
+ bottle_z = z_top + self._FIXTURE_HALF_SIZE[2] + self._BOTTLE_HALF_HEIGHT + 0.002
1139
+ qpos = self.sim.data.get_joint_qpos("bottle_joint").copy()
1140
+ qpos[:3] = np.array([x_src, y_src, bottle_z])
1141
+ qpos[3:7] = np.array([1.0, 0.0, 0.0, 0.0]) # upright
1142
+ self.sim.data.set_joint_qpos("bottle_joint", qpos)
1143
+
1144
+ self._randomize_table_texture()
1145
+ RobotPoseRandomizer.set_pose(self, (-0.3, -0.16), (-0.2, 0.2), (-np.pi / 6, np.pi / 6))
1146
+
1147
+ # --- distance via MuJoCo ---
1148
+ def _min_signed_distance_mj(self, geoms_a: list[str], geoms_b: list[str]) -> float:
1149
+ model, data = self.sim.model, self.sim.data
1150
+ dmin = np.inf
1151
+ fromto = np.empty(6, dtype=np.float64)
1152
+ a_ids = [model.geom_name2id(n) for n in geoms_a]
1153
+ b_ids = [model.geom_name2id(n) for n in geoms_b]
1154
+ for ga in a_ids:
1155
+ for gb in b_ids:
1156
+ dist = mujoco.mj_geomDistance(
1157
+ model._model, data._data, ga, gb, self.DISTMAX_SCAN_M + 0.01, fromto
1158
+ )
1159
+ dmin = min(dmin, float(dist))
1160
+ return dmin
1161
+
1162
+ def get_subtask_term_signals(self) -> dict[str, int]:
1163
+ """
1164
+ 1 iff (no contact between bottle and source fixture) AND
1165
+ (min signed distance > DISTMAX_SCAN_M).
1166
+ """
1167
+ in_contact = self.check_contact(self.bottle.contact_geoms, [self._src_coll])
1168
+ min_dist = self._min_signed_distance_mj(self.bottle.contact_geoms, [self._src_coll])
1169
+ return {
1170
+ "obj_off_source_fixture": int((not in_contact) and (min_dist > self.DISTMAX_SCAN_M))
1171
+ }
1172
+
1173
+ def get_object(self) -> dict:
1174
+ return dict(
1175
+ bottle=dict(obj_name=self.objects["bottle"]["name"], obj_type="body"),
1176
+ source_fixture=dict(obj_name=self._src_body, obj_type="body"),
1177
+ target_fixture=dict(obj_name=self._tgt_body, obj_type="body"),
1178
+ )
1179
+
1180
+ @staticmethod
1181
+ def task_config() -> dict:
1182
+ task = DexMGConfigHelper.AttrDict()
1183
+ # Subtask 1: pick (leave source fixture)
1184
+ task.task_spec_0.subtask_1 = dict(
1185
+ object_ref="bottle",
1186
+ subtask_term_signal="obj_off_source_fixture",
1187
+ subtask_term_offset_range=(5, 10),
1188
+ selection_strategy="random",
1189
+ selection_strategy_kwargs=None,
1190
+ action_noise=0.05,
1191
+ num_interpolation_steps=5,
1192
+ num_fixed_steps=0,
1193
+ apply_noise_during_interpolation=False,
1194
+ )
1195
+ # Subtask 2: place on target fixture
1196
+ task.task_spec_0.subtask_2 = dict(
1197
+ object_ref="target_fixture",
1198
+ subtask_term_signal=None,
1199
+ subtask_term_offset_range=None,
1200
+ selection_strategy="random",
1201
+ selection_strategy_kwargs=None,
1202
+ action_noise=0.05,
1203
+ num_interpolation_steps=5,
1204
+ num_fixed_steps=0,
1205
+ apply_noise_during_interpolation=False,
1206
+ )
1207
+ # Default filler for task_spec_1, mirroring other tasks
1208
+ task.task_spec_1.subtask_1 = dict(
1209
+ object_ref=None,
1210
+ subtask_term_signal=None,
1211
+ subtask_term_offset_range=None,
1212
+ selection_strategy="random",
1213
+ selection_strategy_kwargs=None,
1214
+ action_noise=0.05,
1215
+ num_interpolation_steps=5,
1216
+ num_fixed_steps=0,
1217
+ apply_noise_during_interpolation=False,
1218
+ )
1219
+ return task.to_dict()
1220
+
1221
+
1222
+ class PnPBottleFixtureToFixtureSourceDemo(PnPBottleFixtureToFixture):
1223
+ """
1224
+ Environment for collecting source demo for PnPBottleFixtureToFixture tasks.
1225
+ """
1226
+
1227
+ _X_SRC_RANGE = (0.375, 0.375)
1228
+ _X_TGT_RANGE = (0.375, 0.375)
1229
+ _Y_SRC_RANGE = (-0.15, -0.15)
1230
+ _Y_TGT_RANGE = (0.1, 0.1)
1231
+ _SRC_FIXTURE_VISIBLE = False
1232
+ _TGT_FIXTURE_VISIBLE = True
1233
+
1234
+
1235
+ class PnPBottleShelfToTable(PnPBottleFixtureToFixture):
1236
+ """
1237
+ Task: Robot picks up bottle from a fixture on a shelf and places it on a fixture on a table.
1238
+
1239
+ Initialization: bottle rests on a source fixture on the shelf.
1240
+ Target: place bottle on target fixture on the table.
1241
+ """
1242
+
1243
+ # Adjust ranges for shelf-to-table layout
1244
+ _X_SRC_RANGE = (-0.05, 0.05) # Shelf position range
1245
+ _X_TGT_RANGE = (-0.05 - 0.2, 0.05 - 0.2) # Table position range
1246
+ # TODO: could be better to have some 'center' specified here
1247
+ _Y_SRC_RANGE = (-0.05, 0.05) # Shelf position range
1248
+ _Y_TGT_RANGE = (-0.05, 0.05) # Table position range
1249
+ _SRC_FIXTURE_VISIBLE = True
1250
+ _TGT_FIXTURE_VISIBLE = True
1251
+ _FIXTURE_HALF_SIZE = np.array([0.05, 0.05, 0.001])
1252
+
1253
+ # Shelf height constants (from PnPBottleShelf)
1254
+ # _SHELF_HEIGHT = 0.386660 # Level 2 of shelf from the original shelf environment
1255
+ _SHELF_HEIGHT = 0.753321 + 0.015 # Level 3 of shelf from the original shelf environment
1256
+
1257
+ def _load_model(self):
1258
+ # Create both shelf and table
1259
+ self.mujoco_objects = [
1260
+ self._create_table("table_body", [0.5, 0.6, 0], [0, 0, np.pi / 2]),
1261
+ create_shelf(pos=[0.8, -0.4, 0], euler=[0, 0, np.pi / 2]),
1262
+ ]
1263
+
1264
+ LocoManipulationEnv._load_model(self)
1265
+
1266
+ self.bottle = self._create_bottle()
1267
+ self._create_fixture(self._SRC_NAME, visible=self._SRC_FIXTURE_VISIBLE, rgb="1 0 0")
1268
+ self._create_fixture(self._TGT_NAME, visible=self._TGT_FIXTURE_VISIBLE, rgb="0 1 0")
1269
+ self._src_body = f"{self._SRC_NAME}_body"
1270
+ self._tgt_body = f"{self._TGT_NAME}_body"
1271
+ self._src_coll = f"{self._SRC_NAME}_collider"
1272
+ self._tgt_coll = f"{self._TGT_NAME}_collider"
1273
+
1274
+ def _setup_references(self):
1275
+ super()._setup_references()
1276
+ # Add reference to shelf
1277
+ self.shelf_body_id = self.sim.model.body_name2id("shelf_body_main")
1278
+
1279
+ def _shelf_top_z(self) -> float:
1280
+ """Get the Z coordinate of the shelf top surface"""
1281
+ # Use the same shelf height as in PnPBottleShelf
1282
+ return self._SHELF_HEIGHT
1283
+
1284
+ def _shelf_xy(self) -> tuple[float, float]:
1285
+ """Get the XY coordinates of the shelf"""
1286
+ return self.sim.data.body_xpos[self.shelf_body_id][:2]
1287
+
1288
+ def _table_xy(self) -> tuple[float, float]:
1289
+ """Get the XY coordinates of the table"""
1290
+ return self.sim.data.body_xpos[self.table_body_id][:2]
1291
+
1292
+ def _reset_internal(self):
1293
+ LocoManipulationEnv._reset_internal(self)
1294
+
1295
+ if not self.deterministic_reset:
1296
+ # Sample fixture XY positions
1297
+ x_src = self.rng.uniform(*self._X_SRC_RANGE)
1298
+ y_src = self.rng.uniform(*self._Y_SRC_RANGE)
1299
+
1300
+ x_tgt = self.rng.uniform(*self._X_TGT_RANGE)
1301
+ y_tgt = self.rng.uniform(*self._Y_TGT_RANGE)
1302
+
1303
+ # Source fixture on shelf
1304
+ z_shelf = self._shelf_top_z()
1305
+ x_shelf, y_shelf = self._shelf_xy()
1306
+ src_pos = np.array([x_src, y_src, z_shelf])
1307
+ src_pos += np.array([x_shelf, y_shelf, 0])
1308
+
1309
+ # table pos
1310
+ # Target fixture on table
1311
+ z_table = self._table_top_z()
1312
+ x_table, y_table = self._table_xy()
1313
+ tgt_pos = np.array([x_tgt, y_tgt, z_table])
1314
+ tgt_pos += np.array([x_table, y_table, 0])
1315
+
1316
+ # Reset fixture body poses
1317
+ self.sim.model.body_pos[self.src_fixture_id] = src_pos
1318
+ self.sim.model.body_pos[self.tgt_fixture_id] = tgt_pos
1319
+
1320
+ # Place bottle on source fixture (shelf): top of fixture + bottle half-height + clearance
1321
+ bottle_z = z_shelf + self._FIXTURE_HALF_SIZE[2] + self._BOTTLE_HALF_HEIGHT + 0.002
1322
+ qpos = self.sim.data.get_joint_qpos("bottle_joint").copy()
1323
+ qpos[:3] = np.array([src_pos[0], src_pos[1], bottle_z])
1324
+ qpos[3:7] = np.array([1.0, 0.0, 0.0, 0.0]) # upright
1325
+ self.sim.data.set_joint_qpos("bottle_joint", qpos)
1326
+
1327
+ self._randomize_table_texture()
1328
+ RobotPoseRandomizer.set_pose(self, (-0.3, -0.16), (-0.2, 0.2), (-np.pi / 6, np.pi / 6))
1329
+
1330
+ def _randomize_table_texture(self):
1331
+ """Randomize textures for the table (shelf texture is static)"""
1332
+ # Only randomize the table texture (index 1), not the shelf
1333
+ table = self.mujoco_objects[1]
1334
+ randomize_materials_rgba(
1335
+ rng=self.rng, mjcf_obj=table, gradient=self.TABLE_GRADIENT, linear=True
1336
+ )
1337
+
1338
+
1339
+ class PnPBottleTableToTable(PnPBottle):
1340
+ def _load_model(self):
1341
+ # Create both the original table and the target table
1342
+ self.mujoco_objects = [
1343
+ self._create_table("table_body", [0.5, 0, 0], [0, 0, np.pi / 2]),
1344
+ self._create_table("target_table_body", [0.5, 1.2, 0], [0, 0, np.pi / 2]),
1345
+ ]
1346
+
1347
+ LocoManipulationEnv._load_model(self)
1348
+
1349
+ self.bottle = self._create_bottle()
1350
+
1351
+ def _setup_references(self):
1352
+ super()._setup_references()
1353
+
1354
+ # Add reference to target table - note the _main suffix
1355
+ self.target_table_body_id = self.sim.model.body_name2id("target_table_body_main")
1356
+
1357
+ def _check_success(self):
1358
+ """Check if bottle is successfully placed on the target table"""
1359
+ bottle_on_table = self.check_contact(self.bottle.contact_geoms, self.mujoco_objects[1])
1360
+ bottle_is_upright = check_obj_upright(self, "bottle", threshold=0.8, symmetric=True)
1361
+ return bottle_on_table and bottle_is_upright
1362
+
1363
+ def _randomize_table_texture(self):
1364
+ """Randomize textures for both tables"""
1365
+ # Randomize original table
1366
+ original_table = self.mujoco_objects[0]
1367
+ randomize_materials_rgba(
1368
+ rng=self.rng, mjcf_obj=original_table, gradient=self.TABLE_GRADIENT, linear=True
1369
+ )
1370
+
1371
+ # Randomize target table
1372
+ target_table = self.mujoco_objects[1]
1373
+ randomize_materials_rgba(
1374
+ rng=self.rng, mjcf_obj=target_table, gradient=self.TABLE_GRADIENT, linear=True
1375
+ )
1376
+
1377
+ def get_object(self):
1378
+ return dict(
1379
+ bottle=dict(obj_name=self.objects["bottle"]["name"], obj_type="body"),
1380
+ target_table=dict(obj_name="target_table_body_main", obj_type="body"),
1381
+ )
1382
+
1383
+ @staticmethod
1384
+ def task_config():
1385
+ task = DexMGConfigHelper.AttrDict()
1386
+ task.task_spec_0.subtask_1 = dict(
1387
+ object_ref="bottle",
1388
+ subtask_term_signal="obj_off_table",
1389
+ subtask_term_offset_range=(5, 10),
1390
+ selection_strategy="random",
1391
+ selection_strategy_kwargs=None,
1392
+ action_noise=0.05,
1393
+ num_interpolation_steps=5,
1394
+ num_fixed_steps=0,
1395
+ apply_noise_during_interpolation=False,
1396
+ )
1397
+ # Second subtask for placing on target table
1398
+ task.task_spec_0.subtask_2 = dict(
1399
+ object_ref="target_table",
1400
+ subtask_term_signal=None,
1401
+ subtask_term_offset_range=None,
1402
+ selection_strategy="random",
1403
+ selection_strategy_kwargs=None,
1404
+ action_noise=0.05,
1405
+ num_interpolation_steps=5,
1406
+ num_fixed_steps=0,
1407
+ apply_noise_during_interpolation=False,
1408
+ )
1409
+ task.task_spec_1.subtask_1 = dict(
1410
+ object_ref=None,
1411
+ subtask_term_signal=None,
1412
+ subtask_term_offset_range=None,
1413
+ selection_strategy="random",
1414
+ selection_strategy_kwargs=None,
1415
+ action_noise=0.05,
1416
+ num_interpolation_steps=5,
1417
+ num_fixed_steps=0,
1418
+ apply_noise_during_interpolation=False,
1419
+ )
1420
+ return task.to_dict()
1421
+
1422
+ def get_subtask_term_signals(self):
1423
+ """
1424
+ Retrieve signals used to define subtask termination conditions.
1425
+
1426
+ Returns:
1427
+ dict: Dictionary mapping signal names to their current values
1428
+ """
1429
+ signals = dict()
1430
+
1431
+ obj_z = self.sim.data.body_xpos[self.obj_body_id["bottle"]][2]
1432
+ target_table_pos = self.sim.data.body_xpos[self.target_table_body_id]
1433
+ target_table_z = target_table_pos[2] + self.mujoco_objects[1].top_offset[2]
1434
+
1435
+ th = 0.15
1436
+ signals["obj_off_table"] = int(obj_z - target_table_z > th)
1437
+
1438
+ return signals
1439
+
1440
+
1441
+ class PickBottleGround(PnPBottle):
1442
+ """
1443
+ Pick-and-Place Bottle environment with bottle initialized on the ground.
1444
+ """
1445
+
1446
+ def _reset_internal(self):
1447
+ """
1448
+ Resets simulation internal configurations.
1449
+ """
1450
+ LocoManipulationEnv._reset_internal(self)
1451
+
1452
+ if not self.deterministic_reset:
1453
+ # Base position on ground (z=0.075 is bottle radius)
1454
+ self._randomize_bottle_placement(base_pos=np.ndarray([0.4, 0, 0.075]))
1455
+ self._randomize_table_texture()
1456
+
1457
+ def _randomize_table_texture(self):
1458
+ pass
1459
+
1460
+ def _check_success(self):
1461
+ check_grasp = self._check_grasp(self.robots[0].gripper["right"], "bottle")
1462
+
1463
+ bottle_z = self.sim.data.body_xpos[self.obj_body_id["bottle"]][2]
1464
+ ground_z = 0
1465
+ check_bottle_in_air = bottle_z > ground_z + 0.2
1466
+ # check bottle and table collision
1467
+ # check_bottle_in_air = not self.check_contact("bottle", "table")
1468
+ return check_grasp and check_bottle_in_air
1469
+
1470
+ def _load_model(self):
1471
+ self.mujoco_objects = []
1472
+
1473
+ super(PnPBottle, self)._load_model()
1474
+ self._create_bottle()
1475
+
1476
+
1477
+ class PickBottles(PnPBottle):
1478
+ BOTTLE_POS_RANGE_X = (-0.08, 0.04)
1479
+ BOTTLE_POS_RANGE_Y = (-0.04, 0.04)
1480
+
1481
+ COLOURS: list[list[float]] = [[0.3, 0.7, 0.8], [0.8, 0.4, 0.3]]
1482
+ BOTTLES_COUNT = 2
1483
+ Y_OFFSET_STEP = 0.1
1484
+
1485
+ @staticmethod
1486
+ def _get_bottle_names() -> list[str]:
1487
+ return [f"bottle_{i}" for i in range(PickBottles.BOTTLES_COUNT)]
1488
+
1489
+ def _load_model(self):
1490
+ self.mujoco_objects = [self._create_table("table_body", [0.5, 0, 0], [0, 0, np.pi / 2])]
1491
+
1492
+ LocoManipulationEnv._load_model(self)
1493
+
1494
+ self.bottles = self._create_bottles()
1495
+
1496
+ def _create_bottles(self) -> list[PrimitiveBottle]:
1497
+ bottles = []
1498
+ for i, name in enumerate(self._get_bottle_names()):
1499
+ rgb = self.COLOURS[i % len(self.COLOURS)]
1500
+ bottles.append(self._create_bottle(name=name, rgb=rgb))
1501
+ return bottles
1502
+
1503
+ def _reset_internal(self):
1504
+ LocoManipulationEnv._reset_internal(self)
1505
+
1506
+ n = len(self.bottles)
1507
+ offsets = np.arange(n) - (n - 1) / 2.0
1508
+ for i, bottle in enumerate(self.bottles):
1509
+ self._randomize_bottle_placement(
1510
+ name=bottle.name,
1511
+ base_pos=self.DEFAULT_BOTTLE_POS
1512
+ + np.array([0, self.Y_OFFSET_STEP * offsets[i], 0]),
1513
+ )
1514
+ self._randomize_table_texture()
1515
+
1516
+ def _check_success(self):
1517
+ for bottle in self.bottles:
1518
+ check_grasp = self._check_grasp(
1519
+ self.robots[0].gripper["right"], bottle.contact_geoms
1520
+ ) or self._check_grasp(self.robots[0].gripper["left"], bottle.contact_geoms)
1521
+ bottle_z = self.sim.data.body_xpos[self.obj_body_id[bottle.name]][2]
1522
+ table_z = self.mujoco_objects[0].top_offset[2]
1523
+ check_bottle_in_air = bottle_z > table_z + 0.2
1524
+ if check_grasp and check_bottle_in_air:
1525
+ continue
1526
+ return False
1527
+ return True
1528
+
1529
+ def get_object(self):
1530
+ result = {}
1531
+ for bottle in self.bottles:
1532
+ result[bottle.name] = dict(obj_name=self.objects[bottle.name]["name"], obj_type="body")
1533
+ return result
1534
+
1535
+ def get_subtask_term_signals(self):
1536
+ signals = dict()
1537
+ for bottle in self.bottles:
1538
+ signals[f"grasp_{bottle.name}"] = int(
1539
+ self._check_grasp(self.robots[0].gripper["right"], bottle.contact_geoms)
1540
+ or self._check_grasp(self.robots[0].gripper["left"], bottle.contact_geoms)
1541
+ )
1542
+ return signals
1543
+
1544
+ @staticmethod
1545
+ def task_config():
1546
+ task = DexMGConfigHelper.AttrDict()
1547
+ bottle_names = PickBottles._get_bottle_names()
1548
+ assert len(bottle_names) == 2
1549
+ for i, name in enumerate(bottle_names):
1550
+ subtask = dict(
1551
+ object_ref=name,
1552
+ subtask_term_signal=f"grasp_{name}",
1553
+ subtask_term_offset_range=None,
1554
+ selection_strategy="random",
1555
+ selection_strategy_kwargs=None,
1556
+ action_noise=0.05,
1557
+ num_interpolation_steps=5,
1558
+ num_fixed_steps=0,
1559
+ apply_noise_during_interpolation=False,
1560
+ )
1561
+ spec_attr = f"task_spec_{i}"
1562
+ setattr(getattr(task, spec_attr), "subtask_1", subtask)
1563
+ return task.to_dict()
1564
+
1565
+
1566
+ class NavPickBottles(PickBottles):
1567
+ """
1568
+ PickBottles environment with robot position randomized further from table at reset.
1569
+ """
1570
+
1571
+ def _reset_internal(self):
1572
+ super()._reset_internal()
1573
+
1574
+ if not self.deterministic_reset:
1575
+ RobotPoseRandomizer.set_pose(self, (-0.3, -0.16), (-0.2, 0.2), (-np.pi / 6, np.pi / 6))
1576
+
1577
+
1578
+ class PnPBottlesTableToTable(PickBottles):
1579
+ def _load_model(self):
1580
+ self.mujoco_objects = [
1581
+ self._create_table("table_body", [0.5, 0, 0], [0, 0, np.pi / 2]),
1582
+ self._create_table("target_table_body", [0.5, 1.2, 0], [0, 0, np.pi / 2]),
1583
+ ]
1584
+
1585
+ LocoManipulationEnv._load_model(self)
1586
+
1587
+ self.bottles = self._create_bottles()
1588
+
1589
+ def _check_success(self):
1590
+ """Check if bottles are successfully placed on the target table"""
1591
+ for bottle in self.bottles:
1592
+ bottle_on_table = self.check_contact(bottle.contact_geoms, self.mujoco_objects[1])
1593
+ bottle_is_upright = check_obj_upright(self, bottle.name, threshold=0.8, symmetric=True)
1594
+ if bottle_on_table and bottle_is_upright:
1595
+ continue
1596
+ return False
1597
+ return True
1598
+
1599
+ def _setup_references(self):
1600
+ super()._setup_references()
1601
+
1602
+ # Add reference to target table - note the _main suffix
1603
+ self.target_table_body_id = self.sim.model.body_name2id("target_table_body_main")
1604
+
1605
+ def get_object(self):
1606
+ result = super().get_object()
1607
+ result["target_table"] = dict(obj_name="target_table_body_main", obj_type="body")
1608
+ return result
1609
+
1610
+ @staticmethod
1611
+ def task_config():
1612
+ task = DexMGConfigHelper.AttrDict()
1613
+
1614
+ bottle_names = PickBottles._get_bottle_names()
1615
+ assert len(bottle_names) == 2
1616
+ for i, name in enumerate(bottle_names):
1617
+
1618
+ # pick subtask per arm
1619
+ subtask = dict(
1620
+ object_ref=name,
1621
+ subtask_term_signal=f"{name}_off_table",
1622
+ subtask_term_offset_range=None,
1623
+ selection_strategy="random",
1624
+ selection_strategy_kwargs=None,
1625
+ action_noise=0.05,
1626
+ num_interpolation_steps=5,
1627
+ num_fixed_steps=0,
1628
+ apply_noise_during_interpolation=False,
1629
+ )
1630
+ spec_attr = f"task_spec_{i}"
1631
+ setattr(getattr(task, spec_attr), "subtask_1", subtask)
1632
+
1633
+ # place subtask per arm
1634
+ subtask = dict(
1635
+ object_ref="target_table",
1636
+ subtask_term_signal=None,
1637
+ subtask_term_offset_range=None,
1638
+ selection_strategy="random",
1639
+ selection_strategy_kwargs=None,
1640
+ action_noise=0.05,
1641
+ num_interpolation_steps=5,
1642
+ num_fixed_steps=0,
1643
+ apply_noise_during_interpolation=False,
1644
+ )
1645
+ spec_attr = f"task_spec_{i}"
1646
+ setattr(getattr(task, spec_attr), "subtask_2", subtask)
1647
+
1648
+ return task.to_dict()
1649
+
1650
+ def get_subtask_term_signals(self):
1651
+ signals = dict()
1652
+ for bottle in self.bottles:
1653
+ obj_z = self.sim.data.body_xpos[self.obj_body_id[bottle.name]][2]
1654
+ target_table_pos = self.sim.data.body_xpos[self.target_table_body_id]
1655
+ target_table_z = target_table_pos[2] + self.mujoco_objects[1].top_offset[2]
1656
+ th = 0.15
1657
+ signals[f"{bottle.name}_off_table"] = int(obj_z - target_table_z > th)
1658
+ return signals
GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/environments/locomanipulation/locomanip.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import abstractmethod
2
+ from typing import Optional
3
+
4
+ from robocasa.environments.locomanipulation.base import LocoManipulationEnv
5
+ from robocasa.models.scenes import GroundArena
6
+ from robocasa.models.scenes.factory_arena import FactoryArena
7
+ from robocasa.utils.scene.configs import SceneConfig, SceneScaleConfig
8
+ from robocasa.utils.scene.scene import Scene, SceneObject
9
+ from robocasa.utils.scene.success_criteria import SuccessCriteria
10
+
11
+
12
+ class LMEnvBase(LocoManipulationEnv):
13
+ SCENE_SCALE = SceneScaleConfig()
14
+
15
+ def __init__(
16
+ self,
17
+ translucent_robot: bool = False,
18
+ use_object_obs: bool = False,
19
+ scene_scale: Optional[SceneScaleConfig] = None,
20
+ *args,
21
+ **kwargs,
22
+ ):
23
+ self.scene_scale = scene_scale or self.SCENE_SCALE
24
+ super().__init__(translucent_robot, use_object_obs, *args, **kwargs)
25
+
26
+ def _load_model(self):
27
+ self.scene = Scene(self, self._get_env_config(), self.scene_scale)
28
+ self.mujoco_objects = self.scene.mujoco_objects
29
+
30
+ super()._load_model()
31
+
32
+ def _reset_internal(self):
33
+ """
34
+ Resets simulation internal configurations.
35
+ """
36
+ super()._reset_internal()
37
+
38
+ if not self.deterministic_reset:
39
+ self.scene.reset()
40
+
41
+ def _setup_references(self):
42
+ super()._setup_references()
43
+
44
+ self.obj_body_id = {}
45
+ for obj in self.mujoco_objects:
46
+ self.obj_body_id[obj.name] = self.sim.model.body_name2id(obj.root_body)
47
+
48
+ def _get_env_config(self) -> SceneConfig:
49
+ return SceneConfig(
50
+ objects=self._get_objects(),
51
+ success=self._get_success_criteria(),
52
+ instruction=self._get_instruction(),
53
+ )
54
+
55
+ @abstractmethod
56
+ def _get_objects(self) -> list[SceneObject]:
57
+ raise NotImplementedError
58
+
59
+ @abstractmethod
60
+ def _get_success_criteria(self) -> SuccessCriteria:
61
+ raise NotImplementedError
62
+
63
+ @abstractmethod
64
+ def _get_instruction(self) -> str:
65
+ raise NotImplementedError
66
+
67
+ def _check_success(self):
68
+ return self.scene.success()
69
+
70
+ def get_ep_meta(self):
71
+ ep_meta = super().get_ep_meta()
72
+ ep_meta["lang"] = self.scene.instruction
73
+ return ep_meta
74
+
75
+
76
+ # noinspection PyAbstractClass
77
+ class LMSimpleEnv(LMEnvBase):
78
+ MUJOCO_ARENA_CLS = GroundArena
79
+
80
+
81
+ # noinspection PyAbstractClass
82
+ class LMFactoryEnv(LMEnvBase):
83
+ MUJOCO_ARENA_CLS = FactoryArena
GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/environments/locomanipulation/locomanip_basic.py ADDED
@@ -0,0 +1,732 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from robocasa.environments.locomanipulation.base import RobotPoseRandomizer
3
+ from robocasa.environments.locomanipulation.locomanip import LMSimpleEnv
4
+ from robocasa.utils.dexmg_utils import DexMGConfigHelper
5
+ from robocasa.utils.scene.configs import (
6
+ ObjectConfig,
7
+ ReferenceConfig,
8
+ SamplingConfig,
9
+ SceneScaleConfig,
10
+ )
11
+ from robocasa.utils.scene.scene import SceneObject
12
+ from robocasa.utils.scene.success_criteria import (
13
+ AllCriteria,
14
+ AnyCriteria,
15
+ IsGrasped,
16
+ IsInContact,
17
+ IsPositionInRange,
18
+ IsRobotInRange,
19
+ IsUpright,
20
+ NotCriteria,
21
+ SuccessCriteria,
22
+ )
23
+ from robocasa.utils.visuals_utls import Gradient, randomize_materials_rgba
24
+
25
+
26
+ class LMPickBottle(LMSimpleEnv, DexMGConfigHelper):
27
+ SCENE_SCALE = SceneScaleConfig(planar_scale=1.0)
28
+
29
+ TABLE_GRADIENT: Gradient = Gradient(
30
+ np.array([0.68, 0.34, 0.07, 1.0]), np.array([1.0, 1.0, 1.0, 1.0])
31
+ )
32
+ LIFT_OFFSET = 0.1
33
+
34
+ def _get_objects(self) -> list[SceneObject]:
35
+ self.table = SceneObject(
36
+ ObjectConfig(
37
+ name="table",
38
+ mjcf_path="objects/omniverse/locomanip/lab_table/model.xml",
39
+ scale=1.0,
40
+ static=True,
41
+ sampler_config=SamplingConfig(
42
+ x_range=np.array([-0.02, 0.02]),
43
+ y_range=np.array([-0.02, 0.02]),
44
+ reference_pos=np.array([0.5, 0, 0]),
45
+ rotation=np.array([np.pi * 0.5, np.pi * 0.5]),
46
+ ),
47
+ )
48
+ )
49
+ self.bottle = SceneObject(
50
+ ObjectConfig(
51
+ name="bottle",
52
+ mjcf_path="objects/omniverse/locomanip/jug_a01/model.xml",
53
+ static=False,
54
+ scale=0.6,
55
+ sampler_config=SamplingConfig(
56
+ x_range=np.array([-0.08, 0.04]),
57
+ y_range=np.array([-0.08, 0.08]),
58
+ rotation=np.array([-np.pi, np.pi]),
59
+ reference_pos=np.array([0.4, 0, self.table.mj_obj.top_offset[2]]),
60
+ ),
61
+ )
62
+ )
63
+ return [self.table, self.bottle]
64
+
65
+ def _get_success_criteria(self) -> SuccessCriteria:
66
+ return AllCriteria(
67
+ IsGrasped(self.bottle, "right"),
68
+ IsPositionInRange(self.bottle, 2, self.table.mj_obj.top_offset[2] + self.LIFT_OFFSET),
69
+ )
70
+
71
+ def _get_instruction(self) -> str:
72
+ return "Pick up the bottle."
73
+
74
+ def get_object(self):
75
+ return dict(
76
+ bottle=dict(obj_name=self.bottle.mj_obj.root_body, obj_type="body"),
77
+ )
78
+
79
+ def get_subtask_term_signals(self):
80
+ signals = dict()
81
+ signals["grasp_bottle"] = int(
82
+ self._check_grasp(self.robots[0].gripper["right"], self.bottle.mj_obj.contact_geoms)
83
+ )
84
+ return signals
85
+
86
+ @staticmethod
87
+ def task_config():
88
+ task = DexMGConfigHelper.AttrDict()
89
+ task.task_spec_0.subtask_1 = dict(
90
+ object_ref="bottle",
91
+ subtask_term_signal=None,
92
+ subtask_term_offset_range=None,
93
+ selection_strategy="random",
94
+ selection_strategy_kwargs=None,
95
+ action_noise=0.05,
96
+ num_interpolation_steps=5,
97
+ num_fixed_steps=0,
98
+ apply_noise_during_interpolation=False,
99
+ )
100
+ task.task_spec_1.subtask_1 = dict(
101
+ object_ref=None,
102
+ subtask_term_signal=None,
103
+ subtask_term_offset_range=None,
104
+ selection_strategy="random",
105
+ selection_strategy_kwargs=None,
106
+ action_noise=0.05,
107
+ num_interpolation_steps=5,
108
+ num_fixed_steps=0,
109
+ apply_noise_during_interpolation=False,
110
+ )
111
+ return task.to_dict()
112
+
113
+ def _reset_internal(self):
114
+ super()._reset_internal()
115
+
116
+ if not self.deterministic_reset:
117
+ self._randomize_table_rgba()
118
+
119
+ def _randomize_table_rgba(self):
120
+ randomize_materials_rgba(
121
+ rng=self.rng, mjcf_obj=self.table.mj_obj, gradient=self.TABLE_GRADIENT, linear=True
122
+ )
123
+
124
+
125
+ class LMPickBottleHigh(LMPickBottle):
126
+ TABLE_OFFSET = 0.1
127
+
128
+ def _get_objects(self) -> list[SceneObject]:
129
+ self.table = SceneObject(
130
+ ObjectConfig(
131
+ name="table",
132
+ mjcf_path="objects/omniverse/locomanip/lab_table/model.xml",
133
+ scale=1.0,
134
+ static=True,
135
+ sampler_config=SamplingConfig(
136
+ x_range=np.array([-0.02, 0.02]),
137
+ y_range=np.array([-0.02, 0.02]),
138
+ reference_pos=np.array([0.5, 0, self.TABLE_OFFSET]),
139
+ rotation=np.array([np.pi * 0.5, np.pi * 0.5]),
140
+ ),
141
+ )
142
+ )
143
+ self.bottle = SceneObject(
144
+ ObjectConfig(
145
+ name="bottle",
146
+ mjcf_path="objects/omniverse/locomanip/jug_a01/model.xml",
147
+ static=False,
148
+ scale=0.6,
149
+ sampler_config=SamplingConfig(
150
+ x_range=np.array([-0.08, 0.04]),
151
+ y_range=np.array([-0.08, 0.08]),
152
+ rotation=np.array([-np.pi, np.pi]),
153
+ reference_pos=np.array(
154
+ [0.4, 0, self.TABLE_OFFSET + self.table.mj_obj.top_offset[2]]
155
+ ),
156
+ reference=ReferenceConfig(obj=self.table),
157
+ ),
158
+ )
159
+ )
160
+ return [self.table, self.bottle]
161
+
162
+
163
+ class LMNavPickBottle(LMPickBottle):
164
+ def _reset_internal(self):
165
+ super()._reset_internal()
166
+
167
+ if not self.deterministic_reset:
168
+ RobotPoseRandomizer.set_pose(self, (-0.3, -0.16), (-0.2, 0.2), (-np.pi / 6, np.pi / 6))
169
+
170
+ def _get_instruction(self) -> str:
171
+ return "Walk forward and pick up the bottle from the table."
172
+
173
+
174
+ class LMPickBottleGround(LMPickBottle):
175
+ def _get_objects(self) -> list[SceneObject]:
176
+ self.bottle = SceneObject(
177
+ ObjectConfig(
178
+ name="bottle",
179
+ mjcf_path="objects/omniverse/locomanip/jug_a01/model.xml",
180
+ static=False,
181
+ scale=0.6,
182
+ sampler_config=SamplingConfig(
183
+ x_range=np.array([-0.08, 0.04]),
184
+ y_range=np.array([-0.08, 0.08]),
185
+ rotation=np.array([-np.pi, np.pi]),
186
+ reference_pos=np.array(
187
+ [0.4, 0, 0.075]
188
+ ), # Base position on ground (z=0.075 is bottle radius)
189
+ ),
190
+ )
191
+ )
192
+ return [self.bottle]
193
+
194
+ def _get_success_criteria(self) -> SuccessCriteria:
195
+ return AllCriteria(
196
+ IsGrasped(self.bottle, "right"),
197
+ IsPositionInRange(self.bottle, 2, self.LIFT_OFFSET, 10),
198
+ )
199
+
200
+ def _randomize_table_rgba(self):
201
+ pass
202
+
203
+
204
+ class LMPnPBottle(LMPickBottle):
205
+ LIFT_OFFSET = 0.15
206
+
207
+ def _get_objects(self) -> list[SceneObject]:
208
+ super()._get_objects()
209
+ self.table_target = SceneObject(
210
+ ObjectConfig(
211
+ name="table_target",
212
+ mjcf_path="objects/omniverse/locomanip/lab_table/model.xml",
213
+ scale=1.0,
214
+ static=True,
215
+ sampler_config=SamplingConfig(
216
+ x_range=np.array([-0.02, 0.02]),
217
+ y_range=np.array([-0.02, 0.02]),
218
+ reference_pos=np.array([0.5, 1.2, 0]),
219
+ rotation=np.array([np.pi * 0.5, np.pi * 0.5]),
220
+ ),
221
+ )
222
+ )
223
+ return [self.table, self.table_target, self.bottle]
224
+
225
+ def _get_success_criteria(self) -> SuccessCriteria:
226
+ return AllCriteria(
227
+ IsUpright(self.bottle, symmetric=True), IsInContact(self.bottle, self.table_target)
228
+ )
229
+
230
+ def _get_instruction(self) -> str:
231
+ return "Pick up the bottle and place it on the other table."
232
+
233
+ def get_object(self):
234
+ return dict(
235
+ bottle=dict(obj_name=self.bottle.mj_obj.root_body, obj_type="body"),
236
+ target_table=dict(obj_name=self.table_target.mj_obj.root_body, obj_type="body"),
237
+ )
238
+
239
+ def get_subtask_term_signals(self):
240
+ obj_z = self.sim.data.body_xpos[self.obj_body_id(self.bottle.mj_obj.name)][2]
241
+ target_table_pos = self.sim.data.body_xpos[self.obj_body_id(self.table_target.mj_obj.name)]
242
+ target_table_z = target_table_pos[2] + self.table_target.mj_obj.top_offset[2]
243
+ return dict(obj_off_table=int(obj_z - target_table_z > self.LIFT_OFFSET))
244
+
245
+ @staticmethod
246
+ def task_config():
247
+ task = DexMGConfigHelper.AttrDict()
248
+ task.task_spec_0.subtask_1 = dict(
249
+ object_ref="bottle",
250
+ subtask_term_signal="obj_off_table",
251
+ subtask_term_offset_range=(5, 10),
252
+ selection_strategy="random",
253
+ selection_strategy_kwargs=None,
254
+ action_noise=0.05,
255
+ num_interpolation_steps=5,
256
+ num_fixed_steps=0,
257
+ apply_noise_during_interpolation=False,
258
+ )
259
+ # Second subtask for placing on target table
260
+ task.task_spec_0.subtask_2 = dict(
261
+ object_ref="target_table",
262
+ subtask_term_signal=None,
263
+ subtask_term_offset_range=None,
264
+ selection_strategy="random",
265
+ selection_strategy_kwargs=None,
266
+ action_noise=0.05,
267
+ num_interpolation_steps=5,
268
+ num_fixed_steps=0,
269
+ apply_noise_during_interpolation=False,
270
+ )
271
+ task.task_spec_1.subtask_1 = dict(
272
+ object_ref=None,
273
+ subtask_term_signal=None,
274
+ subtask_term_offset_range=None,
275
+ selection_strategy="random",
276
+ selection_strategy_kwargs=None,
277
+ action_noise=0.05,
278
+ num_interpolation_steps=5,
279
+ num_fixed_steps=0,
280
+ apply_noise_during_interpolation=False,
281
+ )
282
+ return task.to_dict()
283
+
284
+ def _randomize_table_rgba(self):
285
+ for table in [self.table_target, self.table]:
286
+ randomize_materials_rgba(
287
+ rng=self.rng, mjcf_obj=table.mj_obj, gradient=self.TABLE_GRADIENT, linear=True
288
+ )
289
+
290
+
291
+ class LMPickMultipleBottles(LMPickBottle):
292
+ BOTTLE_COLOURS = [(0.3, 0.7, 0.8, 1.0), (0.8, 0.4, 0.3, 1.0)]
293
+ BOTTLES_COUNT = 2
294
+ Y_OFFSET_STEP = 0.1
295
+
296
+ def _get_objects(self) -> list[SceneObject]:
297
+ self.table = SceneObject(
298
+ ObjectConfig(
299
+ name="table",
300
+ mjcf_path="objects/omniverse/locomanip/lab_table/model.xml",
301
+ scale=1.0,
302
+ static=True,
303
+ sampler_config=SamplingConfig(
304
+ x_range=np.array([-0.02, 0.02]),
305
+ y_range=np.array([-0.02, 0.02]),
306
+ reference_pos=np.array([0.5, 0, 0]),
307
+ rotation=np.array([np.pi * 0.5, np.pi * 0.5]),
308
+ ),
309
+ )
310
+ )
311
+
312
+ self.bottles = []
313
+ offsets = np.arange(self.BOTTLES_COUNT) - (self.BOTTLES_COUNT - 1) / 2.0
314
+ for i in range(self.BOTTLES_COUNT):
315
+ reference_pos = np.array([0.4, 0, self.table.mj_obj.top_offset[2]])
316
+ reference_pos += np.array([0, self.Y_OFFSET_STEP * offsets[i], 0])
317
+ bottle = SceneObject(
318
+ ObjectConfig(
319
+ name=f"bottle_{i}",
320
+ mjcf_path="objects/omniverse/locomanip/jug_a01/model.xml",
321
+ static=False,
322
+ scale=0.6,
323
+ sampler_config=SamplingConfig(
324
+ x_range=np.array([-0.08, 0.04]),
325
+ y_range=np.array([-0.04, 0.04]),
326
+ rotation=np.array([-np.pi, np.pi]),
327
+ reference_pos=reference_pos,
328
+ ),
329
+ rgba=self.BOTTLE_COLOURS[i % len(self.BOTTLE_COLOURS)],
330
+ )
331
+ )
332
+ self.bottles.append(bottle)
333
+ return [self.table, *self.bottles]
334
+
335
+ def _get_success_criteria(self) -> SuccessCriteria:
336
+ criteria = []
337
+ for bottle in self.bottles:
338
+ criteria.append(AnyCriteria(IsGrasped(bottle, "right"), IsGrasped(bottle, "left")))
339
+ criteria.append(
340
+ IsPositionInRange(bottle, 2, self.table.mj_obj.top_offset[2] + self.LIFT_OFFSET, 10)
341
+ )
342
+ return AllCriteria(*criteria)
343
+
344
+ def _get_instruction(self) -> str:
345
+ return "Pick up bottles."
346
+
347
+ def get_object(self):
348
+ return {
349
+ bottle.mj_obj.name: dict(obj_name=bottle.mj_obj.root_body, obj_type="body")
350
+ for bottle in self.bottles
351
+ }
352
+
353
+ def get_subtask_term_signals(self):
354
+ return {
355
+ f"grasp_{bottle.mj_obj.name}": int(
356
+ self._check_grasp(self.robots[0].gripper["right"], bottle.mj_obj)
357
+ or self._check_grasp(self.robots[0].gripper["left"], bottle.mj_obj)
358
+ )
359
+ for bottle in self.bottles
360
+ }
361
+
362
+ @staticmethod
363
+ def task_config():
364
+ task = DexMGConfigHelper.AttrDict()
365
+ for i in range(LMPickMultipleBottles.BOTTLES_COUNT):
366
+ subtask = dict(
367
+ object_ref=f"bottle_{i}",
368
+ subtask_term_signal=None,
369
+ subtask_term_offset_range=None,
370
+ selection_strategy="random",
371
+ selection_strategy_kwargs=None,
372
+ action_noise=0.05,
373
+ num_interpolation_steps=5,
374
+ num_fixed_steps=0,
375
+ apply_noise_during_interpolation=False,
376
+ )
377
+ setattr(task.task_spec_0, f"subtask_{i+1}", subtask)
378
+ task.task_spec_1.subtask_1 = dict(
379
+ object_ref=None,
380
+ subtask_term_signal=None,
381
+ subtask_term_offset_range=None,
382
+ selection_strategy="random",
383
+ selection_strategy_kwargs=None,
384
+ action_noise=0.05,
385
+ num_interpolation_steps=5,
386
+ num_fixed_steps=0,
387
+ apply_noise_during_interpolation=False,
388
+ )
389
+ return task.to_dict()
390
+
391
+
392
+ class LMPnPMultipleBottles(LMPickMultipleBottles):
393
+ def _get_objects(self) -> list[SceneObject]:
394
+ super()._get_objects()
395
+ self.table_target = SceneObject(
396
+ ObjectConfig(
397
+ name="table_target",
398
+ mjcf_path="objects/omniverse/locomanip/lab_table/model.xml",
399
+ scale=1.0,
400
+ static=True,
401
+ sampler_config=SamplingConfig(
402
+ x_range=np.array([-0.02, 0.02]),
403
+ y_range=np.array([-0.02, 0.02]),
404
+ reference_pos=np.array([0.5, 1.2, 0]),
405
+ rotation=np.array([np.pi * 0.5, np.pi * 0.5]),
406
+ ),
407
+ )
408
+ )
409
+ return [self.table, self.table_target, *self.bottles]
410
+
411
+ def _get_success_criteria(self) -> SuccessCriteria:
412
+ criteria = [
413
+ AllCriteria(IsInContact(bottle, self.table_target), IsUpright(bottle, symmetric=True))
414
+ for bottle in self.bottles
415
+ ]
416
+ return AllCriteria(*criteria)
417
+
418
+ def _get_instruction(self) -> str:
419
+ return "Pick up bottles from one table and place it on the other."
420
+
421
+ @staticmethod
422
+ def task_config():
423
+ task = DexMGConfigHelper.AttrDict()
424
+ for i in range(LMPnPMultipleBottles.BOTTLES_COUNT):
425
+ bottle_name = f"bottle_{i}"
426
+ subtask = dict(
427
+ object_ref=bottle_name,
428
+ subtask_term_signal=f"{bottle_name}_off_table",
429
+ subtask_term_offset_range=None,
430
+ selection_strategy="random",
431
+ selection_strategy_kwargs=None,
432
+ action_noise=0.05,
433
+ num_interpolation_steps=5,
434
+ num_fixed_steps=0,
435
+ apply_noise_during_interpolation=False,
436
+ )
437
+ setattr(task.task_spec_0, f"subtask_{i+1}", subtask)
438
+ # Next subtask for placing on target table
439
+ task.task_spec_0.subtask_3 = dict(
440
+ object_ref="target_table",
441
+ subtask_term_signal=None,
442
+ subtask_term_offset_range=None,
443
+ selection_strategy="random",
444
+ selection_strategy_kwargs=None,
445
+ action_noise=0.05,
446
+ num_interpolation_steps=5,
447
+ num_fixed_steps=0,
448
+ apply_noise_during_interpolation=False,
449
+ )
450
+ task.task_spec_1.subtask_1 = dict(
451
+ object_ref=None,
452
+ subtask_term_signal=None,
453
+ subtask_term_offset_range=None,
454
+ selection_strategy="random",
455
+ selection_strategy_kwargs=None,
456
+ action_noise=0.05,
457
+ num_interpolation_steps=5,
458
+ num_fixed_steps=0,
459
+ apply_noise_during_interpolation=False,
460
+ )
461
+ return task.to_dict()
462
+
463
+ def get_subtask_term_signals(self):
464
+ signals = dict()
465
+ for bottle in self.bottles:
466
+ obj_z = self.sim.data.body_xpos[self.obj_body_id(self.bottle.mj_obj.name)][2]
467
+ target_table_pos = self.sim.data.body_xpos[
468
+ self.obj_body_id(self.table_target.mj_obj.name)
469
+ ]
470
+ target_table_z = target_table_pos[2] + self.table_target.mj_obj.top_offset[2]
471
+ signals[f"{bottle.mj_obj.name}_off_table"] = int(
472
+ obj_z - target_table_z > self.LIFT_OFFSET
473
+ )
474
+ return signals
475
+
476
+ def _randomize_table_rgba(self):
477
+ for table in [self.table_target, self.table]:
478
+ randomize_materials_rgba(
479
+ rng=self.rng, mjcf_obj=table.mj_obj, gradient=self.TABLE_GRADIENT, linear=True
480
+ )
481
+
482
+
483
+ class LMPickBottleShelf(LMPickBottle):
484
+ def _get_objects(self) -> list[SceneObject]:
485
+ super()._get_objects()
486
+ self.shelf = SceneObject(
487
+ ObjectConfig(
488
+ name="shelf",
489
+ mjcf_path="objects/omniverse/locomanip/lab_shelf/model.xml",
490
+ static=True,
491
+ sampler_config=SamplingConfig(
492
+ rotation=np.array([np.pi / 2, np.pi / 2]),
493
+ reference_pos=np.array([0.9, 0, 0]),
494
+ ),
495
+ )
496
+ )
497
+ self.bottle = SceneObject(
498
+ ObjectConfig(
499
+ name="bottle",
500
+ mjcf_path="objects/omniverse/locomanip/jug_a01/model.xml",
501
+ static=False,
502
+ scale=0.6,
503
+ sampler_config=SamplingConfig(
504
+ x_range=np.array([-0.14, -0.06]),
505
+ y_range=np.array([-0.08, 0.08]),
506
+ rotation=np.array([-np.pi, np.pi]),
507
+ reference=ReferenceConfig(self.shelf, spawn_id=2),
508
+ ),
509
+ )
510
+ )
511
+ return [self.shelf, self.bottle]
512
+
513
+ def _get_success_criteria(self) -> SuccessCriteria:
514
+ return AllCriteria(
515
+ IsGrasped(self.bottle, "right"),
516
+ NotCriteria(IsInContact(self.bottle, self.shelf)),
517
+ )
518
+
519
+
520
+ class LMNavPickBottleShelf(LMPickBottleShelf):
521
+ ROBOT_DISTANCE_THRESHOLD = 1.0
522
+
523
+ def _reset_internal(self):
524
+ super()._reset_internal()
525
+ if not self.deterministic_reset:
526
+ RobotPoseRandomizer.set_pose(self, (-0.1, 0.1), (-0.1, 0.1), (-np.pi / 6, np.pi / 6))
527
+
528
+ def _get_success_criteria(self) -> SuccessCriteria:
529
+ return AllCriteria(
530
+ NotCriteria(IsRobotInRange(self.shelf, self.ROBOT_DISTANCE_THRESHOLD, True)),
531
+ IsGrasped(self.bottle, "right"),
532
+ NotCriteria(IsInContact(self.bottle, self.shelf)),
533
+ )
534
+
535
+ def _get_instruction(self) -> str:
536
+ return "Pick up the bottle from the shelf and move backward away from it."
537
+
538
+
539
+ class LMPickBottleShelfLow(LMPickBottleShelf):
540
+ def _get_objects(self) -> list[SceneObject]:
541
+ super()._get_objects()
542
+ self.bottle = SceneObject(
543
+ ObjectConfig(
544
+ name="bottle",
545
+ mjcf_path="objects/omniverse/locomanip/jug_a01/model.xml",
546
+ static=False,
547
+ scale=0.6,
548
+ sampler_config=SamplingConfig(
549
+ x_range=np.array([-0.14, -0.06]),
550
+ y_range=np.array([-0.08, 0.08]),
551
+ rotation=np.array([-np.pi, np.pi]),
552
+ reference=ReferenceConfig(self.shelf, spawn_id=1),
553
+ ),
554
+ )
555
+ )
556
+ return [self.shelf, self.bottle]
557
+
558
+
559
+ class LMNavPickBottleShelfLow(LMNavPickBottleShelf):
560
+ def _get_objects(self) -> list[SceneObject]:
561
+ super()._get_objects()
562
+ self.bottle = SceneObject(
563
+ ObjectConfig(
564
+ name="bottle",
565
+ mjcf_path="objects/omniverse/locomanip/jug_a01/model.xml",
566
+ static=False,
567
+ scale=0.6,
568
+ sampler_config=SamplingConfig(
569
+ x_range=np.array([-0.14, -0.06]),
570
+ y_range=np.array([-0.08, 0.08]),
571
+ rotation=np.array([-np.pi, np.pi]),
572
+ reference=ReferenceConfig(self.shelf, spawn_id=1),
573
+ ),
574
+ )
575
+ )
576
+ return [self.shelf, self.bottle]
577
+
578
+
579
+ class LMPnPBottleToPlate(LMPnPBottle):
580
+ def _get_objects(self) -> list[SceneObject]:
581
+ super()._get_objects()
582
+ self.plate = SceneObject(
583
+ ObjectConfig(
584
+ name="plate",
585
+ mjcf_path="objects/omniverse/locomanip/plate_1/model.xml",
586
+ scale=1.0,
587
+ static=True,
588
+ sampler_config=SamplingConfig(
589
+ x_range=np.array([-0.2 - 0.08, -0.2 + 0.04]),
590
+ y_range=np.array([-0.08, 0.08]),
591
+ rotation=np.array([-np.pi, np.pi]),
592
+ reference=ReferenceConfig(self.table_target),
593
+ ),
594
+ )
595
+ )
596
+ return [self.table, self.table_target, self.bottle, self.plate]
597
+
598
+ def _get_success_criteria(self) -> SuccessCriteria:
599
+ return AllCriteria(
600
+ IsUpright(self.bottle, symmetric=True), IsInContact(self.bottle, self.plate)
601
+ )
602
+
603
+ def _get_instruction(self) -> str:
604
+ return "Pick up the bottle and place it on the plate."
605
+
606
+ def get_object(self):
607
+ return dict(
608
+ bottle=dict(obj_name=self.bottle.mj_obj.root_body, obj_type="body"),
609
+ plate=dict(obj_name=self.plate.mj_obj.root_body, obj_type="body"),
610
+ )
611
+
612
+ def get_subtask_term_signals(self):
613
+ obj_z = self.sim.data.body_xpos[self.obj_body_id(self.bottle.mj_obj.name)][2]
614
+ target_table_pos = self.sim.data.body_xpos[self.obj_body_id(self.table_target.mj_obj.name)]
615
+ target_table_z = target_table_pos[2] + self.table_target.mj_obj.top_offset[2]
616
+ return dict(obj_off_table=int(obj_z - target_table_z > self.LIFT_OFFSET))
617
+
618
+ @staticmethod
619
+ def task_config():
620
+ task = DexMGConfigHelper.AttrDict()
621
+ task.task_spec_0.subtask_1 = dict(
622
+ object_ref="bottle",
623
+ subtask_term_signal="obj_off_table",
624
+ subtask_term_offset_range=(5, 10),
625
+ selection_strategy="random",
626
+ selection_strategy_kwargs=None,
627
+ action_noise=0.05,
628
+ num_interpolation_steps=5,
629
+ num_fixed_steps=0,
630
+ apply_noise_during_interpolation=False,
631
+ )
632
+ # Second subtask for placing on plate
633
+ task.task_spec_0.subtask_2 = dict(
634
+ object_ref="plate",
635
+ subtask_term_signal=None,
636
+ subtask_term_offset_range=None,
637
+ selection_strategy="random",
638
+ selection_strategy_kwargs=None,
639
+ action_noise=0.05,
640
+ num_interpolation_steps=5,
641
+ num_fixed_steps=0,
642
+ apply_noise_during_interpolation=False,
643
+ )
644
+ task.task_spec_1.subtask_1 = dict(
645
+ object_ref=None,
646
+ subtask_term_signal=None,
647
+ subtask_term_offset_range=None,
648
+ selection_strategy="random",
649
+ selection_strategy_kwargs=None,
650
+ action_noise=0.05,
651
+ num_interpolation_steps=5,
652
+ num_fixed_steps=0,
653
+ apply_noise_during_interpolation=False,
654
+ )
655
+ return task.to_dict()
656
+
657
+
658
+ class LMPnPAppleToPlate(LMPnPBottleToPlate):
659
+ def _get_objects(self) -> list[SceneObject]:
660
+ super()._get_objects()
661
+ self.apple = SceneObject(
662
+ ObjectConfig(
663
+ name="apple",
664
+ mjcf_path="objects/omniverse/locomanip/apple_0/model.xml",
665
+ static=False,
666
+ scale=1.0,
667
+ sampler_config=SamplingConfig(
668
+ x_range=np.array([-0.08, 0.04]),
669
+ y_range=np.array([-0.08, 0.08]),
670
+ rotation=np.array([-np.pi, np.pi]),
671
+ reference_pos=np.array([0.4, 0, self.table.mj_obj.top_offset[2]]),
672
+ ),
673
+ )
674
+ )
675
+ return [self.table, self.table_target, self.apple, self.plate]
676
+
677
+ def _get_success_criteria(self) -> SuccessCriteria:
678
+ return IsInContact(self.apple, self.plate)
679
+
680
+ def _get_instruction(self) -> str:
681
+ return "pick up the apple, walk left and place the apple on the plate."
682
+
683
+ def get_object(self):
684
+ return dict(
685
+ apple=dict(obj_name=self.apple.mj_obj.root_body, obj_type="body"),
686
+ plate=dict(obj_name=self.plate.mj_obj.root_body, obj_type="body"),
687
+ )
688
+
689
+ def get_subtask_term_signals(self):
690
+ obj_z = self.sim.data.body_xpos[self.obj_body_id(self.apple.mj_obj.name)][2]
691
+ target_table_pos = self.sim.data.body_xpos[self.obj_body_id(self.table_target.mj_obj.name)]
692
+ target_table_z = target_table_pos[2] + self.table_target.mj_obj.top_offset[2]
693
+ return dict(obj_off_table=int(obj_z - target_table_z > self.LIFT_OFFSET))
694
+
695
+ @staticmethod
696
+ def task_config():
697
+ task = DexMGConfigHelper.AttrDict()
698
+ task.task_spec_0.subtask_1 = dict(
699
+ object_ref="apple",
700
+ subtask_term_signal="obj_off_table",
701
+ subtask_term_offset_range=(5, 10),
702
+ selection_strategy="random",
703
+ selection_strategy_kwargs=None,
704
+ action_noise=0.05,
705
+ num_interpolation_steps=5,
706
+ num_fixed_steps=0,
707
+ apply_noise_during_interpolation=False,
708
+ )
709
+ # Second subtask for placing on plate
710
+ task.task_spec_0.subtask_2 = dict(
711
+ object_ref="plate",
712
+ subtask_term_signal=None,
713
+ subtask_term_offset_range=None,
714
+ selection_strategy="random",
715
+ selection_strategy_kwargs=None,
716
+ action_noise=0.05,
717
+ num_interpolation_steps=5,
718
+ num_fixed_steps=0,
719
+ apply_noise_during_interpolation=False,
720
+ )
721
+ task.task_spec_1.subtask_1 = dict(
722
+ object_ref=None,
723
+ subtask_term_signal=None,
724
+ subtask_term_offset_range=None,
725
+ selection_strategy="random",
726
+ selection_strategy_kwargs=None,
727
+ action_noise=0.05,
728
+ num_interpolation_steps=5,
729
+ num_fixed_steps=0,
730
+ apply_noise_during_interpolation=False,
731
+ )
732
+ return task.to_dict()
GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/environments/locomanipulation/locomanip_dc.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from robocasa import (
2
+ LMNavPickBottle,
3
+ LMPnPAppleToPlate,
4
+ )
5
+ from robocasa.models.scenes.lab_arena import LabArena
6
+
7
+
8
+ class LabEnvMixin:
9
+ MUJOCO_ARENA_CLS = LabArena
10
+
11
+
12
+ class LMNavPickBottleDC(LabEnvMixin, LMNavPickBottle): ...
13
+
14
+
15
+ class LMPnPAppleToPlateDC(LabEnvMixin, LMPnPAppleToPlate): ...
GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/environments/locomanipulation/locomanip_pnp.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from robocasa.environments.locomanipulation.locomanip import LMFactoryEnv
3
+ from robocasa.utils.scene.configs import (
4
+ ObjectConfig,
5
+ ReferenceConfig,
6
+ SamplingConfig,
7
+ SceneHandedness,
8
+ SceneScaleConfig,
9
+ )
10
+ from robocasa.utils.scene.scene import SceneObject
11
+ from robocasa.utils.scene.success_criteria import (
12
+ AllCriteria,
13
+ IsInContact,
14
+ IsUpright,
15
+ SuccessCriteria,
16
+ )
17
+
18
+
19
+ class LMBottlePnP(LMFactoryEnv):
20
+ SCENE_SCALE = SceneScaleConfig(planar_scale=(1, 1), handedness=SceneHandedness.RIGHT)
21
+
22
+ def _get_objects(self) -> list[SceneObject]:
23
+ self.table_target = SceneObject(
24
+ ObjectConfig(
25
+ name="table_target",
26
+ mjcf_path="objects/omniverse/locomanip/factory_ergo_table/model.xml",
27
+ static=True,
28
+ sampler_config=SamplingConfig(
29
+ x_range=np.array([-0.02, 0.02]),
30
+ y_range=np.array([-0.02, 0.02]),
31
+ reference_pos=np.array([1.2, 0.8, 0]),
32
+ rotation=np.array([np.pi, np.pi]),
33
+ ),
34
+ )
35
+ )
36
+ self.table_origin = SceneObject(
37
+ ObjectConfig(
38
+ name="table_origin",
39
+ mjcf_path="objects/omniverse/locomanip/factory_ergo_table/model.xml",
40
+ static=True,
41
+ sampler_config=SamplingConfig(
42
+ x_range=np.array([-0.02, 0.02]),
43
+ y_range=np.array([-0.02, 0.02]),
44
+ reference_pos=np.array([1.2, -0.8, 0]),
45
+ rotation=np.array([np.pi, np.pi]),
46
+ ),
47
+ )
48
+ )
49
+ self.bottle = SceneObject(
50
+ ObjectConfig(
51
+ name="obj",
52
+ mjcf_path="objects/omniverse/locomanip/jug_a01/model.xml",
53
+ static=False,
54
+ scale=0.6,
55
+ sampler_config=SamplingConfig(
56
+ x_range=np.array([-0.4, -0.35]),
57
+ y_range=np.array([-0.1, 0.1]),
58
+ rotation=np.array([-np.pi, np.pi]),
59
+ reference=ReferenceConfig(self.table_origin),
60
+ ),
61
+ )
62
+ )
63
+ return [self.table_origin, self.table_target, self.bottle]
64
+
65
+ def _get_success_criteria(self) -> SuccessCriteria:
66
+ return AllCriteria(IsInContact(self.bottle, self.table_target), IsUpright(self.bottle))
67
+
68
+ def _get_instruction(self) -> str:
69
+ return "Pick up the bottle from one table and place it on the other."
70
+
71
+
72
+ class LMBoxPnP(LMBottlePnP):
73
+ SCENE_SCALE = SceneScaleConfig(planar_scale=(1, 1), handedness=SceneHandedness.RIGHT)
74
+
75
+ def _get_objects(self) -> list[SceneObject]:
76
+ super()._get_objects()
77
+ self.box = SceneObject(
78
+ ObjectConfig(
79
+ name="obj",
80
+ mjcf_path="objects/omniverse/locomanip/cardbox_a1/model.xml",
81
+ static=False,
82
+ scale=0.7,
83
+ density=1,
84
+ friction=(2, 1, 1),
85
+ sampler_config=SamplingConfig(
86
+ x_range=np.array([-0.35, -0.3]),
87
+ y_range=np.array([-0.1, 0.1]),
88
+ rotation=np.array([np.pi * 0.9, np.pi * 1.1]),
89
+ reference=ReferenceConfig(self.table_origin),
90
+ ),
91
+ )
92
+ )
93
+ return [self.table_origin, self.table_target, self.box]
94
+
95
+ def _get_success_criteria(self) -> SuccessCriteria:
96
+ return AllCriteria(IsInContact(self.box, self.table_target), IsUpright(self.box))
97
+
98
+ def _get_instruction(self) -> str:
99
+ return "Pick up the box from one table and place it on the other."
GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/examples/third_party_controller/__init__.py ADDED
File without changes
GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/examples/third_party_controller/default_mink_ik_g1_gear_wbc.json ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "type": "HYBRID_WHOLE_BODY_MINK_IK",
3
+ "composite_controller_specific_configs": {
4
+ "ref_name": ["gripper0_right_grip_site", "gripper0_left_grip_site"],
5
+ "interpolation": null,
6
+ "actuation_part_names": ["torso", "left", "right"],
7
+ "external_part_names": ["legs"],
8
+ "max_dq": 4,
9
+ "ik_pseudo_inverse_damping": 5e-2,
10
+ "ik_integration_dt": 1e-1,
11
+ "ik_input_type": "absolute",
12
+ "ik_input_ref_frame": "base",
13
+ "ik_input_rotation_repr": "axis_angle",
14
+ "verbose": false,
15
+ "ik_posture_weights": {
16
+ "robot0_waist_yaw_joint": 100.0,
17
+ "robot0_waist_roll_joint": 200.0,
18
+ "robot0_waist_pitch_joint": 400.0,
19
+ "robot0_left_shoulder_pitch_joint": 4.0,
20
+ "robot0_left_shoulder_roll_joint": 3.0,
21
+ "robot0_left_shoulder_yaw_joint": 2.0,
22
+ "robot0_left_elbow_joint": 1.0,
23
+ "robot0_right_shoulder_pitch_joint": 4.0,
24
+ "robot0_right_shoulder_roll_joint": 3.0,
25
+ "robot0_right_shoulder_yaw_joint": 2.0,
26
+ "robot0_right_elbow_joint": 1.0
27
+ },
28
+ "ik_hand_pos_cost": 10.0,
29
+ "ik_hand_ori_cost": 5,
30
+ "use_joint_angle_action_input": false
31
+ },
32
+ "body_parts": {
33
+ "legs": {
34
+ "type" : "JOINT_POSITION",
35
+ "input_max": 100,
36
+ "input_min": -100,
37
+ "input_type": "absolute",
38
+ "output_max": 100,
39
+ "output_min": -100,
40
+ "kd": [2, 2, 2, 4, 2, 2, 2, 2, 2, 4, 2, 2],
41
+ "kv": 0,
42
+ "kp": [150, 150, 150, 300, 40, 40, 150, 150, 150, 200, 40, 40],
43
+ "velocity_limits": [-1,1],
44
+ "kp_limits": [0, 1000],
45
+ "interpolation": null,
46
+ "ramp_ratio": 0.2,
47
+ "use_torque_compensation": false,
48
+ "desired_torque_as_acceleration": false
49
+ },
50
+ "arms": {
51
+ "left": {
52
+ "type" : "JOINT_POSITION",
53
+ "input_max": 100,
54
+ "input_min": -100,
55
+ "input_type": "absolute",
56
+ "output_max": 100,
57
+ "output_min": -100,
58
+ "kd": [5.0, 5.0, 2.0, 2.0, 2.0, 2.0, 2.0],
59
+ "kp": [100, 100, 40, 40, 20, 20, 20],
60
+ "velocity_limits": [-1,1],
61
+ "kp_limits": [0, 1000],
62
+ "interpolation": null,
63
+ "ramp_ratio": 0.2,
64
+ "gripper": {
65
+ "type": "GRIP",
66
+ "use_action_scaling": false
67
+ },
68
+ "use_torque_compensation": false,
69
+ "desired_torque_as_acceleration": false
70
+ },
71
+ "right": {
72
+ "type" : "JOINT_POSITION",
73
+ "input_max": 100,
74
+ "input_min": -100,
75
+ "input_type": "absolute",
76
+ "output_max": 100,
77
+ "output_min": -100,
78
+ "kd": [5.0, 5.0, 2.0, 2.0, 2.0, 2.0, 2.0],
79
+ "kp": [100, 100, 40, 40, 20, 20, 20],
80
+ "velocity_limits": [-1,1],
81
+ "kp_limits": [0, 1000],
82
+ "interpolation": null,
83
+ "ramp_ratio": 0.2,
84
+ "gripper": {
85
+ "type": "GRIP",
86
+ "use_action_scaling": false
87
+ },
88
+ "use_torque_compensation": false,
89
+ "desired_torque_as_acceleration": false
90
+ }
91
+ },
92
+ "torso": {
93
+ "type" : "JOINT_POSITION",
94
+ "input_max": 100,
95
+ "input_min": -100,
96
+ "input_type": "absolute",
97
+ "output_max": 100,
98
+ "output_min": -100,
99
+ "kd": 5.0,
100
+ "kp": 250.0,
101
+ "velocity_limits": [-1,1],
102
+ "kp_limits": [0, 1000],
103
+ "interpolation": null,
104
+ "ramp_ratio": 0.2,
105
+ "use_torque_compensation": false,
106
+ "desired_torque_as_acceleration": false
107
+ },
108
+ "base": {
109
+ "type": "JOINT_VELOCITY_AND_POSITION",
110
+ "interpolation": "null"
111
+ }
112
+ }
113
+ }
GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/examples/third_party_controller/default_mink_ik_g1_gear_wbc_gc.json ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "type": "HYBRID_WHOLE_BODY_MINK_IK",
3
+ "composite_controller_specific_configs": {
4
+ "ref_name": ["gripper0_right_grip_site", "gripper0_left_grip_site"],
5
+ "interpolation": null,
6
+ "actuation_part_names": ["torso", "left", "right"],
7
+ "external_part_names": ["legs"],
8
+ "max_dq": 4,
9
+ "ik_pseudo_inverse_damping": 5e-2,
10
+ "ik_integration_dt": 1e-1,
11
+ "ik_input_type": "absolute",
12
+ "ik_input_ref_frame": "base",
13
+ "ik_input_rotation_repr": "axis_angle",
14
+ "verbose": false,
15
+ "ik_posture_weights": {
16
+ "robot0_waist_yaw_joint": 100.0,
17
+ "robot0_waist_roll_joint": 200.0,
18
+ "robot0_waist_pitch_joint": 400.0,
19
+ "robot0_left_shoulder_pitch_joint": 4.0,
20
+ "robot0_left_shoulder_roll_joint": 3.0,
21
+ "robot0_left_shoulder_yaw_joint": 2.0,
22
+ "robot0_left_elbow_joint": 1.0,
23
+ "robot0_right_shoulder_pitch_joint": 4.0,
24
+ "robot0_right_shoulder_roll_joint": 3.0,
25
+ "robot0_right_shoulder_yaw_joint": 2.0,
26
+ "robot0_right_elbow_joint": 1.0
27
+ },
28
+ "ik_hand_pos_cost": 10.0,
29
+ "ik_hand_ori_cost": 5,
30
+ "use_joint_angle_action_input": false
31
+ },
32
+ "body_parts": {
33
+ "legs": {
34
+ "type" : "JOINT_POSITION",
35
+ "input_max": 100,
36
+ "input_min": -100,
37
+ "input_type": "absolute",
38
+ "output_max": 100,
39
+ "output_min": -100,
40
+ "kd": [2, 2, 2, 4, 2, 2, 2, 2, 2, 4, 2, 2],
41
+ "kv": 0,
42
+ "kp": [150, 150, 150, 300, 40, 40, 150, 150, 150, 200, 40, 40],
43
+ "velocity_limits": [-1,1],
44
+ "kp_limits": [0, 1000],
45
+ "interpolation": null,
46
+ "ramp_ratio": 0.2,
47
+ "use_torque_compensation": false,
48
+ "use_external_torque_compensation": true,
49
+ "desired_torque_as_acceleration": false
50
+ },
51
+ "arms": {
52
+ "left": {
53
+ "type" : "JOINT_POSITION",
54
+ "input_max": 100,
55
+ "input_min": -100,
56
+ "input_type": "absolute",
57
+ "output_max": 100,
58
+ "output_min": -100,
59
+ "kd": [5.0, 5.0, 2.0, 2.0, 2.0, 2.0, 2.0],
60
+ "kp": [100, 100, 40, 40, 20, 20, 20],
61
+ "velocity_limits": [-1,1],
62
+ "kp_limits": [0, 1000],
63
+ "interpolation": null,
64
+ "ramp_ratio": 0.2,
65
+ "gripper": {
66
+ "type": "GRIP",
67
+ "use_action_scaling": false
68
+ },
69
+ "use_torque_compensation": false,
70
+ "use_external_torque_compensation": true,
71
+ "desired_torque_as_acceleration": false
72
+ },
73
+ "right": {
74
+ "type" : "JOINT_POSITION",
75
+ "input_max": 100,
76
+ "input_min": -100,
77
+ "input_type": "absolute",
78
+ "output_max": 100,
79
+ "output_min": -100,
80
+ "kd": [5.0, 5.0, 2.0, 2.0, 2.0, 2.0, 2.0],
81
+ "kp": [100, 100, 40, 40, 20, 20, 20],
82
+ "velocity_limits": [-1,1],
83
+ "kp_limits": [0, 1000],
84
+ "interpolation": null,
85
+ "ramp_ratio": 0.2,
86
+ "gripper": {
87
+ "type": "GRIP",
88
+ "use_action_scaling": false
89
+ },
90
+ "use_torque_compensation": false,
91
+ "use_external_torque_compensation": true,
92
+ "desired_torque_as_acceleration": false
93
+ }
94
+ },
95
+ "torso": {
96
+ "type" : "JOINT_POSITION",
97
+ "input_max": 100,
98
+ "input_min": -100,
99
+ "input_type": "absolute",
100
+ "output_max": 100,
101
+ "output_min": -100,
102
+ "kd": 5.0,
103
+ "kp": 250.0,
104
+ "velocity_limits": [-1,1],
105
+ "kp_limits": [0, 1000],
106
+ "interpolation": null,
107
+ "ramp_ratio": 0.2,
108
+ "use_torque_compensation": false,
109
+ "use_external_torque_compensation": true,
110
+ "desired_torque_as_acceleration": false
111
+ },
112
+ "base": {
113
+ "type": "JOINT_VELOCITY_AND_POSITION",
114
+ "interpolation": "null"
115
+ }
116
+ }
117
+ }
GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from .box_pattern_object import BoxPatternObject
2
+ from .needle import NeedleObject
3
+ from .ring_tripod import RingTripodObject
4
+ from .bin import Bin
5
+ from .lid import Lid
6
+ from .pot_with_handles import PotWithHandlesObject
GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite/bin.py ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+
3
+ import robosuite.utils.transform_utils as T
4
+ from robosuite.models.objects import CompositeObject
5
+ from robosuite.utils.mjcf_utils import CustomMaterial, add_to_dict
6
+
7
+
8
+ class Bin(CompositeObject):
9
+ """
10
+ Generates a four-walled bin container with an open top.
11
+ Args:
12
+ name (str): Name of this Bin object
13
+ bin_size (3-array): (x,y,z) full size of bin
14
+ wall_thickness (float): How thick to make walls of bin
15
+ transparent_walls (bool): If True, walls will be semi-translucent
16
+ friction (3-array or None): If specified, sets friction values for this bin. None results in default values
17
+ density (float): Density value to use for all geoms. Defaults to 1000
18
+ use_texture (bool): If true, geoms will be defined by realistic textures and rgba values will be ignored
19
+ rgba (4-array or None): If specified, sets rgba values for all geoms. None results in default values
20
+ material: If specified, use this material
21
+ upside_down (bool): if True, construct and initialize the Bin so the bottom geom is at the top
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ name,
27
+ bin_size=(0.3, 0.3, 0.15),
28
+ wall_thickness=0.01,
29
+ transparent_walls=True,
30
+ friction=None,
31
+ density=1000.0,
32
+ use_texture=True,
33
+ rgba=(0.2, 0.1, 0.0, 1.0),
34
+ material=None,
35
+ upside_down=False,
36
+ add_second_base=False,
37
+ transparent_base=False,
38
+ ):
39
+ # Set name
40
+ self._name = name
41
+
42
+ # Set object attributes
43
+ self.bin_size = np.array(bin_size)
44
+ self.wall_thickness = wall_thickness
45
+ self.transparent_walls = transparent_walls
46
+ self.friction = friction if friction is None else np.array(friction)
47
+ self.density = density
48
+ self.use_texture = use_texture
49
+ self.rgba = rgba
50
+ self.bin_mat_name = "dark_wood_mat"
51
+
52
+ # if box should be constructed and initialized upside down
53
+ self.upside_down = upside_down
54
+
55
+ # if box should have a second base (so it will be a closed box)
56
+ self.add_second_base = add_second_base
57
+ if self.add_second_base:
58
+ assert not self.upside_down
59
+
60
+ # if base(s) should be transparent
61
+ self.transparent_base = transparent_base
62
+
63
+ self.has_material = material is not None
64
+ if self.has_material:
65
+ assert isinstance(material, CustomMaterial)
66
+ self.material = material
67
+ self.bin_mat_name = self.material.mat_attrib["name"]
68
+ else:
69
+ # default material
70
+ tex_attrib = {
71
+ "type": "cube",
72
+ }
73
+ mat_attrib = {
74
+ "texrepeat": "3 3",
75
+ "specular": "0.4",
76
+ "shininess": "0.1",
77
+ }
78
+ bin_mat = CustomMaterial(
79
+ texture="WoodDark",
80
+ tex_name="dark_wood",
81
+ mat_name=self.bin_mat_name,
82
+ tex_attrib=tex_attrib,
83
+ mat_attrib=mat_attrib,
84
+ )
85
+ self.material = bin_mat
86
+
87
+ # Element references
88
+ self._base_geom = "base"
89
+ if self.add_second_base:
90
+ self._second_base_geom = "base2"
91
+
92
+ # Other private attributes
93
+ self._important_sites = {}
94
+
95
+ # Create dictionary of values to create geoms for composite object and run super init
96
+ super().__init__(**self._get_geom_attrs())
97
+
98
+ # Define materials we want to use for this object
99
+ self.append_material(self.material)
100
+
101
+ def _get_geom_attrs(self):
102
+ """
103
+ Creates geom elements that will be passed to superclass CompositeObject constructor
104
+ Returns:
105
+ dict: args to be used by CompositeObject to generate geoms
106
+ """
107
+ # Initialize dict of obj args that we'll pass to the CompositeObject constructor
108
+ base_args = {
109
+ "total_size": self.bin_size / 2.0,
110
+ "name": self.name,
111
+ "locations_relative_to_center": True,
112
+ "obj_types": "all",
113
+ "density": self.density,
114
+ }
115
+ obj_args = {}
116
+
117
+ # Base(s)
118
+ base_geom_loc = (0, 0, -(self.bin_size[2] - self.wall_thickness) / 2)
119
+ if self.upside_down:
120
+ base_geom_loc = (
121
+ base_geom_loc[0],
122
+ base_geom_loc[1],
123
+ -1.0 * base_geom_loc[2],
124
+ )
125
+ if self.transparent_base:
126
+ base_rgba = (1.0, 1.0, 1.0, 0.3)
127
+ base_mat = None
128
+ else:
129
+ base_rgba = None if self.use_texture else self.rgba
130
+ base_mat = self.bin_mat_name if self.use_texture else None
131
+
132
+ base_geom_names = [self._base_geom]
133
+ base_geom_locs = [base_geom_loc]
134
+ if self.add_second_base:
135
+ base_geom_names.append(self._second_base_geom)
136
+ base_geom_locs.append((base_geom_loc[0], base_geom_loc[1], -1.0 * base_geom_loc[2]))
137
+
138
+ for base_g_name, base_g_loc in zip(base_geom_names, base_geom_locs):
139
+ add_to_dict(
140
+ dic=obj_args,
141
+ geom_types="box",
142
+ geom_locations=base_g_loc,
143
+ geom_quats=(1, 0, 0, 0),
144
+ geom_sizes=(
145
+ np.array((self.bin_size[0], self.bin_size[1], self.wall_thickness))
146
+ - np.array((self.wall_thickness, self.wall_thickness, 0))
147
+ )
148
+ / 2,
149
+ geom_names=base_g_name,
150
+ geom_rgbas=base_rgba,
151
+ geom_materials=base_mat,
152
+ geom_frictions=self.friction,
153
+ )
154
+
155
+ # Walls
156
+ x_vals = np.array(
157
+ [
158
+ 0,
159
+ -(self.bin_size[0] - self.wall_thickness) / 2,
160
+ 0,
161
+ (self.bin_size[0] - self.wall_thickness) / 2,
162
+ ]
163
+ )
164
+ y_vals = np.array(
165
+ [
166
+ -(self.bin_size[1] - self.wall_thickness) / 2,
167
+ 0,
168
+ (self.bin_size[1] - self.wall_thickness) / 2,
169
+ 0,
170
+ ]
171
+ )
172
+ w_vals = np.array([self.bin_size[0], self.bin_size[1], self.bin_size[0], self.bin_size[1]])
173
+ r_vals = np.array([np.pi / 2, 0, -np.pi / 2, np.pi])
174
+ if self.transparent_walls:
175
+ wall_rgba = (1.0, 1.0, 1.0, 0.3)
176
+ wall_mat = None
177
+ else:
178
+ wall_rgba = None if self.use_texture else self.rgba
179
+ wall_mat = self.bin_mat_name if self.use_texture else None
180
+ for i, (x, y, w, r) in enumerate(zip(x_vals, y_vals, w_vals, r_vals)):
181
+ add_to_dict(
182
+ dic=obj_args,
183
+ geom_types="box",
184
+ geom_locations=(x, y, 0),
185
+ geom_quats=T.convert_quat(T.axisangle2quat(np.array([0, 0, r])), to="wxyz"),
186
+ geom_sizes=(self.wall_thickness / 2, w / 2, self.bin_size[2] / 2),
187
+ geom_names=f"wall{i}",
188
+ geom_rgbas=wall_rgba,
189
+ geom_materials=wall_mat,
190
+ geom_frictions=self.friction,
191
+ )
192
+
193
+ # Add back in base args and site args
194
+ obj_args.update(base_args)
195
+
196
+ # Return this dict
197
+ return obj_args
198
+
199
+ @property
200
+ def base_geoms(self):
201
+ """
202
+ Returns:
203
+ list of str: geom names corresponding to bin base
204
+ """
205
+ return [self.correct_naming(self._base_geom)]
GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite/box_pattern_object.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+
3
+ from robosuite.models.objects import CompositeObject
4
+ from robosuite.utils.mjcf_utils import add_to_dict
5
+ import robosuite.utils.transform_utils as T
6
+
7
+
8
+ class BoxPatternObject(CompositeObject):
9
+ """
10
+ Generates shapes by using a pattern of unit-size boxes.
11
+
12
+ Args:
13
+ name (str): Name of this Needle object
14
+ """
15
+
16
+ def __init__(
17
+ self,
18
+ name,
19
+ unit_size,
20
+ pattern,
21
+ rgba=None,
22
+ material=None,
23
+ density=100.0,
24
+ # solref=[0.02, 1.],
25
+ # solimp=[0.9, 0.95, 0.001],
26
+ friction=None,
27
+ ):
28
+ """
29
+ Args:
30
+ unit_size (3d array / list): size of each unit block in each dimension
31
+
32
+ pattern (3d array / list): array of normalized sizes specifying the
33
+ geometry of the shape. A "0" indicates the absence of a cube and
34
+ a "1" indicates the presence of a full unit block. The dimensions
35
+ correspond to z, x, and y respectively.
36
+ """
37
+ self._name = name
38
+ self.rgba = rgba
39
+ self.material = material
40
+ self.density = density
41
+ self.friction = friction
42
+
43
+ # number of blocks in z, x, and y
44
+ self.pattern = np.array(pattern)
45
+ self.nz, self.nx, self.ny = self.pattern.shape
46
+ self.unit_size = unit_size
47
+ self.total_size = [
48
+ self.nx * unit_size[0],
49
+ self.ny * unit_size[1],
50
+ self.nz * unit_size[2],
51
+ ]
52
+
53
+ # Other private attributes
54
+ self._important_sites = {}
55
+
56
+ # Create dictionary of values to create geoms for composite object and run super init
57
+ super().__init__(**self._get_geom_attrs())
58
+
59
+ # Define materials we want to use for this object
60
+ if self.material is not None:
61
+ self.append_material(self.material)
62
+
63
+ def _get_geom_attrs(self):
64
+ """
65
+ Creates geom elements that will be passed to superclass CompositeObject constructor
66
+
67
+ Returns:
68
+ dict: args to be used by CompositeObject to generate geoms
69
+ """
70
+ # Initialize dict of obj args that we'll pass to the CompositeObject constructor
71
+ base_args = {
72
+ "total_size": self.total_size,
73
+ "name": self.name,
74
+ "locations_relative_to_center": False,
75
+ "obj_types": "all",
76
+ "density": self.density,
77
+ }
78
+ obj_args = {}
79
+
80
+ geom_locations = []
81
+ geom_sizes = []
82
+ geom_names = []
83
+ nz, nx, ny = self.pattern.shape
84
+ for k in range(nz):
85
+ for i in range(nx):
86
+ for j in range(ny):
87
+ if self.pattern[k, i, j] > 0:
88
+ geom_sizes.append(
89
+ [
90
+ self.unit_size[0],
91
+ self.unit_size[1],
92
+ self.unit_size[2],
93
+ ]
94
+ )
95
+ geom_locations.append(
96
+ [
97
+ i * 2.0 * self.unit_size[0],
98
+ j * 2.0 * self.unit_size[1],
99
+ k * 2.0 * self.unit_size[2],
100
+ ]
101
+ )
102
+ geom_names.append("{}_{}_{}".format(k, i, j))
103
+
104
+ # geom_rgbas = [rgba for _ in geom_locations]
105
+ # geom_frictions = [friction for _ in geom_locations]
106
+ for i in range(len(geom_locations)):
107
+ add_to_dict(
108
+ dic=obj_args,
109
+ geom_types="box",
110
+ # needle geom needs to be offset from boundary in (x, z)
111
+ geom_locations=tuple(geom_locations[i]),
112
+ geom_quats=(1, 0, 0, 0),
113
+ geom_sizes=tuple(geom_sizes[i]),
114
+ geom_names=geom_names[i],
115
+ geom_rgbas=self.rgba,
116
+ geom_materials=self.material.name if self.material is not None else None,
117
+ geom_frictions=None,
118
+ )
119
+
120
+ # Add back in base args and site args
121
+ obj_args.update(base_args)
122
+
123
+ # Return this dict
124
+ return obj_args
GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite/lid.py ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+
3
+ import robosuite.utils.transform_utils as T
4
+ from robosuite.models.objects import CompositeObject
5
+ from robosuite.utils.mjcf_utils import CustomMaterial, add_to_dict
6
+
7
+
8
+ class Lid(CompositeObject):
9
+ """
10
+ Generates a square lid with a simple handle.
11
+ Args:
12
+ name (str): Name of this Lid object
13
+ lid_size (3-array): (length, width, thickness) of lid
14
+ handle_size (3-array): (thickness, length, height) of handle
15
+ transparent (bool): If True, lid will be semi-translucent
16
+ friction (3-array or None): If specified, sets friction values for this lid. None results in default values
17
+ density (float): Density value to use for all geoms. Defaults to 1000
18
+ use_texture (bool): If true, geoms will be defined by realistic textures and rgba values will be ignored
19
+ rgba (4-array or None): If specified, sets rgba values for all geoms. None results in default values
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ name,
25
+ lid_size=(0.3, 0.3, 0.01),
26
+ handle_size=(0.02, 0.08, 0.03),
27
+ transparent=True,
28
+ friction=None,
29
+ density=250.0,
30
+ use_texture=True,
31
+ rgba=(0.2, 0.1, 0.0, 1.0),
32
+ ):
33
+ # Set name
34
+ self._name = name
35
+
36
+ # Set object attributes
37
+ self.lid_size = np.array(lid_size)
38
+ self.handle_size = np.array(handle_size)
39
+ self.transparent = transparent
40
+ self.friction = friction if friction is None else np.array(friction)
41
+ self.density = density
42
+ self.use_texture = use_texture
43
+ self.rgba = rgba
44
+ self.lid_mat_name = "dark_wood_mat"
45
+
46
+ # Element references
47
+ self._handle_geom = "handle"
48
+
49
+ # Other private attributes
50
+ self._important_sites = {}
51
+
52
+ # Create dictionary of values to create geoms for composite object and run super init
53
+ super().__init__(**self._get_geom_attrs())
54
+
55
+ # Define materials we want to use for this object
56
+ tex_attrib = {
57
+ "type": "cube",
58
+ }
59
+ mat_attrib = {
60
+ "texrepeat": "3 3",
61
+ "specular": "0.4",
62
+ "shininess": "0.1",
63
+ }
64
+ lid_mat = CustomMaterial(
65
+ texture="WoodDark",
66
+ tex_name="dark_wood",
67
+ mat_name=self.lid_mat_name,
68
+ tex_attrib=tex_attrib,
69
+ mat_attrib=mat_attrib,
70
+ )
71
+ self.append_material(lid_mat)
72
+
73
+ def _get_geom_attrs(self):
74
+ """
75
+ Creates geom elements that will be passed to superclass CompositeObject constructor
76
+ Returns:
77
+ dict: args to be used by CompositeObject to generate geoms
78
+ """
79
+ full_height = self.lid_size[2] + self.handle_size[2]
80
+ full_size = np.array([self.lid_size[0], self.lid_size[1], full_height])
81
+ # Initialize dict of obj args that we'll pass to the CompositeObject constructor
82
+ base_args = {
83
+ "total_size": full_size / 2.0,
84
+ "name": self.name,
85
+ "locations_relative_to_center": True,
86
+ "obj_types": "all",
87
+ }
88
+ obj_args = {}
89
+
90
+ # Top
91
+ if self.transparent:
92
+ top_rgba = (1.0, 1.0, 1.0, 0.3)
93
+ top_mat = None
94
+ else:
95
+ top_rgba = None if self.use_texture else self.rgba
96
+ top_mat = self.lid_mat_name if self.use_texture else None
97
+ add_to_dict(
98
+ dic=obj_args,
99
+ geom_types="box",
100
+ geom_locations=(0, 0, (-full_size[2] + self.lid_size[2]) / 2),
101
+ geom_quats=(1, 0, 0, 0),
102
+ geom_sizes=np.array((full_size[0], full_size[1], self.lid_size[2])) / 2,
103
+ geom_names="top",
104
+ geom_rgbas=top_rgba,
105
+ geom_materials=top_mat,
106
+ geom_frictions=self.friction,
107
+ density=self.density,
108
+ )
109
+
110
+ # Handle
111
+ add_to_dict(
112
+ dic=obj_args,
113
+ geom_types="box",
114
+ geom_locations=(0, 0, (full_size[2] - self.handle_size[2]) / 2),
115
+ geom_quats=(1, 0, 0, 0),
116
+ geom_sizes=self.handle_size / 2,
117
+ geom_names=self._handle_geom,
118
+ geom_rgbas=None if self.use_texture else self.rgba,
119
+ geom_materials=self.lid_mat_name if self.use_texture else None,
120
+ geom_frictions=self.friction,
121
+ density=self.density * 2,
122
+ )
123
+
124
+ # Add back in base args and site args
125
+ obj_args.update(base_args)
126
+
127
+ # Return this dict
128
+ return obj_args
129
+
130
+ @property
131
+ def handle_geoms(self):
132
+ """
133
+ Returns:
134
+ list of str: geom names corresponding to lid handle
135
+ """
136
+ return [self.correct_naming(self._handle_geom)]
GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite/needle.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+
3
+ from robosuite.models.objects import CompositeObject
4
+ from robosuite.utils.mjcf_utils import add_to_dict, CustomMaterial
5
+ import robosuite.utils.transform_utils as T
6
+
7
+
8
+ class NeedleObject(CompositeObject):
9
+ """
10
+ Generates a needle with a handle (used in Threading task)
11
+
12
+ Args:
13
+ name (str): Name of this Needle object
14
+ """
15
+
16
+ def __init__(
17
+ self,
18
+ name,
19
+ ):
20
+
21
+ ### TODO: make this object more general (with more args and configuration options) later ###
22
+
23
+ # Set object attributes
24
+ self._name = name
25
+ self.needle_mat_name = "darkwood_mat"
26
+
27
+ # Other private attributes
28
+ self._important_sites = {}
29
+
30
+ # Create dictionary of values to create geoms for composite object and run super init
31
+ super().__init__(**self._get_geom_attrs())
32
+
33
+ # Define materials we want to use for this object
34
+ tex_attrib = {
35
+ "type": "cube",
36
+ }
37
+ mat_attrib = {
38
+ "texrepeat": "1 1",
39
+ "specular": "0.4",
40
+ "shininess": "0.1",
41
+ }
42
+ needle_mat = CustomMaterial(
43
+ texture="WoodDark",
44
+ tex_name="darkwood",
45
+ mat_name="darkwood_mat",
46
+ tex_attrib=tex_attrib,
47
+ mat_attrib=mat_attrib,
48
+ )
49
+ self.append_material(needle_mat)
50
+
51
+ def _get_geom_attrs(self):
52
+ """
53
+ Creates geom elements that will be passed to superclass CompositeObject constructor
54
+
55
+ Returns:
56
+ dict: args to be used by CompositeObject to generate geoms
57
+ """
58
+ # Initialize dict of obj args that we'll pass to the CompositeObject constructor
59
+ base_args = {
60
+ "total_size": [0.02, 0.08, 0.02],
61
+ "name": self.name,
62
+ "locations_relative_to_center": False,
63
+ "obj_types": "all",
64
+ "density": 100.0,
65
+ }
66
+ obj_args = {}
67
+
68
+ # make a skinny needle object with a large handle
69
+ needle_size = [0.005, 0.06, 0.005]
70
+ handle_size = [0.02, 0.02, 0.02]
71
+
72
+ # Needle
73
+ add_to_dict(
74
+ dic=obj_args,
75
+ geom_types="box",
76
+ # needle geom needs to be offset from boundary in (x, z)
77
+ geom_locations=(
78
+ (handle_size[0] - needle_size[0]),
79
+ 0.0,
80
+ (handle_size[2] - needle_size[2]),
81
+ ),
82
+ geom_quats=(1, 0, 0, 0),
83
+ geom_sizes=tuple(needle_size),
84
+ geom_names="needle",
85
+ geom_rgbas=None,
86
+ geom_materials=self.needle_mat_name,
87
+ # make the needle low friction to ensure easy insertion
88
+ geom_frictions=(0.3, 5e-3, 1e-4),
89
+ )
90
+
91
+ # Handle
92
+ add_to_dict(
93
+ dic=obj_args,
94
+ geom_types="box",
95
+ # handle geom needs to be offset in y
96
+ geom_locations=(0.0, 2.0 * needle_size[1], 0.0),
97
+ geom_quats=(1, 0, 0, 0),
98
+ geom_sizes=tuple(handle_size),
99
+ geom_names="handle",
100
+ geom_rgbas=None,
101
+ geom_materials=self.needle_mat_name,
102
+ geom_frictions=None,
103
+ )
104
+
105
+ # Add back in base args and site args
106
+ obj_args.update(base_args)
107
+
108
+ # Return this dict
109
+ return obj_args
GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite/pot_with_handles.py ADDED
@@ -0,0 +1,396 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+
3
+ import robosuite.utils.transform_utils as T
4
+ from robosuite.models.objects import CompositeObject
5
+ from robosuite.utils.mjcf_utils import (
6
+ BLUE,
7
+ GREEN,
8
+ RED,
9
+ CustomMaterial,
10
+ add_to_dict,
11
+ array_to_string,
12
+ )
13
+
14
+
15
+ class PotWithHandlesObject(CompositeObject):
16
+ """
17
+ Generates the Pot object with side handles (used in TwoArmLift)
18
+
19
+ Args:
20
+ name (str): Name of this Pot object
21
+
22
+ body_half_size (3-array of float): If specified, defines the (x,y,z) half-dimensions of the main pot
23
+ body. Otherwise, defaults to [0.07, 0.07, 0.07]
24
+
25
+ handle_radius (float): Determines the pot handle radius
26
+
27
+ handle_length (float): Determines the pot handle length
28
+
29
+ handle_width (float): Determines the pot handle width
30
+
31
+ handle_friction (float): Friction value to use for pot handles. Defauls to 1.0
32
+
33
+ density (float): Density value to use for all geoms. Defaults to 1000
34
+
35
+ use_texture (bool): If true, geoms will be defined by realistic textures and rgba values will be ignored
36
+
37
+ rgba_body (4-array or None): If specified, sets pot body rgba values
38
+
39
+ rgba_handle_0 (4-array or None): If specified, sets handle 0 rgba values
40
+
41
+ rgba_handle_1 (4-array or None): If specified, sets handle 1 rgba values
42
+
43
+ solid_handle (bool): If true, uses a single geom to represent the handle
44
+
45
+ thickness (float): How thick to make the pot body walls
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ name,
51
+ body_half_size=(0.07, 0.07, 0.07),
52
+ handle_radius=0.01,
53
+ handle_length=0.09,
54
+ handle_width=0.09,
55
+ handle_friction=1.0,
56
+ density=1000,
57
+ use_texture=True,
58
+ rgba_body=None,
59
+ rgba_handle_0=None,
60
+ rgba_handle_1=None,
61
+ solid_handle=False,
62
+ thickness=0.01, # For body
63
+ ):
64
+ # Set name
65
+ self._name = name
66
+
67
+ # Set object attributes
68
+ self.body_half_size = np.array(body_half_size)
69
+ self.thickness = thickness
70
+ self.handle_radius = handle_radius
71
+ self.handle_length = handle_length
72
+ self.handle_width = handle_width
73
+ self.handle_friction = handle_friction
74
+ self.density = density
75
+ self.use_texture = use_texture
76
+ self.rgba_body = np.array(rgba_body) if rgba_body else RED
77
+ self.rgba_handle_0 = np.array(rgba_handle_0) if rgba_handle_0 else GREEN
78
+ self.rgba_handle_1 = np.array(rgba_handle_1) if rgba_handle_1 else BLUE
79
+ self.solid_handle = solid_handle
80
+
81
+ # Element references to be filled when generated
82
+ self._handle0_geoms = None
83
+ self._handle1_geoms = None
84
+ self.pot_base = None
85
+
86
+ # Other private attributes
87
+ self._important_sites = {}
88
+
89
+ # Create dictionary of values to create geoms for composite object and run super init
90
+ super().__init__(**self._get_geom_attrs())
91
+
92
+ # Define materials we want to use for this object
93
+ tex_attrib = {
94
+ "type": "cube",
95
+ }
96
+ mat_attrib = {
97
+ "texrepeat": "1 1",
98
+ "specular": "0.4",
99
+ "shininess": "0.1",
100
+ }
101
+ redwood = CustomMaterial(
102
+ texture="WoodRed",
103
+ tex_name="redwood",
104
+ mat_name="pot_mat",
105
+ tex_attrib=tex_attrib,
106
+ mat_attrib=mat_attrib,
107
+ )
108
+ greenwood = CustomMaterial(
109
+ texture="WoodGreen",
110
+ tex_name="greenwood",
111
+ mat_name="handle0_mat",
112
+ tex_attrib=tex_attrib,
113
+ mat_attrib=mat_attrib,
114
+ )
115
+ bluewood = CustomMaterial(
116
+ texture="WoodBlue",
117
+ tex_name="bluewood",
118
+ mat_name="handle1_mat",
119
+ tex_attrib=tex_attrib,
120
+ mat_attrib=mat_attrib,
121
+ )
122
+ self.append_material(redwood)
123
+ self.append_material(greenwood)
124
+ self.append_material(bluewood)
125
+
126
+ def _get_geom_attrs(self):
127
+ """
128
+ Creates geom elements that will be passed to superclass CompositeObject constructor
129
+
130
+ Returns:
131
+ dict: args to be used by CompositeObject to generate geoms
132
+ """
133
+ full_size = np.array(
134
+ (
135
+ self.body_half_size,
136
+ self.body_half_size + self.handle_length * 2,
137
+ self.body_half_size,
138
+ )
139
+ )
140
+ # Initialize dict of obj args that we'll pass to the CompositeObject constructor
141
+ base_args = {
142
+ "total_size": full_size / 2.0,
143
+ "name": self.name,
144
+ "locations_relative_to_center": True,
145
+ "obj_types": "all",
146
+ }
147
+ site_attrs = []
148
+ obj_args = {}
149
+
150
+ # Initialize geom lists
151
+ self._handle0_geoms = []
152
+ self._handle1_geoms = []
153
+
154
+ # Add main pot body
155
+ # Base geom
156
+ name = f"base"
157
+ self.pot_base = [name]
158
+ add_to_dict(
159
+ dic=obj_args,
160
+ geom_types="box",
161
+ geom_locations=(0, 0, -self.body_half_size[2] + self.thickness / 2),
162
+ geom_quats=(1, 0, 0, 0),
163
+ geom_sizes=np.array(
164
+ [self.body_half_size[0], self.body_half_size[1], self.thickness / 2]
165
+ ),
166
+ geom_names=name,
167
+ geom_rgbas=None if self.use_texture else self.rgba_body,
168
+ geom_materials="pot_mat" if self.use_texture else None,
169
+ geom_frictions=None,
170
+ density=self.density,
171
+ )
172
+
173
+ # Walls
174
+ x_off = np.array(
175
+ [
176
+ 0,
177
+ -(self.body_half_size[0] - self.thickness / 2),
178
+ 0,
179
+ self.body_half_size[0] - self.thickness / 2,
180
+ ]
181
+ )
182
+ y_off = np.array(
183
+ [
184
+ -(self.body_half_size[1] - self.thickness / 2),
185
+ 0,
186
+ self.body_half_size[1] - self.thickness / 2,
187
+ 0,
188
+ ]
189
+ )
190
+ w_vals = np.array(
191
+ [
192
+ self.body_half_size[0],
193
+ self.body_half_size[1],
194
+ self.body_half_size[0],
195
+ self.body_half_size[1],
196
+ ]
197
+ )
198
+ r_vals = np.array([np.pi / 2, 0, -np.pi / 2, np.pi])
199
+ for i, (x, y, w, r) in enumerate(zip(x_off, y_off, w_vals, r_vals)):
200
+ add_to_dict(
201
+ dic=obj_args,
202
+ geom_types="box",
203
+ geom_locations=(x, y, 0),
204
+ geom_quats=T.convert_quat(T.axisangle2quat(np.array([0, 0, r])), to="wxyz"),
205
+ geom_sizes=np.array([self.thickness / 2, w, self.body_half_size[2]]),
206
+ geom_names=f"body{i}",
207
+ geom_rgbas=None if self.use_texture else self.rgba_body,
208
+ geom_materials="pot_mat" if self.use_texture else None,
209
+ geom_frictions=None,
210
+ density=self.density,
211
+ )
212
+
213
+ # Add handles
214
+ main_bar_size = np.array(
215
+ [
216
+ self.handle_width / 2 + self.handle_radius,
217
+ self.handle_radius,
218
+ self.handle_radius,
219
+ ]
220
+ )
221
+ side_bar_size = np.array([self.handle_radius, self.handle_length / 2, self.handle_radius])
222
+ handle_z = self.body_half_size[2] - self.handle_radius
223
+ for i, (g_list, handle_side, rgba) in enumerate(
224
+ zip(
225
+ [self._handle0_geoms, self._handle1_geoms],
226
+ [1.0, -1.0],
227
+ [self.rgba_handle_0, self.rgba_handle_1],
228
+ )
229
+ ):
230
+ handle_center = np.array(
231
+ (
232
+ 0,
233
+ handle_side * (self.body_half_size[1] + self.handle_length),
234
+ handle_z,
235
+ )
236
+ )
237
+ # Solid handle case
238
+ if self.solid_handle:
239
+ handle_center = np.array(
240
+ (
241
+ 0,
242
+ handle_side * (self.body_half_size[1] + self.handle_length / 2),
243
+ handle_z,
244
+ )
245
+ )
246
+ name = f"handle{i}"
247
+ g_list.append(name)
248
+ add_to_dict(
249
+ dic=obj_args,
250
+ geom_types="box",
251
+ geom_locations=handle_center,
252
+ geom_quats=(1, 0, 0, 0),
253
+ geom_sizes=np.array(
254
+ [
255
+ self.handle_width / 2,
256
+ self.handle_length / 2,
257
+ self.handle_radius,
258
+ ]
259
+ ),
260
+ geom_names=name,
261
+ geom_rgbas=None if self.use_texture else rgba,
262
+ geom_materials=f"handle{i}_mat" if self.use_texture else None,
263
+ geom_frictions=(self.handle_friction, 0.005, 0.0001),
264
+ density=self.density,
265
+ )
266
+ # Hollow handle case
267
+ else:
268
+ # Center bar
269
+ name = f"handle{i}_c"
270
+ g_list.append(name)
271
+ add_to_dict(
272
+ dic=obj_args,
273
+ geom_types="box",
274
+ geom_locations=handle_center,
275
+ geom_quats=(1, 0, 0, 0),
276
+ geom_sizes=main_bar_size,
277
+ geom_names=name,
278
+ geom_rgbas=None if self.use_texture else rgba,
279
+ geom_materials=f"handle{i}_mat" if self.use_texture else None,
280
+ geom_frictions=(self.handle_friction, 0.005, 0.0001),
281
+ density=self.density,
282
+ )
283
+ # Side bars
284
+ for bar_side, suffix in zip([-1.0, 1.0], ["-", "+"]):
285
+ name = f"handle{i}_{suffix}"
286
+ g_list.append(name)
287
+ add_to_dict(
288
+ dic=obj_args,
289
+ geom_types="box",
290
+ geom_locations=(
291
+ bar_side * self.handle_width / 2,
292
+ handle_side * (self.body_half_size[1] + self.handle_length / 2),
293
+ handle_z,
294
+ ),
295
+ geom_quats=(1, 0, 0, 0),
296
+ geom_sizes=side_bar_size,
297
+ geom_names=name,
298
+ geom_rgbas=None if self.use_texture else rgba,
299
+ geom_materials=f"handle{i}_mat" if self.use_texture else None,
300
+ geom_frictions=(self.handle_friction, 0.005, 0.0001),
301
+ density=self.density,
302
+ )
303
+ # Add relevant site
304
+ handle_site = self.get_site_attrib_template()
305
+ handle_name = f"handle{i}"
306
+ handle_site.update(
307
+ {
308
+ "name": handle_name,
309
+ "pos": array_to_string(handle_center - handle_side * np.array([0, 0.005, 0])),
310
+ "size": "0.005",
311
+ "rgba": rgba,
312
+ }
313
+ )
314
+ site_attrs.append(handle_site)
315
+ # Add to important sites
316
+ self._important_sites[f"handle{i}"] = self.naming_prefix + handle_name
317
+
318
+ # Add pot body site
319
+ pot_site = self.get_site_attrib_template()
320
+ center_name = "center"
321
+ pot_site.update(
322
+ {
323
+ "name": center_name,
324
+ "size": "0.005",
325
+ }
326
+ )
327
+ site_attrs.append(pot_site)
328
+ # Add to important sites
329
+ self._important_sites["center"] = self.naming_prefix + center_name
330
+
331
+ # Add back in base args and site args
332
+ obj_args.update(base_args)
333
+ obj_args["sites"] = site_attrs # All sites are part of main (top) body
334
+
335
+ # Return this dict
336
+ return obj_args
337
+
338
+ @property
339
+ def handle_distance(self):
340
+ """
341
+ Calculates how far apart the handles are
342
+
343
+ Returns:
344
+ float: handle distance
345
+ """
346
+ return self.body_half_size[1] * 2 + self.handle_length * 2
347
+
348
+ @property
349
+ def handle0_geoms(self):
350
+ """
351
+ Returns:
352
+ list of str: geom names corresponding to handle0 (green handle)
353
+ """
354
+ return self.correct_naming(self._handle0_geoms)
355
+
356
+ @property
357
+ def handle1_geoms(self):
358
+ """
359
+ Returns:
360
+ list of str: geom names corresponding to handle1 (blue handle)
361
+ """
362
+ return self.correct_naming(self._handle1_geoms)
363
+
364
+ @property
365
+ def handle_geoms(self):
366
+ """
367
+ Returns:
368
+ list of str: geom names corresponding to both handles
369
+ """
370
+ return self.handle0_geoms + self.handle1_geoms
371
+
372
+ @property
373
+ def important_sites(self):
374
+ """
375
+ Returns:
376
+ dict: In addition to any default sites for this object, also provides the following entries
377
+
378
+ :`'handle0'`: Name of handle0 location site
379
+ :`'handle1'`: Name of handle1 location site
380
+ """
381
+ # Get dict from super call and add to it
382
+ dic = super().important_sites
383
+ dic.update(self._important_sites)
384
+ return dic
385
+
386
+ @property
387
+ def bottom_offset(self):
388
+ return np.array([0, 0, -1 * self.body_half_size[2]])
389
+
390
+ @property
391
+ def top_offset(self):
392
+ return np.array([0, 0, self.body_half_size[2]])
393
+
394
+ @property
395
+ def horizontal_radius(self):
396
+ return np.sqrt(2) * (max(self.body_half_size) + self.handle_length)
GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite/ring_tripod.py ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+
3
+ from robosuite.models.objects import CompositeObject
4
+ from robosuite.utils.mjcf_utils import add_to_dict
5
+ from robosuite.utils.mjcf_utils import CustomMaterial
6
+ import robosuite.utils.transform_utils as T
7
+
8
+
9
+ class RingTripodObject(CompositeObject):
10
+ """
11
+ Generates a tripod base with a small ring for threading a needle through it (used in Threading task)
12
+
13
+ Args:
14
+ name (str): Name of this RingTripod object
15
+ """
16
+
17
+ def __init__(
18
+ self,
19
+ name,
20
+ ):
21
+
22
+ ### TODO: make this object more general (with more args and configuration options) later ###
23
+
24
+ # Set object attributes
25
+ self._name = name
26
+ self.tripod_mat_name = "lightwood_mat"
27
+
28
+ # Other private attributes
29
+ self._important_sites = {}
30
+
31
+ # Create dictionary of values to create geoms for composite object and run super init
32
+ super().__init__(**self._get_geom_attrs())
33
+
34
+ # Define materials we want to use for this object
35
+ tex_attrib = {
36
+ "type": "cube",
37
+ }
38
+ mat_attrib = {
39
+ "texrepeat": "1 1",
40
+ "specular": "0.4",
41
+ "shininess": "0.1",
42
+ }
43
+ tripod_mat = CustomMaterial(
44
+ texture="WoodLight",
45
+ tex_name="lightwood",
46
+ mat_name="lightwood_mat",
47
+ tex_attrib=tex_attrib,
48
+ mat_attrib=mat_attrib,
49
+ )
50
+ self.append_material(tripod_mat)
51
+
52
+ def _get_geom_attrs(self):
53
+ """
54
+ Creates geom elements that will be passed to superclass CompositeObject constructor
55
+
56
+ Returns:
57
+ dict: args to be used by CompositeObject to generate geoms
58
+ """
59
+ # Initialize dict of obj args that we'll pass to the CompositeObject constructor
60
+ total_size = (0.05, 0.05, 0.1)
61
+ base_args = {
62
+ "total_size": total_size,
63
+ "name": self.name,
64
+ "locations_relative_to_center": False,
65
+ "obj_types": "all",
66
+ "density": 100.0,
67
+ # NOTE: this lower value of solref allows the thin hole wall to avoid penetration through it
68
+ "solref": (0.02, 1.0),
69
+ "solimp": (0.9, 0.95, 0.001),
70
+ }
71
+ obj_args = {}
72
+
73
+ # pattern for threading ring
74
+ unit_size = [0.005, 0.002, 0.002]
75
+ pattern = np.ones((6, 1, 6))
76
+ for i in range(1, 5):
77
+ pattern[i][0][1:5] = np.zeros(4)
78
+ ring_size = [
79
+ unit_size[0] * pattern.shape[1],
80
+ unit_size[1] * pattern.shape[2],
81
+ unit_size[2] * pattern.shape[0],
82
+ ]
83
+ self.ring_size = np.array(ring_size)
84
+
85
+ # ring offset for where the ring is located relative to the (0, 0, 0) corner
86
+ ring_offset = [
87
+ total_size[0] - ring_size[0],
88
+ total_size[1] - ring_size[1],
89
+ 2.0 * (total_size[2] - ring_size[2]),
90
+ ]
91
+
92
+ # RING-GEOMS: use the pattern to instantiate geoms corresponding to the threading ring
93
+ nz, nx, ny = pattern.shape
94
+ self.num_ring_geoms = 0
95
+ for k in range(nz):
96
+ for i in range(nx):
97
+ for j in range(ny):
98
+ if pattern[k, i, j] > 0:
99
+ add_to_dict(
100
+ dic=obj_args,
101
+ geom_types="box",
102
+ # needle geom needs to be offset from boundary in (x, z)
103
+ geom_locations=(
104
+ (i * 2.0 * unit_size[0]) + ring_offset[0],
105
+ (j * 2.0 * unit_size[1]) + ring_offset[1],
106
+ (k * 2.0 * unit_size[2]) + ring_offset[2],
107
+ ),
108
+ geom_quats=(1, 0, 0, 0),
109
+ geom_sizes=tuple(unit_size),
110
+ geom_names="ring_{}".format(self.num_ring_geoms),
111
+ geom_rgbas=None,
112
+ geom_materials=self.tripod_mat_name,
113
+ # make the ring low friction to ensure easy insertion
114
+ geom_frictions=(0.3, 5e-3, 1e-4),
115
+ )
116
+ self.num_ring_geoms += 1
117
+
118
+ # TRIPOD-GEOMS: legs of the tripod
119
+ tripod_capsule_r = 0.01
120
+ tripod_capsule_h = 0.03
121
+ tripod_geom_locations = [
122
+ (0.0, 0.0, 0.0),
123
+ (0.0, 2.0 * total_size[1] - 2.0 * tripod_capsule_r, 0.0),
124
+ (
125
+ 2.0 * total_size[0] - 2.0 * tripod_capsule_r,
126
+ total_size[1] - tripod_capsule_r,
127
+ 0.0,
128
+ ),
129
+ ]
130
+ # rotate the legs to resemble a tripod
131
+ tripod_center = np.array([total_size[0], total_size[1], 0.0])
132
+ xy_offset = np.array([tripod_capsule_r, tripod_capsule_r, 0.0])
133
+ rotation_angle = -np.pi / 6.0 # 30 degrees
134
+ tripod_geom_quats = []
135
+ for i in range(3):
136
+ capsule_loc = np.array(tripod_geom_locations[i]) + xy_offset
137
+ capsule_loc[2] = 0.0 # only care about location in x-y plane
138
+ vec_to_center = tripod_center - capsule_loc
139
+ vec_to_center = vec_to_center / np.linalg.norm(vec_to_center)
140
+ # cross-product with z unit vector to get vector to rotate about
141
+ rot_vec = np.cross(vec_to_center, np.array([0.0, 0.0, 1.0]))
142
+ rot_quat = T.mat2quat(T.rotation_matrix(angle=rotation_angle, direction=rot_vec))
143
+ tripod_geom_quats.append(T.convert_quat(rot_quat, to="wxyz"))
144
+
145
+ for i in range(3):
146
+ add_to_dict(
147
+ dic=obj_args,
148
+ geom_types="capsule",
149
+ geom_locations=tripod_geom_locations[i],
150
+ geom_quats=tripod_geom_quats[i],
151
+ geom_sizes=(tripod_capsule_r, tripod_capsule_h),
152
+ geom_names="tripod_{}".format(i),
153
+ geom_rgbas=None,
154
+ geom_materials=self.tripod_mat_name,
155
+ geom_frictions=None,
156
+ )
157
+
158
+ # POST-GEOMS: mounted base + post
159
+ base_thickness = 0.005
160
+ post_size = 0.005
161
+ post_geom_sizes = [
162
+ (total_size[0], total_size[1], base_thickness),
163
+ (
164
+ post_size,
165
+ post_size,
166
+ total_size[2] - ring_size[2] - base_thickness - tripod_capsule_r - tripod_capsule_h,
167
+ ),
168
+ ]
169
+ post_geom_locations = [
170
+ (0.0, 0.0, 2.0 * (tripod_capsule_r + tripod_capsule_h)),
171
+ (
172
+ total_size[0] - post_size,
173
+ total_size[1] - post_size,
174
+ 2.0 * (tripod_capsule_r + tripod_capsule_h + base_thickness),
175
+ ),
176
+ ]
177
+ for i in range(2):
178
+ add_to_dict(
179
+ dic=obj_args,
180
+ geom_types="box",
181
+ geom_locations=post_geom_locations[i],
182
+ geom_quats=(1, 0, 0, 0),
183
+ geom_sizes=post_geom_sizes[i],
184
+ geom_names="post_{}".format(i),
185
+ geom_rgbas=None,
186
+ geom_materials=self.tripod_mat_name,
187
+ geom_frictions=None,
188
+ )
189
+
190
+ # Add back in base args and site args
191
+ obj_args.update(base_args)
192
+
193
+ # Return this dict
194
+ return obj_args
GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite_body/bin_with_handles.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from robosuite.models.objects import CompositeBodyObject, BoxObject, Bin
2
+ import numpy as np
3
+
4
+ import robosuite.utils.transform_utils as T
5
+ from robosuite.utils.mjcf_utils import array_to_string
6
+ from robosuite.utils.mjcf_utils import RED, BLUE, CustomMaterial
7
+
8
+
9
+ class BinWithHandles(CompositeBodyObject):
10
+ """
11
+ Bin with simple square handles on each side.
12
+ """
13
+
14
+ def __init__(
15
+ self,
16
+ name,
17
+ bin_size,
18
+ bin_wall_thickness,
19
+ bin_transparent_walls,
20
+ bin_upside_down,
21
+ center_handle_size,
22
+ adjacent_handle_size,
23
+ joints="default",
24
+ rgba=(0.2, 0.1, 0.0, 1.0),
25
+ material=None,
26
+ density=1000.0,
27
+ friction=None,
28
+ ):
29
+
30
+ # Object properties
31
+
32
+ # FULL size of bin
33
+ self.bin_size = list(bin_size)
34
+ self.bin_wall_thickness = bin_wall_thickness
35
+ self.bin_transparent_walls = bin_transparent_walls
36
+ self.bin_upside_down = bin_upside_down
37
+
38
+ # half-sizes of box geom used for center part of handle (which you grab)
39
+ self.center_handle_size = list(center_handle_size)
40
+
41
+ # half-sizes of box geoms used for adjacent parts of handle (not grabbed)
42
+ self.adjacent_handle_size = list(adjacent_handle_size)
43
+
44
+ # Create objects
45
+ objects = []
46
+ object_locations = []
47
+ object_quats = []
48
+ object_parents = []
49
+
50
+ # bin
51
+ self.bin = Bin(
52
+ name="bin",
53
+ bin_size=self.bin_size,
54
+ wall_thickness=self.bin_wall_thickness,
55
+ transparent_walls=self.bin_transparent_walls,
56
+ rgba=rgba,
57
+ material=material,
58
+ density=density,
59
+ friction=friction,
60
+ upside_down=bin_upside_down,
61
+ )
62
+ objects.append(self.bin)
63
+ object_locations.append([0.0, 0.0, 0.0])
64
+ object_quats.append([1.0, 0.0, 0.0, 0.0])
65
+ object_parents.append(None)
66
+
67
+ # handles on each side
68
+
69
+ left_handle_1_loc = [
70
+ 0.0,
71
+ -(
72
+ self.bin_size[1] / 2.0
73
+ + 2.0 * self.adjacent_handle_size[1]
74
+ + self.center_handle_size[1]
75
+ ),
76
+ 0.0,
77
+ ]
78
+ left_handle_1_size = self.center_handle_size
79
+
80
+ left_handle_2_loc = [
81
+ (self.center_handle_size[0] - self.adjacent_handle_size[0]),
82
+ -(self.bin_size[1] / 2.0 + self.adjacent_handle_size[1]),
83
+ 0.0,
84
+ ]
85
+ left_handle_2_size = self.adjacent_handle_size
86
+
87
+ left_handle_3_loc = [
88
+ -(self.center_handle_size[0] - self.adjacent_handle_size[0]),
89
+ -(self.bin_size[1] / 2.0 + self.adjacent_handle_size[1]),
90
+ 0.0,
91
+ ]
92
+ left_handle_3_size = self.adjacent_handle_size
93
+
94
+ right_handle_1_loc = [
95
+ 0.0,
96
+ (
97
+ self.bin_size[1] / 2.0
98
+ + 2.0 * self.adjacent_handle_size[1]
99
+ + self.center_handle_size[1]
100
+ ),
101
+ 0.0,
102
+ ]
103
+ right_handle_1_size = self.center_handle_size
104
+
105
+ right_handle_2_loc = [
106
+ (self.center_handle_size[0] - self.adjacent_handle_size[0]),
107
+ (self.bin_size[1] / 2.0 + self.adjacent_handle_size[1]),
108
+ 0.0,
109
+ ]
110
+ right_handle_2_size = self.adjacent_handle_size
111
+
112
+ right_handle_3_loc = [
113
+ -(self.center_handle_size[0] - self.adjacent_handle_size[0]),
114
+ (self.bin_size[1] / 2.0 + self.adjacent_handle_size[1]),
115
+ 0.0,
116
+ ]
117
+ right_handle_3_size = self.adjacent_handle_size
118
+
119
+ handle_locs = [
120
+ left_handle_1_loc,
121
+ left_handle_2_loc,
122
+ left_handle_3_loc,
123
+ right_handle_1_loc,
124
+ right_handle_2_loc,
125
+ right_handle_3_loc,
126
+ ]
127
+ handle_sizes = [
128
+ left_handle_1_size,
129
+ left_handle_2_size,
130
+ left_handle_3_size,
131
+ right_handle_1_size,
132
+ right_handle_2_size,
133
+ right_handle_3_size,
134
+ ]
135
+ handle_ind = 1
136
+ for b_loc, b_size in zip(handle_locs, handle_sizes):
137
+ this_handle = BoxObject(
138
+ name="handle_{}".format(handle_ind),
139
+ size=b_size,
140
+ rgba=rgba,
141
+ material=material,
142
+ density=density,
143
+ friction=friction,
144
+ joints=None,
145
+ )
146
+ objects.append(this_handle)
147
+ object_locations.append(b_loc)
148
+ object_quats.append([1.0, 0.0, 0.0, 0.0])
149
+ object_parents.append(None)
150
+ handle_ind += 1
151
+
152
+ # Run super init
153
+ super().__init__(
154
+ name=name,
155
+ objects=objects,
156
+ object_locations=object_locations,
157
+ object_quats=object_quats,
158
+ object_parents=object_parents,
159
+ joints=joints,
160
+ # total_size=body_total_size,
161
+ # locations_relative_to_corner=True,
162
+ )
GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite_body/coffee_machine.py ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from robosuite.models.objects import CompositeBodyObject, BoxObject
2
+ from robocasa.models.objects.composite_body.cup import (
3
+ CupObject,
4
+ )
5
+ from robocasa.models.objects.xml_objects import (
6
+ CoffeeMachineBodyObject,
7
+ CoffeeMachineLidObject,
8
+ CoffeeMachineBaseObject,
9
+ )
10
+ import numpy as np
11
+
12
+ from robosuite.utils.mjcf_utils import array_to_string
13
+ from robosuite.utils.mjcf_utils import RED, BLUE, CustomMaterial
14
+
15
+
16
+ class CoffeeMachineObject(CompositeBodyObject):
17
+ """
18
+ Coffee machine object with a lid fixed on a hinge joint.
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ name,
24
+ add_cup=True,
25
+ pod_holder_friction=None,
26
+ joints="default",
27
+ density=1000.0,
28
+ ):
29
+
30
+ # pieces of the coffee machine
31
+ body = CoffeeMachineBodyObject(name="body")
32
+ body_size = body.get_bounding_box_half_size()
33
+ body_location = [0.0, 0.0, 0.0]
34
+
35
+ lid = CoffeeMachineLidObject(name="lid")
36
+ lid_size = self.lid_size = lid.get_bounding_box_half_size()
37
+ # add tolerance to allow lid to open fully
38
+ lid_location = [
39
+ body_size[0] - lid_size[0],
40
+ 2.0 * body_size[1] + 0.01,
41
+ 2.0 * (body_size[2] - lid_size[2]) + 0.005,
42
+ ]
43
+
44
+ # add in hinge joint to lid
45
+ hinge_pos = [0.0, -lid_size[1], 0.0]
46
+ hinge_joint = dict(
47
+ type="hinge",
48
+ axis="1 0 0",
49
+ pos=array_to_string(hinge_pos),
50
+ limited="true",
51
+ range="{} {}".format(0, 2.0 * np.pi / 3.0),
52
+ damping="0.005",
53
+ )
54
+ body_joints = dict(lid_main=[hinge_joint]) # note: "main" gets appended to body name
55
+ lid = CoffeeMachineLidObject(name="lid")
56
+
57
+ base = CoffeeMachineBaseObject(name="base")
58
+ base_size = base.get_bounding_box_half_size()
59
+ base_location = [body_size[0] - base_size[0], 2.0 * body_size[1], 0.0]
60
+
61
+ pod_holder_holder = BoxObject(
62
+ name="pod_holder_holder",
63
+ size=[
64
+ 0.01,
65
+ # tolerance for having the lid stick out a little from the holder
66
+ 0.9 * (lid_size[1] - lid_size[0]),
67
+ 0.005,
68
+ ],
69
+ rgba=[0.839, 0.839, 0.839, 1], # silver
70
+ joints=None,
71
+ )
72
+ pod_holder_holder_size = pod_holder_holder.get_bounding_box_half_size()
73
+ pod_holder_holder_location = [
74
+ body_size[0] - pod_holder_holder_size[0],
75
+ 2.0 * body_size[1],
76
+ # put right underneath lid
77
+ 2.0 * (body_size[2] - lid_size[2] - pod_holder_holder_size[2]),
78
+ ]
79
+
80
+ pod_holder = CupObject(
81
+ name="pod_holder",
82
+ outer_cup_radius=lid_size[0],
83
+ inner_cup_radius=0.028,
84
+ cup_height=0.028,
85
+ cup_ngeoms=64, # 8,
86
+ cup_base_height=0.005,
87
+ cup_base_offset=0.002,
88
+ add_handle=False,
89
+ rgba=[1, 0, 0, 1],
90
+ density=density,
91
+ joints=None,
92
+ friction=pod_holder_friction,
93
+ )
94
+ pod_holder_size = self.pod_holder_size = pod_holder.get_bounding_box_half_size()
95
+ # pod_holder_size = self.pod_holder_size = np.array([0.0295, 0.0295, 0.028 ])
96
+ pod_holder_location = [
97
+ body_size[0] - pod_holder_size[0],
98
+ 2.0 * (body_size[1] + pod_holder_holder_size[1]),
99
+ # put right underneath lid
100
+ 2.0 * (body_size[2] - lid_size[2] - pod_holder_size[2]),
101
+ ]
102
+
103
+ total_size = [
104
+ body_size[0],
105
+ body_size[1] + base_size[1],
106
+ body_size[2],
107
+ ]
108
+
109
+ objects = [
110
+ body,
111
+ lid,
112
+ base,
113
+ pod_holder_holder,
114
+ pod_holder,
115
+ ]
116
+
117
+ object_locations = [
118
+ body_location,
119
+ lid_location,
120
+ base_location,
121
+ pod_holder_holder_location,
122
+ pod_holder_location,
123
+ ]
124
+
125
+ object_quats = [
126
+ [0.0, 0.0, 0.0, 1.0], # z-rotate body and base by 180
127
+ [1.0, 0.0, 0.0, 0.0],
128
+ [0.0, 0.0, 0.0, 1.0],
129
+ [1.0, 0.0, 0.0, 0.0],
130
+ [1.0, 0.0, 0.0, 0.0],
131
+ ]
132
+
133
+ # add a rigidly mounted cup to the base
134
+ self.add_cup = add_cup
135
+ if self.add_cup:
136
+ cup = CupObject(
137
+ name="cupppp",
138
+ outer_cup_radius=0.03,
139
+ inner_cup_radius=0.025,
140
+ cup_height=0.025,
141
+ cup_ngeoms=64, # 8,
142
+ cup_base_height=0.005,
143
+ cup_base_offset=0.005,
144
+ add_handle=True,
145
+ handle_outer_radius=0.015,
146
+ handle_inner_radius=0.010,
147
+ handle_thickness=0.003,
148
+ handle_ngeoms=64,
149
+ rgba=[0.839, 0.839, 0.839, 1],
150
+ density=1000.0,
151
+ joints=None,
152
+ )
153
+ cup_total_size = cup.get_bounding_box_half_size()
154
+ # cup_total_size = np.array([0.03 , 0.045, 0.025])
155
+ objects.append(cup)
156
+ object_locations.append(
157
+ [
158
+ body_size[0] - cup_total_size[0],
159
+ 2.0 * (body_size[1] + pod_holder_holder_size[1])
160
+ + pod_holder_size[1]
161
+ - cup_total_size[1],
162
+ 2.0 * base_size[2],
163
+ ]
164
+ )
165
+ rot_angle = -np.pi / 2.0
166
+ object_quats.append([np.cos(rot_angle / 2), 0, 0, np.sin(rot_angle / 2)])
167
+
168
+ object_parents = [None] * len(objects)
169
+
170
+ """
171
+ Variables to compare:
172
+
173
+ objects
174
+ [<robosuite.models.objects.xml_objects.CoffeeMachineBodyObject at 0x10f0f7640>,
175
+ <robosuite.models.objects.xml_objects.CoffeeMachineLidObject at 0x10f0f7c10>,
176
+ <robosuite.models.objects.xml_objects.CoffeeMachineBaseObject at 0x10f0f7190>,
177
+ <robosuite.models.objects.generated_objects.BoxObject at 0x10f0f7d90>,
178
+ <robosuite.models.objects.generated_objects.CupObject at 0x10f0f71f0>,
179
+ <robosuite.models.objects.generated_objects.CupObject at 0x10f0f7070>]
180
+
181
+ object_locations
182
+ [[0.0, 0.0, 0.0],
183
+ [0.056999999999999995, 0.21100000000000002, 0.20700000000000002],
184
+ [0.04449999999999999, 0.201, 0.0],
185
+ [0.0765, 0.201, 0.192],
186
+ [0.056999999999999995, 0.22710000000000002, 0.14600000000000002],
187
+ [0.056499999999999995, 0.21160000000000007, 0.01]]
188
+
189
+ object_quats
190
+ [[0.0, 0.0, 0.0, 1.0],
191
+ [1.0, 0.0, 0.0, 0.0],
192
+ [0.0, 0.0, 0.0, 1.0],
193
+ [1.0, 0.0, 0.0, 0.0],
194
+ [1.0, 0.0, 0.0, 0.0],
195
+ [0.7071067811865476, 0, 0, -0.7071067811865475]]
196
+
197
+ body_size
198
+ array([0.0865, 0.1005, 0.1105])
199
+
200
+ lid_size
201
+ array([0.0295, 0.044 , 0.0095])
202
+
203
+ lid_location
204
+ [0.056999999999999995, 0.21100000000000002, 0.20700000000000002]
205
+
206
+ base_size
207
+ array([0.042, 0.05 , 0.005])
208
+
209
+ base_location
210
+ [0.04449999999999999, 0.201, 0.0]
211
+
212
+ pod_holder_holder_size
213
+ array([0.01 , 0.01305, 0.005 ])
214
+
215
+ pod_holder_holder_location
216
+ [0.0765, 0.201, 0.192]
217
+
218
+ pod_holder_size
219
+ array([0.0295, 0.0295, 0.028 ])
220
+
221
+ pod_holder_location
222
+ [0.056999999999999995, 0.22710000000000002, 0.14600000000000002]
223
+
224
+ total_size
225
+ [0.0865, 0.15050000000000002, 0.1105]
226
+
227
+ cup.total_size
228
+ array([0.03 , 0.045, 0.025])
229
+ """
230
+
231
+ # Run super init
232
+ super().__init__(
233
+ name=name,
234
+ objects=objects,
235
+ object_locations=object_locations,
236
+ object_quats=object_quats,
237
+ object_parents=object_parents,
238
+ body_joints=body_joints, # make sure hinge joint is added
239
+ joints=joints,
240
+ # joints="default", # coffee machine can move
241
+ # joints=None, # coffee machine does not move
242
+ total_size=total_size,
243
+ locations_relative_to_corner=True,
244
+ )
GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite_body/inverse_stacked_cylinder.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from robosuite.models.objects import (
2
+ CompositeBodyObject,
3
+ BoxObject,
4
+ CylinderObject,
5
+ HollowCylinderObject,
6
+ )
7
+ import numpy as np
8
+
9
+ import robosuite.utils.transform_utils as T
10
+ from robosuite.utils.mjcf_utils import array_to_string
11
+ from robosuite.utils.mjcf_utils import RED, BLUE, CustomMaterial
12
+
13
+
14
+ class InverseStackedCylinderObject(CompositeBodyObject):
15
+ """
16
+ Inverse of stacked cylinder object, where the top piece is a hollow cylinder object
17
+ and the bottom piece is a cylinder object. Optionally add a square base for stability.
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ name,
23
+ radius_1,
24
+ radius_2,
25
+ height_1,
26
+ height_2,
27
+ ngeoms=64,
28
+ joints="default",
29
+ rgba=None,
30
+ material=None,
31
+ density=100.0,
32
+ friction=None,
33
+ square_base_width=None,
34
+ square_base_height=None,
35
+ ):
36
+
37
+ # Object properties
38
+
39
+ # radius of first (bottom) cylinder and inner radius of second (top) hollow cylinder
40
+ self.r1 = radius_1
41
+ self.r2 = radius_2
42
+
43
+ # half-height of first (bottom) cylinder and second (top) hollow cylinder
44
+ self.h1 = height_1
45
+ self.h2 = height_2
46
+
47
+ # num geoms to approximate the hollow cylinder
48
+ self.ngeoms = ngeoms
49
+
50
+ # whether to add square base
51
+ self.add_square_base = (square_base_width is not None) and (square_base_height is not None)
52
+
53
+ # half-width and half-height for square base
54
+ self.square_base_width = square_base_width
55
+ self.square_base_height = square_base_height
56
+
57
+ # Create objects
58
+ objects = []
59
+ object_locations = []
60
+ object_quats = []
61
+ object_parents = []
62
+
63
+ # NOTE: we will place the object frame at the vertical center of the two stacked cylinders
64
+ z_center = (self.h1 + self.h2) / 2.0
65
+ c1_offset = self.h1 - z_center
66
+ c2_offset = 2.0 * self.h1 + self.h2 - z_center
67
+
68
+ # first (bottom) cylinder
69
+ self.cylinder_1 = CylinderObject(
70
+ name="cylinder_1",
71
+ size=[self.r1, self.h1],
72
+ rgba=rgba,
73
+ material=material,
74
+ density=density,
75
+ friction=friction,
76
+ solref=[0.02, 1.0],
77
+ # solimp=[0.998, 0.998, 0.001],
78
+ solimp=[0.9, 0.95, 0.001],
79
+ joints=None,
80
+ )
81
+ objects.append(self.cylinder_1)
82
+ object_locations.append([0.0, 0.0, c1_offset])
83
+ object_quats.append([1.0, 0.0, 0.0, 0.0])
84
+ object_parents.append(None)
85
+
86
+ # second (top) hollow cylinder
87
+ self.cylinder_2 = HollowCylinderObject(
88
+ name="cylinder_2",
89
+ outer_radius=self.r1, # match radius of first cylinder
90
+ inner_radius=self.r2,
91
+ height=self.h2,
92
+ ngeoms=self.ngeoms,
93
+ rgba=rgba,
94
+ material=material,
95
+ density=density,
96
+ friction=friction,
97
+ # TODO: maybe tune solimp and try (0.998, 0.998, 0.001)
98
+ solref=[0.02, 1.0],
99
+ solimp=[0.9, 0.95, 0.001],
100
+ # solimp=(0.998, 0.998, 0.001),
101
+ )
102
+ objects.append(self.cylinder_2)
103
+ object_locations.append([0.0, 0.0, c2_offset])
104
+ object_quats.append([1.0, 0.0, 0.0, 0.0])
105
+ object_parents.append(None)
106
+
107
+ # # total size of object
108
+ # max_r = max(self.r1, self.r2)
109
+ # body_total_size = [max_r, max_r, self.h1 + self.h2]
110
+
111
+ if self.add_square_base:
112
+ # add square base underneath bottom cylinder
113
+ s1_offset = c1_offset - (self.square_base_height + self.h1)
114
+ self.square_base = BoxObject(
115
+ name="square_base",
116
+ size=[
117
+ self.square_base_width,
118
+ self.square_base_width,
119
+ self.square_base_height,
120
+ ],
121
+ rgba=rgba,
122
+ material=material,
123
+ )
124
+ objects.append(self.square_base)
125
+ object_locations.append([0.0, 0.0, s1_offset])
126
+ object_quats.append([1.0, 0.0, 0.0, 0.0])
127
+ object_parents.append(None)
128
+
129
+ # Run super init
130
+ super().__init__(
131
+ name=name,
132
+ objects=objects,
133
+ object_locations=object_locations,
134
+ object_quats=object_quats,
135
+ object_parents=object_parents,
136
+ joints=joints,
137
+ # total_size=body_total_size,
138
+ # locations_relative_to_corner=True,
139
+ )
GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite_body/lightbulb.py ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from robosuite.models.objects import (
2
+ CompositeBodyObject,
3
+ BoxObject,
4
+ CylinderObject,
5
+ BallObject,
6
+ )
7
+ import numpy as np
8
+
9
+ import robosuite.utils.transform_utils as T
10
+ from robosuite.utils.mjcf_utils import array_to_string, new_site
11
+ from robosuite.utils.mjcf_utils import RED, BLUE, CustomMaterial
12
+
13
+
14
+ class BallObjectWithSite(BallObject):
15
+ """
16
+ A ball object with a inner site (used for the bulb).
17
+ """
18
+
19
+ def _get_object_subtree(self):
20
+ # tree = super()._get_object_subtree()
21
+ tree = self._get_object_subtree_(ob_type="sphere")
22
+ site_element_attr = self.get_site_attrib_template()
23
+
24
+ site_element_attr["pos"] = "0 0 0.1"
25
+ site_element_attr["name"] = "center_site"
26
+ site_element_attr["size"] = "{} {} {}".format(
27
+ 2.0 * self.size[0], 2.0 * self.size[0], 2.0 * self.size[0]
28
+ )
29
+ site_element_attr["rgba"] = "1.0 0.0 0.0 1.0"
30
+ site_element_attr["group"] = "1"
31
+ # site_element_attr["rgba"] = "1.0 0.976 0.839 0.9"
32
+ # site_element_attr["rgba"] = "1.0 0.976 0.839 0.0"
33
+ tree.append(new_site(**site_element_attr))
34
+ return tree
35
+
36
+
37
+ class LightbulbObject(CompositeBodyObject):
38
+ """
39
+ A simple lightbulb constructed out of a base of alternating radius cylinders
40
+ and a sphere on top.
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ name,
46
+ radius_low,
47
+ radius_high,
48
+ cylinder_height,
49
+ num_cylinders,
50
+ sphere_radius,
51
+ joints="default",
52
+ density=100.0,
53
+ friction=None,
54
+ ):
55
+
56
+ # Object properties
57
+
58
+ # radii of alternating cylinders for base
59
+ self.radius_low = radius_low
60
+ self.radius_high = radius_high
61
+
62
+ # half-height of each cylinder
63
+ self.cylinder_height = cylinder_height
64
+
65
+ # number of cylinders for base
66
+ self.num_cylinders = num_cylinders
67
+
68
+ # radius of sphere at top
69
+ self.sphere_radius = sphere_radius
70
+
71
+ # toggle between translucent and yellow
72
+ self.translucent_rgba = (1.0, 1.0, 1.0, 0.3)
73
+ self.yellow_rgba = (1.0, 0.976, 0.839, 0.7)
74
+ # self.yellow_rgba = (0.0, 0.0, 0.0, 0.0)
75
+
76
+ # Create objects
77
+ objects = []
78
+ object_locations = []
79
+ object_quats = []
80
+ object_parents = []
81
+
82
+ # NOTE: we will place the object frame at the vertical center of all the stacked objects
83
+ self.z_center = (
84
+ (2.0 * self.cylinder_height) * self.num_cylinders + 2.0 * self.sphere_radius
85
+ ) / 2.0
86
+
87
+ metal = CustomMaterial(
88
+ texture="Metal",
89
+ tex_name="metal",
90
+ mat_name="MatMetal",
91
+ tex_attrib={"type": "cube"},
92
+ mat_attrib={"specular": "1", "shininess": "0.3", "rgba": "0.9 0.9 0.9 1"},
93
+ )
94
+
95
+ # we will define all objects relative to the bottom of the object, and then subtract the z_center value
96
+ for cylinder_ind in range(self.num_cylinders):
97
+ r = self.radius_low if ((cylinder_ind % 2) == 0) else self.radius_high
98
+ cyl_obj = CylinderObject(
99
+ name="cylinder_{}".format(cylinder_ind),
100
+ size=[r, self.cylinder_height],
101
+ rgba=None,
102
+ material=metal,
103
+ density=density,
104
+ friction=friction,
105
+ solref=[0.02, 1.0],
106
+ solimp=[0.9, 0.95, 0.001],
107
+ joints=None,
108
+ )
109
+ objects.append(cyl_obj)
110
+ z_cyl = (2.0 * cylinder_ind + 1) * self.cylinder_height
111
+ object_locations.append([0.0, 0.0, z_cyl])
112
+ object_quats.append([1.0, 0.0, 0.0, 0.0])
113
+ object_parents.append(None)
114
+
115
+ # then add translucent sphere at top
116
+ self.bulb = BallObject(
117
+ name="bulb",
118
+ size=[self.sphere_radius],
119
+ density=density,
120
+ friction=friction,
121
+ rgba=self.translucent_rgba,
122
+ material=None,
123
+ joints=None,
124
+ )
125
+ objects.append(self.bulb)
126
+ z_bulb = object_locations[-1][2] + self.cylinder_height + self.sphere_radius
127
+ object_locations.append([0.0, 0.0, z_bulb])
128
+ object_quats.append([1.0, 0.0, 0.0, 0.0])
129
+ object_parents.append(None)
130
+
131
+ # do frame conversion from bottom of object to z_center
132
+ object_locations = [[loc[0], loc[1], loc[2] - self.z_center] for loc in object_locations]
133
+
134
+ # add site that can be toggled to turn lightbulb on
135
+ sites = [
136
+ dict(
137
+ name="bulb_on",
138
+ pos=array_to_string(object_locations[-1]),
139
+ size="{}".format(0.95 * self.sphere_radius),
140
+ rgba=array_to_string(self.yellow_rgba),
141
+ )
142
+ ]
143
+
144
+ # Run super init
145
+ super().__init__(
146
+ name=name,
147
+ objects=objects,
148
+ object_locations=object_locations,
149
+ object_quats=object_quats,
150
+ object_parents=object_parents,
151
+ joints=joints,
152
+ sites=sites,
153
+ )
GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite_body/sliding_box.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+
3
+ from robosuite.models.objects import BoxObject, CompositeBodyObject, CylinderObject
4
+ from robosuite.utils.mjcf_utils import BLUE, RED, CustomMaterial, array_to_string
5
+
6
+
7
+ class SlidingBoxObject(CompositeBodyObject):
8
+ """
9
+ An example object that demonstrates the CompositeBodyObject functionality. This object consists of two cube bodies
10
+ joined together by a slide joint allowing one box to slide on top of the other.
11
+
12
+ Args:
13
+ name (str): Name of this object
14
+
15
+ box1_size (3-array): (L, W, H) half-sizes for the first box
16
+
17
+ box2_size (3-array): (L, W, H) half-sizes for the second box
18
+
19
+ use_texture (bool): set True if using wood textures for the blocks
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ name,
25
+ box1_size=(0.1, 0.1, 0.02),
26
+ box2_size=(0.02, 0.02, 0.02),
27
+ use_texture=True,
28
+ ):
29
+ # Set box sizes
30
+ self.box1_size = np.array(box1_size)
31
+ self.box2_size = np.array(box2_size)
32
+
33
+ # Set box densities
34
+ self.box1_density = 10000.0
35
+ self.box2_density = 100.0
36
+
37
+ # Set texture attributes
38
+ self.use_texture = use_texture
39
+ self.box1_material = None
40
+ self.box2_material = None
41
+ self.box1_rgba = RED
42
+ self.box2_rgba = BLUE
43
+
44
+ # Define materials we want to use for this object
45
+ if self.use_texture:
46
+ # Remove RGBAs
47
+ self.box1_rgba = None
48
+ self.box2_rgba = None
49
+
50
+ # Set materials for each box
51
+ tex_attrib = {
52
+ "type": "cube",
53
+ }
54
+ mat_attrib = {
55
+ "texrepeat": "3 3",
56
+ "specular": "0.4",
57
+ "shininess": "0.1",
58
+ }
59
+ self.box1_material = CustomMaterial(
60
+ texture="WoodRed",
61
+ tex_name="box1_tex",
62
+ mat_name="box1_mat",
63
+ tex_attrib=tex_attrib,
64
+ mat_attrib=mat_attrib,
65
+ )
66
+ self.box2_material = CustomMaterial(
67
+ texture="WoodBlue",
68
+ tex_name="box2_tex",
69
+ mat_name="box2_mat",
70
+ tex_attrib=tex_attrib,
71
+ mat_attrib=mat_attrib,
72
+ )
73
+
74
+ # Create objects
75
+ objects = []
76
+ for i, (size, mat, rgba, density) in enumerate(
77
+ zip(
78
+ (self.box1_size, self.box2_size),
79
+ (self.box1_material, self.box2_material),
80
+ (self.box1_rgba, self.box2_rgba),
81
+ (self.box1_density, self.box2_density),
82
+ )
83
+ ):
84
+ objects.append(
85
+ BoxObject(
86
+ name=f"box{i + 1}",
87
+ size=size,
88
+ rgba=rgba,
89
+ material=mat,
90
+ )
91
+ )
92
+
93
+ # Define slide joint
94
+ rel_joint_pos = [0, 0, 0] # at second box
95
+ joint_lim = self.box1_size[1] - self.box2_size[1]
96
+ slide_joint = {
97
+ "name": "box_slide",
98
+ "type": "slide",
99
+ "axis": "0 1 0", # y-axis slide
100
+ "pos": array_to_string(rel_joint_pos),
101
+ "springref": "0",
102
+ "springdamper": "0.1 1.0", # mass-spring system with 0.1 time constant, 1.0 damping ratio
103
+ "limited": "true",
104
+ "range": "{} {}".format(-joint_lim, joint_lim),
105
+ }
106
+
107
+ # Define positions -- second box should lie on top of first box
108
+ positions = [
109
+ np.zeros(3), # First box is centered at top-level body anyways
110
+ np.array([0, 0, self.box1_size[2] + self.box2_size[2]]),
111
+ ]
112
+
113
+ quats = [
114
+ None, # Default quaternion for box 1
115
+ None, # Default quaternion for box 2
116
+ ]
117
+
118
+ # Define parents -- which body each is aligned to
119
+ parents = [
120
+ None, # box 1 attached to top-level body
121
+ objects[0].root_body, # box 2 attached to box 1
122
+ ]
123
+
124
+ # Run super init
125
+ super().__init__(
126
+ name=name,
127
+ objects=objects,
128
+ object_locations=positions,
129
+ object_quats=quats,
130
+ object_parents=parents,
131
+ body_joints={objects[1].root_body: [slide_joint]},
132
+ )
GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite_body/stacked_box.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from robosuite.models.objects import CompositeBodyObject, BoxObject, CylinderObject, Bin
2
+ import numpy as np
3
+
4
+ import robosuite.utils.transform_utils as T
5
+ from robosuite.utils.mjcf_utils import array_to_string
6
+ from robosuite.utils.mjcf_utils import RED, BLUE, CustomMaterial
7
+
8
+
9
+ class StackedBoxObject(CompositeBodyObject):
10
+ """
11
+ Two boxes - one stacked on top of the other.
12
+ """
13
+
14
+ def __init__(
15
+ self,
16
+ name,
17
+ box_1_size,
18
+ box_2_size,
19
+ joints="default",
20
+ box_1_rgba=None,
21
+ box_2_rgba=None,
22
+ box_1_material=None,
23
+ box_2_material=None,
24
+ density=100.0,
25
+ friction=None,
26
+ make_box_2_transparent=False,
27
+ ):
28
+
29
+ # Object properties
30
+
31
+ # half-sizes of first (bottom) box
32
+ self.box_1_size = list(box_1_size)
33
+
34
+ # half-sizes of second (top) box
35
+ self.box_2_size = list(box_2_size)
36
+
37
+ # maybe make box 2 have transparent top and bottom walls
38
+ self.make_box_2_transparent = make_box_2_transparent
39
+
40
+ # Create objects
41
+ objects = []
42
+ object_locations = []
43
+ object_quats = []
44
+ object_parents = []
45
+
46
+ # NOTE: we will place the object frame at the vertical center of the two stacked boxes
47
+ z_center = (self.box_1_size[2] + self.box_2_size[2]) / 2.0
48
+ b1_offset = self.box_1_size[2] - z_center
49
+ b2_offset = 2.0 * self.box_1_size[2] + self.box_2_size[2] - z_center
50
+
51
+ # first (bottom) box
52
+ self.box_1 = BoxObject(
53
+ name="box_1",
54
+ size=self.box_1_size,
55
+ rgba=box_1_rgba,
56
+ material=box_1_material,
57
+ density=density,
58
+ friction=friction,
59
+ joints=None,
60
+ )
61
+ objects.append(self.box_1)
62
+ object_locations.append([0.0, 0.0, b1_offset])
63
+ object_quats.append([1.0, 0.0, 0.0, 0.0])
64
+ object_parents.append(None)
65
+
66
+ # second (top) box
67
+ if self.make_box_2_transparent:
68
+ self.box_2 = Bin(
69
+ name="box_2",
70
+ bin_size=(
71
+ 2.0 * self.box_2_size[0],
72
+ 2.0 * self.box_2_size[1],
73
+ 2.0 * self.box_2_size[2],
74
+ ),
75
+ wall_thickness=0.01,
76
+ transparent_walls=False,
77
+ friction=friction,
78
+ density=density,
79
+ use_texture=True,
80
+ rgba=box_2_rgba,
81
+ material=box_2_material,
82
+ upside_down=False,
83
+ add_second_base=True,
84
+ transparent_base=True,
85
+ )
86
+ else:
87
+ self.box_2 = BoxObject(
88
+ name="box_2",
89
+ size=self.box_2_size,
90
+ rgba=box_2_rgba,
91
+ material=box_2_material,
92
+ density=density,
93
+ friction=friction,
94
+ joints=None,
95
+ )
96
+ objects.append(self.box_2)
97
+ object_locations.append([0.0, 0.0, b2_offset])
98
+ object_quats.append([1.0, 0.0, 0.0, 0.0])
99
+ object_parents.append(None)
100
+
101
+ # Run super init
102
+ super().__init__(
103
+ name=name,
104
+ objects=objects,
105
+ object_locations=object_locations,
106
+ object_quats=object_quats,
107
+ object_parents=object_parents,
108
+ joints=joints,
109
+ # total_size=body_total_size,
110
+ # locations_relative_to_corner=True,
111
+ )
GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite_body/stacked_cylinder.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from robosuite.models.objects import CompositeBodyObject, BoxObject, CylinderObject
2
+ import numpy as np
3
+
4
+ import robosuite.utils.transform_utils as T
5
+ from robosuite.utils.mjcf_utils import array_to_string
6
+ from robosuite.utils.mjcf_utils import RED, BLUE, CustomMaterial
7
+
8
+
9
+ class StackedCylinderObject(CompositeBodyObject):
10
+ """
11
+ Two cylinders - one stacked on top of the other.
12
+ Optionally add a square base for stability.
13
+ """
14
+
15
+ def __init__(
16
+ self,
17
+ name,
18
+ radius_1,
19
+ radius_2,
20
+ height_1,
21
+ height_2,
22
+ joints="default",
23
+ rgba=None,
24
+ material=None,
25
+ density=100.0,
26
+ friction=None,
27
+ square_base_width=None,
28
+ square_base_height=None,
29
+ ):
30
+
31
+ # Object properties
32
+
33
+ # radius of first (bottom) cylinder and second (top) cylinder
34
+ self.r1 = radius_1
35
+ self.r2 = radius_2
36
+
37
+ # half-height of first (bottom) cylinder and second (top) cylinder
38
+ self.h1 = height_1
39
+ self.h2 = height_2
40
+
41
+ # whether to add square base
42
+ self.add_square_base = (square_base_width is not None) and (square_base_height is not None)
43
+
44
+ # half-width and half-height for square base
45
+ self.square_base_width = square_base_width
46
+ self.square_base_height = square_base_height
47
+
48
+ # Create objects
49
+ objects = []
50
+ object_locations = []
51
+ object_quats = []
52
+ object_parents = []
53
+
54
+ # NOTE: we will place the object frame at the vertical center of the two stacked cylinders
55
+ z_center = (self.h1 + self.h2) / 2.0
56
+ c1_offset = self.h1 - z_center
57
+ c2_offset = 2.0 * self.h1 + self.h2 - z_center
58
+
59
+ # first (bottom) cylinder
60
+ self.cylinder_1 = CylinderObject(
61
+ name="cylinder_1",
62
+ size=[self.r1, self.h1],
63
+ rgba=rgba,
64
+ material=material,
65
+ density=density,
66
+ friction=friction,
67
+ solref=[0.02, 1.0],
68
+ solimp=[0.9, 0.95, 0.001],
69
+ # solimp=[0.998, 0.998, 0.001],
70
+ joints=None,
71
+ )
72
+ objects.append(self.cylinder_1)
73
+ object_locations.append([0.0, 0.0, c1_offset])
74
+ object_quats.append([1.0, 0.0, 0.0, 0.0])
75
+ object_parents.append(None)
76
+
77
+ # second (top) cylinder
78
+ self.cylinder_2 = CylinderObject(
79
+ name="cylinder_2",
80
+ size=[self.r2, self.h2],
81
+ rgba=rgba,
82
+ material=material,
83
+ density=density,
84
+ friction=friction,
85
+ solref=[0.02, 1.0],
86
+ # solimp=[0.998, 0.998, 0.001],
87
+ solimp=[0.9, 0.95, 0.001],
88
+ joints=None,
89
+ )
90
+ objects.append(self.cylinder_2)
91
+ object_locations.append([0.0, 0.0, c2_offset])
92
+ object_quats.append([1.0, 0.0, 0.0, 0.0])
93
+ object_parents.append(None)
94
+
95
+ # # total size of object
96
+ # max_r = max(self.r1, self.r2)
97
+ # body_total_size = [max_r, max_r, self.h1 + self.h2]
98
+
99
+ if self.add_square_base:
100
+ # add square base underneath bottom cylinder
101
+ s1_offset = c1_offset - (self.square_base_height + self.h1)
102
+ self.square_base = BoxObject(
103
+ name="square_base",
104
+ size=[
105
+ self.square_base_width,
106
+ self.square_base_width,
107
+ self.square_base_height,
108
+ ],
109
+ rgba=rgba,
110
+ material=material,
111
+ )
112
+ objects.append(self.square_base)
113
+ object_locations.append([0.0, 0.0, s1_offset])
114
+ object_quats.append([1.0, 0.0, 0.0, 0.0])
115
+ object_parents.append(None)
116
+
117
+ # Run super init
118
+ super().__init__(
119
+ name=name,
120
+ objects=objects,
121
+ object_locations=object_locations,
122
+ object_quats=object_quats,
123
+ object_parents=object_parents,
124
+ joints=joints,
125
+ # total_size=body_total_size,
126
+ # locations_relative_to_corner=True,
127
+ )
GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite_body/stove_plug.py ADDED
@@ -0,0 +1,396 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from robosuite.models.objects import CompositeBodyObject, BoxObject, CylinderObject
2
+ import numpy as np
3
+
4
+ import robosuite.utils.transform_utils as T
5
+ from robosuite.utils.mjcf_utils import array_to_string
6
+ from robosuite.utils.mjcf_utils import RED, BLUE, CustomMaterial
7
+ from .inverse_stacked_cylinder import InverseStackedCylinderObject
8
+ from .lightbulb import LightbulbObject
9
+
10
+ import robosuite_task_zoo
11
+ from robosuite_task_zoo.models.kitchen import StoveObject
12
+
13
+
14
+ class StoveObjectNew(StoveObject):
15
+ """
16
+ Override some offsets for placement sampler.
17
+ """
18
+
19
+ @property
20
+ def bottom_offset(self):
21
+ # unused since we directly hardcode z
22
+ return np.array([0, 0, -0.02])
23
+
24
+ @property
25
+ def top_offset(self):
26
+ # unused since we directly hardcode z
27
+ return np.array([0, 0, 0.02])
28
+
29
+ @property
30
+ def horizontal_radius(self):
31
+ return 0.1
32
+
33
+
34
+ class StovePlugObject(CompositeBodyObject):
35
+ """
36
+ Stove on wooden block with chain (plug) connected. Optionally replace the stove
37
+ with a lightbulb if @lightbulb_args is provided.
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ name,
43
+ joints="default",
44
+ rgba=None,
45
+ material=None,
46
+ density=100.0,
47
+ friction=None,
48
+ stove_base_size=(0.12, 0.12, 0.01),
49
+ stove_z_half_size=0.025,
50
+ wire_box_geom_size=(0.005, 0.02, 0.005),
51
+ wire_box_geom_rgba=(0.0, 0.0, 0.0, 1.0),
52
+ num_box_geoms_left=5,
53
+ num_box_geoms_vert=8,
54
+ num_box_geoms_right=8,
55
+ merge_box_geoms=False,
56
+ merge_size=1,
57
+ cylinder_args=None,
58
+ lightbulb_args=None,
59
+ ):
60
+
61
+ # Object properties
62
+
63
+ # half sizes for stove base box object
64
+ # self.stove_base_size = (0.1, 0.1, 0.02)
65
+ self.stove_base_size = stove_base_size
66
+ self.stove_z_half_size = stove_z_half_size # note: estimated approximately
67
+
68
+ # box geoms used for wire
69
+ self.wire_box_geom_size = wire_box_geom_size
70
+ self.wire_box_geom_rgba = wire_box_geom_rgba
71
+
72
+ # wire parameters - number of geoms to use for left, down, and right portions
73
+ self.num_box_geoms_left = num_box_geoms_left
74
+ self.num_box_geoms_vert = num_box_geoms_vert
75
+ self.num_box_geoms_right = num_box_geoms_right
76
+
77
+ # if true, merge the box geoms along each direction of the wire into a single box geom
78
+ self.merge_box_geoms = merge_box_geoms
79
+
80
+ # number of box geoms to use for each merged size (set to higher than 1 to merge the geoms into more
81
+ # than one box geom)
82
+ self.merge_size = merge_size
83
+
84
+ if cylinder_args is None:
85
+ # default cylinder args
86
+ cylinder_args = dict(
87
+ # bottom cylinder radius and half-height
88
+ radius_1=0.03,
89
+ height_1=0.01,
90
+ # top hollow cylinder inner radius and half-height
91
+ radius_2=0.025,
92
+ height_2=0.025,
93
+ # NOTE: reduce to 8 geoms if desired
94
+ ngeoms=64,
95
+ rgba=[0.839, 0.839, 0.839, 1],
96
+ density=1000.0,
97
+ # add square base
98
+ square_base_width=0.03,
99
+ square_base_height=0.005,
100
+ )
101
+ self.cylinder_args = dict(cylinder_args)
102
+ self.cylinder_args["joints"] = None
103
+
104
+ self.use_lightbulb = lightbulb_args is not None
105
+ if self.use_lightbulb:
106
+ self.lightbulb_args = dict(lightbulb_args)
107
+ self.lightbulb_args["joints"] = None
108
+
109
+ # materials
110
+ box_geom_material = CustomMaterial(
111
+ texture="Metal",
112
+ tex_name="metal",
113
+ mat_name="MatMetal",
114
+ tex_attrib={"type": "cube"},
115
+ mat_attrib={"specular": "1", "shininess": "0.3", "rgba": "0.9 0.9 0.9 1"},
116
+ )
117
+ stove_base_material = CustomMaterial(
118
+ texture="WoodLight",
119
+ tex_name="lightwood",
120
+ mat_name="lightwood_mat",
121
+ tex_attrib={"type": "cube"},
122
+ mat_attrib={"texrepeat": "1 1", "specular": "0.4", "shininess": "0.1"},
123
+ )
124
+
125
+ # params for ball joints used
126
+
127
+ # <joint name="J1" type="ball" pos="0 0 0" frictionloss="&friction;"/>
128
+ ball_joint_spec = {
129
+ "type": "ball",
130
+ "pos": "0 {} 0".format(self.wire_box_geom_size[1]),
131
+ "springref": "0",
132
+ "springdamper": "0.1 1.0", # mass-spring system with 0.1 time constant, 1.0 damping ratio
133
+ "limited": "true",
134
+ "range": "0 {}".format(np.pi / 4),
135
+ }
136
+
137
+ # Create objects
138
+ objects = []
139
+ object_locations = []
140
+ object_quats = []
141
+ object_parents = []
142
+ object_joints = dict()
143
+
144
+ # NOTE: For absolute object locations (objects not defined relative to parent) we will use the stove base frame
145
+ # as a frame of reference, and add an offset from the center of the object (approximated via full width)
146
+ # to it.
147
+
148
+ # get an approximate x-y bounding box below, and place the center there, and define offset relative to stove base cente
149
+ approx_full_width = self.num_box_geoms_left * (2.0 * self.wire_box_geom_size[1]) + (
150
+ 2.0 * self.stove_base_size[1]
151
+ )
152
+ approx_full_height = (self.num_box_geoms_vert + 2) * (2.0 * self.wire_box_geom_size[1])
153
+
154
+ stove_base_x_off = -(approx_full_height / 2.0) + self.stove_base_size[0]
155
+ stove_base_y_off = (approx_full_width / 2.0) - self.stove_base_size[1]
156
+
157
+ # base of stove
158
+ self.stove_base = BoxObject(
159
+ name="stove_base",
160
+ size=list(self.stove_base_size),
161
+ rgba=rgba,
162
+ material=stove_base_material,
163
+ joints=None,
164
+ )
165
+ objects.append(self.stove_base)
166
+ object_locations.append([stove_base_x_off, stove_base_y_off, 0.0])
167
+ object_quats.append([1.0, 0.0, 0.0, 0.0])
168
+ object_parents.append(None)
169
+
170
+ if self.use_lightbulb:
171
+ # lightbulb
172
+ self.lightbulb = LightbulbObject(
173
+ name="lightbulb",
174
+ **self.lightbulb_args,
175
+ )
176
+ objects.append(self.lightbulb)
177
+ object_locations.append(
178
+ [
179
+ stove_base_x_off,
180
+ stove_base_y_off,
181
+ (self.stove_base_size[2] + self.lightbulb.z_center),
182
+ ]
183
+ )
184
+ object_quats.append([1.0, 0.0, 0.0, 0.0])
185
+ object_parents.append(None)
186
+ else:
187
+ # stove
188
+ self.stove = StoveObjectNew(
189
+ name="new_stove",
190
+ joints=None,
191
+ )
192
+ objects.append(self.stove)
193
+ object_locations.append(
194
+ [
195
+ stove_base_x_off,
196
+ stove_base_y_off,
197
+ (self.stove_base_size[2] + self.stove_z_half_size),
198
+ ]
199
+ )
200
+ object_quats.append([1.0, 0.0, 0.0, 0.0])
201
+ object_parents.append(None)
202
+
203
+ # chain to the left of stove base
204
+ chain_ind = 0
205
+ left_chain_size = list(self.wire_box_geom_size)
206
+ num_geoms_iter = self.num_box_geoms_left - 1
207
+ if self.merge_box_geoms:
208
+ # only one big geom instead of chain of geoms
209
+ left_chain_size[1] *= self.num_box_geoms_left
210
+ left_chain_size[1] /= self.merge_size
211
+ # number of additional geoms to add
212
+ num_geoms_iter = self.merge_size - 1
213
+ left_chain_obj = BoxObject(
214
+ name="chain_{}".format(chain_ind),
215
+ size=list(left_chain_size),
216
+ rgba=list(self.wire_box_geom_rgba),
217
+ material=box_geom_material,
218
+ joints=None,
219
+ )
220
+ chain_ind += 1
221
+ objects.append(left_chain_obj)
222
+ object_locations.append(
223
+ [
224
+ stove_base_x_off - 0.75 * self.stove_base_size[0],
225
+ stove_base_y_off - (self.stove_base_size[1] + left_chain_size[1]),
226
+ 0.0,
227
+ ]
228
+ )
229
+ # object_locations.append([0., -(self.stove_base_size[1] + left_chain_size[1]), 0.])
230
+ object_quats.append([1.0, 0.0, 0.0, 0.0])
231
+ object_parents.append(None)
232
+ if self.merge_box_geoms:
233
+ # add ball joint and make sure to place it at the edge of the geom
234
+ ball_joint = dict(ball_joint_spec)
235
+ ball_joint["name"] = "ball_joint_{}".format(chain_ind)
236
+ ball_joint["pos"] = "0 {} 0".format(left_chain_size[1])
237
+ object_joints[left_chain_obj.root_body] = [ball_joint]
238
+ for i in range(num_geoms_iter):
239
+ left_chain_obj = BoxObject(
240
+ name="chain_{}".format(chain_ind),
241
+ size=list(left_chain_size),
242
+ rgba=list(self.wire_box_geom_rgba),
243
+ material=box_geom_material,
244
+ joints=None,
245
+ )
246
+ chain_ind += 1
247
+ parent = objects[-1].root_body
248
+ objects.append(left_chain_obj)
249
+ object_locations.append([0.0, -2.0 * left_chain_size[1], 0.0])
250
+ object_quats.append([1.0, 0.0, 0.0, 0.0])
251
+ object_parents.append(parent)
252
+
253
+ # add ball joint and make sure to place it at the edge of the geom
254
+ ball_joint = dict(ball_joint_spec)
255
+ ball_joint["name"] = "ball_joint_{}".format(chain_ind)
256
+ ball_joint["pos"] = "0 {} 0".format(left_chain_size[1])
257
+ object_joints[left_chain_obj.root_body] = [ball_joint]
258
+
259
+ # add chain in downward direction
260
+ rot_quat = T.convert_quat(
261
+ T.axisangle2quat(np.array([0.0, 0.0, 1.0]) * (np.pi / 2.0)), to="wxyz"
262
+ )
263
+ vert_chain_size = list(self.wire_box_geom_size)
264
+ num_geoms_iter = self.num_box_geoms_vert - 1
265
+ if self.merge_box_geoms:
266
+ # only one big geom instead of chain of geoms
267
+ vert_chain_size[1] *= self.num_box_geoms_vert
268
+ vert_chain_size[1] /= self.merge_size
269
+ # number of additional geoms to add
270
+ num_geoms_iter = self.merge_size - 1
271
+ vert_chain_obj = BoxObject(
272
+ name="chain_{}".format(chain_ind),
273
+ size=list(vert_chain_size),
274
+ rgba=list(self.wire_box_geom_rgba),
275
+ material=box_geom_material,
276
+ joints=None,
277
+ )
278
+ chain_ind += 1
279
+ parent = objects[-1].root_body
280
+ objects.append(vert_chain_obj)
281
+ object_locations.append([vert_chain_size[1], -left_chain_size[1], 0.0])
282
+ object_quats.append(rot_quat)
283
+ object_parents.append(parent)
284
+
285
+ ball_joint = dict(ball_joint_spec)
286
+ ball_joint["name"] = "ball_joint_{}".format(chain_ind)
287
+ ball_joint["pos"] = "0 {} 0".format(vert_chain_size[1])
288
+ object_joints[vert_chain_obj.root_body] = [ball_joint]
289
+
290
+ for i in range(num_geoms_iter):
291
+ vert_chain_obj = BoxObject(
292
+ name="chain_{}".format(chain_ind),
293
+ size=list(vert_chain_size),
294
+ rgba=list(self.wire_box_geom_rgba),
295
+ material=box_geom_material,
296
+ joints=None,
297
+ )
298
+ chain_ind += 1
299
+ parent = objects[-1].root_body
300
+ objects.append(vert_chain_obj)
301
+ object_locations.append([0.0, -2.0 * vert_chain_size[1], 0.0])
302
+ object_quats.append([1.0, 0.0, 0.0, 0.0])
303
+ object_parents.append(parent)
304
+
305
+ ball_joint = dict(ball_joint_spec)
306
+ ball_joint["name"] = "ball_joint_{}".format(chain_ind)
307
+ ball_joint["pos"] = "0 {} 0".format(vert_chain_size[1])
308
+ object_joints[vert_chain_obj.root_body] = [ball_joint]
309
+
310
+ # add chain in rightward direction
311
+ rot_quat = T.convert_quat(
312
+ T.axisangle2quat(np.array([0.0, 0.0, 1.0]) * (np.pi / 2.0)), to="wxyz"
313
+ )
314
+ right_chain_size = list(self.wire_box_geom_size)
315
+ num_geoms_iter = self.num_box_geoms_right - 1
316
+ if self.merge_box_geoms:
317
+ # only one big geom instead of chain of geoms
318
+ right_chain_size[1] *= self.num_box_geoms_right
319
+ right_chain_size[1] /= self.merge_size
320
+ # number of additional geoms to add
321
+ num_geoms_iter = self.merge_size - 1
322
+ right_chain_obj = BoxObject(
323
+ name="chain_{}".format(chain_ind),
324
+ size=list(right_chain_size),
325
+ rgba=list(self.wire_box_geom_rgba),
326
+ material=box_geom_material,
327
+ joints=None,
328
+ )
329
+ chain_ind += 1
330
+ parent = objects[-1].root_body
331
+ objects.append(right_chain_obj)
332
+ object_locations.append([right_chain_size[1], -vert_chain_size[1], 0.0])
333
+ object_quats.append(rot_quat)
334
+ object_parents.append(parent)
335
+
336
+ ball_joint = dict(ball_joint_spec)
337
+ ball_joint["name"] = "ball_joint_{}".format(chain_ind)
338
+ ball_joint["pos"] = "0 {} 0".format(right_chain_size[1])
339
+ object_joints[right_chain_obj.root_body] = [ball_joint]
340
+
341
+ for i in range(num_geoms_iter):
342
+ right_chain_obj = BoxObject(
343
+ name="chain_{}".format(chain_ind),
344
+ size=list(right_chain_size),
345
+ rgba=list(self.wire_box_geom_rgba),
346
+ material=box_geom_material,
347
+ joints=None,
348
+ )
349
+ chain_ind += 1
350
+ parent = objects[-1].root_body
351
+ objects.append(right_chain_obj)
352
+ object_locations.append([0.0, -2.0 * right_chain_size[1], 0.0])
353
+ object_quats.append([1.0, 0.0, 0.0, 0.0])
354
+ object_parents.append(parent)
355
+
356
+ ball_joint = dict(ball_joint_spec)
357
+ ball_joint["name"] = "ball_joint_{}".format(chain_ind)
358
+ ball_joint["pos"] = "0 {} 0".format(right_chain_size[1])
359
+ object_joints[right_chain_obj.root_body] = [ball_joint]
360
+
361
+ # add cylinder object
362
+ self.cylinder_obj = InverseStackedCylinderObject(
363
+ name="cylinder_obj",
364
+ **self.cylinder_args,
365
+ )
366
+
367
+ x_off = 0.0
368
+ y_off = -(right_chain_size[1] + self.cylinder_obj.square_base_width)
369
+ z_off = ((self.cylinder_obj.h1 + self.cylinder_obj.h2) / 2.0) - right_chain_size[2]
370
+ parent = objects[-1].root_body
371
+ objects.append(self.cylinder_obj)
372
+ object_locations.append([x_off, y_off, z_off])
373
+ object_quats.append([1.0, 0.0, 0.0, 0.0])
374
+ object_parents.append(parent)
375
+
376
+ # # add debug site to see object center
377
+ # sites = [
378
+ # dict(
379
+ # name="TMP",
380
+ # pos=array_to_string([0., 0., 0.]),
381
+ # size="{}".format(0.1),
382
+ # rgba=array_to_string([1., 0., 0., 1.]),
383
+ # )
384
+ # ]
385
+
386
+ # Run super init
387
+ super().__init__(
388
+ name=name,
389
+ objects=objects,
390
+ object_locations=object_locations,
391
+ object_quats=object_quats,
392
+ object_parents=object_parents,
393
+ joints=joints,
394
+ body_joints=object_joints,
395
+ # sites=sites,
396
+ )
GR00T-WholeBodyControl/decoupled_wbc/docker/entrypoint/bash.sh ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -e # Exit on error
3
+
4
+ echo "Dependencies installed successfully. Starting interactive bash shell..."
5
+ exec /bin/bash
GR00T-WholeBodyControl/decoupled_wbc/docker/entrypoint/deploy.sh ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -e # Exit on error
3
+
4
+ # Run the deployment script
5
+ # Check for script existence before running
6
+ DEPLOY_SCRIPT="decoupled_wbc/scripts/deploy_g1.py"
7
+ if [ -f "$DEPLOY_SCRIPT" ]; then
8
+ echo "Running deployment script at $DEPLOY_SCRIPT"
9
+ echo "Using python from $(which python)"
10
+ echo "Deploy args: $@"
11
+ exec python "$DEPLOY_SCRIPT" "$@"
12
+ else
13
+ echo "ERROR: Deployment script not found at $DEPLOY_SCRIPT"
14
+ echo "Current directory structure:"
15
+ find . -type f -name "*.py" | grep -i deploy
16
+ echo "Available script options:"
17
+ find . -type f -name "*.py" | sort
18
+ echo "Starting a bash shell for troubleshooting..."
19
+ exec /bin/bash
20
+ fi
GR00T-WholeBodyControl/decoupled_wbc/docker/entrypoint/install_deps.sh ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ # Source virtual environment and ROS2
5
+ source ${HOME}/venv/bin/activate
6
+ source /opt/ros/humble/setup.bash
7
+ export ROS_LOCALHOST_ONLY=1
8
+
9
+ # Install external dependencies
10
+ echo "Current directory: $(pwd)"
11
+ echo "Installing dependencies..."
12
+
13
+ # Install Unitree SDK and LeRobot
14
+ if [ -d "external_dependencies/unitree_sdk2_python" ]; then
15
+ cd external_dependencies/unitree_sdk2_python/
16
+ uv pip install -e . --no-deps
17
+ cd ../..
18
+ fi
19
+
20
+ # Install project packages
21
+ if [ -f "decoupled_wbc/pyproject.toml" ]; then
22
+ UV_GIT_LFS=1 uv pip install -e "decoupled_wbc[full,dev]" -e "gear_sonic[sim]"
23
+ fi
GR00T-WholeBodyControl/decoupled_wbc/tests/control/__init__.py ADDED
File without changes
GR00T-WholeBodyControl/decoupled_wbc/tests/control/main/__init__.py ADDED
File without changes
GR00T-WholeBodyControl/decoupled_wbc/tests/control/main/teleop/__init__.py ADDED
File without changes
GR00T-WholeBodyControl/decoupled_wbc/tests/control/main/teleop/test_g1_control_loop.py ADDED
@@ -0,0 +1,469 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import os
3
+ from pathlib import Path
4
+ import signal
5
+ import subprocess
6
+ import threading
7
+ import time
8
+
9
+ import numpy as np
10
+ import pytest
11
+ import rclpy
12
+ from scipy.spatial.transform import Rotation as R
13
+ from std_msgs.msg import String as RosStringMsg
14
+
15
+ from decoupled_wbc.control.main.constants import (
16
+ CONTROL_GOAL_TOPIC,
17
+ KEYBOARD_INPUT_TOPIC,
18
+ STATE_TOPIC_NAME,
19
+ )
20
+ from decoupled_wbc.control.utils.ros_utils import ROSMsgPublisher, ROSMsgSubscriber
21
+ from decoupled_wbc.control.utils.term_color_constants import GREEN_BOLD, RESET, YELLOW_BOLD
22
+ from decoupled_wbc.data.viz.rerun_viz import RerunViz
23
+
24
+
25
+ class KeyboardPublisher:
26
+ def __init__(self, topic_name: str = KEYBOARD_INPUT_TOPIC):
27
+ assert rclpy.ok(), "Expected ROS2 to be initialized in this process..."
28
+ executor = rclpy.get_global_executor()
29
+ self.node = executor.get_nodes()[0]
30
+ self.publisher = self.node.create_publisher(RosStringMsg, topic_name, 1)
31
+
32
+ def publish(self, key: str):
33
+ msg = RosStringMsg()
34
+ msg.data = key
35
+ self.publisher.publish(msg)
36
+
37
+
38
+ def is_robot_fallen_from_quat(mujoco_quat):
39
+ # Convert MuJoCo [w, x, y, z] → SciPy [x, y, z, w]
40
+ w, x, y, z = mujoco_quat
41
+ scipy_quat = [x, y, z, w]
42
+
43
+ r = R.from_quat(scipy_quat)
44
+ roll, pitch, _ = r.as_euler("xyz", degrees=False)
45
+
46
+ MAX_ROLL_PITCH = np.radians(60)
47
+ print(f"[Fall Check] roll={roll:.3f} rad, pitch={pitch:.3f} rad")
48
+ return abs(roll) > MAX_ROLL_PITCH or abs(pitch) > MAX_ROLL_PITCH
49
+
50
+
51
+ class LocomotionRunner:
52
+ def __init__(self, test_mode: str = "squat"):
53
+ self.test_mode = test_mode
54
+ if not rclpy.ok():
55
+ rclpy.init(args=None)
56
+ self.node = rclpy.create_node(f"EvalDriver_{test_mode}_{int(time.time())}")
57
+
58
+ # gracefully shutdown the spin thread when the test is done
59
+ self._stop_event = threading.Event()
60
+
61
+ self.spin_thread = threading.Thread(target=self._spin_loop, daemon=False)
62
+ self.spin_thread.start()
63
+
64
+ self.keyboard_event_publisher = KeyboardPublisher(KEYBOARD_INPUT_TOPIC)
65
+ self.control_publisher = ROSMsgPublisher(CONTROL_GOAL_TOPIC)
66
+ self.state_subscriber = ROSMsgSubscriber(STATE_TOPIC_NAME)
67
+ print(f"{test_mode} test initialized...")
68
+
69
+ def _spin_loop(self):
70
+ try:
71
+ while rclpy.ok() and not self._stop_event.is_set():
72
+ rclpy.spin_once(self.node)
73
+ except rclpy.executors.ExternalShutdownException:
74
+ print("[INFO] Spin thread exiting due to shutdown.")
75
+ finally:
76
+ print("spin loop stopped...")
77
+
78
+ def warm_up(self):
79
+ """Stabilize and release the robot."""
80
+ print("waiting for 2 seconds...")
81
+ time.sleep(2)
82
+ print(f"running {self.test_mode} test...")
83
+ self.activate()
84
+ print("activated...")
85
+ time.sleep(1)
86
+ self.release()
87
+ print("released...")
88
+ time.sleep(5)
89
+
90
+ def _run_walk_test(self):
91
+ self.walk_forward() # speed up to 0.2 m/s
92
+ time.sleep(1)
93
+ self.walk_forward() # speed up to 0.4 m/s
94
+
95
+ rate = self.node.create_rate(0.5)
96
+ start_time = time.time()
97
+ while rclpy.ok() and (time.time() - start_time) < 10.0:
98
+ obs = self.state_subscriber.get_msg()
99
+
100
+ if is_robot_fallen_from_quat(obs["torso_quat"]):
101
+ print("robot fallen...")
102
+ return 0
103
+ elif self._check_success_condition(obs):
104
+ print(f"robot reaching target ({self.test_mode})...")
105
+ return 1, {}
106
+ else:
107
+ rate.sleep()
108
+
109
+ print("test timed out after 10 seconds...")
110
+ return 0, {}
111
+
112
+ def _run_squat_test(self):
113
+ rate = self.node.create_rate(0.5)
114
+ start_time = time.time()
115
+ while rclpy.ok() and (time.time() - start_time) < 10.0:
116
+ obs = self.state_subscriber.get_msg()
117
+
118
+ if is_robot_fallen_from_quat(obs["torso_quat"]):
119
+ print("robot fallen...")
120
+ return 0, {}
121
+ elif self._check_success_condition(obs):
122
+ print(f"robot reaching target ({self.test_mode})...")
123
+ return 1, {}
124
+ else:
125
+ self.go_down()
126
+ rate.sleep()
127
+
128
+ print("test timed out after 10 seconds...")
129
+ return 0, {}
130
+
131
+ def cmd_to_velocity(self, cmd_list):
132
+ cmd_to_velocity = {
133
+ "w": np.array([0.2, 0.0, 0.0]),
134
+ "s": np.array([-0.2, 0.0, 0.0]),
135
+ "q": np.array([0.0, 0.2, 0.0]),
136
+ "e": np.array([0.0, -0.2, 0.0]),
137
+ "z": np.array([0.0, 0.0, 0.0]),
138
+ }
139
+
140
+ accumulated_velocity = np.array([0.0, 0.0, 0.0])
141
+ velocity_list = []
142
+ for cmd in cmd_list:
143
+ if cmd == "z":
144
+ accumulated_velocity = [0.0, 0.0, 0.0]
145
+ elif cmd in ["CHECK", "SKIP"]:
146
+ accumulated_velocity = velocity_list[-1]
147
+ else:
148
+ accumulated_velocity += cmd_to_velocity[cmd]
149
+ velocity_list.append(accumulated_velocity.copy())
150
+
151
+ return velocity_list
152
+
153
+ def _run_stop_test(self):
154
+ base_vel_thres = 0.25
155
+
156
+ cmd_list = (
157
+ ["w", "w", "w", "w", "s", "s", "s", "z", "SKIP", "CHECK"]
158
+ + ["s", "s", "q", "w", "w", "w", "e", "s", "s", "z", "SKIP", "CHECK"]
159
+ + ["q", "q", "w", "q", "e", "s", "s", "e", "w", "z", "SKIP", "CHECK"]
160
+ + ["w", "w", "w", "w", "w", "s", "s", "s", "s", "z", "SKIP", "CHECK"]
161
+ )
162
+
163
+ success_flag = 1
164
+
165
+ statistics = {
166
+ "floating_base_pose": {"state": []},
167
+ "floating_base_vel": {"state": [], "cmd": []},
168
+ "timestamp": [],
169
+ }
170
+ for cmd in cmd_list:
171
+ self.keyboard_event_publisher.publish(cmd)
172
+ time.sleep(0.5)
173
+ obs = self.state_subscriber.get_msg()
174
+ statistics["floating_base_pose"]["state"].append(
175
+ np.linalg.norm(obs["floating_base_pose"])
176
+ )
177
+ statistics["floating_base_vel"]["state"].append(
178
+ np.linalg.norm(obs["floating_base_vel"])
179
+ )
180
+ statistics["timestamp"].append(time.time())
181
+
182
+ if cmd == "CHECK" and np.linalg.norm(obs["floating_base_vel"]) > base_vel_thres:
183
+ print(
184
+ f" [{YELLOW_BOLD}WARNING{RESET}] robot is not stopped fully. "
185
+ f"Current base velocity: {np.linalg.norm(obs['floating_base_vel']):.3f} > {base_vel_thres:.3f}"
186
+ )
187
+ # success_flag = 0 # robot is not stopped
188
+
189
+ time.sleep(0.5)
190
+
191
+ vel_cmd = self.cmd_to_velocity(cmd_list)
192
+ vel_cmd = [np.linalg.norm(v) for v in vel_cmd]
193
+ statistics["floating_base_vel"]["cmd"] = vel_cmd
194
+ return success_flag, statistics
195
+
196
+ def _run_eef_track_test(self):
197
+ from decoupled_wbc.control.policy.lerobot_replay_policy import LerobotReplayPolicy
198
+
199
+ parquet_path = (
200
+ Path(__file__).parent.parent.parent.parent / "replay_data" / "g1_pnpbottle.parquet"
201
+ )
202
+ replay_policy = LerobotReplayPolicy(parquet_path=str(parquet_path))
203
+
204
+ freq = 50
205
+ rate = self.node.create_rate(freq)
206
+
207
+ statistics = {
208
+ # "floating_base_pose": {"state": [], "cmd": []},
209
+ "eef_base_pose": {"state": [], "cmd": []},
210
+ "timestamp": [],
211
+ }
212
+
213
+ for ii in range(500):
214
+ action = replay_policy.get_action()
215
+ action = replay_policy.action_to_cmd(action)
216
+ action["timestamp"] = time.monotonic()
217
+ action["target_time"] = time.monotonic() + ii / freq
218
+ self.control_publisher.publish(action)
219
+ obs = self.state_subscriber.get_msg()
220
+ if obs is None:
221
+ print("no obs...")
222
+ continue
223
+ gt_obs = replay_policy.get_observation()
224
+
225
+ # statistics["floating_base_pose"]["state"].append(obs["floating_base_pose"])
226
+ # statistics["floating_base_pose"]["cmd"].append(np.zeros_like(obs["floating_base_pose"]))
227
+ statistics["eef_base_pose"]["state"].append(obs["wrist_pose"])
228
+ statistics["eef_base_pose"]["cmd"].append(gt_obs["wrist_pose"])
229
+ statistics["timestamp"].append(time.time())
230
+
231
+ pos_err = np.linalg.norm(obs["wrist_pose"][:3] - gt_obs["wrist_pose"][:3])
232
+ if pos_err > 1e-1:
233
+ print(
234
+ f" [{YELLOW_BOLD}WARNING{RESET}] robot failed to track the eef, "
235
+ f"error: {pos_err:.3f} ({self.test_mode})..."
236
+ )
237
+ return 0, statistics
238
+
239
+ if is_robot_fallen_from_quat(obs["torso_quat"]):
240
+ print("robot fallen...")
241
+ return 0, statistics
242
+ else:
243
+ rate.sleep()
244
+
245
+ return 1, statistics
246
+
247
+ def run(self):
248
+ self.warm_up()
249
+
250
+ test_mode_to_func = {
251
+ "squat": self._run_squat_test,
252
+ "walk": self._run_walk_test,
253
+ "stop": self._run_stop_test,
254
+ "eef_track": self._run_eef_track_test,
255
+ }
256
+
257
+ result, statistics = test_mode_to_func[self.test_mode]()
258
+
259
+ self.post_process(statistics)
260
+ return result
261
+
262
+ def _check_success_condition(self, obs):
263
+ if self.test_mode == "squat":
264
+ return obs["floating_base_pose"][2] < 0.4
265
+ elif self.test_mode == "walk":
266
+ return np.linalg.norm(obs["floating_base_pose"][0:2]) > 1.0
267
+ return False
268
+
269
+ def activate(self):
270
+ self.keyboard_event_publisher.publish("]")
271
+
272
+ def release(self):
273
+ self.keyboard_event_publisher.publish("9")
274
+
275
+ def go_down(self):
276
+ self.keyboard_event_publisher.publish("2")
277
+
278
+ def walk_forward(self):
279
+ self.keyboard_event_publisher.publish("w")
280
+
281
+ def walk_stop(self):
282
+ self.keyboard_event_publisher.publish("z")
283
+
284
+ def post_process(self, statistics):
285
+ if len(statistics) == 0:
286
+ return
287
+
288
+ # plot the statistics
289
+ plot_keys = [key for key in statistics.keys() if key != "timestamp"]
290
+ viz = RerunViz(
291
+ image_keys=[],
292
+ tensor_keys=plot_keys,
293
+ window_size=10.0,
294
+ app_name=f"{self.test_mode}_test",
295
+ )
296
+
297
+ for ii in range(len(statistics[plot_keys[0]]["state"])):
298
+ tensor_data = {}
299
+ for k in plot_keys:
300
+ if "state" in statistics[k] and "cmd" in statistics[k]:
301
+ tensor_data[k] = np.array(
302
+ (statistics[k]["state"][ii], statistics[k]["cmd"][ii])
303
+ ).reshape(2, -1)
304
+ else:
305
+ tensor_data[k] = np.asarray(statistics[k]["state"][ii]).reshape(1, -1)
306
+ viz.plot_tensors(
307
+ tensor_data,
308
+ statistics["timestamp"][ii],
309
+ )
310
+
311
+ if self.test_mode == "stop":
312
+ base_velocity = statistics["floating_base_vel"]["state"]
313
+ base_velocity_cmd = statistics["floating_base_vel"]["cmd"]
314
+
315
+ base_velocity_tracking_err = []
316
+ for v_cmd, v in zip(base_velocity_cmd, base_velocity): # TODO: check if this is correct
317
+ if v_cmd.max() < 1e-4:
318
+ base_velocity_tracking_err.append(v)
319
+ print(
320
+ f" [{GREEN_BOLD}INFO{RESET}] Base velocity tracking when stopped: "
321
+ f"{np.mean(base_velocity_tracking_err):.3f}"
322
+ )
323
+
324
+ if self.test_mode == "eef_track":
325
+ eef_pose = statistics["eef_base_pose"]["state"]
326
+ eef_pose_cmd = statistics["eef_base_pose"]["cmd"]
327
+ eef_pose_tracking_err = []
328
+ for p_cmd, p in zip(eef_pose_cmd, eef_pose):
329
+ eef_pose_tracking_err.append(np.linalg.norm(p - p_cmd))
330
+ print(
331
+ f" [{GREEN_BOLD}INFO{RESET}] Eef pose tracking error: {np.mean(eef_pose_tracking_err):.3f}"
332
+ )
333
+
334
+ def shutdown(self):
335
+ self._stop_event.set()
336
+ self.spin_thread.join()
337
+ del self.state_subscriber
338
+ del self.keyboard_event_publisher
339
+ # Don't shutdown ROS between tests - let pytest handle it
340
+
341
+
342
+ def start_g1_control_loop():
343
+ proc = subprocess.Popen(
344
+ [
345
+ "python3",
346
+ "decoupled_wbc/control/main/teleop/run_g1_control_loop.py",
347
+ "--keyboard_dispatcher_type",
348
+ "ros",
349
+ "--enable-offscreen",
350
+ ],
351
+ preexec_fn=os.setsid,
352
+ )
353
+ time.sleep(10)
354
+ return proc
355
+
356
+
357
+ def run_test(test_mode: str):
358
+ """Run a single test with the specified mode."""
359
+ proc = start_g1_control_loop()
360
+ print(f"G1 control loop started for {test_mode} test...")
361
+
362
+ test = LocomotionRunner(test_mode)
363
+ result = test.run()
364
+
365
+ print("Shutting down...")
366
+ test.shutdown()
367
+ proc.send_signal(signal.SIGKILL)
368
+ proc.wait()
369
+
370
+ return result
371
+
372
+
373
+ def test_squat():
374
+ """Pytest function for squat test."""
375
+ result = run_test("squat")
376
+ assert result == 1, "Squat test failed - robot either fell or didn't reach target height"
377
+
378
+
379
+ def test_walk():
380
+ """Pytest function for walk test."""
381
+ result = run_test("walk")
382
+ assert result == 1, "Walk test failed - robot either fell or didn't reach target distance"
383
+
384
+
385
+ @pytest.mark.skip(reason="skipping test for now, cicd test always gets killed")
386
+ def test_stop():
387
+ """Pytest function for walking to a nearby position and stop test."""
388
+ result = run_test("stop")
389
+ assert result == 1, "Stop test failed - robot either fell or didn't reach target distance"
390
+
391
+
392
+ @pytest.mark.skip(reason="skipping test for now, cicd test always gets killed")
393
+ def test_eef_track():
394
+ """Pytest function for eef track test."""
395
+ result = run_test("eef_track")
396
+ assert result == 1, "Eef track test failed - robot either fell or didn't reach target distance"
397
+
398
+
399
+ def main():
400
+ parser = argparse.ArgumentParser(description="Run locomotion tests")
401
+ parser.add_argument("--squat", action="store_true", help="Run squat test only")
402
+ parser.add_argument("--walk", action="store_true", help="Run walk test only")
403
+ parser.add_argument("--stop", action="store_true", help="Run stop test only")
404
+ parser.add_argument("--eef_track", action="store_true", help="Run eef track test only")
405
+
406
+ args = parser.parse_args()
407
+
408
+ if args.squat and args.walk:
409
+ print("Error: Cannot specify both --squat and --walk")
410
+ return 1
411
+
412
+ if args.squat:
413
+ print("Running squat test only...")
414
+ result = run_test("squat")
415
+ if result == 1:
416
+ print("✓ Squat test PASSED")
417
+ return 0
418
+ else:
419
+ print("✗ Squat test FAILED")
420
+ return 1
421
+
422
+ elif args.walk:
423
+ print("Running walk test only...")
424
+ result = run_test("walk")
425
+ if result == 1:
426
+ print("✓ Walk test PASSED")
427
+ return 0
428
+ else:
429
+ print("✗ Walk test FAILED")
430
+ return 1
431
+
432
+ elif args.stop:
433
+ print("Running stop test only...")
434
+ result = run_test("stop")
435
+ if result == 1:
436
+ print("✓ Stop test PASSED")
437
+ return 0
438
+ else:
439
+ print("✗ Stop test FAILED")
440
+ return 1
441
+
442
+ elif args.eef_track:
443
+ print("Running eef track test only...")
444
+ result = run_test("eef_track")
445
+ if result == 1:
446
+ print("✓ Eef track test PASSED")
447
+ return 0
448
+ else:
449
+ print("✗ Eef track test FAILED")
450
+ return 1
451
+
452
+ else:
453
+ print("Running both tests...")
454
+ squat_result = run_test("squat")
455
+ walk_result = run_test("walk")
456
+
457
+ if squat_result == 1 and walk_result == 1:
458
+ print("✓ All tests PASSED")
459
+ return 0
460
+ else:
461
+ print(
462
+ f"✗ Test results: squat={'PASSED' if squat_result == 1 else 'FAILED'}, "
463
+ f"walk={'PASSED' if walk_result == 1 else 'FAILED'}"
464
+ )
465
+ return 1
466
+
467
+
468
+ if __name__ == "__main__":
469
+ exit(main())
GR00T-WholeBodyControl/decoupled_wbc/tests/control/main/test_data_exporter_loop.py ADDED
@@ -0,0 +1,403 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import glob
2
+ import os
3
+ import tempfile
4
+ from unittest.mock import MagicMock, patch
5
+
6
+ import numpy as np
7
+ import pytest
8
+
9
+ try:
10
+ from decoupled_wbc.control.main.teleop.run_g1_data_exporter import Gr00tDataCollector
11
+ from decoupled_wbc.control.robot_model.instantiation.g1 import instantiate_g1_robot_model
12
+ from decoupled_wbc.data.constants import RS_VIEW_CAMERA_HEIGHT, RS_VIEW_CAMERA_WIDTH
13
+ from decoupled_wbc.data.exporter import Gr00tDataExporter
14
+ from decoupled_wbc.data.utils import get_dataset_features
15
+ except ModuleNotFoundError as e:
16
+ if "No module named 'rclpy'" in str(e):
17
+ pytestmark = pytest.mark.skip(reason="ROS (rclpy) is not installed")
18
+ else:
19
+ raise e
20
+
21
+
22
+ import json
23
+
24
+ # How does mocking ROS work?
25
+ #
26
+ # This test file uses mocking to simulate a ROS environment without requiring actual ROS hardware:
27
+ #
28
+ # 1. ros_ok_side_effect: Controls how long the ROS loop runs by returning a sequence of
29
+ # True/False values. [True, True, False] means "run for 2 iterations then stop"
30
+ #
31
+ # 2. MockROSMsgSubscriber: Simulates sensors (camera/state) by returning pre-defined data:
32
+ #
33
+ # 3. MockKeyboardListenerSubscriber: Simulates user input:
34
+ # - 'c' = start/stop recording
35
+ # - 'd' = discard episode
36
+ # - KeyboardInterrupt = simulate Ctrl+C
37
+ # - None = no input
38
+ #
39
+ # 4. MockROSEnvironment: A context manager that patches all ROS dependencies to use our mocks,
40
+ # allowing us to test ROS-dependent code without actual ROS running.
41
+
42
+
43
+ class MockROSMsgSubscriber:
44
+ def __init__(self, return_value: list[dict]):
45
+ self.return_value = return_value
46
+ self.counter = 0
47
+
48
+ def get_image(self):
49
+ if self.counter < len(self.return_value):
50
+ self.counter += 1
51
+ return self.return_value[self.counter - 1]
52
+ else:
53
+ return None
54
+
55
+ def get_msg(self):
56
+ if self.counter < len(self.return_value):
57
+ self.counter += 1
58
+ return self.return_value[self.counter - 1]
59
+ else:
60
+ return None
61
+
62
+
63
+ class MockKeyboardListenerSubscriber:
64
+ def __init__(self, return_value: list[str]):
65
+ self.return_value = return_value
66
+ self.counter = 0
67
+
68
+ def get_keyboard_input(self):
69
+ return self.return_value[self.counter]
70
+
71
+ def read_msg(self):
72
+ if self.counter < len(self.return_value):
73
+ result = self.return_value[self.counter]
74
+ if isinstance(result, KeyboardInterrupt):
75
+ raise result
76
+ self.counter += 1
77
+ return result
78
+ return None
79
+
80
+
81
+ class MockROSEnvironment:
82
+ """Context manager for mocking ROS environment and subscribers."""
83
+
84
+ def __init__(self, ok_side_effect, keyboard_listener, img_subscriber, state_subscriber):
85
+ self.ok_side_effect = ok_side_effect
86
+ self.keyboard_listener = keyboard_listener
87
+ self.img_subscriber = img_subscriber
88
+ self.state_subscriber = state_subscriber
89
+ self.patches = []
90
+
91
+ def __enter__(self):
92
+ self.patches = [
93
+ patch("rclpy.init"),
94
+ patch("rclpy.create_node"),
95
+ patch("rclpy.spin"),
96
+ patch("rclpy.ok", side_effect=self.ok_side_effect),
97
+ patch("rclpy.shutdown"),
98
+ patch(
99
+ "decoupled_wbc.control.main.teleop.run_g1_data_exporter.KeyboardListenerSubscriber",
100
+ return_value=self.keyboard_listener,
101
+ ),
102
+ patch(
103
+ "decoupled_wbc.control.main.teleop.run_g1_data_exporter.ROSImgMsgSubscriber",
104
+ return_value=self.img_subscriber,
105
+ ),
106
+ patch(
107
+ "decoupled_wbc.control.main.teleop.run_g1_data_exporter.ROSMsgSubscriber",
108
+ return_value=self.state_subscriber,
109
+ ),
110
+ ]
111
+
112
+ for p in self.patches:
113
+ p.start()
114
+ return self
115
+
116
+ def __exit__(self, exc_type, exc_val, exc_tb):
117
+ for p in reversed(self.patches):
118
+ p.stop()
119
+ return False
120
+
121
+
122
+ def verify_parquet_files_exist(file_path: str, num_episodes: int):
123
+ parquet_files = glob.glob(os.path.join(file_path, "data/chunk-*/episode_*.parquet"))
124
+ assert (
125
+ len(parquet_files) == num_episodes
126
+ ), f"Expected {num_episodes} parquet files, but found {len(parquet_files)}"
127
+
128
+
129
+ def verify_video_files_exist(file_path: str, observation_keys: list[str], num_episodes: int):
130
+ for observation_key in observation_keys:
131
+ video_files = glob.glob(
132
+ os.path.join(file_path, f"videos/chunk-*/{observation_key}/episode_*.mp4")
133
+ )
134
+ assert (
135
+ len(video_files) == num_episodes
136
+ ), f"Expected {num_episodes} video files, but found {len(video_files)}"
137
+
138
+
139
+ def verify_metadata_files(file_path: str):
140
+ files_to_check = ["episodes.jsonl", "info.json", "tasks.jsonl", "modality.json"]
141
+ for file in files_to_check:
142
+ assert os.path.exists(os.path.join(file_path, "meta", file)), f"meta/{file} not created"
143
+
144
+
145
+ @pytest.fixture
146
+ def lerobot_features():
147
+ robot_model = instantiate_g1_robot_model()
148
+ return get_dataset_features(robot_model)
149
+
150
+
151
+ @pytest.fixture
152
+ def modality_config():
153
+ return {
154
+ "state": {"feature1": {"start": 0, "end": 4}, "feature2": {"start": 4, "end": 9}},
155
+ "action": {"feature1": {"start": 0, "end": 4}, "feature2": {"start": 4, "end": 9}},
156
+ "video": {"rs_view": {"original_key": "observation.images.ego_view"}},
157
+ "annotation": {"human.task_description": {"original_key": "task_index"}},
158
+ }
159
+
160
+
161
+ def _get_image_stream_data(episode_length: int, frame_rate: int, img_height: int, img_width: int):
162
+ return [
163
+ {
164
+ "image": np.zeros((img_height, img_width, 3), dtype=np.uint8),
165
+ "timestamp": (i * 1 / frame_rate),
166
+ }
167
+ for i in range(episode_length)
168
+ ]
169
+
170
+
171
+ def _get_state_act_stream_data(
172
+ episode_length: int, frame_rate: int, state_dim: int, action_dim: int
173
+ ):
174
+ return [
175
+ {
176
+ "q": np.zeros(state_dim),
177
+ "action": np.zeros(action_dim),
178
+ "timestamp": (i * 1 / frame_rate),
179
+ "navigate_command": np.zeros(3, dtype=np.float64),
180
+ "base_height_command": 0.0,
181
+ "wrist_pose": np.zeros(14, dtype=np.float64),
182
+ "action.eef": np.zeros(14, dtype=np.float64),
183
+ }
184
+ for i in range(episode_length)
185
+ ]
186
+
187
+
188
+ def test_control_loop_happy_path_workflow(lerobot_features, modality_config):
189
+ """
190
+ This test records a single episode and saves it to disk.
191
+ """
192
+ episode_length = 10
193
+ frame_rate = 20
194
+ img_stream_data = _get_image_stream_data(
195
+ episode_length, frame_rate, RS_VIEW_CAMERA_HEIGHT, RS_VIEW_CAMERA_WIDTH
196
+ )
197
+ robot_model = instantiate_g1_robot_model()
198
+ state_act_stream_data = _get_state_act_stream_data(
199
+ episode_length, frame_rate, robot_model.num_joints, robot_model.num_joints
200
+ )
201
+
202
+ keyboard_sub_output = [None for _ in range(episode_length)]
203
+ keyboard_sub_output[0] = "c" # Start recording
204
+ keyboard_sub_output[-1] = "c" # Stop recording and save
205
+
206
+ # --------- Save the first episode ---------
207
+ mock_img_sub = MockROSMsgSubscriber(img_stream_data)
208
+ mock_state_sub = MockROSMsgSubscriber(state_act_stream_data)
209
+ mock_keyboard_listner = MockKeyboardListenerSubscriber(keyboard_sub_output)
210
+
211
+ with tempfile.TemporaryDirectory() as temp_dir:
212
+ dataset_dir = os.path.join(temp_dir, "dataset")
213
+
214
+ data_exporter = Gr00tDataExporter.create(
215
+ save_root=dataset_dir,
216
+ fps=frame_rate,
217
+ features=lerobot_features,
218
+ modality_config=modality_config,
219
+ task="test",
220
+ )
221
+
222
+ ros_ok_side_effect = [True] * (episode_length + 1) + [False]
223
+ with MockROSEnvironment(
224
+ ros_ok_side_effect, mock_keyboard_listner, mock_img_sub, mock_state_sub
225
+ ):
226
+ data_collector = Gr00tDataCollector(
227
+ camera_topic_name="mock_camera_topic",
228
+ state_topic_name="mock_state_topic",
229
+ data_exporter=data_exporter,
230
+ frequency=frame_rate,
231
+ )
232
+
233
+ # mocking to avoid actual sleeping
234
+ data_collector.rate = MagicMock()
235
+
236
+ data_collector.run()
237
+
238
+ verify_parquet_files_exist(dataset_dir, 1)
239
+ verify_video_files_exist(dataset_dir, data_exporter.meta.video_keys, 1)
240
+ verify_metadata_files(dataset_dir)
241
+
242
+ # --------- Save the second episode ---------
243
+ # we reset the mock subscribers and re-run the control loop
244
+ # This immitates the case where the user starts recording a new episode on an existing dataset
245
+ mock_img_sub = MockROSMsgSubscriber(img_stream_data)
246
+ mock_state_sub = MockROSMsgSubscriber(state_act_stream_data)
247
+ ros_ok_side_effect = [True] * (episode_length + 1) + [False]
248
+ mock_keyboard_listner = MockKeyboardListenerSubscriber(keyboard_sub_output)
249
+ with MockROSEnvironment(
250
+ ros_ok_side_effect, mock_keyboard_listner, mock_img_sub, mock_state_sub
251
+ ):
252
+ data_collector = Gr00tDataCollector(
253
+ camera_topic_name="mock_camera_topic",
254
+ state_topic_name="mock_state_topic",
255
+ data_exporter=data_exporter,
256
+ frequency=frame_rate,
257
+ )
258
+
259
+ # mocking to avoid actual sleeping
260
+ data_collector.rate = MagicMock()
261
+
262
+ data_collector.run()
263
+
264
+ # now there should be 2 episodes in the dataset
265
+ verify_parquet_files_exist(dataset_dir, 2)
266
+ verify_video_files_exist(dataset_dir, data_exporter.meta.video_keys, 2)
267
+ verify_metadata_files(dataset_dir)
268
+
269
+
270
+ def test_control_loop_keyboard_interrupt_workflow(lerobot_features, modality_config):
271
+ """
272
+ This test simulates a keyboard interruption in the middle of recording.
273
+ Expected behavior:
274
+ - The episode is saved to disk
275
+ - The episode is marked as discarded
276
+ """
277
+ episode_length = 15
278
+ frame_rate = 20
279
+ img_stream_data = _get_image_stream_data(
280
+ episode_length, frame_rate, RS_VIEW_CAMERA_HEIGHT, RS_VIEW_CAMERA_WIDTH
281
+ )
282
+ robot_model = instantiate_g1_robot_model()
283
+ state_act_stream_data = _get_state_act_stream_data(
284
+ episode_length, frame_rate, robot_model.num_joints, robot_model.num_joints
285
+ )
286
+
287
+ keyboard_sub_output = [None for _ in range(episode_length)]
288
+ keyboard_sub_output[0] = "c" # Start recording
289
+ keyboard_sub_output[5] = KeyboardInterrupt() # keyboard interruption in the middle of recording
290
+
291
+ mock_img_sub = MockROSMsgSubscriber(img_stream_data)
292
+ mock_state_sub = MockROSMsgSubscriber(state_act_stream_data)
293
+ mock_keyboard_listener = MockKeyboardListenerSubscriber(keyboard_sub_output)
294
+
295
+ with tempfile.TemporaryDirectory() as temp_dir:
296
+ dataset_dir = os.path.join(temp_dir, "dataset")
297
+
298
+ data_exporter = Gr00tDataExporter.create(
299
+ save_root=dataset_dir,
300
+ fps=frame_rate,
301
+ features=lerobot_features,
302
+ modality_config=modality_config,
303
+ task="test",
304
+ )
305
+
306
+ ros_ok_side_effect = [True] * episode_length + [False]
307
+ with MockROSEnvironment(
308
+ ros_ok_side_effect, mock_keyboard_listener, mock_img_sub, mock_state_sub
309
+ ):
310
+ data_collector = Gr00tDataCollector(
311
+ camera_topic_name="mock_camera_topic",
312
+ state_topic_name="mock_state_topic",
313
+ data_exporter=data_exporter,
314
+ frequency=frame_rate,
315
+ )
316
+
317
+ data_collector.rate = MagicMock()
318
+ # try:
319
+ data_collector.run()
320
+ # except KeyboardInterrupt:
321
+ # pass
322
+
323
+ verify_parquet_files_exist(dataset_dir, 1)
324
+ verify_video_files_exist(dataset_dir, data_exporter.meta.video_keys, 1)
325
+ verify_metadata_files(dataset_dir)
326
+
327
+ # verify that the episode is marked as discarded
328
+ ep_info = json.load(open(os.path.join(dataset_dir, "meta", "info.json")))
329
+ assert ep_info["discarded_episode_indices"][0] == 0
330
+ assert ep_info["total_frames"] == 5
331
+ assert ep_info["total_episodes"] == 1
332
+
333
+
334
+ def test_discarded_episode_workflow(lerobot_features, modality_config):
335
+ """
336
+ This test simulates a case where the user discards an episode in the middle of recording.
337
+ Expected behavior:
338
+ - Record 3 episodes, discard episode 0 and 2
339
+ - There should be 3 episodes saved to disk
340
+ - Episode 0 and 2 should be flagged as discarded
341
+ """
342
+ episode_length = 17
343
+ frame_rate = 20
344
+ robot_model = instantiate_g1_robot_model()
345
+ state_dim = robot_model.num_joints
346
+ action_dim = robot_model.num_joints
347
+ img_stream_data = _get_image_stream_data(
348
+ episode_length, frame_rate, RS_VIEW_CAMERA_HEIGHT, RS_VIEW_CAMERA_WIDTH
349
+ )
350
+ state_act_stream_data = _get_state_act_stream_data(
351
+ episode_length, frame_rate, state_dim, action_dim
352
+ )
353
+
354
+ keyboard_sub_output = [None for _ in range(episode_length)]
355
+ keyboard_sub_output[0] = "c" # Start recording episode index 0
356
+ keyboard_sub_output[5] = "x" # Discard episode index 0
357
+ keyboard_sub_output[7] = "c" # Start recording episode index 1
358
+ keyboard_sub_output[10] = "c" # stop recording and save episode index 1
359
+ keyboard_sub_output[12] = "c" # start recording episode index 2
360
+ keyboard_sub_output[15] = "x" # discard episode index 2
361
+
362
+ mock_img_sub = MockROSMsgSubscriber(img_stream_data)
363
+ mock_state_sub = MockROSMsgSubscriber(state_act_stream_data)
364
+ mock_keyboard_listener = MockKeyboardListenerSubscriber(keyboard_sub_output)
365
+
366
+ with tempfile.TemporaryDirectory() as temp_dir:
367
+ dataset_dir = os.path.join(temp_dir, "dataset")
368
+
369
+ data_exporter = Gr00tDataExporter.create(
370
+ save_root=dataset_dir,
371
+ fps=frame_rate,
372
+ features=lerobot_features,
373
+ modality_config=modality_config,
374
+ task="test",
375
+ )
376
+
377
+ ros_ok_side_effect = [True] * episode_length + [False]
378
+ with MockROSEnvironment(
379
+ ros_ok_side_effect, mock_keyboard_listener, mock_img_sub, mock_state_sub
380
+ ):
381
+ data_collector = Gr00tDataCollector(
382
+ camera_topic_name="mock_camera_topic",
383
+ state_topic_name="mock_state_topic",
384
+ data_exporter=data_exporter,
385
+ frequency=frame_rate,
386
+ )
387
+
388
+ data_collector.rate = MagicMock()
389
+ try:
390
+ data_collector.run()
391
+ except Exception:
392
+ pass
393
+
394
+ # vrify if the episode is marked as discarded
395
+ ep_info = json.load(open(os.path.join(dataset_dir, "meta", "info.json")))
396
+ assert len(ep_info["discarded_episode_indices"]) == 2
397
+ assert ep_info["discarded_episode_indices"][0] == 0
398
+ assert ep_info["discarded_episode_indices"][1] == 2
399
+
400
+ # verify that all episodes are saved regardless of being discarded
401
+ verify_parquet_files_exist(dataset_dir, 3)
402
+ verify_video_files_exist(dataset_dir, data_exporter.meta.video_keys, 3)
403
+ verify_metadata_files(dataset_dir)
GR00T-WholeBodyControl/decoupled_wbc/tests/control/policy/__init__.py ADDED
File without changes
GR00T-WholeBodyControl/decoupled_wbc/tests/control/policy/interpolation_policy/__init__.py ADDED
File without changes
GR00T-WholeBodyControl/decoupled_wbc/tests/control/policy/interpolation_policy/test_interpolation_policy.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import pickle
3
+
4
+ import numpy as np
5
+ import pytest
6
+
7
+ from decoupled_wbc.control.policy.interpolation_policy import (
8
+ InterpolationPolicy,
9
+ )
10
+
11
+
12
+ def get_test_data_path(filename: str) -> str:
13
+ """Get the absolute path to a test data file."""
14
+ test_dir = Path(__file__).parent
15
+ return str(test_dir / ".." / ".." / ".." / "replay_data" / filename)
16
+
17
+
18
+ @pytest.fixture
19
+ def logged_data():
20
+ """Load the logged data from file."""
21
+ data_path = get_test_data_path("interpolation_data.pkl")
22
+ with open(data_path, "rb") as f:
23
+ return pickle.load(f)
24
+
25
+
26
+ def test_replay_logged_data(logged_data):
27
+ """Test that the wrapper produces the same pose commands as logged data."""
28
+ init_args = logged_data["init_args"]
29
+ interp = InterpolationPolicy(
30
+ init_time=init_args["curr_t"],
31
+ init_values={"target_pose": init_args["curr_pose"]},
32
+ max_change_rate=np.inf,
33
+ )
34
+
35
+ # Test all data points including the first one
36
+ for c in logged_data["calls"]:
37
+ # Get the action from wrapper
38
+ if c["type"] == "get_action":
39
+ action = interp.get_action(**c["args"])
40
+ expected_action = c["result"]
41
+ np.testing.assert_allclose(
42
+ action["target_pose"], expected_action["q"], rtol=1e-9, atol=1e-9
43
+ )
44
+ # print(action, expected_action)
45
+
46
+ else:
47
+ interp.set_goal(**c["args"])
GR00T-WholeBodyControl/decoupled_wbc/tests/control/policy/interpolation_policy/test_interpolation_ramp_up.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import pytest
3
+
4
+ from decoupled_wbc.control.policy.interpolation_policy import (
5
+ InterpolationPolicy,
6
+ )
7
+
8
+
9
+ def test_trajectory_interpolation():
10
+ """
11
+ Test that the InterpolationPolicy correctly interpolates between waypoints.
12
+
13
+ Initial pose is at all zeros.
14
+ At t=4sec, the index 27 position (right_shoulder_yaw_joint) should be -1.5.
15
+ We run at 100Hz to see all intermediate waypoints.
16
+
17
+ Notes:
18
+ - The trajectory data is at 'trajectory_data.npy' in the current directory
19
+ - The visualization is at 'trajectory.png' in the current directory
20
+ """
21
+ # Create a pose with 32 joints (all zeros initially)
22
+ num_joints = 32
23
+ initial_pose = np.zeros(num_joints)
24
+
25
+ # Initial time (use a fixed value for reproducibility)
26
+ initial_time = 0.0
27
+
28
+ # Create the wrapper with initial pose
29
+ interpolator = InterpolationPolicy(
30
+ init_time=initial_time,
31
+ init_values={"target_pose": initial_pose},
32
+ max_change_rate=np.inf,
33
+ )
34
+
35
+ # Target pose: all zeros except index 27 which should be -1.5
36
+ target_pose = np.zeros(num_joints)
37
+ target_pose[27] = -1.5 # right_shoulder_yaw_joint
38
+ target_time = 4.0 # 4 seconds from now
39
+
40
+ # Set the planner command to schedule the waypoint
41
+ interpolator.set_goal(
42
+ {
43
+ "target_pose": target_pose,
44
+ "target_time": target_time,
45
+ "interpolation_garbage_collection_time": initial_time,
46
+ }
47
+ )
48
+
49
+ # Sample the trajectory at 100Hz
50
+ frequency = 100
51
+ dt = 1.0 / frequency
52
+ sample_times = np.arange(initial_time, target_time + dt, dt)
53
+
54
+ # Collect the interpolated poses
55
+ sampled_poses = []
56
+ for t in sample_times:
57
+ action = interpolator.get_action(t)
58
+ sampled_poses.append(action["target_pose"])
59
+
60
+ # Convert to numpy array for easier analysis
61
+ sampled_poses = np.array(sampled_poses)
62
+
63
+ # Check specific requirements
64
+ # Verify we actually moved from 0 to -1.5
65
+ joint_27_positions = sampled_poses[:, 27]
66
+ assert joint_27_positions[0] == pytest.approx(0.0)
67
+ assert joint_27_positions[-1] == pytest.approx(-1.5)
68
+
69
+ # Calculate the absolute changes between each step
70
+ changes = np.abs(np.diff(joint_27_positions))
71
+ assert np.all(changes < 0.004), "Joint 27 position should change by less than 0.004"
72
+
73
+ # Print some statistics about the trajectory
74
+ print(f"Total time steps: {len(sample_times)}")
75
+ print(f"Joint 27 trajectory start: {joint_27_positions[0]}")
76
+ print(f"Joint 27 trajectory end: {joint_27_positions[-1]}")
77
+ print(f"Joint 27 max velocity: {np.max(np.abs(np.diff(joint_27_positions) / dt))}")
78
+ print(f"Max velocity timestep: {np.argmax(np.abs(np.diff(joint_27_positions) / dt))}")
GR00T-WholeBodyControl/decoupled_wbc/tests/control/policy/interpolation_policy/trajectory.png ADDED
GR00T-WholeBodyControl/decoupled_wbc/tests/control/robot_model/__init__.py ADDED
File without changes
GR00T-WholeBodyControl/decoupled_wbc/tests/control/robot_model/robot_model_test.py ADDED
@@ -0,0 +1,911 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # test_robot_model.py
2
+
3
+ import numpy as np
4
+ import pinocchio as pin
5
+ import pytest
6
+
7
+ from decoupled_wbc.control.robot_model import ReducedRobotModel
8
+ from decoupled_wbc.control.robot_model.instantiation.g1 import instantiate_g1_robot_model
9
+
10
+
11
+ @pytest.fixture
12
+ def g1_robot_model():
13
+ """
14
+ Fixture that creates and returns a G1 RobotModel instance.
15
+ """
16
+ return instantiate_g1_robot_model()
17
+
18
+
19
+ def test_robot_model_initialization(g1_robot_model):
20
+ """
21
+ Test initialization of the RobotModel and its main attributes.
22
+ """
23
+ for robot_model in [g1_robot_model]:
24
+ # Check that the Pinocchio wrapper exists
25
+ assert robot_model.pinocchio_wrapper is not None
26
+
27
+ # Check number of degrees of freedom (nq)
28
+ assert robot_model.num_dofs > 0
29
+
30
+ # Check we have the expected number of joints beyond the floating base
31
+ assert len(robot_model.joint_names) > 0
32
+
33
+ # Check that supplemental info is present
34
+ assert robot_model.supplemental_info is not None
35
+
36
+
37
+ def test_robot_model_joint_names(g1_robot_model):
38
+ """
39
+ Test that joint_names is populated correctly
40
+ and that dof_index works.
41
+ """
42
+ for robot_model in [g1_robot_model]:
43
+ # Extract joint names
44
+ joint_names = robot_model.joint_names
45
+
46
+ # Pick the first joint name and get its index
47
+ first_joint_name = joint_names[0]
48
+ idx = robot_model.dof_index(first_joint_name)
49
+ assert idx >= 0
50
+
51
+ # Test that an unknown joint name raises an error
52
+ with pytest.raises(ValueError, match="Unknown joint name"):
53
+ _ = robot_model.dof_index("non_existent_joint")
54
+
55
+
56
+ def test_robot_model_forward_kinematics_valid_q(g1_robot_model):
57
+ """
58
+ Test that cache_forward_kinematics works with a valid q.
59
+ """
60
+ for robot_model in [g1_robot_model]:
61
+ nq = robot_model.num_dofs
62
+
63
+ # Construct a valid configuration (e.g., zero vector)
64
+ q_valid = np.zeros(nq)
65
+
66
+ # Should not raise any exception
67
+ robot_model.cache_forward_kinematics(q_valid)
68
+
69
+
70
+ def test_robot_model_forward_kinematics_invalid_q(g1_robot_model):
71
+ """
72
+ Test that cache_forward_kinematics raises an error with an invalid q.
73
+ """
74
+ for robot_model in [g1_robot_model]:
75
+ nq = robot_model.num_dofs
76
+
77
+ # Construct an invalid configuration (wrong size)
78
+ q_invalid = np.zeros(nq + 1)
79
+
80
+ with pytest.raises(ValueError, match="Expected q of length"):
81
+ robot_model.cache_forward_kinematics(q_invalid)
82
+
83
+
84
+ def test_robot_model_frame_placement(g1_robot_model):
85
+ """
86
+ Test the frame_placement method with a valid and invalid frame name.
87
+ Also test that frame placements change with different configurations.
88
+ """
89
+ for robot_model in [g1_robot_model]:
90
+ # Skip if no supplemental info
91
+ if robot_model.supplemental_info is None:
92
+ pytest.skip("No supplemental info available for testing")
93
+
94
+ # Use the hand frame from supplemental info
95
+ test_frame = robot_model.supplemental_info.hand_frame_names["left"]
96
+
97
+ # Test with zero configuration
98
+ q_zero = np.zeros(robot_model.num_dofs)
99
+ robot_model.cache_forward_kinematics(q_zero)
100
+ placement_zero = robot_model.frame_placement(test_frame)
101
+ assert isinstance(placement_zero, pin.SE3)
102
+
103
+ # Test with non-zero configuration
104
+ q_non_zero = np.zeros(robot_model.num_dofs)
105
+ root_nq = 7 if robot_model.is_floating_base_model else 0
106
+
107
+ # Set a more significant configuration change
108
+ # Use π/2 for all joints to create a more noticeable difference
109
+ q_non_zero[root_nq:] = np.pi / 2 # 90 degrees for all joints
110
+
111
+ robot_model.cache_forward_kinematics(q_non_zero)
112
+ placement_non_zero = robot_model.frame_placement(test_frame)
113
+
114
+ # Verify that frame placements are different with different configurations
115
+ assert not np.allclose(
116
+ placement_zero.translation, placement_non_zero.translation
117
+ ) or not np.allclose(placement_zero.rotation, placement_non_zero.rotation)
118
+
119
+ # Should raise an error for an invalid frame
120
+ with pytest.raises(ValueError, match="Unknown frame"):
121
+ robot_model.frame_placement("non_existent_frame")
122
+
123
+
124
+ # Tests for ReducedRobotModel
125
+ def test_reduced_robot_model_initialization(g1_robot_model):
126
+ """
127
+ Test initialization of the ReducedRobotModel.
128
+ """
129
+ for robot_model in [g1_robot_model]:
130
+ # Create a reduced model by fixing some actual joints from the robot
131
+ fixed_joints = robot_model.joint_names[:2] # Use first two joints from the robot
132
+ reduced_robot = ReducedRobotModel(robot_model, fixed_joints)
133
+
134
+ # Check that the full robot is stored
135
+ assert reduced_robot.full_robot is robot_model
136
+
137
+ # Check that fixed joints are stored correctly
138
+ assert reduced_robot.fixed_joints == fixed_joints
139
+ assert len(reduced_robot.fixed_values) == len(fixed_joints)
140
+
141
+ # Check that the number of dofs is reduced
142
+ assert reduced_robot.num_dofs == robot_model.num_dofs - len(fixed_joints)
143
+
144
+
145
+ def test_reduced_robot_model_joint_names(g1_robot_model):
146
+ """
147
+ Test that joint_names in ReducedRobotModel excludes fixed joints.
148
+ """
149
+ for robot_model in [g1_robot_model]:
150
+ # Use actual joints from the robot
151
+ fixed_joints = robot_model.joint_names[:2] # Use first two joints from the robot
152
+ reduced_robot = ReducedRobotModel(robot_model, fixed_joints)
153
+
154
+ # Check that fixed joints are not in the reduced model's joint names
155
+ for joint in fixed_joints:
156
+ assert joint not in reduced_robot.joint_names
157
+
158
+ # Check that other joints are still present
159
+ for joint in robot_model.joint_names:
160
+ if joint not in fixed_joints:
161
+ assert joint in reduced_robot.joint_names
162
+
163
+
164
+ def test_reduced_robot_model_configuration_conversion(g1_robot_model):
165
+ """
166
+ Test conversion between reduced and full configurations.
167
+ """
168
+ for robot_model in [g1_robot_model]:
169
+ # Use actual joints from the robot
170
+ fixed_joints = robot_model.joint_names[:2] # Use first two joints from the robot
171
+ fixed_values = [0.5, 1.0]
172
+ reduced_robot = ReducedRobotModel(robot_model, fixed_joints, fixed_values)
173
+
174
+ # Create a reduced configuration
175
+ q_reduced = np.zeros(reduced_robot.num_dofs)
176
+ q_reduced[0] = 0.3 # Set some value for testing
177
+
178
+ # Convert to full configuration
179
+ q_full = reduced_robot.reduced_to_full_configuration(q_reduced)
180
+
181
+ # Check that fixed joints have the correct values
182
+ for joint_name, value in zip(fixed_joints, fixed_values):
183
+ full_idx = robot_model.dof_index(joint_name)
184
+ assert q_full[full_idx] == value
185
+
186
+ # Convert back to reduced configuration
187
+ q_reduced_back = reduced_robot.full_to_reduced_configuration(q_full)
188
+
189
+ # Check that the conversion is reversible
190
+ np.testing.assert_array_almost_equal(q_reduced, q_reduced_back)
191
+
192
+
193
+ def test_reduced_robot_model_forward_kinematics(g1_robot_model):
194
+ """
195
+ Test forward kinematics with the reduced model.
196
+ """
197
+ for robot_model in [g1_robot_model]:
198
+ # Use actual joints from the robot
199
+ fixed_joints = robot_model.joint_names[:2] # Use first two joints from the robot
200
+ reduced_robot = ReducedRobotModel(robot_model, fixed_joints)
201
+
202
+ # Create a reduced configuration
203
+ q_reduced = np.zeros(reduced_robot.num_dofs)
204
+
205
+ # Should not raise any exception
206
+ reduced_robot.cache_forward_kinematics(q_reduced)
207
+
208
+ # Check that frame placement works
209
+ model = robot_model.pinocchio_wrapper.model
210
+ if len(model.frames) > 1:
211
+ valid_frame = model.frames[1].name
212
+ placement = reduced_robot.frame_placement(valid_frame)
213
+ assert isinstance(placement, pin.SE3)
214
+
215
+
216
+ def test_robot_model_clip_configuration(g1_robot_model):
217
+ """
218
+ Test that clip_configuration properly clips values to joint limits.
219
+ """
220
+ for robot_model in [g1_robot_model]:
221
+ # Create a configuration with some values outside limits
222
+ q = np.zeros(robot_model.num_dofs)
223
+ root_nq = 7 if robot_model.is_floating_base_model else 0
224
+ # Create extreme values for all joints
225
+ q[root_nq:] = np.array([100.0, -100.0, 50.0, -50.0] * (robot_model.num_joints // 4 + 1))[
226
+ : robot_model.num_joints
227
+ ]
228
+
229
+ # Clip the configuration
230
+ q_clipped = robot_model.clip_configuration(q)
231
+
232
+ # Check that values are within limits
233
+ assert np.all(q_clipped[root_nq:] <= robot_model.upper_joint_limits)
234
+ assert np.all(q_clipped[root_nq:] >= robot_model.lower_joint_limits)
235
+
236
+
237
+ def test_robot_model_get_actuated_joints(g1_robot_model):
238
+ """
239
+ Test getting body and hand actuated joints from configuration.
240
+ """
241
+ for robot_model in [g1_robot_model]:
242
+ # Skip if no supplemental info
243
+ if robot_model.supplemental_info is None:
244
+ pytest.skip("No supplemental info available for testing actuated joints")
245
+
246
+ # Create a test configuration
247
+ q = np.zeros(robot_model.num_dofs)
248
+ root_nq = 7 if robot_model.is_floating_base_model else 0
249
+ q[root_nq:] = np.arange(robot_model.num_joints) # Set some values for joints
250
+
251
+ # Test body actuated joints
252
+ body_joints = robot_model.get_body_actuated_joints(q)
253
+ assert len(body_joints) == len(robot_model.get_body_actuated_joint_indices())
254
+
255
+ # Test hand actuated joints
256
+ hand_joints = robot_model.get_hand_actuated_joints(q)
257
+ assert len(hand_joints) == len(robot_model.get_hand_actuated_joint_indices())
258
+
259
+ # Test left hand joints
260
+ left_hand_joints = robot_model.get_hand_actuated_joints(q, side="left")
261
+ assert len(left_hand_joints) == len(robot_model.get_hand_actuated_joint_indices("left"))
262
+
263
+ # Test right hand joints
264
+ right_hand_joints = robot_model.get_hand_actuated_joints(q, side="right")
265
+ assert len(right_hand_joints) == len(robot_model.get_hand_actuated_joint_indices("right"))
266
+
267
+
268
+ def test_robot_model_get_configuration_from_actuated_joints(g1_robot_model):
269
+ """
270
+ Test creating full configuration from actuated joint values.
271
+ """
272
+ for robot_model in [g1_robot_model]:
273
+ # Skip if no supplemental info
274
+ if robot_model.supplemental_info is None:
275
+ pytest.skip("No supplemental info available for testing actuated joints")
276
+
277
+ # Create test values for body and hands
278
+ body_values = np.ones(len(robot_model.get_body_actuated_joint_indices()))
279
+ hand_values = np.ones(len(robot_model.get_hand_actuated_joint_indices()))
280
+ left_hand_values = np.ones(len(robot_model.get_hand_actuated_joint_indices("left")))
281
+ right_hand_values = np.ones(len(robot_model.get_hand_actuated_joint_indices("right")))
282
+
283
+ # Test with combined hand values
284
+ q = robot_model.get_configuration_from_actuated_joints(
285
+ body_actuated_joint_values=body_values, hand_actuated_joint_values=hand_values
286
+ )
287
+ assert q.shape == (robot_model.num_dofs,)
288
+
289
+ # Test with separate hand values
290
+ q = robot_model.get_configuration_from_actuated_joints(
291
+ body_actuated_joint_values=body_values,
292
+ left_hand_actuated_joint_values=left_hand_values,
293
+ right_hand_actuated_joint_values=right_hand_values,
294
+ )
295
+ assert q.shape == (robot_model.num_dofs,)
296
+
297
+
298
+ def test_robot_model_reset_forward_kinematics(g1_robot_model):
299
+ """
300
+ Test resetting forward kinematics to default configuration.
301
+ """
302
+ for robot_model in [g1_robot_model]:
303
+ # Skip if no supplemental info
304
+ if robot_model.supplemental_info is None:
305
+ pytest.skip("No supplemental info available for testing")
306
+
307
+ # Create a more significant configuration change
308
+ q = np.zeros(robot_model.num_dofs)
309
+ root_nq = 7 if robot_model.is_floating_base_model else 0
310
+ # Set some extreme joint angles
311
+ q[root_nq:] = np.pi / 2 # 90 degrees for all joints
312
+ robot_model.cache_forward_kinematics(q)
313
+
314
+ # Use a hand frame from supplemental info
315
+ test_frame = robot_model.supplemental_info.hand_frame_names["left"]
316
+
317
+ # Reset to default
318
+ robot_model.reset_forward_kinematics()
319
+ # Get frame placement after reset
320
+ placement_default = robot_model.frame_placement(test_frame)
321
+
322
+ # Check that frame placement matches what we get with q_zero
323
+ robot_model.cache_forward_kinematics(robot_model.q_zero)
324
+ placement_q_zero = robot_model.frame_placement(test_frame)
325
+ np.testing.assert_array_almost_equal(
326
+ placement_default.translation, placement_q_zero.translation
327
+ )
328
+ np.testing.assert_array_almost_equal(placement_default.rotation, placement_q_zero.rotation)
329
+
330
+
331
+ # Additional tests for ReducedRobotModel
332
+ def test_reduced_robot_model_clip_configuration(g1_robot_model):
333
+ """
334
+ Test that clip_configuration works in reduced space.
335
+ """
336
+ for robot_model in [g1_robot_model]:
337
+ fixed_joints = robot_model.joint_names[:2]
338
+ reduced_robot = ReducedRobotModel(robot_model, fixed_joints)
339
+
340
+ # Create a configuration with some values outside limits
341
+ q_reduced = np.zeros(reduced_robot.num_dofs)
342
+ root_nq = 7 if reduced_robot.full_robot.is_floating_base_model else 0
343
+ # Create extreme values for all joints
344
+ q_reduced[root_nq:] = np.array(
345
+ [100.0, -100.0, 50.0, -50.0] * (reduced_robot.num_joints // 4 + 1)
346
+ )[: reduced_robot.num_joints]
347
+
348
+ # Clip the configuration
349
+ q_clipped = reduced_robot.clip_configuration(q_reduced)
350
+
351
+ # Check that values are within limits
352
+ assert np.all(q_clipped[root_nq:] <= reduced_robot.upper_joint_limits)
353
+ assert np.all(q_clipped[root_nq:] >= reduced_robot.lower_joint_limits)
354
+
355
+
356
+ def test_reduced_robot_model_get_actuated_joints(g1_robot_model):
357
+ """
358
+ Test getting body and hand actuated joints from reduced configuration.
359
+ """
360
+ for robot_model in [g1_robot_model]:
361
+ # Skip if no supplemental info
362
+ if robot_model.supplemental_info is None:
363
+ pytest.skip("No supplemental info available for testing actuated joints")
364
+
365
+ fixed_joints = robot_model.joint_names[:2]
366
+ reduced_robot = ReducedRobotModel(robot_model, fixed_joints)
367
+
368
+ # Create a test configuration
369
+ q_reduced = np.zeros(reduced_robot.num_dofs)
370
+ root_nq = 7 if reduced_robot.full_robot.is_floating_base_model else 0
371
+ q_reduced[root_nq:] = np.arange(reduced_robot.num_joints)
372
+
373
+ # Test body actuated joints
374
+ body_joints = reduced_robot.get_body_actuated_joints(q_reduced)
375
+ assert len(body_joints) == len(reduced_robot.get_body_actuated_joint_indices())
376
+
377
+ # Test hand actuated joints
378
+ hand_joints = reduced_robot.get_hand_actuated_joints(q_reduced)
379
+ assert len(hand_joints) == len(reduced_robot.get_hand_actuated_joint_indices())
380
+
381
+
382
+ def test_reduced_robot_model_get_configuration_from_actuated_joints(g1_robot_model):
383
+ """
384
+ Test creating reduced configuration from actuated joint values.
385
+ """
386
+ for robot_model in [g1_robot_model]:
387
+ # Skip if no supplemental info
388
+ if robot_model.supplemental_info is None:
389
+ pytest.skip("No supplemental info available for testing actuated joints")
390
+
391
+ fixed_joints = robot_model.joint_names[:2]
392
+ reduced_robot = ReducedRobotModel(robot_model, fixed_joints)
393
+
394
+ # Create test values for body and hands
395
+ body_values = np.ones(len(reduced_robot.get_body_actuated_joint_indices()))
396
+ hand_values = np.ones(len(reduced_robot.get_hand_actuated_joint_indices()))
397
+ left_hand_values = np.ones(len(reduced_robot.get_hand_actuated_joint_indices("left")))
398
+ right_hand_values = np.ones(len(reduced_robot.get_hand_actuated_joint_indices("right")))
399
+
400
+ # Test with combined hand values
401
+ q_reduced = reduced_robot.get_configuration_from_actuated_joints(
402
+ body_actuated_joint_values=body_values, hand_actuated_joint_values=hand_values
403
+ )
404
+ assert q_reduced.shape == (reduced_robot.num_dofs,)
405
+
406
+ # Test with separate hand values
407
+ q_reduced = reduced_robot.get_configuration_from_actuated_joints(
408
+ body_actuated_joint_values=body_values,
409
+ left_hand_actuated_joint_values=left_hand_values,
410
+ right_hand_actuated_joint_values=right_hand_values,
411
+ )
412
+ assert q_reduced.shape == (reduced_robot.num_dofs,)
413
+
414
+ # Verify that the values were set correctly in the reduced configuration
415
+ # Check body actuated joints
416
+ body_indices = reduced_robot.get_body_actuated_joint_indices()
417
+ np.testing.assert_array_almost_equal(q_reduced[body_indices], body_values)
418
+
419
+ # Check left hand actuated joints
420
+ left_hand_indices = reduced_robot.get_hand_actuated_joint_indices("left")
421
+ np.testing.assert_array_almost_equal(q_reduced[left_hand_indices], left_hand_values)
422
+
423
+ # Check right hand actuated joints
424
+ right_hand_indices = reduced_robot.get_hand_actuated_joint_indices("right")
425
+ np.testing.assert_array_almost_equal(q_reduced[right_hand_indices], right_hand_values)
426
+
427
+
428
+ def test_reduced_robot_model_reset_forward_kinematics(g1_robot_model):
429
+ """
430
+ Test resetting forward kinematics in reduced model.
431
+ """
432
+ for robot_model in [g1_robot_model]:
433
+ # Skip if no supplemental info
434
+ if robot_model.supplemental_info is None:
435
+ pytest.skip("No supplemental info available for testing")
436
+
437
+ fixed_joints = robot_model.joint_names[:2]
438
+ reduced_robot = ReducedRobotModel(robot_model, fixed_joints)
439
+
440
+ # Create a more significant configuration change
441
+ q_reduced = np.zeros(reduced_robot.num_dofs)
442
+ root_nq = 7 if reduced_robot.full_robot.is_floating_base_model else 0
443
+ # Set some extreme joint angles
444
+ q_reduced[root_nq:] = np.pi / 2 # 90 degrees for all joints
445
+ reduced_robot.cache_forward_kinematics(q_reduced)
446
+
447
+ # Reset to default
448
+ reduced_robot.reset_forward_kinematics()
449
+
450
+ # Check that frame placement matches what we get with q_zero
451
+ reduced_robot.cache_forward_kinematics(reduced_robot.q_zero)
452
+ placement_q_zero = reduced_robot.frame_placement(
453
+ reduced_robot.supplemental_info.hand_frame_names["left"]
454
+ )
455
+ placement_reset = reduced_robot.frame_placement(
456
+ reduced_robot.supplemental_info.hand_frame_names["left"]
457
+ )
458
+ np.testing.assert_array_almost_equal(
459
+ placement_reset.translation, placement_q_zero.translation
460
+ )
461
+ np.testing.assert_array_almost_equal(placement_reset.rotation, placement_q_zero.rotation)
462
+
463
+
464
+ def test_reduced_robot_model_from_fixed_groups(g1_robot_model):
465
+ """
466
+ Test creating reduced model from fixed joint groups.
467
+ """
468
+ for robot_model in [g1_robot_model]:
469
+ # Skip if no supplemental info
470
+ if robot_model.supplemental_info is None:
471
+ pytest.skip("No supplemental info available for testing joint groups")
472
+
473
+ # Get a group name from the supplemental info
474
+ group_name = next(iter(robot_model.supplemental_info.joint_groups.keys()))
475
+ group_info = robot_model.supplemental_info.joint_groups[group_name]
476
+
477
+ # Get all joints that should be fixed (including those from subgroups)
478
+ expected_fixed_joints = set()
479
+ # Add direct joints
480
+ expected_fixed_joints.update(group_info["joints"])
481
+ # Add joints from subgroups
482
+ for subgroup_name in group_info["groups"]:
483
+ subgroup_joints = robot_model.get_joint_group_indices(subgroup_name)
484
+ expected_fixed_joints.update([robot_model.joint_names[idx] for idx in subgroup_joints])
485
+
486
+ # Test from_fixed_groups
487
+ reduced_robot = ReducedRobotModel.from_fixed_groups(robot_model, [group_name])
488
+ assert reduced_robot.full_robot is robot_model
489
+
490
+ # Verify that fixed joints are not in reduced model's joint names
491
+ for joint in expected_fixed_joints:
492
+ assert joint not in reduced_robot.joint_names
493
+
494
+ # Verify that fixed joints maintain their values in configuration
495
+ q_reduced = np.ones(reduced_robot.num_dofs) # Set some non-zero values
496
+ q_full = reduced_robot.reduced_to_full_configuration(q_reduced)
497
+
498
+ # Get the fixed values from the reduced model
499
+ fixed_values = dict(zip(reduced_robot.fixed_joints, reduced_robot.fixed_values))
500
+
501
+ # Check that all expected fixed joints have their values preserved
502
+ for joint in expected_fixed_joints:
503
+ full_idx = robot_model.dof_index(joint)
504
+ assert q_full[full_idx] == fixed_values[joint]
505
+
506
+ # Test from_fixed_group (convenience method)
507
+ reduced_robot = ReducedRobotModel.from_fixed_group(robot_model, group_name)
508
+ assert reduced_robot.full_robot is robot_model
509
+
510
+ # Verify that fixed joints are not in reduced model's joint names
511
+ for joint in expected_fixed_joints:
512
+ assert joint not in reduced_robot.joint_names
513
+
514
+ # Verify that fixed joints maintain their values in configuration
515
+ q_reduced = np.ones(reduced_robot.num_dofs) # Set some non-zero values
516
+ q_full = reduced_robot.reduced_to_full_configuration(q_reduced)
517
+
518
+ # Get the fixed values from the reduced model
519
+ fixed_values = dict(zip(reduced_robot.fixed_joints, reduced_robot.fixed_values))
520
+
521
+ # Check that all expected fixed joints have their values preserved
522
+ for joint in expected_fixed_joints:
523
+ full_idx = robot_model.dof_index(joint)
524
+ assert q_full[full_idx] == fixed_values[joint]
525
+
526
+
527
+ def test_reduced_robot_model_from_active_groups(g1_robot_model):
528
+ """
529
+ Test creating reduced model from active joint groups.
530
+ """
531
+ for robot_model in [g1_robot_model]:
532
+ # Skip if no supplemental info
533
+ if robot_model.supplemental_info is None:
534
+ pytest.skip("No supplemental info available for testing joint groups")
535
+
536
+ # Get a group name from the supplemental info
537
+ group_name = next(iter(robot_model.supplemental_info.joint_groups.keys()))
538
+ group_info = robot_model.supplemental_info.joint_groups[group_name]
539
+
540
+ # Get all joints that should be active (including those from subgroups)
541
+ expected_active_joints = set()
542
+ # Add direct joints
543
+ expected_active_joints.update(group_info["joints"])
544
+ # Add joints from subgroups
545
+ for subgroup_name in group_info["groups"]:
546
+ subgroup_joints = robot_model.get_joint_group_indices(subgroup_name)
547
+ expected_active_joints.update([robot_model.joint_names[idx] for idx in subgroup_joints])
548
+
549
+ # Get all joints from the model
550
+ all_joints = set(robot_model.joint_names)
551
+ # The fixed joints should be all joints minus the active joints
552
+ expected_fixed_joints = all_joints - expected_active_joints
553
+
554
+ # Test from_active_groups
555
+ reduced_robot = ReducedRobotModel.from_active_groups(robot_model, [group_name])
556
+ assert reduced_robot.full_robot is robot_model
557
+
558
+ # Verify that active joints are in reduced model's joint names
559
+ for joint in expected_active_joints:
560
+ assert joint in reduced_robot.joint_names
561
+
562
+ # Verify that fixed joints are not in reduced model's joint names
563
+ for joint in expected_fixed_joints:
564
+ assert joint not in reduced_robot.joint_names
565
+
566
+ # Verify that fixed joints maintain their values in configuration
567
+ q_reduced = np.ones(reduced_robot.num_dofs) # Set some non-zero values
568
+ q_full = reduced_robot.reduced_to_full_configuration(q_reduced)
569
+
570
+ # Get the fixed values from the reduced model
571
+ fixed_values = dict(zip(reduced_robot.fixed_joints, reduced_robot.fixed_values))
572
+
573
+ # Check that all expected fixed joints have their values preserved
574
+ for joint in expected_fixed_joints:
575
+ full_idx = robot_model.dof_index(joint)
576
+ assert q_full[full_idx] == fixed_values[joint]
577
+
578
+ # Test from_active_group (convenience method)
579
+ reduced_robot = ReducedRobotModel.from_active_group(robot_model, group_name)
580
+ assert reduced_robot.full_robot is robot_model
581
+
582
+ # Verify that active joints are in reduced model's joint names
583
+ for joint in expected_active_joints:
584
+ assert joint in reduced_robot.joint_names
585
+
586
+ # Verify that fixed joints are not in reduced model's joint names
587
+ for joint in expected_fixed_joints:
588
+ assert joint not in reduced_robot.joint_names
589
+
590
+ # Verify that fixed joints maintain their values in configuration
591
+ q_reduced = np.ones(reduced_robot.num_dofs) # Set some non-zero values
592
+ q_full = reduced_robot.reduced_to_full_configuration(q_reduced)
593
+
594
+ # Get the fixed values from the reduced model
595
+ fixed_values = dict(zip(reduced_robot.fixed_joints, reduced_robot.fixed_values))
596
+
597
+ # Check that all expected fixed joints have their values preserved
598
+ for joint in expected_fixed_joints:
599
+ full_idx = robot_model.dof_index(joint)
600
+ assert q_full[full_idx] == fixed_values[joint]
601
+
602
+
603
+ def test_reduced_robot_model_frame_placement(g1_robot_model):
604
+ """
605
+ Test the frame_placement method in reduced model with a valid and invalid frame name.
606
+ Also test that frame placements change with different configurations.
607
+ """
608
+ for robot_model in [g1_robot_model]:
609
+ # Skip if no supplemental info
610
+ if robot_model.supplemental_info is None:
611
+ pytest.skip("No supplemental info available for testing")
612
+
613
+ # Create a reduced model by fixing some joints
614
+ fixed_joints = robot_model.joint_names[:2]
615
+ reduced_robot = ReducedRobotModel(robot_model, fixed_joints)
616
+
617
+ # Use the hand frame from supplemental info
618
+ test_frame = reduced_robot.supplemental_info.hand_frame_names["left"]
619
+
620
+ # Test with zero configuration
621
+ q_reduced_zero = np.zeros(reduced_robot.num_dofs)
622
+ reduced_robot.cache_forward_kinematics(q_reduced_zero)
623
+ placement_zero = reduced_robot.frame_placement(test_frame)
624
+ assert isinstance(placement_zero, pin.SE3)
625
+
626
+ # Test with non-zero configuration
627
+ q_reduced_non_zero = np.zeros(reduced_robot.num_dofs)
628
+ root_nq = 7 if reduced_robot.full_robot.is_floating_base_model else 0
629
+
630
+ # Set a valid non-zero value for each joint
631
+ for i in range(root_nq, reduced_robot.num_dofs):
632
+ # Use a value that's within the joint limits
633
+ q_reduced_non_zero[i] = 0.5 # 0.5 radians is within most joint limits
634
+
635
+ reduced_robot.cache_forward_kinematics(q_reduced_non_zero)
636
+ placement_non_zero = reduced_robot.frame_placement(test_frame)
637
+
638
+ # Verify that frame placements are different with different configurations
639
+ assert not np.allclose(
640
+ placement_zero.translation, placement_non_zero.translation
641
+ ) or not np.allclose(placement_zero.rotation, placement_non_zero.rotation)
642
+
643
+ # Should raise an error for an invalid frame
644
+ with pytest.raises(ValueError, match="Unknown frame"):
645
+ reduced_robot.frame_placement("non_existent_frame")
646
+
647
+
648
+ def test_robot_model_gravity_compensation_basic(g1_robot_model):
649
+ """
650
+ Test basic gravity compensation functionality.
651
+ """
652
+ for robot_model in [g1_robot_model]:
653
+ # Skip if no supplemental info
654
+ if robot_model.supplemental_info is None:
655
+ pytest.skip("No supplemental info available for testing gravity compensation")
656
+
657
+ # Create a valid configuration
658
+ q = np.zeros(robot_model.num_dofs)
659
+ if robot_model.is_floating_base_model:
660
+ # Set floating base to upright position
661
+ q[:7] = [0, 0, 1.0, 0, 0, 0, 1] # [x, y, z, qx, qy, qz, qw]
662
+
663
+ # Test gravity compensation for all joints
664
+ gravity_torques = robot_model.compute_gravity_compensation_torques(q)
665
+
666
+ # Check output shape
667
+ assert gravity_torques.shape == (robot_model.num_dofs,)
668
+
669
+ # For a humanoid robot with arms, there should be some non-zero gravity torques
670
+ assert np.any(np.abs(gravity_torques) > 1e-6), "Expected some non-zero gravity torques"
671
+
672
+
673
+ def test_robot_model_gravity_compensation_joint_groups(g1_robot_model):
674
+ """
675
+ Test gravity compensation with different joint group specifications.
676
+ """
677
+ for robot_model in [g1_robot_model]:
678
+ # Skip if no supplemental info
679
+ if robot_model.supplemental_info is None:
680
+ pytest.skip("No supplemental info available for testing gravity compensation")
681
+
682
+ # Create a valid configuration
683
+ q = np.zeros(robot_model.num_dofs)
684
+ if robot_model.is_floating_base_model:
685
+ q[:7] = [0, 0, 1.0, 0, 0, 0, 1]
686
+
687
+ # Get available joint groups
688
+ available_groups = list(robot_model.supplemental_info.joint_groups.keys())
689
+ if not available_groups:
690
+ pytest.skip("No joint groups available for testing")
691
+
692
+ test_group = available_groups[0] # Use first available group
693
+
694
+ # Test with string input
695
+ gravity_str = robot_model.compute_gravity_compensation_torques(q, test_group)
696
+ assert gravity_str.shape == (robot_model.num_dofs,)
697
+
698
+ # Test with list input
699
+ gravity_list = robot_model.compute_gravity_compensation_torques(q, [test_group])
700
+ np.testing.assert_array_equal(gravity_str, gravity_list)
701
+
702
+ # Test with set input
703
+ gravity_set = robot_model.compute_gravity_compensation_torques(q, {test_group})
704
+ np.testing.assert_array_equal(gravity_str, gravity_set)
705
+
706
+ # Test that compensation is selective (some joints should be zero)
707
+ group_indices = robot_model.get_joint_group_indices(test_group)
708
+ if len(group_indices) < robot_model.num_dofs:
709
+ # Check that only specified joints have compensation
710
+ non_zero_mask = np.abs(gravity_str) > 1e-6
711
+ compensated_indices = np.where(non_zero_mask)[0]
712
+ # The compensated indices should be a subset of the group indices
713
+ assert len(compensated_indices) <= len(group_indices)
714
+
715
+
716
+ def test_robot_model_gravity_compensation_multiple_groups(g1_robot_model):
717
+ """
718
+ Test gravity compensation with multiple joint groups.
719
+ """
720
+ for robot_model in [g1_robot_model]:
721
+ # Skip if no supplemental info
722
+ if robot_model.supplemental_info is None:
723
+ pytest.skip("No supplemental info available for testing gravity compensation")
724
+
725
+ # Create a valid configuration
726
+ q = np.zeros(robot_model.num_dofs)
727
+ if robot_model.is_floating_base_model:
728
+ q[:7] = [0, 0, 1.0, 0, 0, 0, 1]
729
+
730
+ # Get available joint groups
731
+ available_groups = list(robot_model.supplemental_info.joint_groups.keys())
732
+ if len(available_groups) < 2:
733
+ pytest.skip("Need at least 2 joint groups for testing")
734
+
735
+ # Test with multiple groups
736
+ test_groups = available_groups[:2]
737
+ gravity_multiple = robot_model.compute_gravity_compensation_torques(q, test_groups)
738
+ assert gravity_multiple.shape == (robot_model.num_dofs,)
739
+
740
+ # Test individual groups
741
+ gravity_1 = robot_model.compute_gravity_compensation_torques(q, test_groups[0])
742
+ gravity_2 = robot_model.compute_gravity_compensation_torques(q, test_groups[1])
743
+
744
+ # The multiple group result should have at least as many non-zero elements
745
+ # as either individual group (could be more due to overlaps)
746
+ nonzero_multiple = np.count_nonzero(np.abs(gravity_multiple) > 1e-6)
747
+ nonzero_1 = np.count_nonzero(np.abs(gravity_1) > 1e-6)
748
+ nonzero_2 = np.count_nonzero(np.abs(gravity_2) > 1e-6)
749
+ assert nonzero_multiple >= max(nonzero_1, nonzero_2)
750
+
751
+
752
+ def test_robot_model_gravity_compensation_configuration_dependency(g1_robot_model):
753
+ """
754
+ Test that gravity compensation changes with robot configuration.
755
+ """
756
+ for robot_model in [g1_robot_model]:
757
+ # Skip if no supplemental info
758
+ if robot_model.supplemental_info is None:
759
+ pytest.skip("No supplemental info available for testing gravity compensation")
760
+
761
+ # Get available joint groups - prefer arms if available
762
+ available_groups = list(robot_model.supplemental_info.joint_groups.keys())
763
+ test_group = None
764
+ for group in ["arms", "left_arm", "right_arm"]:
765
+ if group in available_groups:
766
+ test_group = group
767
+ break
768
+ if test_group is None and available_groups:
769
+ test_group = available_groups[0]
770
+ if test_group is None:
771
+ pytest.skip("No joint groups available for testing")
772
+
773
+ # Test with different configurations
774
+ q1 = np.zeros(robot_model.num_dofs)
775
+ q2 = np.zeros(robot_model.num_dofs)
776
+
777
+ if robot_model.is_floating_base_model:
778
+ # Both configurations upright but different joint positions
779
+ q1[:7] = [0, 0, 1.0, 0, 0, 0, 1]
780
+ q2[:7] = [0, 0, 1.0, 0, 0, 0, 1]
781
+
782
+ # Change arm joint positions specifically (not random joints)
783
+ # This ensures we actually change joints that affect the gravity compensation
784
+ try:
785
+ arm_indices = robot_model.get_joint_group_indices(test_group)
786
+ if len(arm_indices) >= 2:
787
+ # Change first two arm joints significantly
788
+ q2[arm_indices[0]] = np.pi / 4 # 45 degrees
789
+ q2[arm_indices[1]] = np.pi / 6 # 30 degrees
790
+ elif len(arm_indices) >= 1:
791
+ # Change first arm joint if only one available
792
+ q2[arm_indices[0]] = np.pi / 3 # 60 degrees
793
+ except Exception:
794
+ # Fallback to changing some joints if arm indices not available
795
+ if robot_model.is_floating_base_model and robot_model.num_dofs > 9:
796
+ q2[7] = np.pi / 4
797
+ q2[8] = np.pi / 6
798
+ elif not robot_model.is_floating_base_model and robot_model.num_dofs > 2:
799
+ q2[0] = np.pi / 4
800
+ q2[1] = np.pi / 6
801
+
802
+ # Compute gravity compensation for both configurations
803
+ gravity_1 = robot_model.compute_gravity_compensation_torques(q1, test_group)
804
+ gravity_2 = robot_model.compute_gravity_compensation_torques(q2, test_group)
805
+
806
+ # They should be different (unless all compensated joints didn't change)
807
+ # Allow for small numerical differences
808
+ assert not np.allclose(
809
+ gravity_1, gravity_2, atol=1e-10
810
+ ), "Gravity compensation should change with configuration"
811
+
812
+
813
+ def test_robot_model_gravity_compensation_error_handling(g1_robot_model):
814
+ """
815
+ Test error handling in gravity compensation.
816
+ """
817
+ for robot_model in [g1_robot_model]:
818
+ # Test with wrong configuration size
819
+ q_wrong = np.zeros(robot_model.num_dofs + 1)
820
+ with pytest.raises(ValueError, match="Expected q of length"):
821
+ robot_model.compute_gravity_compensation_torques(q_wrong)
822
+
823
+ # Test with invalid joint group
824
+ q_valid = np.zeros(robot_model.num_dofs)
825
+ if robot_model.is_floating_base_model:
826
+ q_valid[:7] = [0, 0, 1.0, 0, 0, 0, 1]
827
+
828
+ with pytest.raises(RuntimeError, match="Error computing gravity compensation"):
829
+ robot_model.compute_gravity_compensation_torques(q_valid, "non_existent_group")
830
+
831
+ # Test with mixed valid/invalid groups
832
+ if robot_model.supplemental_info is not None:
833
+ available_groups = list(robot_model.supplemental_info.joint_groups.keys())
834
+ if available_groups:
835
+ valid_group = available_groups[0]
836
+ with pytest.raises(RuntimeError, match="Error computing gravity compensation"):
837
+ robot_model.compute_gravity_compensation_torques(
838
+ q_valid, [valid_group, "non_existent_group"]
839
+ )
840
+
841
+
842
+ def test_robot_model_gravity_compensation_auto_clip(g1_robot_model):
843
+ """
844
+ Test auto-clipping functionality in gravity compensation.
845
+ """
846
+ for robot_model in [g1_robot_model]:
847
+ # Skip if no supplemental info
848
+ if robot_model.supplemental_info is None:
849
+ pytest.skip("No supplemental info available for testing gravity compensation")
850
+
851
+ # Create configuration with values outside joint limits
852
+ q = np.zeros(robot_model.num_dofs)
853
+ root_nq = 7 if robot_model.is_floating_base_model else 0
854
+
855
+ if robot_model.is_floating_base_model:
856
+ q[:7] = [0, 0, 1.0, 0, 0, 0, 1] # Valid floating base
857
+
858
+ # Set extreme joint values (outside limits)
859
+ if robot_model.num_dofs > root_nq:
860
+ q[root_nq:] = 100.0 # Very large values
861
+
862
+ # Should work with auto_clip=True (default)
863
+ try:
864
+ gravity_clipped = robot_model.compute_gravity_compensation_torques(q, auto_clip=True)
865
+ assert gravity_clipped.shape == (robot_model.num_dofs,)
866
+ except Exception as e:
867
+ pytest.skip(f"Auto-clip test skipped due to: {e}")
868
+
869
+ # Test with auto_clip=False - might work or might not depending on limits
870
+ try:
871
+ gravity_no_clip = robot_model.compute_gravity_compensation_torques(q, auto_clip=False)
872
+ assert gravity_no_clip.shape == (robot_model.num_dofs,)
873
+ except Exception:
874
+ # This is expected if the configuration is invalid
875
+ pass
876
+
877
+
878
+ def test_robot_model_gravity_compensation_arms_specific(g1_robot_model):
879
+ """
880
+ Test gravity compensation specifically for arm joints (if available).
881
+ """
882
+ for robot_model in [g1_robot_model]:
883
+ # Skip if no supplemental info
884
+ if robot_model.supplemental_info is None:
885
+ pytest.skip("No supplemental info available for testing gravity compensation")
886
+
887
+ available_groups = list(robot_model.supplemental_info.joint_groups.keys())
888
+
889
+ # Test arms specifically if available
890
+ if "arms" in available_groups:
891
+ q = np.zeros(robot_model.num_dofs)
892
+ if robot_model.is_floating_base_model:
893
+ q[:7] = [0, 0, 1.0, 0, 0, 0, 1]
894
+
895
+ # Test arms gravity compensation
896
+ gravity_arms = robot_model.compute_gravity_compensation_torques(q, "arms")
897
+ assert gravity_arms.shape == (robot_model.num_dofs,)
898
+
899
+ # Test left and right arms separately if available
900
+ if "left_arm" in available_groups and "right_arm" in available_groups:
901
+ gravity_left = robot_model.compute_gravity_compensation_torques(q, "left_arm")
902
+ gravity_right = robot_model.compute_gravity_compensation_torques(q, "right_arm")
903
+
904
+ # Both arms should have non-zero compensation (for typical configurations)
905
+ if np.any(np.abs(gravity_arms) > 1e-6):
906
+ # If arms have compensation, at least one of left/right should too
907
+ assert np.any(np.abs(gravity_left) > 1e-6) or np.any(
908
+ np.abs(gravity_right) > 1e-6
909
+ )
910
+ else:
911
+ pytest.skip("No arm joint groups available for testing")
GR00T-WholeBodyControl/decoupled_wbc/tests/control/teleop/__init__.py ADDED
File without changes
GR00T-WholeBodyControl/decoupled_wbc/tests/control/teleop/test_teleop_retargeting_ik.py ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+
3
+ import numpy as np
4
+ import pytest
5
+
6
+ from decoupled_wbc.control.robot_model.instantiation.g1 import instantiate_g1_robot_model
7
+ from decoupled_wbc.control.robot_model.robot_model import RobotModel
8
+ from decoupled_wbc.control.teleop.solver.hand.instantiation.g1_hand_ik_instantiation import (
9
+ instantiate_g1_hand_ik_solver,
10
+ )
11
+ from decoupled_wbc.control.teleop.teleop_retargeting_ik import TeleopRetargetingIK
12
+
13
+
14
+ @pytest.fixture(params=["lower_body", "lower_and_upper_body"])
15
+ def retargeting_ik(request):
16
+ waist_location = request.param
17
+ robot_model = instantiate_g1_robot_model(waist_location=waist_location)
18
+ left_hand_ik_solver, right_hand_ik_solver = instantiate_g1_hand_ik_solver()
19
+ return TeleopRetargetingIK(
20
+ robot_model=robot_model,
21
+ left_hand_ik_solver=left_hand_ik_solver,
22
+ right_hand_ik_solver=right_hand_ik_solver,
23
+ enable_visualization=False, # Change to true to visualize movements
24
+ body_active_joint_groups=["upper_body"],
25
+ )
26
+
27
+
28
+ def generate_target_wrist_poses(mode: str, side: str, full_robot: RobotModel) -> dict:
29
+ """
30
+ Args:
31
+ mode: One of "rotation" or "translation"
32
+ side: One of "left" or "right" - specifies which side to animate
33
+ Returns:
34
+ Dictionary mapping link names to target poses for both wrists
35
+ """
36
+
37
+ assert mode in ["rotation", "translation", "both"]
38
+ assert side in ["left", "right", "both"]
39
+
40
+ # Set up initial state
41
+ full_robot.cache_forward_kinematics(full_robot.q_zero)
42
+
43
+ # Get both wrist link names
44
+ left_wrist_link = full_robot.supplemental_info.hand_frame_names["left"]
45
+ right_wrist_link = full_robot.supplemental_info.hand_frame_names["right"]
46
+
47
+ # Initialize default poses for both sides
48
+ left_default_pose = full_robot.frame_placement(left_wrist_link).np
49
+ right_default_pose = full_robot.frame_placement(right_wrist_link).np
50
+
51
+ left_initial_pose_matrix = full_robot.frame_placement(left_wrist_link).np
52
+ right_initial_pose_matrix = full_robot.frame_placement(right_wrist_link).np
53
+
54
+ # Constants
55
+ translation_cycle_duration = 4.0
56
+ rotation_cycle_duration = 4.0
57
+ total_duration = 12.0
58
+ translation_amplitude = 0.1
59
+ rotation_amplitude = np.deg2rad(60) # 30 degrees
60
+
61
+ body_data_list = []
62
+ for t in np.linspace(0, total_duration, 100):
63
+ rotation_matrix = np.eye(3)
64
+ current_left_translation_vector = left_initial_pose_matrix[:3, 3].copy()
65
+ current_right_translation_vector = right_initial_pose_matrix[:3, 3].copy()
66
+
67
+ if mode == "rotation" or mode == "both":
68
+ # For rotation-only mode, start rotating immediately
69
+ rotation_axis_index = int(t // rotation_cycle_duration) % 3
70
+ time_within_cycle = t % rotation_cycle_duration
71
+ angle = rotation_amplitude * np.sin(
72
+ (2 * np.pi / rotation_cycle_duration) * time_within_cycle
73
+ )
74
+
75
+ if rotation_axis_index == 0: # Roll
76
+ rotation_matrix = np.array(
77
+ [
78
+ [1, 0, 0],
79
+ [0, np.cos(angle), -np.sin(angle)],
80
+ [0, np.sin(angle), np.cos(angle)],
81
+ ]
82
+ )
83
+ elif rotation_axis_index == 2: # Pitch
84
+ rotation_matrix = np.array(
85
+ [
86
+ [np.cos(angle), 0, np.sin(angle)],
87
+ [0, 1, 0],
88
+ [-np.sin(angle), 0, np.cos(angle)],
89
+ ]
90
+ )
91
+ else: # Yaw
92
+ rotation_matrix = np.array(
93
+ [
94
+ [np.cos(angle), -np.sin(angle), 0],
95
+ [np.sin(angle), np.cos(angle), 0],
96
+ [0, 0, 1],
97
+ ]
98
+ )
99
+
100
+ if mode == "translation" or mode == "both":
101
+ translation_axis_index = int(t // translation_cycle_duration) % 3
102
+ time_within_cycle = t % translation_cycle_duration
103
+ offset = translation_amplitude * np.sin(
104
+ (2 * np.pi / translation_cycle_duration) * time_within_cycle
105
+ )
106
+ current_left_translation_vector[translation_axis_index] += offset
107
+ current_right_translation_vector[translation_axis_index] += offset
108
+
109
+ # Construct the 4x4 pose matrix for the animated side
110
+ left_animated_pose = np.eye(4)
111
+ left_animated_pose[:3, :3] = rotation_matrix
112
+ left_animated_pose[:3, 3] = current_left_translation_vector
113
+
114
+ right_animated_pose = np.eye(4)
115
+ right_animated_pose[:3, :3] = rotation_matrix
116
+ right_animated_pose[:3, 3] = current_right_translation_vector
117
+
118
+ # Create body_data dictionary with both wrists
119
+ body_data = {}
120
+ if side == "left":
121
+ body_data[left_wrist_link] = left_animated_pose
122
+ body_data[right_wrist_link] = right_default_pose
123
+ elif side == "right":
124
+ body_data[left_wrist_link] = left_default_pose
125
+ body_data[right_wrist_link] = right_animated_pose
126
+ elif side == "both":
127
+ body_data[left_wrist_link] = left_animated_pose
128
+ body_data[right_wrist_link] = right_animated_pose
129
+
130
+ body_data_list.append(body_data)
131
+
132
+ return body_data_list
133
+
134
+
135
+ @pytest.mark.parametrize("mode", ["translation", "rotation"])
136
+ @pytest.mark.parametrize("side", ["both", "left", "right"])
137
+ def test_ik_matches_fk(retargeting_ik, mode, side):
138
+ full_robot = retargeting_ik.full_robot
139
+
140
+ # Generate target wrist poses
141
+ body_data_list = generate_target_wrist_poses(mode, side, full_robot)
142
+
143
+ max_pos_error = 0
144
+ max_rot_error = 0
145
+
146
+ for body_data in body_data_list:
147
+
148
+ time_start = time.time()
149
+
150
+ # Run IK to get joint angles
151
+ q = retargeting_ik.compute_joint_positions(
152
+ body_data,
153
+ left_hand_data=None, # Hand IK not tested
154
+ right_hand_data=None, # Hand IK not tested
155
+ )
156
+
157
+ time_end = time.time()
158
+ ik_time = time_end - time_start
159
+ print(f"IK time: {ik_time} s")
160
+ # Test commented out because of inconsistency in CI/CD computation time
161
+ # assert ik_time < 0.05, f"IK time too high for 20Hz loop: {ik_time} s"
162
+
163
+ # Run FK to compute where the wrists actually ended up
164
+ full_robot.cache_forward_kinematics(q, auto_clip=False)
165
+ left_wrist_link = full_robot.supplemental_info.hand_frame_names["left"]
166
+ right_wrist_link = full_robot.supplemental_info.hand_frame_names["right"]
167
+ T_fk_left = full_robot.frame_placement(left_wrist_link).np
168
+ T_fk_right = full_robot.frame_placement(right_wrist_link).np
169
+ T_target_left = body_data[left_wrist_link]
170
+ T_target_right = body_data[right_wrist_link]
171
+
172
+ # Check that FK translation matches target translation
173
+ pos_fk_left = T_fk_left[:3, 3]
174
+ pos_target_left = T_target_left[:3, 3]
175
+ pos_fk_right = T_fk_right[:3, 3]
176
+ pos_target_right = T_target_right[:3, 3]
177
+
178
+ max_pos_error = max(max_pos_error, np.linalg.norm(pos_fk_left - pos_target_left))
179
+ max_pos_error = max(max_pos_error, np.linalg.norm(pos_fk_right - pos_target_right))
180
+
181
+ # Check that FK rotation matches target rotation
182
+ rot_fk_left = T_fk_left[:3, :3]
183
+ rot_target_left = T_target_left[:3, :3]
184
+ rot_diff_left = rot_fk_left @ rot_target_left.T
185
+ rot_error_left = np.arccos(np.clip((np.trace(rot_diff_left) - 1) / 2, -1, 1))
186
+ rot_fk_right = T_fk_right[:3, :3]
187
+ rot_target_right = T_target_right[:3, :3]
188
+ rot_diff_right = rot_fk_right @ rot_target_right.T
189
+ rot_error_right = np.arccos(np.clip((np.trace(rot_diff_right) - 1) / 2, -1, 1))
190
+
191
+ max_rot_error = max(max_rot_error, rot_error_left)
192
+ max_rot_error = max(max_rot_error, rot_error_right)
193
+
194
+ assert max_pos_error < 0.01 and max_rot_error < np.deg2rad(
195
+ 1
196
+ ), f"Max position error: {max_pos_error}, Max rotation error: {np.rad2deg(max_rot_error)} deg"
GR00T-WholeBodyControl/decoupled_wbc/tests/control/visualization/__init__.py ADDED
File without changes
GR00T-WholeBodyControl/decoupled_wbc/tests/control/visualization/test_meshcat_visualizer_env.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pathlib
2
+ import time
3
+
4
+ import numpy as np
5
+ import pytest
6
+
7
+ from decoupled_wbc.control.robot_model import RobotModel
8
+ from decoupled_wbc.control.robot_model.supplemental_info.g1.g1_supplemental_info import (
9
+ G1SupplementalInfo,
10
+ )
11
+
12
+
13
+ @pytest.fixture
14
+ def env_fixture():
15
+ """
16
+ Pytest fixture that creates and yields the MeshcatVisualizerEnv.
17
+ After the test, it closes the environment to clean up.
18
+ """
19
+ from decoupled_wbc.control.visualization.meshcat_visualizer_env import MeshcatVisualizerEnv
20
+
21
+ root_dir = pathlib.Path(__file__).parent.parent.parent.parent
22
+ urdf_path = str(
23
+ root_dir / "decoupled_wbc/control/robot_model/model_data/g1/g1_29dof_with_hand.urdf"
24
+ )
25
+ asset_path = str(root_dir / "decoupled_wbc/control/robot_model/model_data/g1")
26
+ robot_config = {
27
+ "asset_path": asset_path,
28
+ "urdf_path": urdf_path,
29
+ }
30
+ robot_model = RobotModel(
31
+ robot_config["urdf_path"],
32
+ robot_config["asset_path"],
33
+ supplemental_info=G1SupplementalInfo(),
34
+ )
35
+ env = MeshcatVisualizerEnv(robot_model)
36
+ time.sleep(0.5)
37
+ yield env
38
+ env.close()
39
+
40
+
41
+ def test_meshcat_env_init(env_fixture):
42
+ """
43
+ Test that the environment initializes without errors
44
+ and that reset() returns the proper data structure.
45
+ """
46
+ env = env_fixture
47
+ initial_obs = env.reset()
48
+ assert isinstance(initial_obs, dict), "reset() should return a dictionary."
49
+ assert "q" in initial_obs, "The returned dictionary should contain key 'q'."
50
+ assert (
51
+ len(initial_obs["q"]) == env.robot_model.num_dofs
52
+ ), "Length of 'q' should match the robot's DOF."
53
+
54
+
55
+ def test_meshcat_env_observation(env_fixture):
56
+ """
57
+ Test that the observe() method returns a valid observation
58
+ conforming to the environment's observation space.
59
+ """
60
+ env = env_fixture
61
+ observation = env.observe()
62
+ assert isinstance(observation, dict), "observe() should return a dictionary."
63
+ assert "q" in observation, "The returned dictionary should contain key 'q'."
64
+ assert (
65
+ len(observation["q"]) == env.robot_model.num_dofs
66
+ ), "Length of 'q' should match the robot's DOF."
67
+
68
+
69
+ def test_meshcat_env_action(env_fixture):
70
+ """
71
+ Test that we can queue an action and visualize it without error.
72
+ """
73
+ env = env_fixture
74
+ # Build a dummy action within the action space
75
+ test_action = {"q": 0.2 * np.ones(env.robot_model.num_dofs)}
76
+
77
+ # This should not raise an exception and should visualize the correct configuration
78
+ env.queue_action(test_action)
79
+
80
+
81
+ def test_meshcat_env_close(env_fixture):
82
+ """
83
+ Test closing the environment. (Though the fixture calls env.close()
84
+ automatically, we can invoke it here to ensure it's safe to do so.)
85
+ """
86
+ env = env_fixture
87
+ env.close()
88
+ # If close() triggers no exceptions, we're good.
GR00T-WholeBodyControl/decoupled_wbc/tests/data/test_exporter.py ADDED
@@ -0,0 +1,522 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from pathlib import Path
3
+ import shutil
4
+ import tempfile
5
+ import time
6
+
7
+ from lerobot.common.datasets.lerobot_dataset import LeRobotDataset
8
+ import numpy as np
9
+ import pytest
10
+
11
+ from decoupled_wbc.data.exporter import DataCollectionInfo, Gr00tDataExporter
12
+
13
+
14
+ @pytest.fixture
15
+ def test_features():
16
+ """Fixture providing test features dict."""
17
+ return {
18
+ "observation.images.ego_view": {
19
+ "dtype": "video",
20
+ "shape": [64, 64, 3], # Small images for faster tests
21
+ "names": ["height", "width", "channel"],
22
+ },
23
+ "observation.state": {
24
+ "dtype": "float32",
25
+ "shape": (8,),
26
+ "names": ["x1", "x2", "x3", "x4", "x5", "x6", "x7", "x8"],
27
+ },
28
+ "action": {
29
+ "dtype": "float32",
30
+ "shape": (8,),
31
+ "names": ["a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8"],
32
+ },
33
+ }
34
+
35
+
36
+ @pytest.fixture
37
+ def test_modality_config():
38
+ return {
39
+ "state": {"feature1": {"start": 0, "end": 4}, "feature2": {"start": 4, "end": 9}},
40
+ "action": {"feature1": {"start": 0, "end": 4}, "feature2": {"start": 4, "end": 9}},
41
+ "video": {"rs_view": {"original_key": "observation.images.ego_view"}},
42
+ "annotation": {"human.task_description": {"original_key": "task_index"}},
43
+ }
44
+
45
+
46
+ @pytest.fixture
47
+ def test_data_collection_info():
48
+ return DataCollectionInfo(
49
+ teleoperator_username="test_user",
50
+ support_operator_username="test_user",
51
+ robot_type="test_robot",
52
+ lower_body_policy="test_policy",
53
+ wbc_model_path="test_path",
54
+ )
55
+
56
+
57
+ def get_test_frame(step: int):
58
+ """Generate a test frame with data that varies by step."""
59
+ # Create a simple, small image that will encode quickly
60
+ img = np.ones((64, 64, 3), dtype=np.uint8) * (step % 255)
61
+ # Add a pattern to make each frame unique and verifiable
62
+ img[step % 64, :, :] = 255 - (step % 255)
63
+
64
+ return {
65
+ "observation.images.ego_view": img,
66
+ "observation.state": np.ones(8, dtype=np.float32) * step,
67
+ "action": np.ones(8, dtype=np.float32) * step,
68
+ }
69
+
70
+
71
+ @pytest.fixture
72
+ def temp_dir():
73
+ """Create a temporary directory for test data that's cleaned up after tests."""
74
+ temp_dir = tempfile.mkdtemp()
75
+ yield Path(temp_dir) / "dataset"
76
+ shutil.rmtree(temp_dir)
77
+
78
+
79
+ class TestInterruptAndResume:
80
+ """Test class for simulating interruption and resumption of recording."""
81
+
82
+ # Skip this test if ffmpeg is not installed
83
+ @pytest.mark.skipif(
84
+ shutil.which("ffmpeg") is None, reason="ffmpeg not installed, skipping test"
85
+ )
86
+ def test_interrupted_mid_episode(
87
+ self, temp_dir, test_features, test_modality_config, test_data_collection_info
88
+ ):
89
+ """
90
+ Test that simulates a recording session that gets interrupted and then resumes.
91
+
92
+ This test uses the actual Gr00tDataExporter implementation with no mocks.
93
+ """
94
+ # Constants for the test
95
+ NUM_EPISODES = 2
96
+ FRAMES_PER_EPISODE = 5
97
+
98
+ # Pick a random episode and frame to interrupt at
99
+ interrupt_episode = 1
100
+ interrupt_frame = 3
101
+
102
+ print(f"Will interrupt at episode {interrupt_episode}, frame {interrupt_frame}")
103
+
104
+ # Track what we've added to verify later
105
+ completed_episodes = []
106
+ frames_added_first_session = 0
107
+
108
+ # Initial recording session
109
+ try:
110
+ # Start recording with real Gr00tDataExporter
111
+ exporter1 = Gr00tDataExporter.create(
112
+ save_root=temp_dir,
113
+ fps=30,
114
+ features=test_features,
115
+ modality_config=test_modality_config,
116
+ task="test_task",
117
+ robot_type="test_robot",
118
+ vcodec="libx264", # Use a common codec that should be available
119
+ data_collection_info=test_data_collection_info,
120
+ )
121
+
122
+ # Record episodes until interruption
123
+ for episode in range(NUM_EPISODES):
124
+ for frame in range(FRAMES_PER_EPISODE):
125
+ # Simulate interruption
126
+ if episode == interrupt_episode and frame == interrupt_frame:
127
+ print(f"Simulating interruption at episode {episode}, frame {frame}")
128
+ raise KeyboardInterrupt("Simulated interruption")
129
+
130
+ # Add frame
131
+ exporter1.add_frame(get_test_frame(frame))
132
+ frames_added_first_session += 1
133
+
134
+ # Save episode
135
+ exporter1.save_episode()
136
+ completed_episodes.append(episode)
137
+
138
+ except KeyboardInterrupt:
139
+ print(f"Recording interrupted at episode {interrupt_episode}, frame {interrupt_frame}")
140
+ print(f"Completed episodes: {completed_episodes}")
141
+ # Don't consolidate since we're interrupted
142
+ pass
143
+
144
+ # Verify what was recorded before interruption
145
+ assert len(completed_episodes) == interrupt_episode
146
+ assert (
147
+ frames_added_first_session == interrupt_episode * FRAMES_PER_EPISODE + interrupt_frame
148
+ )
149
+
150
+ # Let file system operations complete
151
+ time.sleep(0.5)
152
+
153
+ # Resume recording - create a new exporter pointing to the same directory
154
+ exporter2 = Gr00tDataExporter.create(
155
+ save_root=temp_dir,
156
+ fps=30,
157
+ features=test_features,
158
+ modality_config=test_modality_config,
159
+ task="test_task",
160
+ robot_type="test_robot",
161
+ vcodec="libx264",
162
+ )
163
+
164
+ # The interrupted episode had frames added but wasn't saved
165
+ # In a real scenario with the current implementation, we need to restart that episode
166
+
167
+ # Record all episodes from the beginning
168
+ frames_added_second_session = 0
169
+ episodes_saved_second_session = 0
170
+
171
+ for episode in range(NUM_EPISODES):
172
+ for frame in range(FRAMES_PER_EPISODE):
173
+ exporter2.add_frame(get_test_frame(frame))
174
+ frames_added_second_session += 1
175
+
176
+ # Save episode
177
+ exporter2.save_episode()
178
+ episodes_saved_second_session += 1
179
+
180
+ # Verify the result
181
+ assert frames_added_second_session == NUM_EPISODES * FRAMES_PER_EPISODE
182
+ assert episodes_saved_second_session == NUM_EPISODES
183
+
184
+ # Verify actual files were created
185
+ for episode_idx in range(NUM_EPISODES):
186
+ video_path = exporter2.root / exporter2.meta.get_video_file_path(
187
+ episode_idx, "observation.images.ego_view"
188
+ )
189
+ assert video_path.exists(), f"Video file not found: {video_path}"
190
+
191
+ @pytest.mark.skipif(
192
+ shutil.which("ffmpeg") is None, reason="ffmpeg not installed, skipping test"
193
+ )
194
+ def test_interrupted_after_episode_completion(
195
+ self, temp_dir, test_features, test_modality_config, test_data_collection_info
196
+ ):
197
+ """
198
+ Test specifically for the case when interruption happens after an episode is completed.
199
+ Uses the real Gr00tDataExporter implementation.
200
+ """
201
+ # First session - record 1 complete episode and then interrupt
202
+ exporter1 = Gr00tDataExporter.create(
203
+ save_root=temp_dir,
204
+ fps=30,
205
+ features=test_features,
206
+ modality_config=test_modality_config,
207
+ task="test_task",
208
+ data_collection_info=test_data_collection_info,
209
+ vcodec="libx264",
210
+ )
211
+
212
+ # Record 1 complete episode
213
+ for frame in range(5):
214
+ exporter1.add_frame(get_test_frame(frame))
215
+ exporter1.save_episode()
216
+
217
+ # Let file system operations complete
218
+ time.sleep(0.5)
219
+
220
+ # Verify the first episode was saved
221
+ video_path = exporter1.root / exporter1.meta.get_video_file_path(
222
+ 0, "observation.images.ego_view"
223
+ )
224
+ assert video_path.exists(), f"First episode video file not found: {video_path}"
225
+
226
+ # Second session - resume and record another episode
227
+ exporter2 = Gr00tDataExporter.create(
228
+ save_root=temp_dir,
229
+ fps=30,
230
+ features=test_features,
231
+ modality_config=test_modality_config,
232
+ task="test_task",
233
+ vcodec="libx264",
234
+ )
235
+
236
+ # Record the second episode
237
+ for frame in range(5):
238
+ exporter2.add_frame(get_test_frame(frame))
239
+ exporter2.save_episode()
240
+
241
+ # Verify the second episode was saved
242
+ video_path = exporter2.root / exporter2.meta.get_video_file_path(
243
+ 1, "observation.images.ego_view"
244
+ )
245
+ assert video_path.exists(), f"Second episode video file not found: {video_path}"
246
+
247
+ @pytest.mark.skipif(
248
+ shutil.which("ffmpeg") is None, reason="ffmpeg not installed, skipping test"
249
+ )
250
+ def test_interrupted_no_episode_completion(
251
+ self, temp_dir, test_features, test_modality_config, test_data_collection_info
252
+ ):
253
+ """
254
+ Test specifically for the case when interruption happens in the middle of recording an episode.
255
+ Uses the real Gr00tDataExporter implementation.
256
+ """
257
+ # First session - add some frames and interrupt before saving
258
+ exporter1 = Gr00tDataExporter.create(
259
+ save_root=temp_dir,
260
+ fps=30,
261
+ features=test_features,
262
+ modality_config=test_modality_config,
263
+ task="test_task",
264
+ data_collection_info=test_data_collection_info,
265
+ vcodec="libx264",
266
+ )
267
+
268
+ # Add 3 frames but don't save
269
+ for frame in range(3):
270
+ exporter1.add_frame(get_test_frame(frame))
271
+ # Don't save episode or consolidate to simulate interruption
272
+ # The episode buffer is only in memory and will be lost on interruption
273
+
274
+ # Let file system operations complete
275
+ time.sleep(0.5)
276
+
277
+ # Verify no episode was saved
278
+ video_path = exporter1.root / exporter1.meta.get_video_file_path(
279
+ 0, "observation.images.ego_view"
280
+ )
281
+ assert not video_path.exists(), f"Episode should not have been saved: {video_path}"
282
+
283
+ # Second session - will raise an error because no meta file exist, so we can't resume
284
+ with pytest.raises(ValueError):
285
+ _ = Gr00tDataExporter.create(
286
+ save_root=temp_dir,
287
+ fps=30,
288
+ features=test_features,
289
+ modality_config=test_modality_config,
290
+ task="test_task",
291
+ vcodec="libx264",
292
+ )
293
+
294
+
295
+ @pytest.mark.skipif(shutil.which("ffmpeg") is None, reason="ffmpeg not installed, skipping test")
296
+ def test_full_workflow(temp_dir, test_features, test_modality_config, test_data_collection_info):
297
+ """
298
+ Test that simulates the complete workflow from the record_session.py example.
299
+ """
300
+ NUM_EPISODES = 2
301
+ FRAMES_PER_EPISODE = 3
302
+
303
+ # Create the exporter
304
+ exporter = Gr00tDataExporter.create(
305
+ save_root=temp_dir,
306
+ fps=20,
307
+ features=test_features,
308
+ modality_config=test_modality_config,
309
+ task="test_task",
310
+ data_collection_info=test_data_collection_info,
311
+ robot_type="dummy",
312
+ )
313
+
314
+ # Create a small dataset
315
+ for episode_index in range(NUM_EPISODES):
316
+ for frame_index in range(FRAMES_PER_EPISODE):
317
+ exporter.add_frame(get_test_frame(frame_index))
318
+ exporter.save_episode()
319
+
320
+ # check modality config
321
+ modality_config_path = exporter.root / "meta" / "modality.json"
322
+ assert modality_config_path.exists(), f"{modality_config_path} does not exists."
323
+ with open(modality_config_path, "rb") as f:
324
+ actual_modality_config = json.load(f)
325
+
326
+ assert (
327
+ actual_modality_config == test_modality_config
328
+ ), f"Modality configs don't match.\nActual: {actual_modality_config}\nExpected: {test_modality_config}"
329
+
330
+ # Verify results
331
+ for episode_idx in range(NUM_EPISODES):
332
+ video_path = exporter.root / exporter.meta.get_video_file_path(
333
+ episode_idx, "observation.images.ego_view"
334
+ )
335
+ assert video_path.exists(), f"Video file not found: {video_path}"
336
+
337
+ # Check that the expected number of episodes exists
338
+ episode_count = 0
339
+ for path in exporter.root.glob("**/*.mp4"):
340
+ episode_count += 1
341
+ assert episode_count == NUM_EPISODES, f"Expected {NUM_EPISODES} episodes, found {episode_count}"
342
+
343
+ # Check the values of the dataset
344
+ dataset = LeRobotDataset(
345
+ repo_id="dataset",
346
+ root=temp_dir,
347
+ )
348
+ for episode_idx in range(NUM_EPISODES):
349
+ for frame_idx in range(FRAMES_PER_EPISODE):
350
+ expected_frame = get_test_frame(frame_idx)
351
+ actual_frame = dataset[episode_idx * FRAMES_PER_EPISODE + frame_idx]
352
+ print(actual_frame["observation.images.ego_view"])
353
+ actual_image_frame = actual_frame["observation.images.ego_view"].permute(1, 2, 0) * 255
354
+ assert np.allclose(
355
+ actual_image_frame.numpy(), expected_frame["observation.images.ego_view"], atol=10
356
+ ) # Allow some tolerance for video compression
357
+ assert np.allclose(
358
+ actual_frame["observation.state"], expected_frame["observation.state"]
359
+ )
360
+ assert np.allclose(actual_frame["action"], expected_frame["action"])
361
+
362
+ # validate data_collection_info
363
+ assert dataset.meta.info["data_collection_info"] == test_data_collection_info.to_dict()
364
+
365
+
366
+ @pytest.mark.skipif(shutil.which("ffmpeg") is None, reason="ffmpeg not installed, skipping test")
367
+ def test_overwrite_existing_dataset_false(
368
+ temp_dir, test_features, test_modality_config, test_data_collection_info
369
+ ):
370
+ """
371
+ Test that appends to the existing dataset when overwrite_existing is set to false.
372
+ """
373
+ # first dataset
374
+ FIRST_NUM_EPISODES = 2
375
+ FIRST_FRAMES_PER_EPISODE = 3
376
+
377
+ exporter = Gr00tDataExporter.create(
378
+ save_root=temp_dir,
379
+ fps=20,
380
+ features=test_features,
381
+ modality_config=test_modality_config,
382
+ task="test_task",
383
+ data_collection_info=test_data_collection_info,
384
+ robot_type="dummy",
385
+ )
386
+ # !! `overwrite_existing` should always be set to false by default
387
+ # So we're deliberately not setting the overwrite_existing argument here.
388
+ # This test ensures that
389
+ # i. the default behavior is overwrite_existing=False
390
+ # ii. the dataset appends to the existing dataset (instead of overwriting)
391
+
392
+ # Create a first dataset
393
+ for episode_index in range(FIRST_NUM_EPISODES):
394
+ for frame_index in range(FIRST_FRAMES_PER_EPISODE):
395
+ exporter.add_frame(get_test_frame(frame_index))
396
+ exporter.save_episode()
397
+
398
+ # second dataset
399
+ del exporter
400
+ SECOND_NUM_EPISODES = 3
401
+ SECOND_FRAMES_PER_EPISODE = 2
402
+
403
+ exporter = Gr00tDataExporter.create(
404
+ save_root=temp_dir,
405
+ fps=20,
406
+ features=test_features,
407
+ modality_config=test_modality_config,
408
+ task="test_task",
409
+ robot_type="dummy",
410
+ )
411
+ for episode_index in range(SECOND_NUM_EPISODES):
412
+ for frame_index in range(SECOND_FRAMES_PER_EPISODE):
413
+ exporter.add_frame(get_test_frame(frame_index))
414
+ exporter.save_episode()
415
+
416
+ # verify that there are
417
+ EXPECTED_NUM_EPISODES = FIRST_NUM_EPISODES + SECOND_NUM_EPISODES
418
+ assert len(list(exporter.root.glob("**/*.mp4"))) == EXPECTED_NUM_EPISODES
419
+ assert len(list(exporter.root.glob("**/*.parquet"))) == EXPECTED_NUM_EPISODES
420
+
421
+
422
+ def test_overwrite_existing_dataset_true(
423
+ temp_dir, test_features, test_modality_config, test_data_collection_info
424
+ ):
425
+ """
426
+ Test that overwrites to an existing dataset when overwrite_existing=True.
427
+ """
428
+ # first dataset
429
+ FIRST_NUM_EPISODES = 2
430
+ FIRST_FRAMES_PER_EPISODE = 3
431
+
432
+ exporter = Gr00tDataExporter.create(
433
+ save_root=temp_dir,
434
+ fps=20,
435
+ features=test_features,
436
+ modality_config=test_modality_config,
437
+ task="test_task",
438
+ data_collection_info=test_data_collection_info,
439
+ robot_type="dummy",
440
+ )
441
+
442
+ # Create a first dataset
443
+ for episode_index in range(FIRST_NUM_EPISODES):
444
+ for frame_index in range(FIRST_FRAMES_PER_EPISODE):
445
+ exporter.add_frame(get_test_frame(frame_index))
446
+ exporter.save_episode()
447
+
448
+ # verify that the dataset is written to the disk
449
+ assert len(list(exporter.root.glob("**/*.mp4"))) == FIRST_NUM_EPISODES
450
+ assert len(list(exporter.root.glob("**/*.parquet"))) == FIRST_NUM_EPISODES
451
+
452
+ # second dataset
453
+ SECOND_NUM_EPISODES = 3
454
+ SECOND_FRAMES_PER_EPISODE = 2
455
+
456
+ # re-initialize the exporter
457
+ del exporter
458
+ exporter = Gr00tDataExporter.create(
459
+ save_root=temp_dir,
460
+ fps=20,
461
+ features=test_features,
462
+ modality_config=test_modality_config,
463
+ task="test_task",
464
+ data_collection_info=test_data_collection_info,
465
+ robot_type="dummy",
466
+ overwrite_existing=True,
467
+ )
468
+ for episode_index in range(SECOND_NUM_EPISODES):
469
+ for frame_index in range(SECOND_FRAMES_PER_EPISODE):
470
+ exporter.add_frame(get_test_frame(frame_index))
471
+ exporter.save_episode()
472
+
473
+ # verify that the dataset is overwritten
474
+ assert len(list(exporter.root.glob("**/*.mp4"))) == SECOND_NUM_EPISODES
475
+ assert len(list(exporter.root.glob("**/*.parquet"))) == SECOND_NUM_EPISODES
476
+
477
+
478
+ def test_save_episode_as_discarded_and_skip(
479
+ temp_dir, test_features, test_modality_config, test_data_collection_info
480
+ ):
481
+ """
482
+ Test that verifies the functionality of saving an episode as discarded and skipping an episode.
483
+ """
484
+ FIRST_NUM_EPISODES = 10
485
+ FIRST_FRAMES_PER_EPISODE = 3
486
+
487
+ exporter = Gr00tDataExporter.create(
488
+ save_root=temp_dir,
489
+ fps=20,
490
+ features=test_features,
491
+ modality_config=test_modality_config,
492
+ task="test_task",
493
+ data_collection_info=test_data_collection_info,
494
+ robot_type="dummy",
495
+ )
496
+
497
+ # Create a first dataset
498
+ saved_episodes = 0
499
+ discarded_episode_indices = []
500
+ for episode_index in range(FIRST_NUM_EPISODES):
501
+ for frame_index in range(FIRST_FRAMES_PER_EPISODE):
502
+ exporter.add_frame(get_test_frame(frame_index))
503
+ if episode_index % 3 == 0:
504
+ exporter.save_episode_as_discarded()
505
+ discarded_episode_indices.append(saved_episodes)
506
+ saved_episodes += 1
507
+ elif episode_index % 3 == 1:
508
+ exporter.skip_and_start_new_episode()
509
+ else:
510
+ exporter.save_episode()
511
+ saved_episodes += 1
512
+
513
+ # verify that the dataset is written to the disk
514
+ assert len(list(exporter.root.glob("**/*.mp4"))) == saved_episodes
515
+ assert len(list(exporter.root.glob("**/*.parquet"))) == saved_episodes
516
+
517
+ dataset = LeRobotDataset(
518
+ repo_id="dataset",
519
+ root=temp_dir,
520
+ )
521
+
522
+ assert dataset.meta.info["discarded_episode_indices"] == discarded_episode_indices
GR00T-WholeBodyControl/decoupled_wbc/tests/sim/test_sim_data_collection.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from decoupled_wbc.control.main.teleop.configs.configs import SyncSimDataCollectionConfig
2
+ from decoupled_wbc.control.main.teleop.run_sync_sim_data_collection import (
3
+ main as data_collection_main,
4
+ )
5
+
6
+
7
+ def test_sim_data_collection_unit(robot_name="G1", task_name="GroundOnly"):
8
+ """
9
+ Fast CI unit test for simulation data collection (50 steps, no tracking checks).
10
+
11
+ This test validates that:
12
+ 1. Data collection completes successfully
13
+ 2. Upper body joints are moving (velocity check)
14
+
15
+ Note: This test runs for only 50 steps and does not perform end effector tracking validation
16
+ for faster CI execution.
17
+ """
18
+ config = SyncSimDataCollectionConfig()
19
+ config.robot = robot_name
20
+ config.task_name = task_name
21
+ config.enable_visualization = False
22
+ config.enable_real_device = False
23
+ config.enable_onscreen = False
24
+ config.save_img_obs = True
25
+ config.ci_test = True
26
+ config.ci_test_mode = "unit"
27
+ config.replay_data_path = "decoupled_wbc/tests/replay_data/all_joints_raw_data_replay.pkl"
28
+ config.remove_existing_dir = True
29
+ config.enable_gravity_compensation = True
30
+ res = data_collection_main(config)
31
+ assert res, "Data collection did not pass for all datasets"
32
+
33
+
34
+ def test_sim_data_collection_pre_merge(robot_name="G1", task_name="GroundOnly"):
35
+ """
36
+ Pre-merge test for simulation data collection with end effector tracking validation (500 steps).
37
+
38
+ This test validates that:
39
+ 1. Data collection completes successfully
40
+ 2. Upper body joints are moving (velocity check)
41
+ 3. End effector tracking error is within thresholds:
42
+ - G1 robots:
43
+ Max position error < 7cm (0.07m), Max rotation error < 17°,
44
+ Average position error < 5cm (0.05m), Average rotation error < 12°
45
+ """
46
+ config = SyncSimDataCollectionConfig()
47
+ config.robot = robot_name
48
+ config.task_name = task_name
49
+ config.enable_visualization = False
50
+ config.enable_real_device = False
51
+ config.enable_onscreen = False
52
+ config.save_img_obs = True
53
+ config.ci_test = True
54
+ config.ci_test_mode = "pre_merge"
55
+ config.replay_data_path = "decoupled_wbc/tests/replay_data/all_joints_raw_data_replay.pkl"
56
+ config.remove_existing_dir = True
57
+ config.enable_gravity_compensation = True
58
+ res = data_collection_main(config)
59
+ assert res, "Data collection did not pass for all datasets"
60
+
61
+
62
+ if __name__ == "__main__":
63
+ # Run unit tests for fast CI
64
+ test_sim_data_collection_unit("G1", "GroundOnly")
GR00T-WholeBodyControl/external_dependencies/XRoboToolkit-PC-Service-Pybind_X86_and_ARM64/CLAUDE.md ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ This project provides Python bindings for the XRoboToolkit PC Service SDK, enabling Python applications to extract XR state data including controller poses, hand tracking, and body motion capture from XR devices (primarily PICO headsets).
8
+
9
+ ## Architecture
10
+
11
+ The project consists of:
12
+
13
+ - **Core C++ Bindings** (`bindings/py_bindings.cpp`): Pybind11-based C++ module that wraps the PXREARobotSDK
14
+ - **SDK Integration**: Uses the XRoboToolkit-PC-Service SDK (cloned from external repository)
15
+ - **Build System**: CMake-based build with Python setuptools integration
16
+ - **Multi-platform Support**: Linux (x86_64/aarch64) and Windows
17
+
18
+ Key components:
19
+ - `PXREARobotSDK.h`: Main SDK header providing device connectivity and data parsing
20
+ - `py_bindings.cpp`: Thread-safe C++ wrapper with mutex-protected global state variables
21
+ - JSON parsing using nlohmann/json for device state data
22
+ - Callback-based data updates from the SDK
23
+
24
+ ## Build Commands
25
+
26
+ ### Ubuntu/Linux Setup and Build
27
+ ```bash
28
+ # Full setup (downloads dependencies and builds)
29
+ bash setup_ubuntu.sh
30
+
31
+ # Manual build after setup
32
+ python setup.py install
33
+
34
+ # Clean build artifacts
35
+ python setup.py clean
36
+ ```
37
+
38
+ ### Windows Setup and Build
39
+ ```batch
40
+ # Full setup (downloads dependencies and builds)
41
+ setup_windows.bat
42
+
43
+ # Manual build after setup
44
+ python setup.py install
45
+ ```
46
+
47
+ ### Development Commands
48
+ ```bash
49
+ # Uninstall existing package
50
+ pip uninstall -y xrobotoolkit_sdk
51
+
52
+ # Install pybind11 dependency
53
+ conda install -c conda-forge pybind11
54
+ # or
55
+ pip install pybind11
56
+
57
+ # Build and install
58
+ python setup.py install
59
+ ```
60
+
61
+ ## Data Flow and Threading
62
+
63
+ The SDK uses a callback-based architecture:
64
+ - `OnPXREAClientCallback`: Main callback function that receives JSON data from connected devices
65
+ - Global state variables (poses, button states, etc.) are updated in real-time
66
+ - Thread-safe access via mutex locks for each data category
67
+ - Data parsing from comma-separated pose strings to arrays
68
+
69
+ ## Key Functions and Data Types
70
+
71
+ ### Controller Data
72
+ - Poses: `std::array<double, 7>` (x,y,z,qx,qy,qz,qw)
73
+ - Buttons: Menu, Primary, Secondary, Axis Click
74
+ - Analog: Trigger, Grip, Axis (x,y)
75
+
76
+ ### Hand Tracking
77
+ - 26 joints per hand with 7 values each (position + quaternion)
78
+ - Hand scale factor
79
+
80
+ ### Body Tracking
81
+ - 24 body joints with pose, velocity, acceleration data
82
+ - IMU timestamps for each joint
83
+ - Availability flag for body tracking system
84
+
85
+ ## Dependencies
86
+
87
+ ### Required
88
+ - pybind11 (Python binding framework)
89
+ - CMake (build system)
90
+ - XRoboToolkit-PC-Service SDK (automatically downloaded during setup)
91
+
92
+ ### Platform-specific Libraries
93
+ - Linux: `libPXREARobotSDK.so`
94
+ - Windows: `PXREARobotSDK.dll` and `PXREARobotSDK.lib`
95
+
96
+ ## Testing
97
+
98
+ No formal test suite is included. Test functionality using the example scripts in `examples/`:
99
+ - `example.py`: Basic controller and headset pose testing
100
+ - `example_body_tracking.py`: Body tracking functionality
101
+ - `run_binding_continuous.py`: Continuous data capture
102
+
103
+ ## Important Notes
104
+
105
+ - The SDK requires active XR device connection (PICO headset)
106
+ - Body tracking requires at least two Pico Swift devices
107
+ - All data access is thread-safe but real-time dependent on device connectivity
108
+ - The project builds a Python extension module that must be installed to site-packages
GR00T-WholeBodyControl/external_dependencies/XRoboToolkit-PC-Service-Pybind_X86_and_ARM64/CMakeLists.txt ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ cmake_minimum_required(VERSION 3.14)
2
+
3
+ project(MyPybind11Project LANGUAGES CXX)
4
+
5
+ set(CMAKE_CXX_STANDARD 17)
6
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
7
+
8
+ cmake_host_system_information(RESULT ISA_NAME QUERY OS_PLATFORM) # Added: Important for UNIX specific logic
9
+ message(STATUS "OS_PLATFORM (ISA_NAME): ${ISA_NAME}")
10
+
11
+ include(GNUInstallDirs) # Add this line
12
+ find_package(pybind11 REQUIRED)
13
+
14
+ # Python Bindings for py_bindings.cpp
15
+ pybind11_add_module(xrobotoolkit_sdk MODULE bindings/py_bindings.cpp)
16
+
17
+ # Link xrobotoolkit_sdk module against pybind11
18
+ target_link_libraries(xrobotoolkit_sdk PRIVATE pybind11::module)
19
+
20
+ # Add include directories and link libraries for PXREARobotSDK to xrobotoolkit_sdk target
21
+ if(WIN32)
22
+ target_include_directories(xrobotoolkit_sdk PUBLIC
23
+ ${PROJECT_SOURCE_DIR}/include
24
+ )
25
+ target_link_directories(xrobotoolkit_sdk PUBLIC ${PROJECT_SOURCE_DIR}/lib)
26
+ target_link_libraries(xrobotoolkit_sdk PUBLIC
27
+ PXREARobotSDK.dll # Assuming this is how PXREARobotSDK is linked, mirroring ConsoleDemo
28
+ )
29
+ endif()
30
+
31
+ if(UNIX)
32
+ # ISA_NAME is set by cmake_host_system_information above
33
+ if(ISA_NAME STREQUAL "aarch64")
34
+ target_include_directories(xrobotoolkit_sdk PUBLIC
35
+ ${PROJECT_SOURCE_DIR}/include/aarch64
36
+ )
37
+ target_link_directories(xrobotoolkit_sdk PUBLIC ${PROJECT_SOURCE_DIR}/lib/aarch64)
38
+ else()
39
+ target_include_directories(xrobotoolkit_sdk PUBLIC
40
+ ${PROJECT_SOURCE_DIR}/include
41
+ )
42
+ target_link_directories(xrobotoolkit_sdk PUBLIC ${PROJECT_SOURCE_DIR}/lib)
43
+ endif()
44
+ target_link_libraries(xrobotoolkit_sdk PUBLIC
45
+ PXREARobotSDK
46
+ )
47
+ endif()
48
+
49
+ # Install the Python module
50
+ install(TARGETS xrobotoolkit_sdk
51
+ LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} # Installs to <prefix>/lib
52
+ # You might want a Python-specific path like:
53
+ # DESTINATION lib/python${Python_VERSION_MAJOR}.${Python_VERSION_MINOR}/site-packages
54
+ )
GR00T-WholeBodyControl/external_dependencies/XRoboToolkit-PC-Service-Pybind_X86_and_ARM64/LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 XR Robotics
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
GR00T-WholeBodyControl/external_dependencies/XRoboToolkit-PC-Service-Pybind_X86_and_ARM64/README.md ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # XRoboToolkit-PC-Service-Pybind
2
+
3
+ This project provides a python interface to extract XR state using XRoboToolkit-PC-Service sdk.
4
+
5
+ ## Requirements
6
+
7
+ - [`pybind11`](https://github.com/pybind/pybind11)
8
+ - [`XRoboRoolkit PC Service`](https://github.com/XR-Robotics/XRoboToolkit-PC-Service#)
9
+
10
+ ## Building the Project
11
+ ### Ubuntu 22.04
12
+
13
+ ```
14
+ conda remove --name xr --all
15
+ conda create -n xr python=3.10
16
+ conda activate xr
17
+
18
+ mkdir -p tmp
19
+ cd tmp
20
+ git clone https://github.com/XR-Robotics/XRoboToolkit-PC-Service.git
21
+ cd XRoboToolkit-PC-Service/RoboticsService/PXREARobotSDK
22
+ bash build.sh
23
+ cd ../../../..
24
+
25
+ mkdir -p lib
26
+ mkdir -p include
27
+ cp tmp/XRoboToolkit-PC-Service/RoboticsService/PXREARobotSDK/PXREARobotSDK.h include/
28
+ cp -r tmp/XRoboToolkit-PC-Service/RoboticsService/PXREARobotSDK/nlohmann include/nlohmann/
29
+ cp tmp/XRoboToolkit-PC-Service/RoboticsService/PXREARobotSDK/build/libPXREARobotSDK.so lib/
30
+ # rm -rf tmp
31
+
32
+ # Build the project
33
+ conda install -c conda-forge pybind11
34
+
35
+ pip uninstall -y xrobotoolkit_sdk
36
+ python setup.py install
37
+ ```
38
+ ### Linux Ubuntu 22.04 arm64 version (Nvidia orin supported)
39
+ ```
40
+ bash setup_orin.sh
41
+ ```
42
+ ### Windows
43
+
44
+ **Ensure pybind11 is installed before running the following command.**
45
+
46
+ ```
47
+ setup_windows.bat
48
+ ```
49
+
50
+ ## Using the Python Bindings
51
+
52
+ **1. Get Controller and Headset Poses**
53
+
54
+ ```python
55
+ import xrobotoolkit_sdk as xrt
56
+
57
+ xrt.init()
58
+
59
+ left_pose = xrt.get_left_controller_pose()
60
+ right_pose = xrt.get_right_controller_pose()
61
+ headset_pose = xrt.get_headset_pose()
62
+
63
+ print(f"Left Controller Pose: {left_pose}")
64
+ print(f"Right Controller Pose: {right_pose}")
65
+ print(f"Headset Pose: {headset_pose}")
66
+
67
+ xrt.close()
68
+ ```
69
+
70
+ **2. Get Controller Inputs (Triggers, Grips, Buttons, Axes)**
71
+
72
+ ```python
73
+ import xrobotoolkit_sdk as xrt
74
+
75
+ xrt.init()
76
+
77
+ # Triggers and Grips
78
+ left_trigger = xrt.get_left_trigger()
79
+ right_grip = xrt.get_right_grip()
80
+ print(f"Left Trigger: {left_trigger}, Right Grip: {right_grip}")
81
+
82
+ # Buttons
83
+ a_button_pressed = xrt.get_A_button()
84
+ x_button_pressed = xrt.get_X_button()
85
+ print(f"A Button Pressed: {a_button_pressed}, X Button Pressed: {x_button_pressed}")
86
+
87
+ # Axes
88
+ left_axis = xrt.get_left_axis()
89
+ right_axis_click = xrt.get_right_axis_click()
90
+ print(f"Left Axis: {left_axis}, Right Axis Clicked: {right_axis_click}")
91
+
92
+ # Timestamp
93
+ timestamp = xrt.get_time_stamp_ns()
94
+ print(f"Current Timestamp (ns): {timestamp}")
95
+
96
+ xrt.close()
97
+ ```
98
+
99
+ **3. Get hand tracking state**
100
+ ```python
101
+ import xrobotoolkit_sdk as xrt
102
+
103
+ xrt.init()
104
+
105
+ # Left Hand State
106
+ left_hand_tracking_state = xrt.get_left_hand_tracking_state()
107
+ print(f"Left Hand State: {left_hand_tracking_state}")
108
+
109
+ # Left Hand isActive
110
+ left_hand_is_active = xrt.get_left_hand_is_active()
111
+ print(f"Left Hand isActive: {left_hand_is_active}")
112
+
113
+ # Right Hand State
114
+ right_hand_tracking_state = xrt.get_right_hand_tracking_state()
115
+ print(f"Right Hand State: {right_hand_tracking_state}")
116
+
117
+ # Right Hand isActive
118
+ right_hand_is_active = xrt.get_right_hand_is_active()
119
+ print(f"Right Hand isActive: {right_hand_is_active}")
120
+
121
+ xrt.close()
122
+ ```
123
+
124
+ **4. Get whole body motion tracking (please refer to this example when check Full Body tracking mode in UNITY app)**
125
+ ```python
126
+ import xrobotoolkit_sdk as xrt
127
+
128
+ xrt.init()
129
+
130
+ # Check if body tracking data is available
131
+ if xrt.is_body_data_available():
132
+ # Get body joint poses (24 joints, 7 values each: x,y,z,qx,qy,qz,qw)
133
+ body_poses = xrt.get_body_joints_pose()
134
+ print(f"Body joints pose data: {body_poses}")
135
+
136
+ # Get body joint velocities (24 joints, 6 values each: vx,vy,vz,wx,wy,wz)
137
+ body_velocities = xrt.get_body_joints_velocity()
138
+ print(f"Body joints velocity data: {body_velocities}")
139
+
140
+ # Get body joint accelerations (24 joints, 6 values each: ax,ay,az,wax,way,waz)
141
+ body_accelerations = xrt.get_body_joints_acceleration()
142
+ print(f"Body joints acceleration data: {body_accelerations}")
143
+
144
+ # Get IMU timestamps for each joint
145
+ imu_timestamps = xrt.get_body_joints_timestamp()
146
+ print(f"IMU timestamps: {imu_timestamps}")
147
+
148
+ # Get body data timestamp
149
+ body_timestamp = xrt.get_body_timestamp_ns()
150
+ print(f"Body data timestamp: {body_timestamp}")
151
+
152
+ # Example: Get specific joint data (Head joint is index 15)
153
+ head_pose = body_poses[15] # Head joint
154
+ x, y, z, qx, qy, qz, qw = head_pose
155
+ print(f"Head pose: Position({x:.3f}, {y:.3f}, {z:.3f}) Rotation({qx:.3f}, {qy:.3f}, {qz:.3f}, {qw:.3f})")
156
+ else:
157
+ print("Body tracking data not available. Make sure:")
158
+ print("1. PICO headset is connected")
159
+ print("2. Body tracking is enabled in the control panel")
160
+ print("3. At least two Pico Swift devices are connected and calibrated")
161
+
162
+ xrt.close()
163
+ ```
164
+
165
+ **Body Joint Indices (similar to SMPL, 24 joints total):**
166
+ - 0: Pelvis, 1: Left Hip, 2: Right Hip, 3: Spine1, 4: Left Knee, 5: Right Knee
167
+ - 6: Spine2, 7: Left Ankle, 8: Right Ankle, 9: Spine3, 10: Left Foot, 11: Right Foot
168
+ - 12: Neck, 13: Left Collar, 14: Right Collar, 15: Head, 16: Left Shoulder, 17: Right Shoulder
169
+ - 18: Left Elbow, 19: Right Elbow, 20: Left Wrist, 21: Right Wrist, 22: Left Hand, 23: Right Hand
GR00T-WholeBodyControl/external_dependencies/XRoboToolkit-PC-Service-Pybind_X86_and_ARM64/setup.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import platform
3
+ import re
4
+ import shutil # Added for shutil.rmtree
5
+ import subprocess
6
+ import sys
7
+ from distutils.version import LooseVersion
8
+
9
+ from setuptools import Command, Extension, find_packages, setup # Added Command
10
+ from setuptools.command.build_ext import build_ext
11
+
12
+
13
+ class CMakeExtension(Extension):
14
+ def __init__(self, name, sourcedir=""):
15
+ Extension.__init__(self, name, sources=[])
16
+ self.sourcedir = os.path.abspath(sourcedir)
17
+
18
+
19
+ class CMakeBuild(build_ext):
20
+ def run(self):
21
+ try:
22
+ out = subprocess.check_output(["cmake", "--version"])
23
+ except OSError:
24
+ raise RuntimeError(
25
+ "CMake must be installed to build the following extensions: "
26
+ + ", ".join(e.name for e in self.extensions)
27
+ )
28
+
29
+ if platform.system() == "Windows":
30
+ cmake_version = LooseVersion(re.search(r"version\s*([\d.]+)", out.decode()).group(1))
31
+ if cmake_version < "3.1.0":
32
+ raise RuntimeError("CMake >= 3.1.0 is required on Windows")
33
+
34
+ for ext in self.extensions:
35
+ self.build_extension(ext)
36
+
37
+ def build_extension(self, ext):
38
+ extdir = os.path.abspath(os.path.dirname(self.get_ext_fullpath(ext.name)))
39
+ # required for auto-detection of auxiliary "native" libs
40
+ if not extdir.endswith(os.path.sep):
41
+ extdir += os.path.sep
42
+
43
+ # Get pybind11 include paths
44
+ cmake_args = [
45
+ "-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=" + extdir,
46
+ "-DPYTHON_EXECUTABLE=" + sys.executable,
47
+ "-DCMAKE_BUILD_TYPE=Release",
48
+ ]
49
+
50
+ cfg = "Debug" if self.debug else "Release"
51
+ build_args = ["--config", cfg]
52
+
53
+ if platform.system() == "Windows":
54
+ cmake_args += ["-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{}={}".format(cfg.upper(), extdir)]
55
+ if sys.maxsize > 2**32:
56
+ cmake_args += ["-A", "x64"]
57
+ build_args += ["--", "/m"]
58
+ else:
59
+ cmake_args += ["-DCMAKE_BUILD_TYPE=" + cfg]
60
+ build_args += ["--", "-j2"] # Adjust core count as needed
61
+
62
+ env = os.environ.copy()
63
+ env["CXXFLAGS"] = '{} -DVERSION_INFO=\\"{}\\"'.format(
64
+ env.get("CXXFLAGS", ""), self.distribution.get_version()
65
+ )
66
+ if not os.path.exists(self.build_temp):
67
+ os.makedirs(self.build_temp)
68
+
69
+ subprocess.check_call(["cmake", ext.sourcedir] + cmake_args, cwd=self.build_temp, env=env)
70
+ subprocess.check_call(["cmake", "--build", "."] + build_args, cwd=self.build_temp)
71
+
72
+
73
+ # New Clean Command
74
+ class CleanCommand(Command):
75
+ """Custom clean command to tidy up the project root."""
76
+
77
+ user_options = []
78
+
79
+ def initialize_options(self):
80
+ pass
81
+
82
+ def finalize_options(self):
83
+ pass
84
+
85
+ def run(self):
86
+ # Remove build directory
87
+ if os.path.exists("build"):
88
+ print("Removing 'build/' directory")
89
+ shutil.rmtree("build")
90
+ # Remove .egg-info directory
91
+ for item in os.listdir("."):
92
+ if item.endswith(".egg-info"):
93
+ print(f"Removing '{item}' directory")
94
+ shutil.rmtree(item)
95
+ for item in os.listdir("."):
96
+ if item.endswith(".eggs"):
97
+ print(f"Removing '{item}' directory")
98
+ shutil.rmtree(item)
99
+ # Optionally, remove dist directory if you generate distributions
100
+ if os.path.exists("dist"):
101
+ print("Removing 'dist/' directory")
102
+ shutil.rmtree("dist")
103
+
104
+
105
+ # New Uninstall Command
106
+ class UninstallCommand(Command):
107
+ """Custom command to uninstall the package."""
108
+
109
+ user_options = []
110
+
111
+ def initialize_options(self):
112
+ pass
113
+
114
+ def finalize_options(self):
115
+ pass
116
+
117
+ def run(self):
118
+ package_name = self.distribution.get_name()
119
+ print(f"Attempting to uninstall {package_name}...")
120
+ try:
121
+ subprocess.check_call([sys.executable, "-m", "pip", "uninstall", "-y", package_name])
122
+ print(f"{package_name} uninstalled successfully.")
123
+ except subprocess.CalledProcessError as e:
124
+ print(
125
+ f"Failed to uninstall {package_name}. It may not be installed or pip uninstall failed."
126
+ )
127
+ print(f"Error: {e}")
128
+ except FileNotFoundError:
129
+ print("pip command not found. Please ensure pip is installed and in your PATH.")
130
+
131
+
132
+ setup(
133
+ name="xrobotoolkit_sdk",
134
+ version="1.0.2",
135
+ author="Zhigen Zhao",
136
+ author_email="zhigen.zhao@bytedance.com",
137
+ description="A Python binding for XRobotoolkit PC Service SDK using pybind11 and CMake",
138
+ long_description="", # Optionally, load from a README.md file
139
+ ext_modules=[CMakeExtension("xrobotoolkit_sdk")],
140
+ cmdclass=dict(
141
+ build_ext=CMakeBuild,
142
+ clean=CleanCommand, # Add clean command
143
+ uninstall=UninstallCommand, # Add uninstall command
144
+ ),
145
+ zip_safe=False,
146
+ python_requires=">=3.10", # Specify your Python version requirement
147
+ packages=find_packages(), # If you have other Python packages in your project
148
+ )