holosoma 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (476) hide show
  1. holosoma/__init__.py +0 -0
  2. holosoma/agents/base_algo/base_algo.py +155 -0
  3. holosoma/agents/callbacks/base_callback.py +21 -0
  4. holosoma/agents/callbacks/payload.py +172 -0
  5. holosoma/agents/callbacks/push.py +263 -0
  6. holosoma/agents/callbacks/recording.py +192 -0
  7. holosoma/agents/fast_sac/fast_sac.py +474 -0
  8. holosoma/agents/fast_sac/fast_sac_agent.py +1056 -0
  9. holosoma/agents/fast_sac/fast_sac_utils.py +385 -0
  10. holosoma/agents/modules/augmentation_utils.py +547 -0
  11. holosoma/agents/modules/data_utils.py +134 -0
  12. holosoma/agents/modules/logging_utils.py +446 -0
  13. holosoma/agents/modules/module_utils.py +52 -0
  14. holosoma/agents/modules/modules.py +407 -0
  15. holosoma/agents/modules/ppo_modules.py +186 -0
  16. holosoma/agents/modules/tests/test_augmentation_utils.py +366 -0
  17. holosoma/agents/modules/tests/test_logging_utils.py +187 -0
  18. holosoma/agents/modules/tests/test_module_dimensions.py +263 -0
  19. holosoma/agents/ppo/ppo.py +918 -0
  20. holosoma/bridge/__init__.py +45 -0
  21. holosoma/bridge/base/__init__.py +9 -0
  22. holosoma/bridge/base/basic_sdk2py_bridge.py +368 -0
  23. holosoma/bridge/booster/__init__.py +7 -0
  24. holosoma/bridge/booster/booster_sdk2py_bridge.py +124 -0
  25. holosoma/bridge/ros2/__init__.py +5 -0
  26. holosoma/bridge/ros2/ros2_bridge.py +135 -0
  27. holosoma/bridge/unitree/__init__.py +13 -0
  28. holosoma/bridge/unitree/tests/__init__.py +0 -0
  29. holosoma/bridge/unitree/tests/_fake_unitree_interface.py +146 -0
  30. holosoma/bridge/unitree/tests/test_unitree_mp_bridge.py +207 -0
  31. holosoma/bridge/unitree/unitree_sdk2py_bridge.py +129 -0
  32. holosoma/bridge/unitree/unitree_sdk2py_bridge_mp.py +320 -0
  33. holosoma/config_types/action.py +33 -0
  34. holosoma/config_types/algo.py +345 -0
  35. holosoma/config_types/command.py +131 -0
  36. holosoma/config_types/curriculum.py +36 -0
  37. holosoma/config_types/distribution.py +193 -0
  38. holosoma/config_types/env.py +80 -0
  39. holosoma/config_types/eval_callback.py +129 -0
  40. holosoma/config_types/experiment.py +227 -0
  41. holosoma/config_types/frequency.py +97 -0
  42. holosoma/config_types/full_sim.py +38 -0
  43. holosoma/config_types/logger.py +89 -0
  44. holosoma/config_types/observation.py +57 -0
  45. holosoma/config_types/plugin.py +385 -0
  46. holosoma/config_types/randomization.py +147 -0
  47. holosoma/config_types/reward.py +37 -0
  48. holosoma/config_types/robot.py +169 -0
  49. holosoma/config_types/run_sim.py +105 -0
  50. holosoma/config_types/scene.py +425 -0
  51. holosoma/config_types/sensor.py +292 -0
  52. holosoma/config_types/simulator.py +343 -0
  53. holosoma/config_types/termination.py +30 -0
  54. holosoma/config_types/terrain.py +216 -0
  55. holosoma/config_types/tests/test_frequency.py +143 -0
  56. holosoma/config_types/tests/test_link_physics.py +215 -0
  57. holosoma/config_types/tests/test_plugin_egress_config.py +79 -0
  58. holosoma/config_types/tests/test_sensor_config.py +63 -0
  59. holosoma/config_types/tests/test_tyro_cli.py +8 -0
  60. holosoma/config_types/video.py +156 -0
  61. holosoma/config_types/viewer.py +53 -0
  62. holosoma/config_values/__init__.py +1 -0
  63. holosoma/config_values/action.py +18 -0
  64. holosoma/config_values/algo.py +117 -0
  65. holosoma/config_values/command.py +24 -0
  66. holosoma/config_values/curriculum.py +22 -0
  67. holosoma/config_values/experiment.py +43 -0
  68. holosoma/config_values/loco/__init__.py +7 -0
  69. holosoma/config_values/loco/g1/__init__.py +7 -0
  70. holosoma/config_values/loco/g1/action.py +16 -0
  71. holosoma/config_values/loco/g1/command.py +40 -0
  72. holosoma/config_values/loco/g1/curriculum.py +59 -0
  73. holosoma/config_values/loco/g1/experiment.py +58 -0
  74. holosoma/config_values/loco/g1/observation.py +119 -0
  75. holosoma/config_values/loco/g1/randomization.py +106 -0
  76. holosoma/config_values/loco/g1/reward.py +193 -0
  77. holosoma/config_values/loco/g1/termination.py +21 -0
  78. holosoma/config_values/loco/t1/__init__.py +7 -0
  79. holosoma/config_values/loco/t1/action.py +16 -0
  80. holosoma/config_values/loco/t1/command.py +40 -0
  81. holosoma/config_values/loco/t1/curriculum.py +53 -0
  82. holosoma/config_values/loco/t1/experiment.py +60 -0
  83. holosoma/config_values/loco/t1/observation.py +119 -0
  84. holosoma/config_values/loco/t1/randomization.py +108 -0
  85. holosoma/config_values/loco/t1/reward.py +193 -0
  86. holosoma/config_values/loco/t1/termination.py +21 -0
  87. holosoma/config_values/logger.py +14 -0
  88. holosoma/config_values/observation.py +21 -0
  89. holosoma/config_values/plugin.py +150 -0
  90. holosoma/config_values/randomization.py +21 -0
  91. holosoma/config_values/reward.py +28 -0
  92. holosoma/config_values/robot.py +1128 -0
  93. holosoma/config_values/run_sim.py +95 -0
  94. holosoma/config_values/scene.py +70 -0
  95. holosoma/config_values/sensor.py +57 -0
  96. holosoma/config_values/simulator.py +117 -0
  97. holosoma/config_values/termination.py +20 -0
  98. holosoma/config_values/terrain.py +78 -0
  99. holosoma/config_values/tests/test_plugin_registry.py +150 -0
  100. holosoma/config_values/tests/test_registry.py +389 -0
  101. holosoma/config_values/wbt/__init__.py +1 -0
  102. holosoma/config_values/wbt/g1/__init__.py +7 -0
  103. holosoma/config_values/wbt/g1/command.py +82 -0
  104. holosoma/config_values/wbt/g1/curriculum.py +19 -0
  105. holosoma/config_values/wbt/g1/experiment.py +205 -0
  106. holosoma/config_values/wbt/g1/observation.py +141 -0
  107. holosoma/config_values/wbt/g1/randomization.py +154 -0
  108. holosoma/config_values/wbt/g1/reward.py +113 -0
  109. holosoma/config_values/wbt/g1/scene.py +16 -0
  110. holosoma/config_values/wbt/g1/sensor.py +110 -0
  111. holosoma/config_values/wbt/g1/termination.py +50 -0
  112. holosoma/data/__init__.py +1 -0
  113. holosoma/data/motions/g1_29dof/whole_body_tracking/motion_crawl_slope.npz +0 -0
  114. holosoma/data/motions/g1_29dof/whole_body_tracking/sub3_largebox_003_mj.npz +0 -0
  115. holosoma/data/motions/g1_29dof/whole_body_tracking/sub3_largebox_003_mj_w_obj.npz +0 -0
  116. holosoma/data/motions/g1_29dof/whole_body_tracking/terrain_slope.obj +61 -0
  117. holosoma/data/robots/g1/g1_29dof.urdf +1133 -0
  118. holosoma/data/robots/g1/g1_29dof.xml +486 -0
  119. holosoma/data/robots/g1/main_mesh_collision_halfspherehand.urdf +1128 -0
  120. holosoma/data/robots/g1/meshes/half_sphere.obj +114743 -0
  121. holosoma/data/robots/g1/meshes/head_link.STL +0 -0
  122. holosoma/data/robots/g1/meshes/left_G1_EE_pad.STL +0 -0
  123. holosoma/data/robots/g1/meshes/left_ankle_pitch_link.STL +0 -0
  124. holosoma/data/robots/g1/meshes/left_ankle_roll_link.STL +0 -0
  125. holosoma/data/robots/g1/meshes/left_elbow_link.STL +0 -0
  126. holosoma/data/robots/g1/meshes/left_elbow_link_merge.STL +0 -0
  127. holosoma/data/robots/g1/meshes/left_hand_index_0_link.STL +0 -0
  128. holosoma/data/robots/g1/meshes/left_hand_index_1_link.STL +0 -0
  129. holosoma/data/robots/g1/meshes/left_hand_middle_0_link.STL +0 -0
  130. holosoma/data/robots/g1/meshes/left_hand_middle_1_link.STL +0 -0
  131. holosoma/data/robots/g1/meshes/left_hand_palm_link.STL +0 -0
  132. holosoma/data/robots/g1/meshes/left_hand_thumb_0_link.STL +0 -0
  133. holosoma/data/robots/g1/meshes/left_hand_thumb_1_link.STL +0 -0
  134. holosoma/data/robots/g1/meshes/left_hand_thumb_2_link.STL +0 -0
  135. holosoma/data/robots/g1/meshes/left_hip_pitch_link.STL +0 -0
  136. holosoma/data/robots/g1/meshes/left_hip_roll_link.STL +0 -0
  137. holosoma/data/robots/g1/meshes/left_hip_yaw_link.STL +0 -0
  138. holosoma/data/robots/g1/meshes/left_knee_link.STL +0 -0
  139. holosoma/data/robots/g1/meshes/left_rubber_hand.STL +0 -0
  140. holosoma/data/robots/g1/meshes/left_shoulder_pitch_link.STL +0 -0
  141. holosoma/data/robots/g1/meshes/left_shoulder_roll_link.STL +0 -0
  142. holosoma/data/robots/g1/meshes/left_shoulder_yaw_link.STL +0 -0
  143. holosoma/data/robots/g1/meshes/left_wrist_pitch_link.STL +0 -0
  144. holosoma/data/robots/g1/meshes/left_wrist_roll_link.STL +0 -0
  145. holosoma/data/robots/g1/meshes/left_wrist_roll_rubber_hand.STL +0 -0
  146. holosoma/data/robots/g1/meshes/left_wrist_yaw_link.STL +0 -0
  147. holosoma/data/robots/g1/meshes/logo_link.STL +0 -0
  148. holosoma/data/robots/g1/meshes/pelvis.STL +0 -0
  149. holosoma/data/robots/g1/meshes/pelvis_contour_link.STL +0 -0
  150. holosoma/data/robots/g1/meshes/right_G1_EE_pad.STL +0 -0
  151. holosoma/data/robots/g1/meshes/right_ankle_pitch_link.STL +0 -0
  152. holosoma/data/robots/g1/meshes/right_ankle_roll_link.STL +0 -0
  153. holosoma/data/robots/g1/meshes/right_elbow_link.STL +0 -0
  154. holosoma/data/robots/g1/meshes/right_elbow_link_merge.STL +0 -0
  155. holosoma/data/robots/g1/meshes/right_hand_index_0_link.STL +0 -0
  156. holosoma/data/robots/g1/meshes/right_hand_index_1_link.STL +0 -0
  157. holosoma/data/robots/g1/meshes/right_hand_middle_0_link.STL +0 -0
  158. holosoma/data/robots/g1/meshes/right_hand_middle_1_link.STL +0 -0
  159. holosoma/data/robots/g1/meshes/right_hand_palm_link.STL +0 -0
  160. holosoma/data/robots/g1/meshes/right_hand_thumb_0_link.STL +0 -0
  161. holosoma/data/robots/g1/meshes/right_hand_thumb_1_link.STL +0 -0
  162. holosoma/data/robots/g1/meshes/right_hand_thumb_2_link.STL +0 -0
  163. holosoma/data/robots/g1/meshes/right_hip_pitch_link.STL +0 -0
  164. holosoma/data/robots/g1/meshes/right_hip_roll_link.STL +0 -0
  165. holosoma/data/robots/g1/meshes/right_hip_yaw_link.STL +0 -0
  166. holosoma/data/robots/g1/meshes/right_knee_link.STL +0 -0
  167. holosoma/data/robots/g1/meshes/right_rubber_hand.STL +0 -0
  168. holosoma/data/robots/g1/meshes/right_shoulder_pitch_link.STL +0 -0
  169. holosoma/data/robots/g1/meshes/right_shoulder_roll_link.STL +0 -0
  170. holosoma/data/robots/g1/meshes/right_shoulder_yaw_link.STL +0 -0
  171. holosoma/data/robots/g1/meshes/right_wrist_pitch_link.STL +0 -0
  172. holosoma/data/robots/g1/meshes/right_wrist_roll_link.STL +0 -0
  173. holosoma/data/robots/g1/meshes/right_wrist_roll_rubber_hand.STL +0 -0
  174. holosoma/data/robots/g1/meshes/right_wrist_yaw_link.STL +0 -0
  175. holosoma/data/robots/g1/meshes/torso_constraint_L_link.STL +0 -0
  176. holosoma/data/robots/g1/meshes/torso_constraint_L_rod_link.STL +0 -0
  177. holosoma/data/robots/g1/meshes/torso_constraint_R_link.STL +0 -0
  178. holosoma/data/robots/g1/meshes/torso_constraint_R_rod_link.STL +0 -0
  179. holosoma/data/robots/g1/meshes/torso_link.STL +0 -0
  180. holosoma/data/robots/g1/meshes/torso_link_23dof_rev_1_0.STL +0 -0
  181. holosoma/data/robots/g1/meshes/torso_link_rev_1_0.STL +0 -0
  182. holosoma/data/robots/g1/meshes/tote_no_bar.STL +0 -0
  183. holosoma/data/robots/g1/meshes/waist_constraint_L.STL +0 -0
  184. holosoma/data/robots/g1/meshes/waist_constraint_R.STL +0 -0
  185. holosoma/data/robots/g1/meshes/waist_roll_link.STL +0 -0
  186. holosoma/data/robots/g1/meshes/waist_roll_link_rev_1_0.STL +0 -0
  187. holosoma/data/robots/g1/meshes/waist_support_link.STL +0 -0
  188. holosoma/data/robots/g1/meshes/waist_yaw_link.STL +0 -0
  189. holosoma/data/robots/g1/meshes/waist_yaw_link_rev_1_0.STL +0 -0
  190. holosoma/data/robots/g1/scenes/scene_g1_29dof_wbt_plane.xml +26 -0
  191. holosoma/data/robots/onelink_box/onelink_box.urdf +45 -0
  192. holosoma/data/robots/onelink_box/onelink_box.xml +10 -0
  193. holosoma/data/robots/t1/meshes/AL1.STL +0 -0
  194. holosoma/data/robots/t1/meshes/AL2.STL +0 -0
  195. holosoma/data/robots/t1/meshes/AL3.STL +0 -0
  196. holosoma/data/robots/t1/meshes/AL4.STL +0 -0
  197. holosoma/data/robots/t1/meshes/AL5.STL +0 -0
  198. holosoma/data/robots/t1/meshes/AL6.STL +0 -0
  199. holosoma/data/robots/t1/meshes/AL7.STL +0 -0
  200. holosoma/data/robots/t1/meshes/AR1.STL +0 -0
  201. holosoma/data/robots/t1/meshes/AR2.STL +0 -0
  202. holosoma/data/robots/t1/meshes/AR3.STL +0 -0
  203. holosoma/data/robots/t1/meshes/AR4.STL +0 -0
  204. holosoma/data/robots/t1/meshes/AR5.STL +0 -0
  205. holosoma/data/robots/t1/meshes/AR6.STL +0 -0
  206. holosoma/data/robots/t1/meshes/AR7.STL +0 -0
  207. holosoma/data/robots/t1/meshes/Ankle_Cross_Left.STL +0 -0
  208. holosoma/data/robots/t1/meshes/Ankle_Cross_Right.STL +0 -0
  209. holosoma/data/robots/t1/meshes/Crank_Down_Left.STL +0 -0
  210. holosoma/data/robots/t1/meshes/Crank_Down_Right.STL +0 -0
  211. holosoma/data/robots/t1/meshes/Crank_Up_Left.STL +0 -0
  212. holosoma/data/robots/t1/meshes/Crank_Up_Right.STL +0 -0
  213. holosoma/data/robots/t1/meshes/Down_Left_XX_Ball.STL +0 -0
  214. holosoma/data/robots/t1/meshes/Down_Left_X_Ball.STL +0 -0
  215. holosoma/data/robots/t1/meshes/Down_Left_Y_Ball.STL +0 -0
  216. holosoma/data/robots/t1/meshes/Down_Right_XX_Ball.STL +0 -0
  217. holosoma/data/robots/t1/meshes/Down_Right_X_Ball.STL +0 -0
  218. holosoma/data/robots/t1/meshes/Down_Right_Y_Ball.STL +0 -0
  219. holosoma/data/robots/t1/meshes/H1.STL +0 -0
  220. holosoma/data/robots/t1/meshes/H2.STL +0 -0
  221. holosoma/data/robots/t1/meshes/Hip_Pitch_Left.STL +0 -0
  222. holosoma/data/robots/t1/meshes/Hip_Pitch_Right.STL +0 -0
  223. holosoma/data/robots/t1/meshes/Hip_Roll_Left.STL +0 -0
  224. holosoma/data/robots/t1/meshes/Hip_Roll_Right.STL +0 -0
  225. holosoma/data/robots/t1/meshes/Hip_Yaw_Left.STL +0 -0
  226. holosoma/data/robots/t1/meshes/Hip_Yaw_Right.STL +0 -0
  227. holosoma/data/robots/t1/meshes/Link_Long_Left.STL +0 -0
  228. holosoma/data/robots/t1/meshes/Link_Long_Right.STL +0 -0
  229. holosoma/data/robots/t1/meshes/Link_Short_Left.STL +0 -0
  230. holosoma/data/robots/t1/meshes/Link_Short_Right.STL +0 -0
  231. holosoma/data/robots/t1/meshes/Shank_Left.STL +0 -0
  232. holosoma/data/robots/t1/meshes/Shank_Right.STL +0 -0
  233. holosoma/data/robots/t1/meshes/Trunk.STL +0 -0
  234. holosoma/data/robots/t1/meshes/Up_Left_XX_Ball.STL +0 -0
  235. holosoma/data/robots/t1/meshes/Up_Left_X_Ball.STL +0 -0
  236. holosoma/data/robots/t1/meshes/Up_Left_Y_Ball.STL +0 -0
  237. holosoma/data/robots/t1/meshes/Up_Right_XX_Ball.STL +0 -0
  238. holosoma/data/robots/t1/meshes/Up_Right_X_Ball.STL +0 -0
  239. holosoma/data/robots/t1/meshes/Up_Right_Y_Ball.STL +0 -0
  240. holosoma/data/robots/t1/meshes/Waist.STL +0 -0
  241. holosoma/data/robots/t1/meshes/left_Link1.STL +0 -0
  242. holosoma/data/robots/t1/meshes/left_Link11.STL +0 -0
  243. holosoma/data/robots/t1/meshes/left_Link2.STL +0 -0
  244. holosoma/data/robots/t1/meshes/left_Link22.STL +0 -0
  245. holosoma/data/robots/t1/meshes/left_base_link.STL +0 -0
  246. holosoma/data/robots/t1/meshes/left_foot_link.STL +0 -0
  247. holosoma/data/robots/t1/meshes/left_hand_link.STL +0 -0
  248. holosoma/data/robots/t1/meshes/left_rubber_hand.STL +0 -0
  249. holosoma/data/robots/t1/meshes/right_Link1.STL +0 -0
  250. holosoma/data/robots/t1/meshes/right_Link11.STL +0 -0
  251. holosoma/data/robots/t1/meshes/right_Link2.STL +0 -0
  252. holosoma/data/robots/t1/meshes/right_Link22.STL +0 -0
  253. holosoma/data/robots/t1/meshes/right_base_link.STL +0 -0
  254. holosoma/data/robots/t1/meshes/right_foot_link.STL +0 -0
  255. holosoma/data/robots/t1/meshes/right_hand_link.STL +0 -0
  256. holosoma/data/robots/t1/meshes/right_rubber_hand.STL +0 -0
  257. holosoma/data/robots/t1/t1_29dof.urdf +1629 -0
  258. holosoma/data/robots/t1/t1_29dof.xml +300 -0
  259. holosoma/data/scene_objects/boxes/large_box.urdf +32 -0
  260. holosoma/data/scene_objects/boxes/largebox.obj +54685 -0
  261. holosoma/data/scene_objects/boxes/small_box.urdf +19 -0
  262. holosoma/data/scene_objects/boxes/small_box.usda +24 -0
  263. holosoma/data/scene_objects/boxes/small_box.xml +9 -0
  264. holosoma/data/scene_objects/multibody/multibody.urdf +47 -0
  265. holosoma/data/scene_objects/multibody/multibody.usda +48 -0
  266. holosoma/data/scene_objects/multibody/multibody.xml +15 -0
  267. holosoma/data/scene_objects/multibody/unauthored_table.usda +45 -0
  268. holosoma/data/scene_objects/panels/red_panel.urdf +23 -0
  269. holosoma/data/scene_objects/panels/red_panel.usda +27 -0
  270. holosoma/data/scene_objects/panels/red_panel.xml +12 -0
  271. holosoma/data/scene_objects/rubber_duck/LICENSE +1 -0
  272. holosoma/data/scene_objects/rubber_duck/RubberDuck.mtl +7 -0
  273. holosoma/data/scene_objects/rubber_duck/RubberDuck.obj +1773 -0
  274. holosoma/data/scene_objects/rubber_duck/RubberDuck_BaseColor.png +0 -0
  275. holosoma/data/scene_objects/rubber_duck/huge_rubber_duck.urdf +33 -0
  276. holosoma/data/scene_objects/rubber_duck/huge_rubber_duck.xml +17 -0
  277. holosoma/data/scene_objects/rubber_duck/large_rubber_duck.urdf +33 -0
  278. holosoma/data/scene_objects/rubber_duck/large_rubber_duck.xml +16 -0
  279. holosoma/data/scene_objects/rubber_duck/rubber_duck.urdf +32 -0
  280. holosoma/data/scene_objects/rubber_duck/rubber_duck.xml +19 -0
  281. holosoma/envs/__init__.py +1 -0
  282. holosoma/envs/base_task/__init__.py +5 -0
  283. holosoma/envs/base_task/base_task.py +616 -0
  284. holosoma/envs/locomotion/__init__.py +9 -0
  285. holosoma/envs/locomotion/locomotion_manager.py +336 -0
  286. holosoma/envs/tests/test_e2e.py +49 -0
  287. holosoma/envs/tests/test_obs_dict_group.py +99 -0
  288. holosoma/envs/tests/test_push_randomization.py +178 -0
  289. holosoma/envs/wbt/wbt_manager.py +245 -0
  290. holosoma/eval_agent.py +110 -0
  291. holosoma/examples/__init__.py +1 -0
  292. holosoma/examples/object_managers_demo.py +380 -0
  293. holosoma/managers/__init__.py +10 -0
  294. holosoma/managers/action/__init__.py +10 -0
  295. holosoma/managers/action/base.py +145 -0
  296. holosoma/managers/action/manager.py +242 -0
  297. holosoma/managers/action/terms/__init__.py +9 -0
  298. holosoma/managers/action/terms/joint_control.py +337 -0
  299. holosoma/managers/command/__init__.py +6 -0
  300. holosoma/managers/command/base.py +41 -0
  301. holosoma/managers/command/manager.py +227 -0
  302. holosoma/managers/command/terms/__init__.py +5 -0
  303. holosoma/managers/command/terms/locomotion.py +224 -0
  304. holosoma/managers/command/terms/wbt.py +1495 -0
  305. holosoma/managers/curriculum/__init__.py +5 -0
  306. holosoma/managers/curriculum/base.py +68 -0
  307. holosoma/managers/curriculum/manager.py +173 -0
  308. holosoma/managers/curriculum/terms/__init__.py +5 -0
  309. holosoma/managers/curriculum/terms/locomotion.py +374 -0
  310. holosoma/managers/observation/__init__.py +19 -0
  311. holosoma/managers/observation/base.py +53 -0
  312. holosoma/managers/observation/manager.py +353 -0
  313. holosoma/managers/observation/terms/__init__.py +5 -0
  314. holosoma/managers/observation/terms/cameras.py +54 -0
  315. holosoma/managers/observation/terms/locomotion.py +179 -0
  316. holosoma/managers/observation/terms/objects.py +103 -0
  317. holosoma/managers/observation/terms/wbt.py +221 -0
  318. holosoma/managers/observation/tests/test_camera_obs_terms.py +140 -0
  319. holosoma/managers/randomization/__init__.py +6 -0
  320. holosoma/managers/randomization/base.py +43 -0
  321. holosoma/managers/randomization/exceptions.py +16 -0
  322. holosoma/managers/randomization/manager.py +274 -0
  323. holosoma/managers/randomization/terms/__init__.py +1 -0
  324. holosoma/managers/randomization/terms/_shared.py +20 -0
  325. holosoma/managers/randomization/terms/locomotion.py +1098 -0
  326. holosoma/managers/randomization/terms/objects.py +938 -0
  327. holosoma/managers/reset_events/__init__.py +10 -0
  328. holosoma/managers/reset_events/base.py +33 -0
  329. holosoma/managers/reset_events/manager.py +57 -0
  330. holosoma/managers/reward/__init__.py +6 -0
  331. holosoma/managers/reward/base.py +53 -0
  332. holosoma/managers/reward/manager.py +333 -0
  333. holosoma/managers/reward/terms/__init__.py +5 -0
  334. holosoma/managers/reward/terms/locomotion.py +392 -0
  335. holosoma/managers/reward/terms/wbt.py +175 -0
  336. holosoma/managers/termination/__init__.py +5 -0
  337. holosoma/managers/termination/base.py +27 -0
  338. holosoma/managers/termination/manager.py +129 -0
  339. holosoma/managers/termination/terms/__init__.py +1 -0
  340. holosoma/managers/termination/terms/common.py +10 -0
  341. holosoma/managers/termination/terms/locomotion.py +81 -0
  342. holosoma/managers/termination/terms/wbt.py +146 -0
  343. holosoma/managers/terrain/__init__.py +5 -0
  344. holosoma/managers/terrain/base.py +104 -0
  345. holosoma/managers/terrain/manager.py +60 -0
  346. holosoma/managers/terrain/terms/__init__.py +1 -0
  347. holosoma/managers/terrain/terms/locomotion.py +278 -0
  348. holosoma/managers/utils.py +56 -0
  349. holosoma/py.typed +0 -0
  350. holosoma/replay.py +44 -0
  351. holosoma/run_sim.py +106 -0
  352. holosoma/simulator/__init__.py +11 -0
  353. holosoma/simulator/base_simulator/base_simulator.py +1315 -0
  354. holosoma/simulator/base_simulator/hooks.py +346 -0
  355. holosoma/simulator/base_simulator/tests/test_hooks.py +274 -0
  356. holosoma/simulator/base_simulator/tests/test_plugins.py +119 -0
  357. holosoma/simulator/isaacgym/isaacgym.py +1555 -0
  358. holosoma/simulator/isaacgym/physics.py +164 -0
  359. holosoma/simulator/isaacgym/urdf_scene_loader.py +770 -0
  360. holosoma/simulator/isaacgym/video_recorder.py +176 -0
  361. holosoma/simulator/isaacsim/__init__.py +1 -0
  362. holosoma/simulator/isaacsim/converters.py +127 -0
  363. holosoma/simulator/isaacsim/event_cfg.py +33 -0
  364. holosoma/simulator/isaacsim/events.py +352 -0
  365. holosoma/simulator/isaacsim/isaaclab_viewpoint_camera_controller.py +250 -0
  366. holosoma/simulator/isaacsim/isaacsim.py +1482 -0
  367. holosoma/simulator/isaacsim/isaacsim_articulation_cfg.py +96 -0
  368. holosoma/simulator/isaacsim/object_spawner.py +565 -0
  369. holosoma/simulator/isaacsim/prim_naming.py +76 -0
  370. holosoma/simulator/isaacsim/prim_utils.py +358 -0
  371. holosoma/simulator/isaacsim/proxy_utils.py +81 -0
  372. holosoma/simulator/isaacsim/spawners/__init__.py +4 -0
  373. holosoma/simulator/isaacsim/spawners/from_files.py +463 -0
  374. holosoma/simulator/isaacsim/spawners/from_files_cfg.py +58 -0
  375. holosoma/simulator/isaacsim/spawners/schema_utils.py +146 -0
  376. holosoma/simulator/isaacsim/state_adapter.py +186 -0
  377. holosoma/simulator/isaacsim/state_utils.py +53 -0
  378. holosoma/simulator/isaacsim/video_recorder.py +292 -0
  379. holosoma/simulator/mujoco/__init__.py +5 -0
  380. holosoma/simulator/mujoco/backends/__init__.py +38 -0
  381. holosoma/simulator/mujoco/backends/base.py +428 -0
  382. holosoma/simulator/mujoco/backends/classic_backend.py +466 -0
  383. holosoma/simulator/mujoco/backends/randomization.py +438 -0
  384. holosoma/simulator/mujoco/backends/warp_backend.py +911 -0
  385. holosoma/simulator/mujoco/backends/warp_bridge.py +241 -0
  386. holosoma/simulator/mujoco/command_registry.py +98 -0
  387. holosoma/simulator/mujoco/fields.py +133 -0
  388. holosoma/simulator/mujoco/mjw_views.py +629 -0
  389. holosoma/simulator/mujoco/mujoco.py +1967 -0
  390. holosoma/simulator/mujoco/scene_manager.py +896 -0
  391. holosoma/simulator/mujoco/tensor_views.py +568 -0
  392. holosoma/simulator/mujoco/video_recorder.py +172 -0
  393. holosoma/simulator/plugins/__init__.py +23 -0
  394. holosoma/simulator/plugins/camera_consumer.py +230 -0
  395. holosoma/simulator/plugins/depth_color.py +67 -0
  396. holosoma/simulator/plugins/ros2/__init__.py +2 -0
  397. holosoma/simulator/plugins/ros2/camera_info.py +50 -0
  398. holosoma/simulator/plugins/ros2/encode.py +120 -0
  399. holosoma/simulator/plugins/ros2/ros2_image_plugin.py +228 -0
  400. holosoma/simulator/plugins/ros2/worker.py +87 -0
  401. holosoma/simulator/plugins/tests/__init__.py +0 -0
  402. holosoma/simulator/plugins/tests/test_camera_consumer.py +226 -0
  403. holosoma/simulator/plugins/tests/test_image_grid.py +150 -0
  404. holosoma/simulator/plugins/tests/test_presets.py +193 -0
  405. holosoma/simulator/plugins/tests/test_ros2_helpers.py +208 -0
  406. holosoma/simulator/plugins/tests/test_ros2_image_plugin.py +384 -0
  407. holosoma/simulator/plugins/viz/__init__.py +5 -0
  408. holosoma/simulator/plugins/viz/image_grid.py +155 -0
  409. holosoma/simulator/plugins/viz/viz_plugin.py +176 -0
  410. holosoma/simulator/shared/__init__.py +1 -0
  411. holosoma/simulator/shared/asset_format.py +69 -0
  412. holosoma/simulator/shared/builtin_plugins.py +23 -0
  413. holosoma/simulator/shared/camera_controller.py +406 -0
  414. holosoma/simulator/shared/camera_sensor.py +154 -0
  415. holosoma/simulator/shared/field_decorators.py +59 -0
  416. holosoma/simulator/shared/image_transform.py +73 -0
  417. holosoma/simulator/shared/object_registry.py +561 -0
  418. holosoma/simulator/shared/root_states_view.py +114 -0
  419. holosoma/simulator/shared/ros2_plugins.py +335 -0
  420. holosoma/simulator/shared/scene_types.py +33 -0
  421. holosoma/simulator/shared/simulator_bridge.py +197 -0
  422. holosoma/simulator/shared/terrain.py +581 -0
  423. holosoma/simulator/shared/terrain_types.py +34 -0
  424. holosoma/simulator/shared/tests/test_camera_runtime_cache.py +95 -0
  425. holosoma/simulator/shared/tests/test_get_camera_data.py +95 -0
  426. holosoma/simulator/shared/tests/test_image_transform.py +126 -0
  427. holosoma/simulator/shared/tests/test_optical_frame.py +59 -0
  428. holosoma/simulator/shared/tests/test_ros2_plugins.py +257 -0
  429. holosoma/simulator/shared/tests/test_sensor_manager.py +109 -0
  430. holosoma/simulator/shared/video_recorder.py +568 -0
  431. holosoma/simulator/shared/virtual_gantry.py +583 -0
  432. holosoma/simulator/types.py +84 -0
  433. holosoma/train_agent.py +340 -0
  434. holosoma/utils/__init__.py +0 -0
  435. holosoma/utils/adapters/draw_utils.py +58 -0
  436. holosoma/utils/adapters/isaacgym_draw_adapter.py +128 -0
  437. holosoma/utils/adapters/isaacsim_draw_adapter.py +62 -0
  438. holosoma/utils/adapters/mujoco_draw_adapter.py +295 -0
  439. holosoma/utils/average_meters.py +82 -0
  440. holosoma/utils/clock.py +152 -0
  441. holosoma/utils/common.py +160 -0
  442. holosoma/utils/config_registry.py +285 -0
  443. holosoma/utils/config_utils.py +1 -0
  444. holosoma/utils/draw.py +71 -0
  445. holosoma/utils/eval_utils.py +298 -0
  446. holosoma/utils/experiment_paths.py +130 -0
  447. holosoma/utils/file_cache.py +418 -0
  448. holosoma/utils/helpers.py +121 -0
  449. holosoma/utils/inference_helpers.py +304 -0
  450. holosoma/utils/logging.py +54 -0
  451. holosoma/utils/module_utils.py +8 -0
  452. holosoma/utils/path.py +82 -0
  453. holosoma/utils/pycompat.py +22 -0
  454. holosoma/utils/rate.py +113 -0
  455. holosoma/utils/rotations.py +743 -0
  456. holosoma/utils/safe_torch_import.py +14 -0
  457. holosoma/utils/sampler.py +452 -0
  458. holosoma/utils/sim_utils.py +607 -0
  459. holosoma/utils/simulator_config.py +102 -0
  460. holosoma/utils/terrain_utils.py +607 -0
  461. holosoma/utils/tests/test_distributions.py +272 -0
  462. holosoma/utils/tests/test_eval_utils.py +380 -0
  463. holosoma/utils/tests/test_keyed_distributions.py +335 -0
  464. holosoma/utils/tests/test_rotations.py +173 -0
  465. holosoma/utils/tests/test_torch_utils.py +111 -0
  466. holosoma/utils/tests/test_tyro_utils.py +186 -0
  467. holosoma/utils/torch_jit.py +276 -0
  468. holosoma/utils/torch_utils.py +181 -0
  469. holosoma/utils/tyro_utils.py +136 -0
  470. holosoma/utils/video_utils.py +226 -0
  471. holosoma/utils/wandb.py +113 -0
  472. holosoma/utils/warp_utils.py +169 -0
  473. holosoma-0.0.1.dist-info/METADATA +349 -0
  474. holosoma-0.0.1.dist-info/RECORD +476 -0
  475. holosoma-0.0.1.dist-info/WHEEL +4 -0
  476. holosoma-0.0.1.dist-info/entry_points.txt +5 -0
