Skip to content

Commit 7a994d6

Browse files
committed
📝 docs: 细化 Pupper MJCF 教程与参数实验
1 parent fe1a7a5 commit 7a994d6

10 files changed

Lines changed: 1585 additions & 114 deletions

File tree

codes/practices/quadruped/cs123/4.quadruped-mjcf/gain_sweep_quick.py

Lines changed: 524 additions & 0 deletions
Large diffs are not rendered by default.

codes/practices/quadruped/cs123/4.quadruped-mjcf/models/pupper_v3_fixed.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
<!-- 初始姿态。fixed 模型的 qpos 只有 12 个腿部关节角,对应 actuator 中的 12 路控制。 -->
55
<key name="home" qpos="0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0"
66
ctrl="0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0" />
7+
<!-- Lab 4.4 实验:如果在 base_link 下打开 freejoint,需要把上面的 fixed home 注释掉,
8+
再打开下面这条 floating home。floating qpos = base 位置 3 维 + base 四元数 4 维 + 12 个关节角。
9+
<key name="home"
10+
qpos="0 0 0.28 1 0 0 0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0"
11+
ctrl="0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0" />
12+
-->
713
</keyframe>
814
<asset>
915
<!-- 背景和地面网格材质。fixed 模型主要用于查看本体,地面可在 viewer 中辅助判断朝向。 -->
@@ -98,6 +104,10 @@
98104
<!-- 机身惯量来自 CAD/转换结果;碰撞体用 box 简化,视觉体用 STL 网格。 -->
99105
<inertial pos="0.025 0 0.015" quat="0 0.677807 0 0.73524" mass="1.506"
100106
diaginertia="0.00854071 0.0085 0.00235929" />
107+
<!-- Lab 4.4 实验:打开下面这一行,base_link 就从 fixed 变成 floating。
108+
记得同时把 keyframe 里的 home qpos 切到上面的 floating 版本。
109+
<freejoint name="world_to_body" />
110+
-->
101111
<geom size="0.04507 0.06379 0.129715" pos="0.02146 0 0.03345"
102112
quat="0.499998 -0.5 -0.500002 0.5" type="box" class="collision" />
103113
<geom quat="0.499998 -0.5 -0.500002 0.5" type="mesh" contype="0" conaffinity="0"
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
"""Make the floating-base Pupper stand still under PD position servo.
2+
3+
Run from this directory with:
4+
mjpython stand_pupper_v3_floating.py # macOS
5+
python stand_pupper_v3_floating.py # Linux / Windows
6+
7+
This extends view_pupper_v3_floating.py with two pieces:
8+
9+
* A target pose STAND_POSE that is written into ``data.ctrl`` every step, so
10+
the position servo pulls the legs into a "knee-bent, body up" stand instead
11+
of just collapsing to the home (all zeros) pose.
12+
* Recording ``data.qpos[2]`` (base z) over time, so when the viewer is closed
13+
we print the final height and the last-1-second standard deviation as the
14+
Lab 4.5 pass criterion (std < 5 mm).
15+
16+
macOS note: do not run ``mjpython -m mujoco.viewer --mjcf=...``; see the
17+
docstring of view_pupper_v3_floating.py for the underlying mjpython + runpy
18+
issue.
19+
"""
20+
21+
from __future__ import annotations
22+
23+
import pathlib
24+
import sys
25+
import time
26+
27+
import numpy as np
28+
29+
import mujoco
30+
import mujoco.viewer
31+
32+
33+
_DIR = pathlib.Path(__file__).parent
34+
35+
MODEL_PATH = _DIR / "models" / "pupper_v3_floating.xml"
36+
37+
# Actuator order: front_r {1,2,3}, front_l {1,2,3}, back_r {1,2,3}, back_l {1,2,3}.
38+
#
39+
# Pupper's mesh + per-body quaternions are set up so that joint angle = 0
40+
# already puts each leg into a knee-bent, body-up stance. That means the
41+
# stand pose is simply the home keyframe's ctrl (all zeros). We still write
42+
# it into data.ctrl every step so the servo target stays locked even when
43+
# external code (e.g. the §5 gait controller) starts touching data.ctrl in
44+
# the same loop.
45+
#
46+
# To experiment with other postures, change any of these 12 entries. Remember
47+
# that the left-side HFE / KFE limits are mirrored (see §4.2.1 in the chapter),
48+
# so non-zero left-leg targets typically need flipped signs.
49+
STAND_POSE = np.zeros(12, dtype=np.float64)
50+
51+
52+
def _load_model(path: pathlib.Path) -> tuple[mujoco.MjModel, mujoco.MjData]:
53+
"""Load the MJCF model and put it into the home keyframe if available."""
54+
model_path = path.expanduser().resolve()
55+
model = mujoco.MjModel.from_xml_path(str(model_path))
56+
data = mujoco.MjData(model)
57+
58+
home_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_KEY, "home")
59+
if home_id >= 0:
60+
mujoco.mj_resetDataKeyframe(model, data, home_id)
61+
else:
62+
mujoco.mj_resetData(model, data)
63+
64+
mujoco.mj_forward(model, data)
65+
return model, data
66+
67+
68+
def _configure_viewer(viewer: mujoco.viewer.Handle) -> None:
69+
viewer.cam.lookat[:] = [0.0, 0.0, 0.15]
70+
viewer.cam.distance = 0.8
71+
viewer.cam.azimuth = 135
72+
viewer.cam.elevation = -22
73+
74+
75+
def _report_stability(heights: list[float], timestep: float) -> None:
76+
"""Print final base z and the last-1-second standard deviation."""
77+
if not heights:
78+
print("No samples recorded; nothing to report.", file=sys.stderr)
79+
return
80+
samples_per_second = max(int(round(1.0 / timestep)), 1)
81+
tail = np.array(heights[-samples_per_second:])
82+
print(
83+
f"final z={tail[-1]:.3f} m, "
84+
f"last-1s std={np.std(tail) * 1000.0:.2f} mm "
85+
f"(< 5 mm = stable)"
86+
)
87+
88+
89+
def main() -> int:
90+
model_path = MODEL_PATH.resolve()
91+
model, data = _load_model(model_path)
92+
93+
print(f"Loaded: {model_path}", flush=True)
94+
print(
95+
f" nq={model.nq}, nv={model.nv}, nu={model.nu}, nbody={model.nbody}",
96+
flush=True,
97+
)
98+
print(f" timestep={model.opt.timestep:.4f} s", flush=True)
99+
print(
100+
"Opening viewer. Close the window to exit and see the stability report.",
101+
flush=True,
102+
)
103+
104+
heights: list[float] = []
105+
try:
106+
with mujoco.viewer.launch_passive(model, data) as viewer:
107+
_configure_viewer(viewer)
108+
while viewer.is_running():
109+
step_start = time.perf_counter()
110+
data.ctrl[:] = STAND_POSE
111+
mujoco.mj_step(model, data)
112+
heights.append(float(data.qpos[2]))
113+
viewer.sync()
114+
115+
elapsed = time.perf_counter() - step_start
116+
if elapsed < model.opt.timestep:
117+
time.sleep(model.opt.timestep - elapsed)
118+
except RuntimeError as exc:
119+
if sys.platform == "darwin" and "mjpython" in str(exc):
120+
print("\nOn macOS, run the interactive viewer with mjpython:", file=sys.stderr)
121+
print(f" mjpython {pathlib.Path(__file__)}", file=sys.stderr)
122+
return 2
123+
raise
124+
125+
_report_stability(heights, model.opt.timestep)
126+
return 0
127+
128+
129+
if __name__ == "__main__":
130+
raise SystemExit(main())

