Chandra Kiran commited on
Commit
cccaf8b
·
unverified ·
1 Parent(s): fab4b5b

Keep free objects where the scene puts them when resetting to the robot keyframe

Browse files

MuJoCo 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.

Files changed (4) hide show
  1. api/mjcf_check.py +2 -3
  2. api/robots.py +35 -0
  3. tests/test_runs.py +19 -0
  4. 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
- if model.nkey:
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
- if model.nkey:
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