holosoma/__init__.py ADDED
File without changes
@@ -0,0 +1,155 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Callable
4
+
5
+ from holosoma.config_types.algo import AlgoInitConfig
6
+ from holosoma.envs.base_task.base_task import BaseTask
7
+ from holosoma.utils.safe_torch_import import torch
8
+
9
+ if TYPE_CHECKING:
10
+ from holosoma.config_types.experiment import ExperimentConfig
11
+
12
+
13
+ class BaseAlgo:
14
+ def __init__(self, env: BaseTask, config: AlgoInitConfig, device, multi_gpu_cfg=None):
15
+ self.env = env
16
+ self.config = config
17
+ self.device = device
18
+
19
+ self.is_multi_gpu = multi_gpu_cfg is not None
20
+ if multi_gpu_cfg is not None:
21
+ self.gpu_global_rank = multi_gpu_cfg["global_rank"]
22
+ self.gpu_local_rank = multi_gpu_cfg["local_rank"]
23
+ self.gpu_world_size = multi_gpu_cfg["world_size"]
24
+ else:
25
+ self.gpu_global_rank = 0
26
+ self.gpu_local_rank = 0
27
+ self.gpu_world_size = 1
28
+ self.is_main_process = self.gpu_global_rank == 0
29
+ self._experiment_config: ExperimentConfig | None = None
30
+ self._wandb_run_path: str | None = None
31
+
32
+ def setup(self):
33
+ return NotImplementedError
34
+
35
+ def learn(self):
36
+ return NotImplementedError
37
+
38
+ def load(self, path):
39
+ return NotImplementedError
40
+
41
+ @property
42
+ def inference_model(self):
43
+ return NotImplementedError
44
+
45
+ @property
46
+ def actor_onnx_wrapper(self):
47
+ return NotImplementedError
48
+
49
+ def env_step(self, actions, extra_info=None):
50
+ obs_dict, rewards, dones, extras = self.env.step(actions, extra_info)
51
+ return obs_dict, rewards, dones, extras
52
+
53
+ def attach_checkpoint_metadata(
54
+ self,
55
+ experiment_config: ExperimentConfig,
56
+ wandb_run_path: str | None = None,
57
+ ) -> None:
58
+ """Attach metadata that should be saved with checkpoints."""
59
+
60
+ self._experiment_config = experiment_config
61
+ self._wandb_run_path = wandb_run_path
62
+
63
+ def _checkpoint_metadata(self, iteration: int | None = None) -> dict[str, Any]:
64
+ if self._experiment_config is None:
65
+ raise RuntimeError("Experiment config metadata missing. Call attach_checkpoint_metadata() before saving.")
66
+
67
+ metadata: dict[str, Any] = {"experiment_config": self._experiment_config.to_serializable_dict()}
68
+ if self._wandb_run_path:
69
+ metadata["wandb_run_path"] = self._wandb_run_path
70
+ if iteration is not None:
71
+ metadata["iteration"] = int(iteration)
72
+ return metadata
73
+
74
+ def has_curricula_enabled(self) -> bool:
75
+ """Check if any curricula are enabled in the environment.
76
+
77
+ This helper method checks for the presence of various curriculum flags
78
+ to determine if any curriculum learning is active. This is commonly used
79
+ for multi-GPU synchronization and logging purposes.
80
+
81
+ Returns
82
+ -------
83
+ bool
84
+ True if any curriculum is enabled, False otherwise.
85
+ """
86
+ return getattr(self.env, "use_reward_penalty_curriculum", False) or getattr(
87
+ self.env, "use_domain_rand_scale_curriculum", False
88
+ )
89
+
90
+ def _synchronize_curriculum_metrics(self):
91
+ """Synchronize curriculum-related metrics across all GPUs."""
92
+ # Check if any curricula are enabled before synchronizing
93
+ if not self.has_curricula_enabled():
94
+ return
95
+
96
+ env = self._unwrap_env()
97
+ env.synchronize_curriculum_state(device=self.device, world_size=self.gpu_world_size)
98
+
99
+ def get_inference_policy(self, device: str | None = None) -> Callable[[dict[str, torch.Tensor]], torch.Tensor]:
100
+ """Get a callable policy function for inference.
101
+
102
+ This method returns a function that takes observations as input and returns
103
+ actions. The policy function is configured to run on the specified device.
104
+
105
+ Parameters
106
+ ----------
107
+ device : str | None, optional
108
+ The device to run the policy on (e.g., 'cuda', 'cpu').
109
+ If None, uses the default device.
110
+
111
+ Returns
112
+ -------
113
+ Callable[[torch.Tensor], torch.Tensor]
114
+ A function that takes observations as input and returns actions.
115
+ The function expects input observations as a torch.Tensor and
116
+ returns actions as a torch.Tensor. Both input and output tensors
117
+ should be on the specified device.
118
+
119
+ Notes
120
+ -----
121
+ This is an abstract method that should be implemented by subclasses.
122
+ The returned policy function should:
123
+ - Run the policy network on the specified device
124
+ - Return actions in the expected format
125
+ """
126
+ raise NotImplementedError
127
+
128
+ @torch.no_grad()
129
+ def evaluate_policy(self, max_eval_steps: int | None = None):
130
+ raise NotImplementedError
131
+
132
+ def save(self, path=None, name="last.ckpt"):
133
+ raise NotImplementedError
134
+
135
+ def _unwrap_env(self) -> BaseTask | Any:
136
+ """Return the underlying environment.
137
+
138
+ Algorithms that wrap the task (e.g. ``FastSACEnv``) keep a reference to the
139
+ original environment as ``unwrapped_env`` during construction, so we simply
140
+ return that when it is present.
141
+ """
142
+ return getattr(self, "unwrapped_env", self.env)
143
+
144
+ def _collect_env_state(self) -> dict[str, torch.Tensor | float]:
145
+ """Collect environment state for checkpointing via the environment interface."""
146
+ env = self._unwrap_env()
147
+ state = env.get_checkpoint_state()
148
+ return state or {}
149
+
150
+ def _restore_env_state(self, env_state: dict[str, torch.Tensor | float] | None) -> None:
151
+ """Restore environment state from checkpoint via the environment interface."""
152
+ if not env_state:
153
+ return
154
+ env = self._unwrap_env()
155
+ env.load_checkpoint_state(env_state)
@@ -0,0 +1,21 @@
1
+ from torch.nn import Module
2
+
3
+
4
+ class RLEvalCallback(Module):
5
+ def __init__(self, config, training_loop):
6
+ super().__init__()
7
+ self.config = config
8
+ self.training_loop = training_loop
9
+ self.device = self.training_loop.device
10
+
11
+ def on_pre_evaluate_policy(self):
12
+ pass
13
+
14
+ def on_pre_eval_env_step(self, actor_state):
15
+ return actor_state
16
+
17
+ def on_post_eval_env_step(self, actor_state):
18
+ return actor_state
19
+
20
+ def on_post_evaluate_policy(self):
21
+ pass
@@ -0,0 +1,172 @@
1
+ """Eval callback that applies constant downward forces on wrists to simulate a held payload.
2
+
3
+ Applies F = mass * g / num_bodies on each resolved wrist/elbow link
4
+ as a sustained downward (-Z) force throughout the evaluation episode. Uses the same
5
+ IsaacLab set_external_force_and_torque API as the push callback, with substep-level
6
+ force application via monkey-patching.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ import numpy as np
14
+ from loguru import logger
15
+
16
+ from holosoma.agents.callbacks.base_callback import RLEvalCallback
17
+ from holosoma.config_types.eval_callback import PayloadConfig
18
+ from holosoma.utils.safe_torch_import import torch
19
+
20
+ GRAVITY = 9.81
21
+
22
+
23
+ class EvalPayloadCallback(RLEvalCallback):
24
+ """Applies constant downward forces on wrist links to simulate holding a payload.
25
+
26
+ The total payload weight (mass * g) is split evenly across all resolved
27
+ wrist bodies. Forces are applied at every physics substep via monkey-patch.
28
+
29
+ Recorded arrays (per env step):
30
+ payload_force_per_body [T] force magnitude per body in N
31
+ payload_body_pos_w [T, N, 3] world positions of payload bodies
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ config: PayloadConfig,
37
+ training_loop: Any = None,
38
+ ):
39
+ super().__init__(config, training_loop)
40
+ self.env_id = config.env_id
41
+ self.candidate_body_names = [s.strip() for s in config.body_names.split(",")]
42
+ self._record_buffers: dict[str, list[np.ndarray]] | None = None
43
+ self._record_metadata: dict[str, Any] | None = None
44
+
45
+ # Resolved at on_pre_evaluate_policy
46
+ self._resolved_bodies: list[tuple[str, int, int]] = []
47
+ self._force_per_body: float = 0.0
48
+ self._sim: Any = None
49
+ self._original_apply_force: Any = None
50
+ self._step_count: int = 0
51
+
52
+ def _get_env(self):
53
+ return self.training_loop._unwrap_env()
54
+
55
+ def _find_recording_callback(self):
56
+ from holosoma.agents.callbacks.recording import EvalRecordingCallback
57
+
58
+ for cb in self.training_loop.eval_callbacks:
59
+ if isinstance(cb, EvalRecordingCallback):
60
+ return cb
61
+ return None
62
+
63
+ def on_pre_evaluate_policy(self) -> None:
64
+ # Discover recording callback for shared buffer/metadata access
65
+ recording_cb = self._find_recording_callback()
66
+ if recording_cb is not None:
67
+ self._record_buffers = recording_cb._buffers
68
+ self._record_metadata = recording_cb._metadata
69
+
70
+ env = self._get_env()
71
+ self._sim = env.simulator
72
+ if self._sim.simulator_config.name != "isaacsim":
73
+ raise RuntimeError(f"EvalPayloadCallback requires IsaacSim, got '{self._sim.simulator_config.name}'")
74
+
75
+ # Resolve body names
76
+ self._resolved_bodies = []
77
+ available = list(getattr(self._sim, "body_names", []))
78
+ for name in self.candidate_body_names:
79
+ body_names_idx = self._sim.find_rigid_body_indice(name)
80
+ if isinstance(body_names_idx, int) and body_names_idx >= 0:
81
+ isaac_body_id = self._sim.body_ids[body_names_idx]
82
+ self._resolved_bodies.append((name, body_names_idx, isaac_body_id))
83
+ else:
84
+ raise ValueError(f"EvalPayloadCallback: body '{name}' not found. Available: {available}")
85
+
86
+ total_force = self.config.mass_kg * GRAVITY
87
+ self._force_per_body = total_force / len(self._resolved_bodies)
88
+
89
+ resolved_names = [name for name, _, _ in self._resolved_bodies]
90
+ logger.info(
91
+ f"EvalPayloadCallback: {len(self._resolved_bodies)} payload bodies: {resolved_names}, "
92
+ f"mass={self.config.mass_kg}kg, total_force={total_force:.1f}N, "
93
+ f"force_per_body={self._force_per_body:.1f}N"
94
+ )
95
+
96
+ # Store config in recording metadata
97
+ if self._record_metadata is not None:
98
+ self._record_metadata["payload_config"] = {
99
+ "body_names": resolved_names,
100
+ "mass_kg": self.config.mass_kg,
101
+ "total_force_n": total_force,
102
+ "force_per_body_n": self._force_per_body,
103
+ }
104
+
105
+ # Initialize recording buffers
106
+ buffers = self._get_buffers()
107
+ if buffers is not None:
108
+ buffers["payload_force_per_body"] = []
109
+ buffers["payload_body_pos_w"] = []
110
+
111
+ # Monkey-patch substep force application
112
+ self._original_apply_force = env._apply_force_in_physics_step
113
+
114
+ def _patched_apply_force():
115
+ self._original_apply_force()
116
+ self._apply_payload_forces()
117
+
118
+ env._apply_force_in_physics_step = _patched_apply_force
119
+
120
+ def on_post_eval_env_step(self, actor_state: dict) -> dict:
121
+ buffers = self._get_buffers()
122
+ if buffers is not None:
123
+ eid = self.env_id
124
+ body_positions = []
125
+ for _name, _body_names_idx, isaac_body_id in self._resolved_bodies:
126
+ pos = self._sim._robot.data.body_pos_w[eid, isaac_body_id].detach().cpu().numpy().copy()
127
+ body_positions.append(pos)
128
+
129
+ buffers["payload_force_per_body"].append(np.array(self._force_per_body, dtype=np.float32))
130
+ buffers["payload_body_pos_w"].append(np.stack(body_positions, axis=0).astype(np.float32))
131
+
132
+ self._step_count += 1
133
+ return actor_state
134
+
135
+ def on_post_evaluate_policy(self) -> None:
136
+ self._get_env()._apply_force_in_physics_step = self._original_apply_force
137
+ logger.info(
138
+ f"EvalPayloadCallback: completed {self._step_count} steps, "
139
+ f"payload={self.config.mass_kg}kg on {len(self._resolved_bodies)} bodies"
140
+ )
141
+
142
+ def _apply_payload_forces(self) -> None:
143
+ """Apply downward force on all resolved wrist bodies (called each substep)."""
144
+ from isaaclab.utils.math import quat_apply_inverse
145
+
146
+ eid = self.env_id
147
+ device = self._sim.sim_device
148
+
149
+ # Force in world frame: straight down (-Z)
150
+ force_world = torch.tensor(
151
+ [0.0, 0.0, -self._force_per_body],
152
+ dtype=torch.float32,
153
+ device=device,
154
+ )
155
+
156
+ for _name, _body_names_idx, isaac_body_id in self._resolved_bodies:
157
+ # Transform world-frame force to body-local frame
158
+ body_quat_w = self._sim._robot.data.body_quat_w[eid, isaac_body_id]
159
+ force_body = quat_apply_inverse(body_quat_w, force_world)
160
+
161
+ forces = force_body.unsqueeze(0).unsqueeze(0) # [1, 1, 3]
162
+ torques = torch.zeros_like(forces)
163
+
164
+ self._sim._robot.set_external_force_and_torque(
165
+ forces=forces,
166
+ torques=torques,
167
+ env_ids=torch.tensor([eid], device=device),
168
+ body_ids=torch.tensor([isaac_body_id], device=device),
169
+ )
170
+
171
+ def _get_buffers(self) -> dict[str, list[np.ndarray]] | None:
172
+ return self._record_buffers
@@ -0,0 +1,263 @@
1
+ """Eval callback that applies random force pulses to test policy robustness.
2
+
3
+ Periodically pushes the robot at random body locations with configurable
4
+ force magnitude, duration, and interval. Uses IsaacLab's
5
+ set_external_force_and_torque API to apply forces in the physics simulation.
6
+
7
+ Push events are optionally recorded for visualization in viser by sharing
8
+ buffer/metadata dicts with EvalRecordingCallback.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import math
14
+ import random
15
+ from typing import Any
16
+
17
+ import numpy as np
18
+ from loguru import logger
19
+
20
+ from holosoma.agents.callbacks.base_callback import RLEvalCallback
21
+ from holosoma.config_types.eval_callback import PushConfig
22
+ from holosoma.utils.safe_torch_import import torch
23
+
24
+
25
+ class EvalPushCallback(RLEvalCallback):
26
+ """Applies random force pulses during evaluation and optionally records them.
27
+
28
+ Forces are applied at the physics substep level by patching the env's
29
+ _apply_force_in_physics_step method, ensuring the force is active for the
30
+ full duration across all substeps (control decimation).
31
+
32
+ Recorded arrays (per env step):
33
+ push_active [T] 1.0 when pushing, 0.0 otherwise
34
+ push_force_w [T, 3] force vector in world frame (zeros when inactive)
35
+ push_body_pos_w[T, 3] world position of pushed body (zeros when inactive)
36
+ push_body_idx [T] index into body_names (-1 when inactive)
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ config: PushConfig,
42
+ training_loop: Any = None,
43
+ ):
44
+ super().__init__(config, training_loop)
45
+ self.env_id = config.env_id
46
+ self.candidate_body_names = [s.strip() for s in config.body_names.split(",")]
47
+ self._record_buffers: dict[str, list[np.ndarray]] | None = None
48
+ self._record_metadata: dict[str, Any] | None = None
49
+
50
+ # Resolved at on_pre_evaluate_policy
51
+ self._resolved_bodies: list[tuple[str, int, int]] = []
52
+ self._dt: float = 0.0
53
+ self._sim: Any = None
54
+ self._original_apply_force: Any = None
55
+
56
+ # Push state machine
57
+ self._push_active: bool = False
58
+ self._push_steps_remaining: int = 0
59
+ self._idle_steps_remaining: int = 0
60
+ self._current_force_w: torch.Tensor | None = None
61
+ self._current_body_names_idx: int = -1
62
+ self._current_isaac_body_id: int = -1
63
+
64
+ self._step_count: int = 0
65
+
66
+ def _get_env(self):
67
+ return self.training_loop._unwrap_env()
68
+
69
+ def _find_recording_callback(self):
70
+ from holosoma.agents.callbacks.recording import EvalRecordingCallback
71
+
72
+ for cb in self.training_loop.eval_callbacks:
73
+ if isinstance(cb, EvalRecordingCallback):
74
+ return cb
75
+ return None
76
+
77
+ def on_pre_evaluate_policy(self) -> None:
78
+ # Discover recording callback for shared buffer/metadata access
79
+ recording_cb = self._find_recording_callback()
80
+ if recording_cb is not None:
81
+ self._record_buffers = recording_cb._buffers
82
+ self._record_metadata = recording_cb._metadata
83
+
84
+ env = self._get_env()
85
+ self._sim = env.simulator
86
+ if self._sim.simulator_config.name != "isaacsim":
87
+ raise RuntimeError(f"EvalPushCallback requires IsaacSim, got '{self._sim.simulator_config.name}'")
88
+ self._dt = float(env.dt)
89
+
90
+ # Resolve body names to simulator indices
91
+ self._resolved_bodies = []
92
+ available = list(getattr(self._sim, "body_names", []))
93
+ for name in self.candidate_body_names:
94
+ body_names_idx = self._sim.find_rigid_body_indice(name)
95
+ if isinstance(body_names_idx, int) and body_names_idx >= 0:
96
+ isaac_body_id = self._sim.body_ids[body_names_idx]
97
+ self._resolved_bodies.append((name, body_names_idx, isaac_body_id))
98
+ else:
99
+ raise ValueError(f"EvalPushCallback: body '{name}' not found. Available: {available}")
100
+
101
+ resolved_names = [name for name, _, _ in self._resolved_bodies]
102
+ logger.info(
103
+ f"EvalPushCallback: {len(self._resolved_bodies)} push bodies: {resolved_names}, "
104
+ f"force={self.config.force_range}N, duration={self.config.duration_s}s, interval={self.config.interval_s}s"
105
+ )
106
+
107
+ # Store push config in recording metadata
108
+ if self._record_metadata is not None:
109
+ self._record_metadata["push_config"] = {
110
+ "body_names": resolved_names,
111
+ "force_range": list(self.config.force_range),
112
+ "duration_s": list(self.config.duration_s),
113
+ "interval_s": list(self.config.interval_s),
114
+ }
115
+
116
+ # Initialize recording buffers
117
+ buffers = self._get_buffers()
118
+ if buffers is not None:
119
+ for key in ("push_active", "push_force_w", "push_body_pos_w", "push_body_idx"):
120
+ buffers[key] = []
121
+
122
+ # Start with a random idle period
123
+ self._idle_steps_remaining = self._sample_interval_steps()
124
+ self._push_active = False
125
+
126
+ # Patch _apply_force_in_physics_step to inject push forces at every substep
127
+ self._original_apply_force = env._apply_force_in_physics_step
128
+
129
+ def _patched_apply_force():
130
+ self._original_apply_force()
131
+ if self._push_active:
132
+ self._apply_push_force()
133
+
134
+ env._apply_force_in_physics_step = _patched_apply_force
135
+
136
+ def on_post_eval_env_step(self, actor_state: dict) -> dict:
137
+ # Manage push schedule
138
+ if self._push_active:
139
+ self._push_steps_remaining -= 1
140
+ if self._push_steps_remaining <= 0:
141
+ self._deactivate_push()
142
+ self._idle_steps_remaining = self._sample_interval_steps()
143
+ else:
144
+ self._idle_steps_remaining -= 1
145
+ if self._idle_steps_remaining <= 0:
146
+ self._activate_push()
147
+
148
+ buffers = self._get_buffers()
149
+ eid = self.env_id
150
+
151
+ if self._push_active:
152
+ assert self._current_force_w is not None
153
+ force_w = self._current_force_w.detach().cpu().numpy().copy()
154
+ body_pos = self._sim._robot.data.body_pos_w[eid, self._current_isaac_body_id].detach().cpu().numpy().copy()
155
+ body_idx = self._current_body_names_idx
156
+ active = 1.0
157
+ else:
158
+ force_w = np.zeros(3, dtype=np.float32)
159
+ body_pos = np.zeros(3, dtype=np.float32)
160
+ body_idx = -1
161
+ active = 0.0
162
+
163
+ if buffers is not None:
164
+ buffers["push_active"].append(np.array(active, dtype=np.float32))
165
+ buffers["push_force_w"].append(force_w.astype(np.float32))
166
+ buffers["push_body_pos_w"].append(body_pos.astype(np.float32))
167
+ buffers["push_body_idx"].append(np.array(body_idx, dtype=np.int32))
168
+
169
+ self._step_count += 1
170
+ return actor_state
171
+
172
+ def on_post_evaluate_policy(self) -> None:
173
+ if self._push_active:
174
+ self._deactivate_push()
175
+ self._get_env()._apply_force_in_physics_step = self._original_apply_force
176
+ logger.info(f"EvalPushCallback: completed {self._step_count} steps")
177
+
178
+ # ------------------------------------------------------------------
179
+ # Push lifecycle
180
+ # ------------------------------------------------------------------
181
+
182
+ def _activate_push(self) -> None:
183
+ name, body_names_idx, isaac_body_id = random.choice(self._resolved_bodies)
184
+ self._current_body_names_idx = body_names_idx
185
+ self._current_isaac_body_id = isaac_body_id
186
+
187
+ # Random force magnitude
188
+ mag = random.uniform(*self.config.force_range)
189
+
190
+ # Random horizontal direction (XY plane)
191
+ angle = random.uniform(0, 2 * math.pi)
192
+ direction = torch.tensor(
193
+ [math.cos(angle), math.sin(angle), 0.0],
194
+ dtype=torch.float32,
195
+ device=self._sim.sim_device,
196
+ )
197
+ self._current_force_w = direction * mag
198
+
199
+ # Duration in env steps
200
+ dur_s = random.uniform(*self.config.duration_s)
201
+ self._push_steps_remaining = max(1, round(dur_s / self._dt))
202
+
203
+ self._push_active = True
204
+ logger.info(
205
+ f"PUSH START body='{name}' force={mag:.0f}N "
206
+ f"dir=[{direction[0].item():.2f},{direction[1].item():.2f}] "
207
+ f"duration={self._push_steps_remaining} steps ({dur_s:.2f}s)"
208
+ )
209
+
210
+ def _deactivate_push(self) -> None:
211
+ self._clear_push_force()
212
+ self._push_active = False
213
+ self._current_force_w = None
214
+ self._current_body_names_idx = -1
215
+ self._current_isaac_body_id = -1
216
+ logger.info("PUSH END")
217
+
218
+ # ------------------------------------------------------------------
219
+ # Force application (called each physics substep via monkey-patch)
220
+ # ------------------------------------------------------------------
221
+
222
+ def _apply_push_force(self) -> None:
223
+ from isaaclab.utils.math import quat_apply_inverse
224
+
225
+ eid = self.env_id
226
+ isaac_body_id = self._current_isaac_body_id
227
+
228
+ # Transform world-frame force to body-local frame
229
+ # (IsaacLab 2.1 hardcodes is_global=False)
230
+ body_quat_w = self._sim._robot.data.body_quat_w[eid, isaac_body_id]
231
+ force_body = quat_apply_inverse(body_quat_w, self._current_force_w)
232
+
233
+ forces = force_body.unsqueeze(0).unsqueeze(0) # [1, 1, 3]
234
+ torques = torch.zeros_like(forces) # [1, 1, 3]
235
+
236
+ self._sim._robot.set_external_force_and_torque(
237
+ forces=forces,
238
+ torques=torques,
239
+ env_ids=torch.tensor([eid], device=self._sim.sim_device),
240
+ body_ids=torch.tensor([isaac_body_id], device=self._sim.sim_device),
241
+ )
242
+
243
+ def _clear_push_force(self) -> None:
244
+ if self._current_isaac_body_id < 0:
245
+ return
246
+ zero = torch.zeros(1, 1, 3, device=self._sim.sim_device)
247
+ self._sim._robot.set_external_force_and_torque(
248
+ forces=zero,
249
+ torques=zero,
250
+ env_ids=torch.tensor([self.env_id], device=self._sim.sim_device),
251
+ body_ids=torch.tensor([self._current_isaac_body_id], device=self._sim.sim_device),
252
+ )
253
+
254
+ # ------------------------------------------------------------------
255
+ # Helpers
256
+ # ------------------------------------------------------------------
257
+
258
+ def _sample_interval_steps(self) -> int:
259
+ s = random.uniform(*self.config.interval_s)
260
+ return max(1, round(s / self._dt))
261
+
262
+ def _get_buffers(self) -> dict[str, list[np.ndarray]] | None:
263
+ return self._record_buffers