Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/environments/locomanipulation/__init__.py +13 -0
- GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/environments/locomanipulation/base.py +1658 -0
- GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/environments/locomanipulation/locomanip.py +83 -0
- GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/environments/locomanipulation/locomanip_basic.py +732 -0
- GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/environments/locomanipulation/locomanip_dc.py +15 -0
- GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/environments/locomanipulation/locomanip_pnp.py +99 -0
- GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/examples/third_party_controller/__init__.py +0 -0
- GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/examples/third_party_controller/default_mink_ik_g1_gear_wbc.json +113 -0
- GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/examples/third_party_controller/default_mink_ik_g1_gear_wbc_gc.json +117 -0
- GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite/__init__.py +6 -0
- GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite/bin.py +205 -0
- GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite/box_pattern_object.py +124 -0
- GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite/lid.py +136 -0
- GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite/needle.py +109 -0
- GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite/pot_with_handles.py +396 -0
- GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite/ring_tripod.py +194 -0
- GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite_body/bin_with_handles.py +162 -0
- GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite_body/coffee_machine.py +244 -0
- GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite_body/inverse_stacked_cylinder.py +139 -0
- GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite_body/lightbulb.py +153 -0
- GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite_body/sliding_box.py +132 -0
- GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite_body/stacked_box.py +111 -0
- GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite_body/stacked_cylinder.py +127 -0
- GR00T-WholeBodyControl/decoupled_wbc/dexmg/gr00trobocasa/robocasa/models/objects/composite_body/stove_plug.py +396 -0
- GR00T-WholeBodyControl/decoupled_wbc/docker/entrypoint/bash.sh +5 -0
- GR00T-WholeBodyControl/decoupled_wbc/docker/entrypoint/deploy.sh +20 -0
- GR00T-WholeBodyControl/decoupled_wbc/docker/entrypoint/install_deps.sh +23 -0
- GR00T-WholeBodyControl/decoupled_wbc/tests/control/__init__.py +0 -0
- GR00T-WholeBodyControl/decoupled_wbc/tests/control/main/__init__.py +0 -0
- GR00T-WholeBodyControl/decoupled_wbc/tests/control/main/teleop/__init__.py +0 -0
- GR00T-WholeBodyControl/decoupled_wbc/tests/control/main/teleop/test_g1_control_loop.py +469 -0
- GR00T-WholeBodyControl/decoupled_wbc/tests/control/main/test_data_exporter_loop.py +403 -0
- GR00T-WholeBodyControl/decoupled_wbc/tests/control/policy/__init__.py +0 -0
- GR00T-WholeBodyControl/decoupled_wbc/tests/control/policy/interpolation_policy/__init__.py +0 -0
- GR00T-WholeBodyControl/decoupled_wbc/tests/control/policy/interpolation_policy/test_interpolation_policy.py +47 -0
- GR00T-WholeBodyControl/decoupled_wbc/tests/control/policy/interpolation_policy/test_interpolation_ramp_up.py +78 -0
- GR00T-WholeBodyControl/decoupled_wbc/tests/control/policy/interpolation_policy/trajectory.png +0 -0
- GR00T-WholeBodyControl/decoupled_wbc/tests/control/robot_model/__init__.py +0 -0
- GR00T-WholeBodyControl/decoupled_wbc/tests/control/robot_model/robot_model_test.py +911 -0
- GR00T-WholeBodyControl/decoupled_wbc/tests/control/teleop/__init__.py +0 -0
- GR00T-WholeBodyControl/decoupled_wbc/tests/control/teleop/test_teleop_retargeting_ik.py +196 -0
- GR00T-WholeBodyControl/decoupled_wbc/tests/control/visualization/__init__.py +0 -0
- GR00T-WholeBodyControl/decoupled_wbc/tests/control/visualization/test_meshcat_visualizer_env.py +88 -0
- GR00T-WholeBodyControl/decoupled_wbc/tests/data/test_exporter.py +522 -0
- GR00T-WholeBodyControl/decoupled_wbc/tests/sim/test_sim_data_collection.py +64 -0
- GR00T-WholeBodyControl/external_dependencies/XRoboToolkit-PC-Service-Pybind_X86_and_ARM64/CLAUDE.md +108 -0
- GR00T-WholeBodyControl/external_dependencies/XRoboToolkit-PC-Service-Pybind_X86_and_ARM64/CMakeLists.txt +54 -0
- GR00T-WholeBodyControl/external_dependencies/XRoboToolkit-PC-Service-Pybind_X86_and_ARM64/LICENSE +21 -0
- GR00T-WholeBodyControl/external_dependencies/XRoboToolkit-PC-Service-Pybind_X86_and_ARM64/README.md +169 -0
- 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 |
+
)
|