Skip to content

Commit d47ba96

Browse files
authored
Merge pull request #7 from KraHsu/fix/unitree_example
Align G1 velocity/motion with mjlab and fix doubled log path
2 parents 04c860e + a575d58 commit d47ba96

8 files changed

Lines changed: 235 additions & 16 deletions

File tree

‎examples/unitree/src/genelab_unitree/g1/env_cfg.py‎

Lines changed: 77 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,46 @@
2626
_IMU_OFFSET = (0.04525, 0.0, -0.08339)
2727
_G1_FOOT_LINKS = ("left_ankle_roll_link", "right_ankle_roll_link")
2828

29+
# Per-joint posture standard deviations used by the ``pose`` reward. Smaller std =
30+
# tighter tolerance. Lower body looser at running speed; arms/wrists looser to allow
31+
# natural swing. Mirrors mjlab's G1 std table verbatim.
32+
_G1_POSE_STD_WALKING: dict[str, float] = {
33+
# Lower body.
34+
r".*hip_pitch.*": 0.3,
35+
r".*hip_roll.*": 0.15,
36+
r".*hip_yaw.*": 0.15,
37+
r".*knee.*": 0.35,
38+
r".*ankle_pitch.*": 0.25,
39+
r".*ankle_roll.*": 0.1,
40+
# Waist.
41+
r".*waist_yaw.*": 0.2,
42+
r".*waist_roll.*": 0.08,
43+
r".*waist_pitch.*": 0.1,
44+
# Arms.
45+
r".*shoulder_pitch.*": 0.15,
46+
r".*shoulder_roll.*": 0.15,
47+
r".*shoulder_yaw.*": 0.1,
48+
r".*elbow.*": 0.15,
49+
r".*wrist.*": 0.3,
50+
}
51+
52+
_G1_POSE_STD_RUNNING: dict[str, float] = {
53+
r".*hip_pitch.*": 0.5,
54+
r".*hip_roll.*": 0.2,
55+
r".*hip_yaw.*": 0.2,
56+
r".*knee.*": 0.6,
57+
r".*ankle_pitch.*": 0.35,
58+
r".*ankle_roll.*": 0.15,
59+
r".*waist_yaw.*": 0.3,
60+
r".*waist_roll.*": 0.08,
61+
r".*waist_pitch.*": 0.2,
62+
r".*shoulder_pitch.*": 0.5,
63+
r".*shoulder_roll.*": 0.2,
64+
r".*shoulder_yaw.*": 0.15,
65+
r".*elbow.*": 0.35,
66+
r".*wrist.*": 0.3,
67+
}
68+
2969