codes/practices/quadruped/cs123/4.quadruped-mjcf/view_pupper_v3_fixed.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
"""Minimal viewer for inspecting the Lab 4 fixed-base Pupper model.
1+
"""Inspect the Lab 4 fixed-base Pupper model in MuJoCo viewer.
22
3-
Run on macOS with:
4-
mjpython lab4/view_fixed_model.py
3+
Run from this directory with:
4+
mjpython view_pupper_v3_fixed.py # macOS
5+
python view_pupper_v3_fixed.py # Linux / Windows
56
"""
67

78
from __future__ import annotations
@@ -19,6 +20,7 @@
1920
MODEL_PATH = _DIR / "models" / "pupper_v3_fixed.xml"
2021

2122
def _load_model(path: pathlib.Path) -> tuple[mujoco.MjModel, mujoco.MjData]:
23+
"""Load the MJCF model and put it into the home keyframe if available."""
2224
model_path = path.expanduser().resolve()
2325
model = mujoco.MjModel.from_xml_path(str(model_path))
2426
data = mujoco.MjData(model)
@@ -34,6 +36,7 @@ def _load_model(path: pathlib.Path) -> tuple[mujoco.MjModel, mujoco.MjData]:
3436

3537

3638
def _configure_viewer(viewer: mujoco.viewer.Handle) -> None:
39+
"""Choose a camera angle that frames the small fixed-base robot clearly."""
3740
viewer.cam.lookat[:] = [0.0, 0.0, 0.10]
3841
viewer.cam.distance = 0.65
3942
viewer.cam.azimuth = 135
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
"""Inspect the Lab 4 floating-base Pupper model in MuJoCo viewer.
2+
3+
Run from this directory with:
4+
mjpython view_pupper_v3_floating.py # macOS
5+
python view_pupper_v3_floating.py # Linux / Windows
6+
7+
Unlike view_pupper_v3_fixed.py, the main loop here calls mj_step instead of
8+
mj_forward, so the simulator actually evolves gravity and contacts. You should
9+
see the robot fall from z=0.28 m, the feet touch the ground, and the position
10+
servo pull the 12 leg joints back toward their home targets.
11+
12+
macOS note: do not run ``mjpython -m mujoco.viewer --mjcf=...`` — mjpython
13+
already imports ``mujoco.viewer`` during startup (to claim the GUI main
14+
thread), so re-executing it via ``-m``/runpy raises
15+
``RuntimeError: Caught an unknown exception!`` at ``_Simulate(...)``. Use the
16+
script entry (``mjpython view_pupper_v3_floating.py``) to bypass this.
17+
"""
18+
19+
from __future__ import annotations
20+
21+
import pathlib
22+
import sys
23+
import time
24+
25+
import mujoco
26+
import mujoco.viewer
27+
28+
29+
_DIR = pathlib.Path(__file__).parent
30+
31+
MODEL_PATH = _DIR / "models" / "pupper_v3_floating.xml"
32+
33+
34+
def _load_model(path: pathlib.Path) -> tuple[mujoco.MjModel, mujoco.MjData]:
35+
"""Load the MJCF model and put it into the home keyframe if available."""
36+
model_path = path.expanduser().resolve()
37+
model = mujoco.MjModel.from_xml_path(str(model_path))
38+
data = mujoco.MjData(model)
39+
40+
home_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_KEY, "home")
41+
if home_id >= 0:
42+
mujoco.mj_resetDataKeyframe(model, data, home_id)
43+
else:
44+
mujoco.mj_resetData(model, data)
45+
46+
mujoco.mj_forward(model, data)
47+
return model, data
48+
49+
50+
def _configure_viewer(viewer: mujoco.viewer.Handle) -> None:
51+
"""Frame the falling robot from a 3/4 angle so the drop is easy to see."""
52+
viewer.cam.lookat[:] = [0.0, 0.0, 0.15]
53+
viewer.cam.distance = 0.8
54+
viewer.cam.azimuth = 135
55+
viewer.cam.elevation = -22
56+
57+
58+
def main() -> int:
59+
model_path = MODEL_PATH.resolve()
60+
model, data = _load_model(model_path)
61+
62+
print(f"Loaded: {model_path}", flush=True)
63+
print(
64+
f" nq={model.nq}, nv={model.nv}, nu={model.nu}, nbody={model.nbody}",
65+
flush=True,
66+
)
67+
print(f" timestep={model.opt.timestep:.4f} s", flush=True)
68+
print("Opening viewer. Close the window to exit.", flush=True)
69+
70+
try:
71+
with mujoco.viewer.launch_passive(model, data) as viewer:
72+
_configure_viewer(viewer)
73+
while viewer.is_running():
74+
step_start = time.perf_counter()
75+
mujoco.mj_step(model, data)
76+
viewer.sync()
77+
78+
elapsed = time.perf_counter() - step_start
79+
if elapsed < model.opt.timestep:
80+
time.sleep(model.opt.timestep - elapsed)
81+
except RuntimeError as exc:
82+
if sys.platform == "darwin" and "mjpython" in str(exc):
83+
print("\nOn macOS, run the interactive viewer with mjpython:", file=sys.stderr)
84+
print(f" mjpython {pathlib.Path(__file__)}", file=sys.stderr)
85+
return 2
86+
raise
87+
88+
return 0
89+
90+
91+
if __name__ == "__main__":
92+
raise SystemExit(main())

0 commit comments

Comments
 (0)