Spaces:
Paused
Paused
Chandra Kiran commited on
Keep free objects where the scene puts them when resetting to the robot keyframe
Browse filesMuJoCo pads a robot keyframe with zeros for joints the scene adds, so every
free body (boxes, balls) started at the world origin inside the robot and
knocked the G1 over. The keyframe is now applied to the robot's own joints
only, in both the validator and the worker.
- api/mjcf_check.py +2 -3
- api/robots.py +35 -0
- tests/test_runs.py +19 -0
- worker/sim_runner.py +3 -3
api/mjcf_check.py
CHANGED
|
@@ -18,7 +18,7 @@ import xml.etree.ElementTree as ET
|
|
| 18 |
from dataclasses import asdict, dataclass, field
|
| 19 |
from pathlib import Path
|
| 20 |
|
| 21 |
-
from .robots import ROBOTS, robot_dir
|
| 22 |
|
| 23 |
MAX_MJCF_BYTES = 300_000
|
| 24 |
SCENE_FILE = "scene.xml"
|
|
@@ -174,8 +174,7 @@ def _load_and_step(directory: Path, sim_seconds: float, robot: str | None) -> Va
|
|
| 174 |
return ValidationResult(False, "load", f"robot root body '{ROBOTS[robot].root_body}' missing", stats=stats)
|
| 175 |
|
| 176 |
data = mujoco.MjData(model)
|
| 177 |
-
|
| 178 |
-
mujoco.mj_resetDataKeyframe(model, data, 0)
|
| 179 |
steps = max(1, int(math.ceil(sim_seconds / model.opt.timestep)))
|
| 180 |
for _ in range(steps):
|
| 181 |
mujoco.mj_step(model, data)
|
|
|
|
| 18 |
from dataclasses import asdict, dataclass, field
|
| 19 |
from pathlib import Path
|
| 20 |
|
| 21 |
+
from .robots import ROBOTS, reset_to_robot_keyframe, robot_dir
|
| 22 |
|
| 23 |
MAX_MJCF_BYTES = 300_000
|
| 24 |
SCENE_FILE = "scene.xml"
|
|
|
|
| 174 |
return ValidationResult(False, "load", f"robot root body '{ROBOTS[robot].root_body}' missing", stats=stats)
|
| 175 |
|
| 176 |
data = mujoco.MjData(model)
|
| 177 |
+
reset_to_robot_keyframe(model, data, ROBOTS[robot].root_body if robot else None)
|
|
|
|
| 178 |
steps = max(1, int(math.ceil(sim_seconds / model.opt.timestep)))
|
| 179 |
for _ in range(steps):
|
| 180 |
mujoco.mj_step(model, data)
|
api/robots.py
CHANGED
|
@@ -72,3 +72,38 @@ def robot_facts(menagerie_dir: str, name: str) -> str:
|
|
| 72 |
"unless the scenario says otherwise.",
|
| 73 |
]
|
| 74 |
return "\n".join(lines)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
"unless the scenario says otherwise.",
|
| 73 |
]
|
| 74 |
return "\n".join(lines)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def reset_to_robot_keyframe(model: mujoco.MjModel, data: mujoco.MjData, root_body: str | None) -> None:
|
| 78 |
+
"""Reset to the robot's first keyframe without disturbing the rest of the scene.
|
| 79 |
+
|
| 80 |
+
Robot keyframes (e.g. the G1's "stand") only list the robot's own joints. When a scene adds
|
| 81 |
+
free bodies, MuJoCo pads the keyframe with zeros for them, which would teleport every box
|
| 82 |
+
or ball to the world origin, inside the robot. So the keyframe is applied to the joints of
|
| 83 |
+
the robot's subtree only; every other joint keeps the scene's default pose (qpos0).
|
| 84 |
+
"""
|
| 85 |
+
mujoco.mj_resetData(model, data)
|
| 86 |
+
if model.nkey == 0:
|
| 87 |
+
return
|
| 88 |
+
root = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_BODY, root_body) if root_body else -1
|
| 89 |
+
if root < 0: # no robot: the scene's own keyframe describes the whole scene
|
| 90 |
+
mujoco.mj_resetDataKeyframe(model, data, 0)
|
| 91 |
+
return
|
| 92 |
+
|
| 93 |
+
def in_robot(body: int) -> bool:
|
| 94 |
+
while body > 0:
|
| 95 |
+
if body == root:
|
| 96 |
+
return True
|
| 97 |
+
body = model.body_parentid[body]
|
| 98 |
+
return False
|
| 99 |
+
|
| 100 |
+
key_qpos = model.key_qpos[0]
|
| 101 |
+
for j in range(model.njnt):
|
| 102 |
+
if not in_robot(model.jnt_bodyid[j]):
|
| 103 |
+
continue
|
| 104 |
+
start = model.jnt_qposadr[j]
|
| 105 |
+
end = model.jnt_qposadr[j + 1] if j + 1 < model.njnt else model.nq
|
| 106 |
+
data.qpos[start:end] = key_qpos[start:end]
|
| 107 |
+
if model.nu:
|
| 108 |
+
data.ctrl[:] = model.key_ctrl[0]
|
| 109 |
+
mujoco.mj_forward(model, data)
|
tests/test_runs.py
CHANGED
|
@@ -418,3 +418,22 @@ def test_importing_the_worker_does_not_change_the_environment():
|
|
| 418 |
|
| 419 |
importlib.reload(worker.handler)
|
| 420 |
assert dict(os.environ) == before
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 418 |
|
| 419 |
importlib.reload(worker.handler)
|
| 420 |
assert dict(os.environ) == before
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
def test_free_objects_keep_their_scene_pose(tmp_path, menagerie):
|
| 424 |
+
"""The robot keyframe must not teleport the scene's free bodies to the origin (seen in CI: G1 knocked over)."""
|
| 425 |
+
import mujoco
|
| 426 |
+
|
| 427 |
+
from api.robots import reset_to_robot_keyframe
|
| 428 |
+
|
| 429 |
+
scene = tmp_path / "scene.xml"
|
| 430 |
+
scene.write_text(GOOD_SCENE)
|
| 431 |
+
(tmp_path / "g1.xml").write_text((menagerie / "unitree_g1" / "g1.xml").read_text())
|
| 432 |
+
model = mujoco.MjModel.from_xml_path(str(scene))
|
| 433 |
+
data = mujoco.MjData(model)
|
| 434 |
+
assert list(model.key_qpos[0][-7:]) == [0, 0, 0, 1, 0, 0, 0] # MuJoCo pads the crate to the origin
|
| 435 |
+
reset_to_robot_keyframe(model, data, "pelvis")
|
| 436 |
+
crate = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_BODY, "crate")
|
| 437 |
+
assert list(np.round(data.xpos[crate], 3)) == [1.0, 1.0, 0.3] # where the scene put it
|
| 438 |
+
pelvis = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_BODY, "pelvis")
|
| 439 |
+
assert data.xpos[pelvis][2] == pytest.approx(0.8) # robot from its keyframe
|
worker/sim_runner.py
CHANGED
|
@@ -28,6 +28,8 @@ from pathlib import Path
|
|
| 28 |
import mujoco
|
| 29 |
import numpy as np
|
| 30 |
|
|
|
|
|
|
|
| 31 |
NS = 1_000_000_000
|
| 32 |
VISIBLE_GROUPS = (0, 1, 2) # MuJoCo's viewer shows groups 0-2 by default; Menagerie puts collision meshes in 3
|
| 33 |
|
|
@@ -234,9 +236,7 @@ def simulate(scene: Path, out_dir: Path, config: RunConfig) -> dict:
|
|
| 234 |
out_dir.mkdir(parents=True, exist_ok=True)
|
| 235 |
model = mujoco.MjModel.from_xml_path(str(scene))
|
| 236 |
data = mujoco.MjData(model)
|
| 237 |
-
|
| 238 |
-
mujoco.mj_resetDataKeyframe(model, data, 0)
|
| 239 |
-
mujoco.mj_forward(model, data)
|
| 240 |
target = _hold_targets(model)
|
| 241 |
root = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_BODY, config.root_body) if config.root_body else -1
|
| 242 |
|
|
|
|
| 28 |
import mujoco
|
| 29 |
import numpy as np
|
| 30 |
|
| 31 |
+
from api.robots import reset_to_robot_keyframe
|
| 32 |
+
|
| 33 |
NS = 1_000_000_000
|
| 34 |
VISIBLE_GROUPS = (0, 1, 2) # MuJoCo's viewer shows groups 0-2 by default; Menagerie puts collision meshes in 3
|
| 35 |
|
|
|
|
| 236 |
out_dir.mkdir(parents=True, exist_ok=True)
|
| 237 |
model = mujoco.MjModel.from_xml_path(str(scene))
|
| 238 |
data = mujoco.MjData(model)
|
| 239 |
+
reset_to_robot_keyframe(model, data, config.root_body)
|
|
|
|
|
|
|
| 240 |
target = _hold_targets(model)
|
| 241 |
root = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_BODY, config.root_body) if config.root_body else -1
|
| 242 |
|