3070
def _obs_terms() -> dict[str, ObservationTermCfg]:
3171
return {
@@ -128,8 +168,8 @@ def unitree_g1_velocity_env_cfg(play: bool = False) -> ManagerBasedRlEnvCfg:
128168
heading_control_stiffness=0.5,
129169
ranges=UniformVelocityCommandCfg.Ranges(
130170
lin_vel_x=(-1.0, 1.0),
131-
lin_vel_y=(-0.5, 0.5),
132-
ang_vel_z=(-0.7, 0.7),
171+
lin_vel_y=(-1.0, 1.0),
172+
ang_vel_z=(-0.5, 0.5),
133173
heading=(-math.pi, math.pi),
134174
),
135175
)
@@ -145,12 +185,26 @@ def unitree_g1_velocity_env_cfg(play: bool = False) -> ManagerBasedRlEnvCfg:
145185
weight=2.0,
146186
params={"command_name": "twist", "std": math.sqrt(0.5)},
147187
),
148-
"upright": RewardTermCfg(func=mdp.flat_orientation_l2, weight=-1.0),
188+
"upright": RewardTermCfg(
189+
func=mdp.upright_exp,
190+
weight=1.0,
191+
params={"std": math.sqrt(0.2)},
192+
),
193+
"pose": RewardTermCfg(
194+
func=mdp.variable_posture,
195+
weight=1.0,
196+
params={
197+
"command_name": "twist",
198+
"walking_threshold": 0.05,
199+
"running_threshold": 1.5,
200+
"default_std": 0.3,
201+
"std_standing": {".*": 0.05},
202+
"std_walking": _G1_POSE_STD_WALKING,
203+
"std_running": _G1_POSE_STD_RUNNING,
204+
},
205+
),
149206
"action_rate": RewardTermCfg(func=mdp.action_rate_l2, weight=-0.1),
150207
"dof_limits": RewardTermCfg(func=mdp.joint_pos_limits, weight=-1.0),
151-
"feet_air_time": RewardTermCfg(
152-
func=mdp.feet_air_time, weight=0.5, params={"threshold": 0.4}
153-
),
154208
},
155209
terminations_cfg={
156210
"time_out": TerminationTermCfg(func=mdp.time_out, time_out=True),
@@ -163,7 +217,12 @@ def unitree_g1_velocity_env_cfg(play: bool = False) -> ManagerBasedRlEnvCfg:
163217
mode="reset",
164218
func=mdp.reset_root_state_uniform,
165219
params={
166-
"pose_range": {"x": (-0.5, 0.5), "y": (-0.5, 0.5), "z": (0.01, 0.05)},
220+
"pose_range": {
221+
"x": (-0.5, 0.5),
222+
"y": (-0.5, 0.5),
223+
"z": (0.01, 0.05),
224+
"yaw": (-math.pi, math.pi),
225+
},
167226
"velocity_range": {},
168227
},
169228
),
@@ -173,8 +232,17 @@ def unitree_g1_velocity_env_cfg(play: bool = False) -> ManagerBasedRlEnvCfg:
173232
if not play:
174233
cfg.events_cfg["push_robot"] = EventTermCfg(
175234
mode="interval",
176-
interval_range_s=(10.0, 15.0),
235+
interval_range_s=(1.0, 3.0),
177236
func=mdp.push_by_setting_velocity,
178-
params={"velocity_range": {"x": (-0.5, 0.5), "y": (-0.5, 0.5)}},
237+
params={
238+
"velocity_range": {
239+
"x": (-0.5, 0.5),
240+
"y": (-0.5, 0.5),
241+
"z": (-0.4, 0.4),
242+
"roll": (-0.52, 0.52),
243+
"pitch": (-0.52, 0.52),
244+
"yaw": (-0.78, 0.78),
245+
},
246+
},
179247
)
180248
return cfg

‎examples/unitree/src/genelab_unitree/g1/ppo_cfg.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ def unitree_g1_ppo_runner_cfg() -> RslRlOnPolicyRunnerCfg:
77
return RslRlOnPolicyRunnerCfg(
88
seed=42,
99
num_steps_per_env=24,
10-
max_iterations=3000,
10+
max_iterations=30_000,
1111
save_interval=50,
1212
experiment_name="g1_velocity_flat",
1313
logger="tensorboard",

‎examples/unitree/src/genelab_unitree/g1/tracking_env_cfg.py‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,15 @@ def unitree_g1_tracking_env_cfg(play: bool = False) -> ManagerBasedRlEnvCfg:
227227
mode="interval",
228228
interval_range_s=(1.0, 3.0),
229229
func=mdp.push_by_setting_velocity,
230-
params={"velocity_range": {"x": (-0.5, 0.5), "y": (-0.5, 0.5)}},
230+
params={
231+
"velocity_range": {
232+
"x": (-0.5, 0.5),
233+
"y": (-0.5, 0.5),
234+
"z": (-0.4, 0.4),
235+
"roll": (-0.52, 0.52),
236+
"pitch": (-0.52, 0.52),
237+
"yaw": (-0.78, 0.78),
238+
},
239+
},
231240
)
232241
return cfg

‎src/genelab/cli/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,7 @@ def _dispatch_train(task: _RunnableTask, runner_args: dict[str, str]) -> None:
388388
num_envs=int(num_envs_raw) if num_envs_raw is not None else None,
389389
max_iterations=int(max_iter_raw) if max_iter_raw is not None else None,
390390
seed=int(seed_raw) if seed_raw is not None else None,
391-
log_root=Path(log_dir_raw) if log_dir_raw is not None else None,
391+
log_dir=Path(log_dir_raw) if log_dir_raw is not None else None,
392392
)
393393

394394

‎src/genelab/mdp/__init__.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
motion_relative_body_position_error_exp,
4747
track_angular_velocity_z_exp,
4848
track_linear_velocity_xy_exp,
49+
upright_exp,
50+
variable_posture,
4951
)
5052
from genelab.mdp.terminations import (
5153
bad_anchor_ori,
@@ -105,4 +107,6 @@
105107
"time_out",
106108
"track_angular_velocity_z_exp",
107109
"track_linear_velocity_xy_exp",
110+
"upright_exp",
111+
"variable_posture",
108112
]

‎src/genelab/mdp/events.py‎

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
import torch
66

7+
from genelab.utils.math import quat_from_euler_xyz, quat_mul
8+
79
if TYPE_CHECKING:
810
from genelab.envs.manager_based_rl_env import ManagerBasedRlEnv
911

@@ -14,7 +16,13 @@ def reset_root_state_uniform(
1416
pose_range: dict[str, tuple[float, float]] | None = None,
1517
velocity_range: dict[str, tuple[float, float]] | None = None,
1618
) -> None:
17-
"""Randomize the floating base pose / velocity within the given ranges."""
19+
"""Randomize the floating base pose / velocity within the given ranges.
20+
21+
``pose_range`` accepts ``x``, ``y``, ``z`` (position offsets, m) and ``roll``, ``pitch``,
22+
``yaw`` (rotation offsets, rad, layered on top of the configured ``init_quat``).
23+
``velocity_range`` accepts ``x``, ``y``, ``z`` (linear, m/s) and ``roll``, ``pitch``,
24+
``yaw`` (angular, rad/s).
25+
"""
1826
if env_ids is None or env_ids.numel() == 0:
1927
return
2028
pose_range = pose_range or {}
@@ -31,12 +39,34 @@ def reset_root_state_uniform(
3139
set_pos(init_pos, envs_idx=env_ids)
3240
except TypeError:
3341
set_pos(init_pos)
42+
if any(axis in pose_range for axis in ("roll", "pitch", "yaw")):
43+
base_quat = torch.tensor(env.cfg.robot.init_quat, device=env.device).expand(n, -1).clone()
44+
zeros = torch.zeros(n, device=env.device)
45+
rpy = []
46+
for axis in ("roll", "pitch", "yaw"):
47+
if axis in pose_range:
48+
lo, hi = pose_range[axis]
49+
rpy.append(torch.empty(n, device=env.device).uniform_(lo, hi))
50+
else:
51+
rpy.append(zeros)
52+
rand_quat = quat_from_euler_xyz(rpy[0], rpy[1], rpy[2])
53+
new_quat = quat_mul(base_quat, rand_quat)
54+
set_quat = getattr(env.robot, "set_quat", None)
55+
if set_quat is not None:
56+
try:
57+
set_quat(new_quat, envs_idx=env_ids)
58+
except TypeError:
59+
set_quat(new_quat)
3460
vel = torch.zeros(n, 3, device=env.device)
3561
ang = torch.zeros(n, 3, device=env.device)
3662
for axis, idx in (("x", 0), ("y", 1), ("z", 2)):
3763
if axis in velocity_range:
3864
lo, hi = velocity_range[axis]
3965
vel[:, idx] = torch.empty(n, device=env.device).uniform_(lo, hi)
66+
for axis, idx in (("roll", 0), ("pitch", 1), ("yaw", 2)):
67+
if axis in velocity_range:
68+
lo, hi = velocity_range[axis]
69+
ang[:, idx] = torch.empty(n, device=env.device).uniform_(lo, hi)
4070
set_vel = getattr(env.robot, "set_vel", None)
4171
set_ang = getattr(env.robot, "set_ang", None)
4272
if set_vel is not None:
@@ -86,18 +116,30 @@ def push_by_setting_velocity(
86116
env_ids: torch.Tensor | None,
87117
velocity_range: dict[str, tuple[float, float]] | None = None,
88118
) -> None:
119+
"""Push selected envs by overwriting linear (x/y/z) and angular (roll/pitch/yaw) base velocity."""
89120
if env_ids is None or env_ids.numel() == 0:
90121
return
91122
velocity_range = velocity_range or {}
92123
n = int(env_ids.numel())
93124
vel = torch.zeros(n, 3, device=env.device)
125+
ang = torch.zeros(n, 3, device=env.device)
94126
for axis, idx in (("x", 0), ("y", 1), ("z", 2)):
95127
if axis in velocity_range:
96128
lo, hi = velocity_range[axis]
97129
vel[:, idx] = torch.empty(n, device=env.device).uniform_(lo, hi)
130+
for axis, idx in (("roll", 0), ("pitch", 1), ("yaw", 2)):
131+
if axis in velocity_range:
132+
lo, hi = velocity_range[axis]
133+
ang[:, idx] = torch.empty(n, device=env.device).uniform_(lo, hi)
98134
set_vel = getattr(env.robot, "set_vel", None)
135+
set_ang = getattr(env.robot, "set_ang", None)
99136
if set_vel is not None:
100137
try:
101138
set_vel(vel, envs_idx=env_ids)
102139
except TypeError:
103140
set_vel(vel)
141+
if set_ang is not None:
142+
try:
143+
set_ang(ang, envs_idx=env_ids)
144+
except TypeError:
145+
set_ang(ang)

‎src/genelab/mdp/rewards.py‎

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
"""Reusable reward term functions for locomotion tasks."""
22

3+
import re
34
from typing import TYPE_CHECKING, cast
45

56
import torch
67

8+
from genelab.managers.reward_manager import RewardTermCfg
79
from genelab.mdp.commands.motion_command import MotionCommand
810
from genelab.utils.math import quat_error_magnitude
911

@@ -45,6 +47,93 @@ def flat_orientation_l2(env: "ManagerBasedRlEnv") -> torch.Tensor:
4547
return torch.sum(env.robot_state.projected_gravity_b[:, :2] ** 2, dim=-1)
4648

4749

50+
def upright_exp(env: "ManagerBasedRlEnv", std: float = 0.45) -> torch.Tensor:
51+
"""``exp(-||projected_gravity_xy||^2 / std^2)`` — positive reward for an upright base.
52+
53+
Port of mjlab's ``upright`` reward (flat-ground variant). Saturates near zero tilt
54+
instead of growing unbounded like an L2 penalty, which matches the policy gradients
55+
the reference implementation relies on.
56+
"""
57+
xy_squared = torch.sum(env.robot_state.projected_gravity_b[:, :2] ** 2, dim=-1)
58+
return torch.exp(-xy_squared / (std * std))
59+
60+
61+
class variable_posture:
62+
"""Reward for staying near the default pose, with per-joint, speed-dependent tolerance.
63+
64+
Port of ``mjlab.tasks.velocity.mdp.rewards.variable_posture``. Std dicts map joint name
65+
regex → std value; per joint the *last* matching pattern wins (same convention as
66+
``RobotEntityCfg.default_joint_pos``). Joints with no match keep the supplied ``default``.
67+
68+
At each step the active std vector is chosen per env from the command magnitude:
69+
70+
* ``total_speed = ||lin_vel_xy|| + |ang_vel_z|``
71+
* standing if ``total_speed < walking_threshold``
72+
* walking if ``walking_threshold <= total_speed < running_threshold``
73+
* running otherwise
74+
75+
Reward: ``exp(-mean((joint_pos - default)^2 / std^2))``.
76+
"""
77+
78+
def __init__(self, cfg: RewardTermCfg, env: "ManagerBasedRlEnv") -> None:
79+
self._env = env
80+
params = cfg.params
81+
default = float(params.get("default_std", 1.0))
82+
std_standing = params.get("std_standing", {})
83+
std_walking = params.get("std_walking", {})
84+
std_running = params.get("std_running", {})
85+
self._std_standing = self._build_std(std_standing, default)
86+
self._std_walking = self._build_std(std_walking, default)
87+
self._std_running = self._build_std(std_running, default)
88+
89+
def _build_std(self, mapping: dict[str, float], default: float) -> torch.Tensor:
90+
out = torch.full((len(self._env.joint_names),), default, device=self._env.device)
91+
for pattern, value in mapping.items():
92+
try:
93+
regex = re.compile(pattern)
94+
except re.error:
95+
regex = re.compile(re.escape(pattern))
96+
for i, name in enumerate(self._env.joint_names):
97+
if regex.fullmatch(name) or regex.search(name):
98+
out[i] = float(value)
99+
return out
100+
101+
def __call__(
102+
self,
103+
env: "ManagerBasedRlEnv",
104+
command_name: str,
105+
std_standing: dict[str, float] | None = None,
106+
std_walking: dict[str, float] | None = None,
107+
std_running: dict[str, float] | None = None,
108+
default_std: float = 1.0,
109+
walking_threshold: float = 0.05,
110+
running_threshold: float = 1.5,
111+
) -> torch.Tensor:
112+
del std_standing, std_walking, std_running, default_std # consumed at __init__
113+
114+
command = env.command_manager.get_command(command_name)
115+
linear_speed = torch.norm(command[:, :2], dim=-1)
116+
angular_speed = torch.abs(command[:, 2])
117+
total_speed = linear_speed + angular_speed
118+
119+
standing_mask = (total_speed < walking_threshold).float().unsqueeze(-1)
120+
walking_mask = (
121+
((total_speed >= walking_threshold) & (total_speed < running_threshold))
122+
.float()
123+
.unsqueeze(-1)
124+
)
125+
running_mask = (total_speed >= running_threshold).float().unsqueeze(-1)
126+
127+
std = (
128+
self._std_standing * standing_mask
129+
+ self._std_walking * walking_mask
130+
+ self._std_running * running_mask
131+
)
132+
133+
error = env.robot_state.joint_pos - env.default_joint_pos
134+
return torch.exp(-torch.mean((error * error) / (std * std), dim=-1))
135+
136+
48137
def joint_pos_limits(env: "ManagerBasedRlEnv") -> torch.Tensor:
49138
"""L2 of joint-position excursion past ±π (cheap stand-in for true limit penalty)."""
50139
excess = (env.robot_state.joint_pos.abs() - 3.14).clamp(min=0.0)

‎src/genelab/rl/runner.py‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,15 @@ def train_task(
9797
max_iterations: int | None = None,
9898
seed: int | None = None,
9999
log_root: Path | None = None,
100+
log_dir: Path | None = None,
100101
resume_from: Path | None = None,
101102
) -> Path:
102-
"""Train ``task_id`` with PPO. Returns the log directory."""
103+
"""Train ``task_id`` with PPO. Returns the log directory.
104+
105+
``log_dir`` (final, pre-resolved) takes precedence over ``log_root`` (parent under
106+
which a ``<experiment>/<timestamp>`` directory is created). The torchrun relaunch
107+
path pre-resolves the log dir so every rank lands in the same folder.
108+
"""
103109
ensure_project_cache()
104110
env_cfg = _resolve_env_cfg(task_id, play=False)
105111
if num_envs is not None:
@@ -115,8 +121,9 @@ def train_task(
115121

116122
from rsl_rl.runners import OnPolicyRunner
117123

118-
log_root = log_root or Path("logs") / "rsl_rl"
119-
log_dir = resolve_log_dir(log_root, agent_cfg.experiment_name, agent_cfg.run_name)
124+
if log_dir is None:
125+
log_root = log_root or Path("logs") / "rsl_rl"
126+
log_dir = resolve_log_dir(log_root, agent_cfg.experiment_name, agent_cfg.run_name)
120127
if is_main_process():
121128
_save_run_params(log_dir, env_cfg, agent_cfg)
122129

0 commit comments

Comments
 (0)